Repository navigation
fix(deps): upgrade pg with correct Worker socket teardown - #1281
Conversation
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
life-ustc | a7a8136 | Oct 23 2026, 07:19 AM |
tiankaima-copilot
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.
3631e1b to
502471c
Compare
tiankaima-copilot
left a comment
There was a problem hiding this comment.
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
_listenTypeError 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.
15def59 to
a97814a
Compare
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>
a97814a to
8838392
Compare
) 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>
|
Re-checked upstream today (2026-10-21): still correctly blocked.
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. |
…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>
* 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>
#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>
`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>
|
🎉 This PR is included in version 1.128.1 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
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 parent08fd8677passed 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.mdrecords 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.