Skip to content

fix(deps): upgrade pg with correct Worker socket teardown - #1281

Merged
tiankaima merged 32 commits into
mainfrom
dependabot/bun/multi-472da96ade
Oct 23, 2026
Merged

tiankaima merged 32 commits into
mainfrom
dependabot/bun/multi-472da96ade

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Goal

Upgrade pg and @types/pg to 8.23.1 while preserving clean Worker database teardown. Closes #1334.

Changed Surfaces

Backport the socket lifecycle fix from brianc/node-postgres#3806 through Bun's version-pinned pg-cloudflare 1.4.1 patch. Active read failures still propagate; normal closure no longer emits a spurious pool error. Add three lifecycle regressions against the installed package.

Evidence

On the original head 8838392, the unchanged Todo Worker cases failed 2/3 during teardown; its direct parent 08fd8677 passed 3/3. The patched build passes all 12 workspace REST/GraphQL/MCP cases with two workers and no socket-closed pool errors. No timeout or business assertion changed.

Fresh frozen install, bun run check (4001 tests), full database integration with all four role gates (972 tests), application build and Worker build passed. The new lifecycle tests fail 2/3 without the patch and pass 3/3 with it.

Current-head full CI passed, including all domain jobs and specification execution evidence.

Docs / Contracts

No business contract change. patches/pg-cloudflare.md records the upstream commit and removal condition.

Risk Areas

The upstream fix is not released yet. The patch includes matching source, runtime code, declarations and source map and applies only to pg-cloudflare 1.4.1.

Cleanup

No scratch artifacts or generated application files are included.

@dependabot dependabot Bot added dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code labels Oct 20, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
life-ustc a7a8136 Oct 23 2026, 07:19 AM

@tiankaima-copilot tiankaima-copilot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delivery is blocked on the current head by CI run 39459269749. The Todo Worker contract job (118762135078) fails three workspace-semantic-parity consumers (REST, GraphQL and MCP) during fixture shutdown: Private Worker did not stop within 15 seconds; 30 other Todo cases pass. The Account job (118762135182) separately fails user.post-login-redirect at tests/e2e/src/app/signin/test.ts:425: the independently observed audit log count is 4 instead of the required 5; 139 Account cases pass. Similar failures affect additional domain jobs. The update includes pg 8.23.1 and pg-cloudflare 1.4.1, with upstream socket close/end and client lifecycle changes; causality still needs isolated reproduction. Keep the existing shutdown timeout and audit assertions. The failed run's remaining work will be cancelled to avoid running a known-blocked head, and this PR remains open pending diagnosis.

@tiankaima-copilot tiankaima-copilot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed a local regression on the exact upgrade head after the failed CI run. With frozen dependencies and rebuilt production/test Workers, the existing todo prepared-state consumers cases completed their REST reads but REST fixture teardown failed with Private Worker did not stop within 15 seconds; GraphQL and MCP passed. The REST Worker repeatedly emitted postgres.pool-error / TypeError: This socket has been closed from pg-cloudflare's _listen path. An isolated control checkout of the direct parent c9b1c6d, with its own frozen install and rebuild against the same disposable PostgreSQL source, passed all three unchanged cases in 16.5 seconds. This narrows the regression to the pg 8.23.1 / pg-cloudflare 1.4.1 upgrade, but does not yet identify which upstream change causes it. No timeout, audit-count or business expectation was changed. The PR remains blocked pending an upstream fix or a validated dependency fix; it must not be merged on the current evidence.

@dependabot
dependabot Bot force-pushed the dependabot/bun/multi-472da96ade branch from 3631e1b to 502471c Compare October 21, 2026 03:23

@tiankaima-copilot tiankaima-copilot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rebased head still locks pg 8.23.1 and pg-cloudflare 1.4.1; it does not include a vendor fix.

Primary-source follow-up on October 21, 2026:

  • Upstream issue #3805 reports the same post-close _listen TypeError on these exact versions. The published source at 0980cef supports the mechanism: the corrected write callback invokes socket.close(), which rejects a pending read; connect() forwards that rejection as an error. pg's ending filter only ignores ECONNRESET/EPIPE, so the code-less TypeError reaches the client/pool.
  • Candidate fix #3806 remains open and unmerged. It handles reads rejected after local end or peer close while retaining live-connection failures.
  • The official npm registry still reports pg 8.23.1 and pg-cloudflare 1.4.1 as latest, both published September 30, 2026. No released replacement contains the proposed fix.

This explains the matching socket-error signature; it does not independently prove the entire local 15-second Worker shutdown failure. The failed upgrade/control comparison remains the delivery blocker. No application-level suppression, idle-timeout disablement, dependency downgrade, or weaker teardown assertion was introduced. Keep this PR open until a sound fix can pass the complete suite.

@dependabot
dependabot Bot force-pushed the dependabot/bun/multi-472da96ade branch 2 times, most recently from 15def59 to a97814a Compare October 21, 2026 06:28
Bumps [pg](https://github.com/brianc/node-postgres/tree/HEAD/packages/pg) and [@types/pg](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/pg). These dependencies needed to be updated together.

Updates `pg` from 8.21.0 to 8.23.1
- [Changelog](https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianc/node-postgres/commits/pg@8.23.1/packages/pg)

Updates `@types/pg` from 8.20.0 to 8.23.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/pg)

---
updated-dependencies:
- dependency-name: "@types/pg"
  dependency-version: 8.23.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
- dependency-name: pg
  dependency-version: 8.23.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot
dependabot Bot force-pushed the dependabot/bun/multi-472da96ade branch from a97814a to 8838392 Compare October 21, 2026 06:50
tiankaima and others added 7 commits October 21, 2026 16:03
)

The /api/docs page rendered the same failure three times when the OpenAPI
schema could not be fetched: the navigation snippet printed common.error in
the sidebar, and the reference panel stacked common.somethingWentWrong
("发生错误!") over common.error ("发生错误") inside a destructive Alert. Both
the sidebar and the 42rem reference panel also stayed mounted as large empty
bordered boxes with nothing to show.

Render a single Empty-based notice in place of the whole docs shell instead.
The sidebar, the mobile navigation trigger and the reference panel are no
longer mounted when there is no schema, so the failure state is compact and
self-contained. Copy moves to dedicated apiDocs.loadErrorTitle /
apiDocs.loadErrorDescription messages that name what failed and what to do;
the retry affordance keeps common.tryAgain.

renderReference now awaits a tick before looking up #api-reference, because
that container is only mounted by the success branch and is still queued when
a retry resolves.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Convert the eleven admin bus components from the legacy `export let` API to
`$props()`. Each file is converted completely, since a component is either
fully runes or fully legacy.

Transformations applied:
- `export let x: T` -> `interface Props { x: T }` + `let { x }: Props = $props()`
- `export let x = default` -> `x?: T` in Props plus a destructuring default
- mutable component locals -> `$state(...)`
- the destructured controller default state -> `$state(createAdminBusControllerDefaultState())`
- `$:` assignments -> `$derived(...)`; none of them were side effects, so no
  `$effect` was introduced

No prop in this cluster is `bind:`-ed from a parent, so no `$bindable()` is
needed, and none of the files used `$$props`, `$$restProps`, `export const` or
`export function`. The cluster already used event attributes and snippets, so
no event-handler migration was required.

The conventions follow the ones established by the native-runes migration in
"refactor(kit): replace legacy route stores with native runes".

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The footer nav links were plain text anchors with no padding or minimum
height. Measured at a 390px viewport they were 56x20, 56x20, 41x20 and
56x20 CSS px, below the 24x24 minimum in WCAG 2.2 SC 2.5.8 (Target Size
Minimum). The inline-link exception does not apply: these sit in a
flex-wrap nav row, not inline within a sentence.

Add `py-1` so every footer link is 28px tall. 28px is the largest value
that still lets the row wrap to two lines inside the fixed 64px bar
(2*28 + gap-y-1 = 60px); `py-1.5` would overflow it.

Also hide the footer brand wordmark on phones with `sr-only
sm:not-sr-only`, matching the existing treatment in AppTopbar. The
wordmark had no responsive hiding and competed with the nav exactly where
space is tightest: at 390px the links ended at x=285 and the brand began
at x=302. Reclaiming that space removes wrapping at zh-cn 320/360px and
en-us 414/430px, and fixes a pre-existing overflow where en-us at 320px
wrapped to three rows (68px of nav in a 64px footer). The wordmark stays
in the accessibility tree and is unchanged from sm: upward, so the
desktop layout is untouched.

The sign-in consent sentence links are deliberately left alone; those are
genuine inline links within a sentence, where the SC 2.5.8 inline
exception applies.

Verified with Playwright at 320/360/390/414/430/600/768/1280px in both
locales and both color schemes: all footer links 28px tall, no row
overflows the footer box.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ttle (#1299)

Cloudflare's Git integration owns production deployment, and a build started
for an older main commit can publish after a newer commit already reached
production. On 2026-10-21 the older commit's build completed ten seconds after
the newer one and left production on the superseded commit while both build
checks reported success, so a successful build check is not evidence that
production serves the commit the check belongs to.

Name the application version after the commit Cloudflare built, so the version
SvelteKit already publishes states which main commit the live deployment came
from. Verification then waits until no observed main commit has a build that
can still publish, reads the commit production actually serves, and compares it
with the newest main commit whose build succeeded. Commit order comes from
main's history rather than build timestamps, because two builds can report the
same completion second and a retried build completes long after the commit it
rebuilds; the completion instants are reported as evidence only. Progress check
runs that share a Cloudflare build identifier collapse into one build state,
since the earlier attempts of a finished build keep their unfinished status.

The verification is read-only: it holds no deployment credential, reads only
main's commit order, public build check results and the public version marker,
never deploys, and lives outside ci.yml so advancing main cannot rerun
current-head CI.

This detects the ordering defect and fails loudly; it does not prevent it.
Preventing a superseded build from publishing needs an enforceable control at
Cloudflare's deployment write, which stays the deferred architecture decision.

Refs: #1295

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…1300)

* feat(home): show live campus information on the anonymous home page

The anonymous landing page fetched no data: it was a logo, a tagline, a
sign-in button, six navigation chips and a large phone mockup. A student
arriving on a phone learned nothing about campus, and /catalog/features
repeated the same explanation across 2232px. Desktop already answered
"what exists" through its sidebar, so the divergence was structural.

Replace the marketing hero with four public, no-login sections, each
reusing an existing read service rather than any new ingestion:

- weather, from the cron-populated KV snapshot (weather-cache first, and
  only a cold entry falls through to the refreshing reader)
- next shuttle departures, from the shared static timetable
- latest news and notices, from listPublications
- upcoming second-classroom activities, from listYoungEvents

An anonymous visitor has no stored campus preference and no mechanism
exists to hold one, so the shuttle board derives its origin from the
imported topology — the campus served by the most routes — instead of
hardcoding one, and shows the soonest trip to every campus it reaches.
That answers "when is the next bus out of here" without asking the
visitor where they are going, and it moves on its own if the network
changes. Weather keeps showing both locations side by side, so neither
section needs a switcher.

Each section is read in parallel behind its own timeout and resolves to
null on failure, so a slow or down upstream degrades one card instead of
blanking or stalling the page. "Unavailable" and "empty" stay distinct in
the payload so an outage never reads as quiet news. / is already excluded
from shared HTML caching, so the departure board is computed against the
request clock as rendering-and-cache.cacheable-public-pages-11 requires.

The marketing pitch is demoted to a sign-in prompt below the information,
keeping the explore links. All strings are added to messages/ in both
locales. Reusing components already in the page graph means the client
budget for / improves rather than regresses: 67 -> 64 requests and
198665 -> 198037 gzip bytes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(home): keep weather cache-only and realign landing contracts

CI surfaced three 390px failures from the landing-page redesign, each a
real finding rather than a flaky test.

Weather on / now reads only the KV snapshot the weather crons write.
getWeatherSnapshot falls through to AMap/Open-Meteo on a stale entry and
then writes back to the cache and the history table — cron work that an
anonymous page view must not perform, and a provider round trip CI has no
network for. A cold entry degrades to the unavailable card instead.

wrangler.dev.jsonc was missing the WEATHER KV binding that production
declares, so the namespace was always null locally and the home weather
card could never render in development. Adding it makes local dev match
production and gives the cache-only read something to read.

showWeatherFixture clicked a[href="/catalog/weather"] document-wide. The
landing page now links there from its own weather card, so .first()
matched that in-page link, which the open sidebar sheet covers at 390px —
the click never settled. Scoped the locator to the sidebar it meant.

overview.decision-page asserted the six removed chips as
a[data-slot="item"] with catalog hrefs. Repointed it at the destinations
in the sign-in prompt, keeping every navigation assertion and adding the
four information sections it should now see.

ui.model-property-priority-discovery-views asserts the landing identity
and its description at 390 as well as 1280, so the description is no
longer hidden below the sm breakpoint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(test): scope the weather fixture click to the shell navigation

The previous fix scoped the /catalog/weather click to the `app-sidebar`
test id, which is a DOM node only on desktop: on mobile `Sidebar.Root`
spreads its rest props onto `Sheet.Root`, a context provider that renders
no element, so the locator never resolved and the click still timed out
at 390px.

Resolve the navigation per viewport, matching the helper in
navigation-context: the Sheet dialog named "Sidebar" on mobile, the
`app-sidebar` node on desktop. Verified against a running dev server at
both widths — at 1280 the document-wide locator matches two links (shell
plus the landing page's own weather card) and the scoped one matches the
shell link; at 390 the only document match is the in-page card that the
open sheet covers, which is exactly what used to hang.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(home): give the anonymous home page a desktop layout

The landing page rendered its phone stack unchanged on a wide canvas: at
1440x900 the 1169px content column carried two equal-height cards, two
three-row lists and a full-width sign-in band, so a desktop visitor saw
about what a 390px phone saw while 700-800px sat unused.

From `xl` the page is now two information columns plus a rail. The
sign-in prompt carries the least information on the page, so it stops
being a full-width band under the content and becomes that rail, handing
its former width back to the lists; its destinations stack one per line
where the column is too narrow to wrap them.

The sections then fill the width they were given. Notices, activities and
the shuttle board fetch the longer list and reveal the extra rows from
`lg` up, and the weather card adds today's range and a three-hour trend
that the cached snapshot already carried. One payload serves every
viewport, so the anonymous representation stays viewer-independent and
the route keeps its 64 initial asset requests.

Campus-now cards no longer share a row height. An unavailable or short
card used to be stretched into a tall empty box by whatever sat beside
it, which is what the weather card became whenever its KV entry was cold.

Phone and tablet layouts are unchanged: 390 and 768 captures are
pixel-identical to main apart from live shuttle times.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(home): keep the desktop rail out of the information grid rows

The sign-in rail spanned the same grid rows as the sections beside it, so
grid sizing grew those rows to fit the rail whenever the sections were
short. With every section degraded that stretched an empty notices block
from 52px to 158px — the tall empty box this layout exists to remove.

The information sections now own a nested grid, so the rail cannot take
part in sizing their rows at any data volume.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The signed overview rendered its mobile single column unchanged on a wide
canvas, so a 1440x900 viewport surfaced roughly what a 390px phone did
while 700-800px of the content column stayed empty.

Keep the vertical stack and every content decision from #1292, and give it
a desktop arrangement from the shared `lg` breakpoint: focus stays at full
content width above the lists, urgency and upcoming share one row as the
left and right halves, and term context and shortcuts span the full width
below. A single remaining list keeps the full width and flows its items
into two columns rather than leaving a dead half-row.

Compact the rows that spent height on one row of information: focus places
its title, status, type and time on one line above its date and context,
and each list row merges weekday/date into the existing metadata line and
gains a separator. Mobile keeps one strictly stacked full-width column and
only gets denser.

At 1440x900 the page now shows ten item titles above the fold instead of
five, and its content ends at 974px instead of 1704px. At 390px the stack
ends at 1692px instead of 1979px with no horizontal overflow.

The overview.card-order geometry assertion is realigned rather than
relaxed: document order stays the declared order at both widths, mobile
keeps the strict full-width stack it already asserted, and desktop now
asserts the specific paired arrangement plus both lists' first row being
in the viewport.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@tiankaima

Copy link
Copy Markdown
Member

Re-checked upstream today (2026-10-21): still correctly blocked.

  • brianc/node-postgres#3805 (pg-cloudflare 1.4.1: client.end() emits "This socket has been closed.") — OPEN
  • brianc/node-postgres#3806 (the fix, fix(pg-cloudflare): do not emit an error for the read that end() cancels) — OPEN, unmerged
  • npm pg@latest is 8.23.1, which is the affected version

So there is no released version to move to, and the Todo REST timeout plus socket-closed pool errors reproduced here would ship with it. Holding.

This PR stays open deliberately rather than being closed: the failing checks are the evidence, and closing it would invite dependabot to re-raise the same bump without that context. Next re-check when #3806 merges and a release follows.

tiankaima and others added 11 commits October 21, 2026 15:46
…ed UI (#1303)

Four documented statements contradicted the code, and one real outcome was
reported nowhere.

ui.shell-layout-2 still described a Workspace container and a separate Explore
group. Neither exists: navigation is grouped by student intent as My day,
Courses, Campus, Activities, Account, Apps and Admin, each a disclosure
labelled by its group name. The requirement now states that intent, and its
acceptance matches what the browser scenario already asserts.

ui.list-table-9 still required admin routes to render "without a duplicate
bottom bar" for a surface that no longer exists. The clause is removed;
ui.shell-layout-4 owns the absence of a bottom tab bar at every shell width,
and a policy rationale entry records that ownership.

Add ui.action-feedback. Completed actions have confirmed themselves in the
bottom-right toast since the inline success alerts were removed, but no
requirement said so - the only neighbouring requirement, ui.action-status-tokens,
governs status colour. The new requirement also fixes which outcome text the
confirmation carries, which the bus importer was getting wrong: the server
reports the imported campus, route and trip counts in its action message and
the client replaced that with a generic "import successful". The enhanced
action now hands the reported outcome to the toast and keeps the generic
phrase as a fallback.

Document the stock-shadcn rule in src/lib/components/AGENTS.md. The code has
held it for months and the user has asked for it repeatedly, but no
instruction file mentioned shadcn at all, so each new agent had to rediscover
that a primitive is never forked or extended in place.

Drop three stale page-frame exemptions. /privacy and /terms were exempt as
prose that "uses its own reading measure" while both actually render through
CollectionPage with width="reading"; the gate passes without the exemption.
/ was exempt for a full-bleed hero that #1300 and #1302 deleted, and it then
had no frame at all, so it stretched to whatever width the shell gave it. It
now resolves the wide tier through PageLayout, the same tier the signed
workspace uses.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…e a title (#1304)

The contribution heatmap shrank its 371 cells from 24 to 16 CSS px at
>=640px, below WCAG 2.2 SC 2.5.8, and the grid already scrolled
horizontally at every width (scrollWidth 901 vs a 609-769px region at
1280/1440), so the shrink bought no extra visible weeks. Keep one 24px
cell size everywhere.

In the exam list the course link (20px) sat 4px above the "更多信息"
summary (16px), so neither the size nor the spacing exception applied;
both now reach 24px, as does the subscription course link.

/workspace/subscriptions/activities set no title and fell back to the
app default, leaving its three sidebar destinations indistinguishable in
history; title it by view. /workspace/uploads was the only page missing
the " - Life@USTC" suffix.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…its targets (#1305)

The month and week grids declare `min-width: 760px` inside a horizontal
ScrollArea, but the wrapper that holds them is a grid item, so its automatic
minimum width resolved to that 760px instead of letting the ScrollArea clip.
At 768px the whole page was dragged 60px wider than the viewport, hiding the
Saturday column and the day/week/month toggle. `min-w-0` lets the wrapper
shrink so the grid scrolls inside its own area, as it already does elsewhere.

In the `lines` density each event row was a 20px line box with no gap and the
"+N more" link a 16px one, leaving 103 links on the production calendar below
the 24px WCAG 2.2 SC 2.5.8 floor with no spacing to exempt them. Both now sit
on a 24px line.

The shared button base relaxes its tap floor from `md` up, which silently
dropped the `min-h-11` that the feature hub had declared for itself and left
its card links, and the teacher mail link, 22px high. Both restate the 24px
floor for wide viewports.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…1309)

`empty.svelte` carried `border-dashed` with no border width, so the
preflight `border-width: 0` meant nothing ever painted. The admin OAuth
status alert guarded on `copyMessageVariant === "destructive"`, but
`copyText` only ever sets a message on the failure path, so the guard
could not be false when there was a message to show.

Three modules had no importer and no path reference anywhere, including
the wrangler alias maps: the skeleton key helper, the workspace card view
action factory superseded by `WorkspacePageController`, and the workspace
calendar count query. Removing the factory orphaned
`workspaceCardViewChange`, which goes with it.

The `/api/docs/[...path]` frame exemption credited a third party with a
sidebar grid that `api-docs-scalar.css` has always owned.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…1307)

/catalog/features is the "what exists" surface now that the anonymous home
page (#1300, #1302) leads with live campus information. Twelve cards
describing what each feature *is* took 2232px at 390 — nearly three screens
for a directory — and only 10 of 18 destinations reached the fold at 1440.

Each destination becomes one row: icon, name, one line of purpose, the whole
row a link. Sub-destinations move out of the card footer and sit under their
row as chips, which also removes the equal-height void: a footer on three of
twelve entries stretched every card beside it, leaving 67px of empty frame
under 校园天气. Rows are unframed and the grid is items-start, so a row that
is taller than its neighbours no longer stretches them and the unused space
is not a void. From md the group name moves into a left rail instead of
costing a line of its own.

The hub still renders from the shared destination model in
shell/navigation.ts; nothing about grouping, search or viewer scoping changes.

390: 2232px -> 1385px anonymous, 3995px -> 2330px signed-in admin.
1440 above the fold: 10 -> 17 of 17 destinations anonymous (the whole
directory), 9 -> 27 of 41 signed-in admin.

The e2e contract selected destinations through [data-slot="card-title"] a.
The row is now the link, so it selects [data-feature-entry] > a, and the two
assertions on a destination's visible name move to its [data-slot="item-title"],
which no longer carries the description text.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…1311)

The page-inventory audit at desktop width reported 95 color-contrast nodes
across all 46 pages and 4 link-in-text-block nodes, which is why both rules
were disabled in tests/e2e/utils/ui-quality.ts.

Three root causes needed no colour-system decision:

- The stock `kbd` primitive pairs its own `bg-muted` with
  `text-muted-foreground`, a foreground calibrated against `--background`,
  giving 4.39:1 at 12px. It is the keyboard hint in the shell's search
  trigger, so it failed on 44 of 46 pages (64 nodes). `text-foreground/70`
  gives 5.34:1.
- Inactive `Tabs.Trigger` inherits `text-foreground/60` over the default
  tabs-list `bg-muted`: 3.95:1. Overridden at the four call sites.
- The legal consent links on the sign-in page distinguished themselves from
  the surrounding sentence by colour alone at 2.23:1, so their underline is
  no longer hover-only.

That clears 70 of 99 nodes and enables `link-in-text-block`. `color-contrast`
stays disabled for the open `--primary`-as-text decision, now recorded at
the call with the nodes it still covers.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
#1312)

A rebuilt personal export was always written back to CALENDAR_EXPORTS, so
an unchanged timetable persisted the same calendar again on every rebuild.
Compare the rebuild against the stored entry first and keep that entry,
and its ETag, when nothing changed. The comparison ignores DTSTAMP, which
ical-generator sets to the build time and which is the only difference
between two rebuilds of the same data.

The write is still taken once the stored copy leaves its freshness window,
which renews both that window and the 24h KV expiration, so a skipped
write cannot strand a stale entry or let a served entry expire.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…#1308)

* refactor(i18n): name teaching sections and subscriptions consistently

班级 and 教学班 both named the same thing; 订阅 appeared unqualified
on pages that subscribe to two different kinds of thing. One concept had
five names across sidebar, heading and buttons.

Sections are 教学班 everywhere. Subscriptions are qualified as 教学班订阅
or 活动订阅 at every surface. The anonymous home rail leads with the
section catalog, since a student looks for the section they attend
rather than the abstract course or the teacher.

E2E assertions that matched the old strings were repointed at the new
ones; none were weakened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(overview): assert the full anonymous rail order

The rail now leads with the section catalog, so the contract's
first-three check expected the old order. It asserts all six
destinations instead of the first three, which is stricter than
before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(i18n): finish the section rename across the test vocabulary

The rename stopped at user-facing copy, leaving 班级 in test titles, describe
blocks and fixture text — the same word for 教学班 that the UI no longer uses.
Renamed those, and repaired the calendar export assertion that still matched
the old subscription-placeholder wording.

Left 班级 where it means something else: 行政班级 (admin classes) and the
young-event activity level, including its admin-class fixtures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…#1317)

/catalog/rooms showed a lookup box and three sample codes, then 426px of
nothing: a 1440px desktop carried no more than a 390px phone (1.00). It now
lists the buildings and floors the room-map manifest already covers, so the
page is browsable before a code is known; the codes run the same lookup as the
sample chips. The index comes from the manifest the lookup itself reads, under
its existing 300s process cache, so it adds no new upstream dependency.

/catalog/links stacked eight two-column tables down one 1122px column. The
groups now flow into balanced columns at xl, which keeps every table, column
and alignment intact; the name and action columns shrink to match the halved
width. A card grid measured worse (2.52 against 2.87) and left ragged trailing
rows in the three- and four-link groups.

Elements fully above the fold inside #main-content, 1440x900 over 390x844:
rooms 1.00 -> 2.90, links 1.74 -> 2.87.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(ui): restore the box declarations responsive bases discarded

The shared bases drop their mobile floor at `md`: `button.svelte` and
`toggle.svelte` carry `min-h-11 min-w-11 … md:min-h-0 md:min-w-0`, and
`input-group.svelte` carries `h-11 … md:h-8`. A breakpoint variant is a
variant selector, so it outranks the unprefixed utility that `twMerge`
keeps beside it — eleven call sites declared their own floor and still
computed 0 above 768px, and `toggle.svelte`'s own `min-w-8` size variant
was discarded the same way. Restate each one at `md` so it reaches the
DOM, and guard the shape with a unit test that derives the neutralizing
breakpoint from the shared source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(tests): scan component tags without exponential backtracking

The guard matched Svelte component tags with one pattern that allowed both
quoted strings and nested braces in the attribute list. CodeQL flagged it as
an inefficient regular expression: the brace alternative overlaps the bare
character class, so `<A` followed by repeated `{{}}` backtracks exponentially.

Replaced with a linear left-to-right walk that tracks quote and brace depth.
Verified identical output on all 324 Button/Toggle/InputGroup call sites in
`src/`, and that reintroducing a `min-h-6` on `AdminBusHeader` still fails
with the same file, line and message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…#1314)

* refactor(settings): co-display short account groups on the desktop canvas

Six sibling settings tabs each rendered only their own group, so five groups
sat one click away from an empty 1121x760 canvas and 1440 showed fewer items
than 390. Profile, preferences and linked accounts are short enough to share
one canvas: each of those three routes now renders all three groups from `lg:`
up, its own group in the leading column and the others beside it, while below
`lg:` only the requested group renders, exactly as before.

Security and authorizations keep a single group because their lists already
fill the canvas. Danger keeps a single group in both directions, so the
deletion trigger stays reachable only from its own destination, as
ui.settings-navigation-7 requires.

Profile and preferences now also read the user's USTC identities, which the
linked-accounts group renders.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(settings): mount companion groups only on a canvas that shows them

The first pass rendered every co-displayed group at all widths and hid two of
them with `hidden lg:`. A phone then paid for markup it could never show: the
settings panel grew from ~11 KB to ~37 KB of HTML, and the linked-account
companion mounted its passkey card, issuing a `list-user-passkeys` request on
profile and preferences that those routes never made before.

Companions now mount from a `(min-width: 1024px)` listener, the same
`onMount` + `matchMedia` shape FilterToolbar already uses, so below that width
they are absent rather than hidden. Measured at 390: identical element counts
on all six tabs, the panel HTML back to its previous size, and no extra
request. The requested group stays mounted in the leading column across the
breakpoint, so crossing it never discards typed input.

`data-settings-active-group` marks that leading column. Two linked-account
locators that resolved page-wide now scope to it: both still match one row
today, but only because no fixture links an OIDC account, which would add a
second provider row from the companion.

ui.settings-navigation-8 owns the arrangement, including that danger renders
no companion and that a narrow canvas renders no companion form at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
tiankaima and others added 2 commits October 22, 2026 21:18
* test(ui): gate the empty-state vocabulary page against page

ui.yaml holds ~113 requirements and nearly all of them compare one page
against a spec, so a page can be individually correct and still be the
only one of its kind. "Nothing here" is the clearest case: CollectionEmpty
and empty-variants.ts have declared a closed five-variant vocabulary since
#1255, and fourteen feature files still hand-write Empty.Root with nine
different class strings beside three parallel workspace wrappers.

The guard compares each empty-state root against the variant system rather
than against prose. The variant names come from empty-variants.ts, the
system's files are discovered by following what the composer renders down
to the Empty.Root primitive resolved through the ui/empty barrel, the
guarded utility groups are read out of those files' own class declarations,
and the call sites come from walking src/features and src/routes, so a new
page is covered when it is added rather than when someone remembers it.

Groups are matched exactly, which is what keeps it usable: the primitive
declares justify-center, so YoungCalendar's justify-self-stretch is a
different property and not a finding, and a bare Empty.Root renders the
shared defaults unchanged and is not one either. Eleven call sites that do
override the system are pinned with the exact class they declare; the pin
is re-read every run, so editing one fails until it is converged or
consciously re-pinned, and a pin may only be removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(ui): share one linear component-tag scanner between the guards

CodeQL flagged this guard's element pattern as an inefficient regular
expression: allowing both quoted strings and nested braces in one attribute
list makes `<A` followed by repeated `{{}}` backtrack exponentially. The
responsive-base guard carried the same pattern and was fixed on main with a
linear walk, so extract that walk instead of writing it twice.

Both guards keep detecting violations: reintroducing `min-h-6` on
AdminBusHeader and `py-20` on TeacherDetailSectionsMobile each still fail
with their original file, line and message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…st (#1316)

* feat(theme): add text-only role colour tokens and enable color-contrast

`--primary`, `--success` and `--warning` are calibrated as surfaces against
their own foregrounds, so as text on a light page they reach 2.16:1, 3.30:1
and 2.17:1. `--primary-text`, `--success-text` and `--warning-text` keep each
role hue at L 0.5, clearing 4.5:1 on every light surface in the ramp, and
alias the role colour again in dark mode, where it already reaches 11.2:1 /
8.0:1 / 9.2:1. Only text moves to them; brand colour on a filled brand
surface is unchanged.

Markdown links darken too, which drops them to 2.82:1 against body text, so
their underline becomes permanent rather than hover-only; that also clears
the dark-theme `link-in-text-block` failure the sweep found on /privacy. The
`mark` rule set a background without a colour, leaving the UA's black over
the amber tint at 1.54:1 on a dark surface.

`color-contrast` is now enabled over the page inventory: 26 nodes on 10 pages
before, 0 after, at desktop and mobile width in both themes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(publications): move the remaining brand-coloured links to --primary-text

The news source directory and publication detail links render only with
grouped sources, attachments or cross-posts, so the inventory sweep never
reached them, but they are the same 2.16:1 brand teal on a light surface.
The directory link sits beside its summary text and so takes a permanent
underline as well.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(theme): add --destructive-text for destructive text on its own tint

Enabling `color-contrast` caught the soft-destructive pairing in three
states the inventory sweep never rendered: the `destructive` button on
/admin/moderation, the `destructive` badge on /workspace/todos, and the
loopback-redirect alert on the OAuth consent screen. `#dc2626` is 4.14:1 on
`bg-destructive/10` and 4.32:1 at 90% opacity on the card.

`--destructive-text` follows the other three: the same hue at L 0.5, giving
5.67:1 and 5.72:1, aliasing `--destructive` in dark mode. It is applied where
the red is text on its own tint or faded; `text-destructive` stays as-is on a
plain card, where it already reaches 4.83:1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(a11y): clear the contrast failures the enabled rule found in CI

Three jobs failed on nodes no local sweep reaches.

`api-docs-scalar.css` maps Scalar's palette onto our tokens, so most of the
API reference's failures were ours: the active nav tag used `--primary` on
`--muted` at 1.96:1, `--scalar-color-2` inherited `--muted-foreground`, which
is calibrated against `--background` and reaches only 4.40:1 on `--muted`, and
the tertiary tier fell to 2.85:1. The accent now uses `--primary-text` and
both text tiers step down from `--foreground` (5.58:1 / 4.76:1 on
background-1, 5.33:1 / 4.58:1 on background-2). What is left is Scalar's own
HTTP-method palette at 4.45:1, which we do not map; it gets a scoped
exception beside the reference's other third-party entries.

The /usage/* failures were not colour at all: those pages fade content in from
`opacity: 0`, and `auditAccessibility` ran once with no settle, so a frame
measured 200ms after load reports 13 failures that are gone by 1.5s. It now
polls like the structural audit beside it, which still fails on a real
violation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(a11y): give the avatar fallback a foreground calibrated against its own fill

Same defect as `kbd.patch`: the stock primitive pairs its own `bg-muted` with
`text-muted-foreground`, which is calibrated against `--background`, so the
initials are 4.40:1 on `#f4f4f5`. `text-foreground/70` gives 5.33:1. The
initials show for any member without an avatar and while an avatar image is
still loading, so a call site cannot opt out of the pair.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
tiankaima and others added 9 commits October 22, 2026 23:41
#1319)

comment_attachment_summaries(text[]) filters commentId = ANY(...) on every
comment list/get/thread/replies request, but CommentAttachment lost every
index on commentId when 20260625140000_fix_collaborative_invariants replaced
the (commentId, uploadId) unique with a uploadId-only one. The lookup has been
a sequential scan since; at 500k rows and 1100 ids the RPC takes 848 ms and
drops to 13 ms with the index restored.

Also drops 26 indexes that exactly duplicate a unique index on the same table
(same access method, key columns, operator class, collation and ordering, no
INCLUDE, not partial), and removes Teacher's @@index([jwId]), which no
migration ever created. 1592 kB of index reclaimed and one write less per row
per duplicate on the nightly JW import.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
`parseRouteJsonBody` degraded to a bare `request.json()` unless a route
passed `maxBytes`, and exactly one of its ~30 call sites did. Anonymous
`POST /api/catalog/sections/match-codes` buffered and parsed a 52 MB body
before its `.max(500)` rejected it. Make 256 KiB the default and keep
`maxBytes` as the opt-up the bulk ingestion route already uses.

Bound the MCP `catalog_section_search` ids/jwIds arrays to the same 100
items GraphQL enforces, and price `Account.clientActivity` by its page
size instead of at multiplier 1.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
#1320)

`renderEmbeddedMarkdown` always passed `headingOffset: 2`, so the memoized
branch was unreachable and every comment node rebuilt a 15-plugin `unified`
chain. Key one processor per options shape instead; a processor freezes on
first use and none of these plugins keep per-run state.

`jsonToolResult` pretty-printed its text block, which is a third of the JSON
every MCP client pays for in tokens. `structuredContent` stays duplicated:
the spec recommends the text form for backwards compatibility and exposes no
client capability that would let the server detect it can be dropped.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Free-text catalog and global search split the query on whitespace and AND one
condition group per term, each group a five-way leading-wildcard ILIKE across
course name, course code, section code and teacher name. Nothing capped the
term count, so the 200-character query bound alone admitted about eighty
groups in a single anonymous request.

The groups are sequential scans whenever a term is under three characters,
because pg_trgm yields no complete trigram for a shorter infix pattern.
Measured on a seeded 200k-row table, `%数学%` and `%ab%` both fall back to a
Seq Scan while `%数学分析%` and `%abc%` use the trigram index — so the terms
that cost the most are the ones the index cannot help.

Keep the first six distinct terms. Dropping the tail only widens the result
set, which is the direction the query parsers already truncate in.

The same uncapped split existed in global search, which the topbar reaches
anonymously, so both now share one helper.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…on (#1322)

`withUserDbContext` is a full interactive transaction — BEGIN, a
set_config round trip, then COMMIT — and `runWithUserRlsContext` makes a
nested one free. The overview's serial tail opened two of them for
queries whose inputs the homework transaction already held: the viewer's
subscription kinds, and the comment counts for the sampled homework ids.

Prefetch both inside that transaction and pass them down instead of
re-reading them. Public relation reads stay outside it as before.

Also run the young calendar events beside `loadCalendarEventSources`
rather than after it, and stop the assistant snapshot serializing itself
behind the current-semester lookup.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…1324)

`ctx.exports.PublicSsr(...).fetch()` is a separate invocation, so the
default entrypoint's runtime context never reached it, and the generated
SvelteKit adapter worker is declared `fetch(req, env)`: it discards the
execution context passed third and calls `server.respond` with no
`platform`. With neither a parent context nor a `platform.ctx`,
`getCloudflareRuntimeTaskScheduler()` returned undefined for every public
page render, so each deferred public runtime cache write was dropped with
`reason = "scheduler_unavailable"` and the shared catalog detail caches
never populated.

Establish the context inside the entrypoint from its own execution
context, as the default entrypoint already does.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
A column's alignment is declared twice, in two vocabularies: the header
says `text-right` as a class, the body says the same thing by passing
`numeric` to CatalogTableLink. Nothing ties them together, so adding one
side and forgetting the other misaligns the column while each side stays
individually well-formed. The browser gates measure geometry this cannot,
but only on the routes they visit and the columns their fixtures fill.

Pair column N of the header row with column N of each body row and compare
what each side declares. Everything is derived: element roles from the
ui/table barrel's exports, the undeclared-column default from
table-head.svelte's own class, and the prop-driven vocabulary by reading
every component's class expressions for a prop that switches an alignment
on, which is how `numeric` is found without naming it. Tables come from
walking src/features and src/routes, and a table whose rows live in an
extracted row component is followed through the import.

Matching is positional, proven per row rather than assumed: a row is paired
only when its cell count matches the header and the Svelte block guards
around each cell match those around its head. A colspan row is not a column
row and is excluded; a row that is neither spanning nor pairable is itself a
failure, so no table can silently leave coverage.

No misalignments exist today. The gate is additive to the browser gates.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
… feed revalidate (#1326)

The bus route-search response declares a shared-cache TTL but carried
`Vary: Accept-Language, Cookie`, and Cloudflare stores a response only when its
`Vary` is absent or `Accept-Encoding`. Every payload builder behind the route
discards the locale, so take it from the query parameter and drop the `Vary`.

The personal calendar feed built an `ETag` and then sent `private, no-store`,
which stops a conforming client sending `If-None-Match` at all, so its working
304 path was unreachable and every iCal poll re-transferred the whole ICS. Serve
`private, max-age=0, must-revalidate` and let the shared private-response hook
pass that one audited form through. CDN `no-store` stays, so the per-user feed is
still never shared.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…1306)

* fix(a11y): meet WCAG target size in the shell and enforce the rule

The axe `target-size` rule was disabled because shell controls failed it.
Four controls are resized or reclassified so the rule can be turned back on:

- `AppSidebar` legal strip: `text-xs` links with no padding measured 48x16
  (zh-cn) and 87.73x16 (en-us). `py-1.5` gives them the same 28px box the
  footer links got in #1297.
- `AppSidebar` submenu chevrons: 20x20. The button box grows to 24x24 while
  the chevron stays 16px, so the icon does not move.
- `AppShell` skip link: `focus:not-sr-only` is a variant selector and beat the
  unprefixed `px-4 py-2`, so the focused link rendered with no padding at all
  (112x24, zh-cn). Moving the padding to `focus:` restores it to 144x40, and
  the unfocused link is now a true 1x1 `sr-only` box.
- `Sidebar.Rail`: a 16px-wide strip whose only neighbours on
  `/admin/moderation` are content links, so it failed on spacing there and
  passed elsewhere. It toggles the same sidebar as `AppTopbar`'s trigger,
  which is localized and already clears 24x24, so the rail becomes a
  pointer-only affordance instead of a second target under a hardcoded
  English name.

Verified with axe across the 53 routes in the page inventory at Desktop
Chrome, and at 320/360/390/414/430 in both locales with the mobile drawer
open: no `target-size` violations remain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(a11y): size the sidebar rail instead of hiding it from keyboards

The rail was made pointer-only to clear SC 2.5.8, which removed its
keyboard reachability and broke the contract asserting tabIndex 0.
The equivalent-control exception does not apply either: the topbar
trigger is lg:hidden, so above 1024px the rail is the only pointer
control that toggles the sidebar.

Widened to 24px instead. It keeps -translate-x-1/2, so the centre
line does not move and each side gains 2px.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(a11y): size the sidebar submenu action to 24px

The enabled target-size rule caught it on every admin page: the box is
w-5 and its after:-inset-2 hit-area extender is md:after:hidden, so
above 768px it is a bare 20x20.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(a11y): move the sidebar rail out of the sidebar's own controls

The 24px rail stayed centred on the sidebar edge, so widening it from
`w-4` to `w-6` pushed its box 20px back into the sidebar: it covered the
right 8px of every menu-action chevron (16x24px left) and 13px of every
icon-mode row (19x32px left), which axe reports as `target-size`
failures.

On the left side the rail now drops the half-width translate and sits at
`-right-5`, spanning 251-275px expanded and 43-67px collapsed, clear of
the chevrons (ending at 243px), the icon rows (40px) and main content
(starting at 276px). `group-data-[side=left]:after:start-1` keeps the
hover hairline on the container border at 255px / 47px, where upstream
drew it at `w-4`. The translate is scoped to `group-data-[side=right]`,
leaving the right-hand and offcanvas branches at upstream geometry;
neither is rendered here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(navigation): give sidebar groups an icon and a parent trail (#1315)

* feat(navigation): give sidebar groups an icon and a parent trail

Every first-level group now carries its own icon and renders like the
destinations it opens, so the row reads as click-to-expand instead of a label
that happens to be clickable. An icon-collapsed group keeps that disclosure:
the row stays visible and operable, and activating it expands the sidebar onto
that group rather than doing nothing.

A destination that is a URL ancestor of the current page is marked as being on
the path, so /workspace/subscriptions no longer looks unrelated while its
/activities child is open. The child keeps the single aria-current="page"; the
parent carries aria-current="location".

Also drops the unreachable /workspace/subscriptions/activities branch in
isShellLinkActive: every activities href carries ?view=, so the query branch
returns first, and the following branch makes the identical comparison.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(navigation): realign the collapsed-group contract with the new affordance

The icon-collapsed group assertion pinned `toBeDisabled`, which is the
over-collapsed behavior this branch removes. Assert the stronger property the
rest of that test depends on instead: the row stays operable and reports its
destinations as open, so the icon links below remain reachable.

Also declare the calendar token the new subscriptions trail test mints when it
loads signed-in workspace pages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
tiankaima and others added 2 commits October 23, 2026 14:48
`auditAccessibility` read only `results.violations`, so every node axe
reported as `incomplete` was silently discarded. axe documents that
`color-contrast` returns incomplete rather than a violation when it cannot
evaluate a node: text over a background image or gradient, text another
element covers, images of text, and partially transparent foregrounds. The
repo has all three shapes — the bus route table's scroll-fade overlays and
the weather panel's temperature gradient — so a contrast regression there
would pass the gate unnoticed.

Failing on these would be unusable; a single page reports hundreds of
undecidable nodes. Summarize them per rule instead and attach the counts to
the test run, so the signal is reviewable without blocking. The violations
path is unchanged: still filtered to critical and serious.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@tiankaima tiankaima changed the title fix(deps-dev): bump pg and @types/pg fix(deps): upgrade pg with correct Worker socket teardown Oct 23, 2026
@tiankaima
tiankaima merged commit 625cd7c into main Oct 23, 2026
46 checks passed
@tiankaima
tiankaima deleted the dependabot/bun/multi-472da96ade branch October 23, 2026 07:44
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.128.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

build(deps): validate the pg upgrade in the Worker runtime

2 participants