Conversation
groupEventsByDay injected a synthetic empty TODAY group when the homepage event log had no events for the current day, rendering as a bare "TODAY" header with no cards. Removed the mechanism entirely: the includeToday option, the injection block, the dead .event-log-day--no-events className branch in EventLog, and its CSS. A real event starting today is unaffected — formatDayLabel still labels its group "TODAY", independent of the removed injection. Added a regression test for that case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switching the faction tab fans a re-render across ~10 react-slot-counter instances in StatGrid. A DevTools profile showed a ~41ms synchronous block (72ms INP) — a visible hitch. Wrapping setFaction in React's startTransition lets React yield through that render instead of blocking the interaction frame. Measured INP for a Global->Bugs switch: 72ms -> 39ms. The key-remount approach was profiled and rejected (722ms, 10x worse — layout thrashing from ~10 slot counters re-measuring glyph width on mount). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a 6th WAR_DURATION card. Global tab: total war elapsed time (season_duration). Faction tab: how long that faction has been deployed — war duration minus the time it spent hidden before introduction. getCampaign now derives per-faction first_seen (earliest non-hidden h1_status bucket) and a top-level war_start. The non-hidden filter matters: updateStatus writes a row for all 3 factions every poll, so pre-introduction factions carry 'hidden' rows from war start — min(time) alone would report day 0 for everyone (confirmed against the season-157 data, which has a real staggered start). Mirrors the archives DURATION card; the auto-fit grid absorbs the 6th card with no layout change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hovering a dashboard region card lights up the matching galaxy-map area: the hovered faction's territory faintly, its active sector strongly, the rest of the map receding — a three-tier opacity focus. New sectorLink.mjs toggles CSS classes directly on the map's SVG nodes (the MermaidDiagram pattern) instead of via React state, so a hover costs no re-render of the card grid or ~33 map paths. DashboardClient tags each card <li> with hover handlers + data-faction-index/data-sector; the data-* attrs also leave cards findable for a future map → card reverse highlight. Ships the card→map direction only, per the issue's "see how it goes" plan. TDD: 5 sectorLink tests (faint+strong, clear, defeated faction, Super Earth, re-highlight). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The card subtitle showed the accidental-death count plus its rate as a percentage of total deaths; it now shows the count with a "Martyrs" label and drops the percentage. Completes the StatGrid refinement begun in the previous commit (asPercent / computeAccidentalRate are now unused and were removed there). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The ENEMIES_KILLED "Last 24h" subtitle showed a raw kill volume with a permanently green arrow — a count, not a verdict, since a cumulative counter only ever grows. It now compares the last 24h of kills against the 24h before it and shows ▲/▼/▪ for whether the killing pace rose, fell, or held — a genuine better/worse signal, matching how HELLDIVERS_ONLINE already reads against its rolling average. - getKills24hAgo → getKillsTrend: fetches two point-in-time snapshots (~24h and ~48h ago) instead of one; prop threaded as `killsTrend`. - killsTrendSubtitle derives last24h/prev24h; neutral ▪ when there is no 48h baseline yet (season 24–48h old). - Extracted the shared ▲/▼/▪ arrow into a `deltaArrow` helper, now used by both playersDeltaSubtitle and killsTrendSubtitle. - formatNumber applies the M suffix from 1M (was 10M): a 7-digit grouped number overflowed the stat-card subtitles; ≥1M now renders as X.XM. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…47.14 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The card→map hover (shipped in 0.47.13) dimmed the whole galaxy map to opacity 0.25 and spared the hovered area — a subtractive focus. It now leaves every sector at full opacity and instead firms up the hovered faction's see-through `.lost` sectors: their translucent ghost stroke (0.15 → 0.55) and near-invisible fill (0.06 → 0.12) gain opacity. Gold `.captured` / `.in_progress` strokes are left untouched so they stay gold; the active sector takes a heavier 3px outline. Pure Map.css restyle — sectorLink.mjs class-toggling logic and its 5 tests are unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Completes the bidirectional hover link. #185 shipped card → map only; this adds map → card: hovering anywhere in a faction's galaxy-map territory firms up that faction's sidebar card border (ghost rgba 0.15 → 0.55 — the mirror of the card → map lost-sector lift). - sectorLink.mjs: highlightCard() / clearCardHighlight(), DOM class toggling on the card <li>s, no React state, zero re-renders. - Map.jsx: onMouseEnter/onMouseLeave on each faction <g> — child-path mouse events bubble to the group, so one handler covers the whole faction territory. - DashboardClient.jsx: the Super Earth defence card gets a data-attacker-index so hovering the attacking faction's own territory highlights it too. - EventCard.css: .card-linked border firm-up + transition. TDD: 6 new cardLink tests (one-card faction, two-card faction, data-attacker-index match, superearth-group match, clear, re-highlight). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`@v4` has no release tag — it resolved to the action's WIP v4 branch, which lost its root action.yml, breaking the Pagespeed workflow with "Can't find 'action.yml'". Pin to v3.34, the latest stable release. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pin lowlighter/metrics action to v3.34 stable tag (was @v4, a WIP branch with no action.yml). Fixes the failing Pagespeed workflow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the archives page's bespoke ArchiveStats (global) + FactionStats (per-faction) — a parallel, drifted reimplementation of the homepage hero's per-faction StatGrid — with the shared StatGrid itself for the six core cards, above a collapsed archives-only extras component. - StatGrid: additive `archived` prop. On seasons predating combat-stat collection, the four telemetry cards render a censored DATA REDACTED — MINISTRY OF TRUTH treatment instead of zeros. Default off, so the homepage path is byte-for-byte unchanged. - ArchiveStats: collapsed the global + per-faction extras (OUTCOME, DEFENSE_RATE, ATTACK_RATE, AVG_DIFFICULTY, WORST_CASCADE, HOTSPOT, CONQUEST, AVG_BATTLE) into one component, mirroring StatGrid's branch. - FactionStats: deleted — its cards now come from StatGrid + ArchiveStats. - formatRatio: extracted to a shared format util. - Drops duplicated cards: DURATION, KILLS, BATTLES, K/D. Built test-first; lint, typecheck, 1347 unit tests and build all pass, DevTools-verified on telemetry and pre-telemetry seasons. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…elease 0.48.0 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The other half of the Phase A split (Part 2 = #391, shipped in 0.48.0). New top-level /stats route reading the full 157-season history through a single getCrossSeasonStats() query (SQL GROUP BY aggregates + a per-season war-outcome derivation that reuses getWarOutcome on a slim per-season slice). Three components: - Faction Threat Ranking — per-faction overall HD win rates as a faction-colored horizontal bar chart, sorted ascending (most threatening enemy first). Recharts. - War Outcomes & Streaks — total wars, victories, defeats, win rate, longest win/loss streaks with season ranges, plus a wrapping per-season outcome timeline of success/danger pills. - All-Time Records — longest war, most events, longest avg battle, most defends/attacks won; each card attributed to the season that owns the extremum. The three telemetry charts from #178 (Friendly Fire Index, Accuracy Trend, Shots per Planet) are deferred until telemetry accumulates beyond season 157 — the query already returns telemetry fields so the charts slot in cleanly later. HeaderNav and BottomNav gain a Stats entry. Built test-first throughout. lint, typecheck, 1365 unit tests and build all pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`ArchiveStats` (the archives-only extras grid that sits under the shared `StatGrid` hero) was hard-capped at `lg:grid-cols-3`, so its 4th and subsequent cards wrapped to a new row — visually inconsistent with the 6-across auto-fit grid right above it. - Swap both grid divs to use the existing `.stat-grid` class (`repeat(auto-fit, minmax(11rem, 1fr))`, same one `<StatGrid>` uses). - Make the previously-implicit dependency on `StatGrid.css` explicit by importing it in `ArchiveStats.jsx` — the component now styles itself rather than relying on `<StatGrid>` happening to render first. Both grids now breathe with the viewport identically; at typical desktop widths every extras card sits on one row. Tests + DevTools verified. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… 0.49.0 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brainstormed design for replacing the archives-only Cyberstan interference with a sitewide opt-in system: rare per-element hijack every 2-5 min plus always-on ambient micro-flicker, with tone derived from humanity's overall war record (winning -> sardonic mocking, losing -> regime/Skynet reassurance). Manual toggle removed in favor of prefers-reduced-motion only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Applies 11 consensus revisions from a 3-round adversarial AI debate (Gemini, Codex, Sonnet, Opus). Architecturally significant changes: - MinistryProvider mounts inside LiveDataProvider, shares its visibility signal and reload-cancel signal (no duplicate listeners, no orphaned timers during guardedReload). - Registry moved from React state to useRef-backed store with stable context callbacks; eliminates re-render storms on navigation when 30+ wrappers exist on a page. - Hijackable: aria-label removed (unreliable on span without role); alt characters now rendered aria-hidden; nav/button/link categories banned (WCAG 2.5.3 compliance). - Scheduler: scope eligibility evaluated at pick-time against pathnameRef (eliminates 16ms stale-registration race); per-element idle check on ambient flicker (prevents double-restore corruption). - One authoritative state machine in useMinistryHijackCycle.mjs (replaces deleted useGlitchCycle); fight phase explicitly dropped; CYCLE_MS=2600 pinned as shared constant. - War tone: returns 'winning' | 'losing' | null; null disables the effect (no more silent forced-losing on DB errors); uses getWarOutcome for "completed war" classification; requires layout.jsx dynamic='force-dynamic' for cache freshness. - v1 adoption whitelist sharply cut to headings + decorative body + archives migration; deferred nav/buttons/footer/stat-values until layout-shift is measured. - Content pools: minimum 12 entries per pool with Vitest enforcement; category enum narrowed to heading/value/body/footer only. One open question remains for the author: tone-direction mapping (losing -> Ministry doubling-down vs. resistance-mocks-regime). Debate transcript at ~/.claude-octopus/debates/debates/001-ministry-interference-spec-critique/. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes Open Question #1 from the adversarial review. The losing tone keeps its anti-government direction but changes speaker identity from "Ministry doubling down" to "Underground / pirate-radio bootleg broadcast" — third-party intruder cutting in with Skynet / surveillance imagery aimed at the regime. Both tones now share consistent "hijack by a third party" framing; only the intruder changes (Resistance mockery on winning, Underground warnings on losing). Spec is now ready for implementation planning. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
20 bite-sized TDD tasks. Each task: failing test, minimal impl, green,
commit. Foundations (Tasks 1-10) build the new src/features/ministry/
subsystem in isolation; Task 11 mounts the provider in root layout;
Tasks 12-16 migrate the existing /archives Cyberstan effect to the new
system and delete the retired files; Tasks 17-18 wrap v1-scope headings
on remaining pages; Task 19 adds a Playwright integration test;
Task 20 is the final verification gate.
Plan documents two pragmatic deviations from the spec:
- MinistryProvider gets its own visibility check (via document.hidden
inside tick) rather than sharing one from LiveDataProvider, which
doesn't expose visibility in its context.
- The guardedReload signal-cancel is dropped because location.reload()
tears down the window and all setTimeouts anyway.
AmbientFlicker.jsx is folded into MinistryProvider (cleaner ownership)
rather than living as a separate file as originally speced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cross-season cascade-failure leaderboard rendered as an EventLog-style section on /stats (grouped by season), reusing the existing EventLog/EventLogCard pattern. Same component renders on /archives filtered to one season. Replaces findWorstCascade with findAllCascades. Approved in brainstorming. Issue: #272 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chore(release): 0.51.3 — rebroadcast consolidation + SEASON_NOT_FOUND export + 500→404 fix Includes the version bump and CHANGELOG move into ## 0.51.3 as part of this merge commit, per CLAUDE.md Git Workflow rule #2. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…nalytics.opts Clears the two jsdoc/require-param-description lint warnings tracked in issue #400. Both sites had the type annotation but no description text, which eslint-plugin-jsdoc requires for completeness. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The three makeFactionMap test helpers across EventCard.test.jsx,
DashboardClient.test.jsx, and computeFrontier.test.mjs were flagged by
desloppify's signature_variance detector as one symbol with 2 different
signatures across 3 files. Closer inspection shows they're genuinely
different helpers that just happened to share a name — each takes a
different region range (1-11 vs 1-10 vs 0-11) and produces a different
field set. Extracting a shared helper would force a fake abstraction
over three distinct test shapes.
Renamed instead:
- EventCard.test.jsx::makeFactionMap → makeSectorMap (per-faction
sectors map keyed on regions 1-11, status+percent only)
- DashboardClient.test.jsx::makeFactionMap → makeDashboardMap (full
DashboardClient mapState shape, regions 0-11, region+status+event+
percent)
- computeFrontier.test.mjs::makeFactionMap unchanged (already scoped
inside a describe() block — symbol is no longer cross-file once
the other two are renamed, so signature_variance dissolves)
Closes issue #401.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ic logs
Promotes two intentional `catch {}` swallows (flagged by desloppify's
smells detector in issue #399) from bare comment to a documented swallow
with console.debug logging of the captured error. Both sites are
genuinely non-critical paths where throwing would do more harm than good:
- MinistryProvider.jsx flicker scheduler — cosmetic animation; a
transient timing error should not crash the provider that wraps
the entire dashboard.
- useLiveData.mjs saveCachedState — localStorage may be full, disabled
in private mode, or unavailable in SSR; the cache is a PWA offline
nicety, never a correctness path.
The console.debug calls make the swallows visible to anyone running
the app with the browser console open at debug level, removing the
"silently lost" failure mode without changing any runtime behavior.
Closes issue #399.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…server-action files Auth-guard helpers (requireSession / requireUser / requireAdmin) and the API-key request validator (validateApiKey) are HTTP-boundary concerns, not database queries. They were sitting in src/db/queries/ behind a misleading directory name. Relocated next to responses.mjs and methodNotAllowed.mjs: - src/db/queries/_authGuards.mjs → src/shared/utils/api/authGuards.mjs - src/db/queries/validateApiKey.mjs → src/shared/utils/api/validateApiKey.mjs Also removes (without replacement in this commit) the three server-action files that move to feature directories in follow-up commits. Their consumers still reference the old paths through HEAD~1; the next two commits in this branch land the merged feature files and update every consumer in lockstep. Reviewing as a series is cleaner than reviewing each commit standalone. Removed (relocated in commits 2 + 3): - src/db/queries/admin.mjs (7 admin actions → features/admin/actions.mjs) - src/db/queries/api.mjs (3 API key actions → features/account/actions.mjs) - src/db/queries/account.mjs (2 user lifecycle actions → features/account/actions.mjs) Updated importers in this commit (only the two outside the relocation set): - src/features/archives/reseedSeason.mjs (requireAdmin) - src/app/api/h1/rebroadcast/route.js (validateApiKey) Plus two test files and the two docs MDX pages. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…/actions.mjs The 7 admin server actions previously in src/db/queries/admin.mjs (getAllUsers, updateUserRole, toggleUserBan, adminGetUserApiKeys, adminRevokeApiKey, getSystemStats, getAllApiKeys) merge into the existing src/features/admin/actions.mjs alongside sendTestNotification. Each action's logic is byte-for-byte identical — only the file location changed; auth-guard imports now point to the new src/shared/utils/api/authGuards.mjs path. Updates 4 admin component importers + the unit test file. Closes the admin_ac design_coherence finding (admin actions previously split between db/queries/admin.mjs and features/admin/actions.mjs with no clear rule for which goes where). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…features/account/actions.mjs
Merges what was previously two separate server-action files into one
co-located actions module next to the components that call them:
- src/db/queries/api.mjs (3 API-key management actions)
- src/db/queries/account.mjs (2 user lifecycle actions)
↓
src/features/account/actions.mjs
The new file has two clearly-headed sections (API key management /
User data lifecycle). Each action's logic is byte-for-byte identical
— only location changed; auth-guard imports now point to
src/shared/utils/api/authGuards.mjs.
Updates 3 component importers (ApiForm, ApiDashboard, AccountActions)
and 4 test files (2 unit tests still in /unit/queries/ for now, 2
jsx component tests with vi.mock paths updated).
Closes the db_queri high_level_elegance finding (src/db/queries/ now
contains only pure data-access: get*/upsert*/the rebroadcast wire-
format reconstruction).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
chore(release): 0.51.5 — split src/db/queries/ by responsibility Includes the version bump and CHANGELOG move into ## 0.51.5 as part of this merge commit, per CLAUDE.md Git Workflow rule #2. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…id* files
isValidStatus.mjs and isValidSeason.mjs were the only two validators
that bound their root schema to a local `const rootSchema = ...` and
then re-exported it on the next line. The intermediate name was never
referenced elsewhere in either file — pure unused indirection. The
other three validators (isValidContentType, isValidNumber, isValidFormData)
already used the direct-export form.
Converged on the direct-export form: `export const isValidX = z.<schema>(...)`.
The `@typedef {z.infer<typeof isValidStatus>} StatusPayload` and
`@typedef {z.infer<typeof isValidSeason>} SeasonPayload` annotations
keep working because they reference the exported name, not the dropped
intermediate.
Pure refactor — no runtime behavior change. Closes the
validator-protocol-unification cluster and 3 contract_coherence
findings from /desloppify issue #406.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ication chore(release): 0.51.6 — unify validator export shape across all 5 isValid* files Includes the version bump and CHANGELOG move into ## 0.51.6 as part of this merge commit, per CLAUDE.md Git Workflow rule #2. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…face error envelopes
- deleteUserAccount: add z.object userId schema; drop the false confirmEmail JSDoc claim
- deleteUserAccount: reverse order to delete-then-revoke so a delete failure leaves the user logged in to retry
- deleteUserAccount: add revalidatePath('/profile', 'layout') every sibling action already calls
- generateApiKey: wrap count + create in db.\$transaction(async tx => ..., { isolationLevel: 'Serializable' }) so the 5-key cap can't be bypassed by parallel calls
- generateApiKey: return { ...newApiKey, key } DTO instead of mutating the Prisma model instance
- deleteApiKey: use check.data.{userId,apikeyId} instead of raw formValues.* in the ownership guard and Prisma where-clause
- ApiDashboard: handle result.errors envelope explicitly (was rendering 'No API keys yet' on auth lapse)
- AccountActions.handleExport: toast on errors envelope (was silent)
- AccountActions.handleDelete: redirect only on explicit result.data.deleted (was redirecting on any undefined too)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… season-0 falsy-zero
- adminRevokeApiKey: drop the unused first parameter — <form action={adminRevokeApiKey}> in AdminApiKeys.jsx is a bare form action (no useActionState), so Next.js invokes it with one arg; the old (_, formData) signature captured FormData into _ and made formData undefined, throwing TypeError on every Revoke click
- updateUserRole: wrap admin-count + user.update in db.\$transaction(async tx => ..., { isolationLevel: 'Serializable' }) — closes the last-admin TOCTOU race
- toggleUserBan: same transaction pattern when banning an admin — closes the parallel race
- updateUserRole: use check.data.{userId,newRole} instead of raw formValues.* in self-edit guard, last-admin branch, and Prisma update
- adminGetUserApiKeys: use check.data in Prisma where-clause instead of raw form value
- getSystemStats: change currentSeason ? to currentSeason !== null ? so season 0 (a valid early-war value) no longer falsy-skips the active-factions count
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…e maps to 503 The old code collapsed Prisma errors and missing-keys into the same INVALID code, so a Postgres outage on /api/h1/rebroadcast surfaced to legitimate API consumers as 401 Unauthorized. Operators saw a flood of "bad API key" 401s in logs instead of the infrastructure failure. - Add API_KEY_ERROR.DB_ERROR to the enum - Split the if (dbError || !row) collapse: DB error → DB_ERROR, missing row → INVALID - /api/h1/rebroadcast: map DB_ERROR to errorResponse(503, start, 'database unreachable') ahead of the DISABLED/INVALID branches (wording matches /api/healthcheck) - Tests updated: validateApiKey.test.mjs's existing dbError case now expects 'db_error'; rebroadcast.test.mjs gains a 503 case Pre-existing regression carried verbatim from the old src/db/queries/validateApiKey.mjs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The directive made requireSession / requireUser / requireAdmin callable as RPC server actions, but every importer (features/admin/actions.mjs, features/account/actions.mjs, features/archives/reseedSeason.mjs) is itself a 'use server' module — the helpers are never reached from 'use client' code. The directive was carried over verbatim from the old src/db/queries/_authGuards.mjs as an R100 byte-identical rename predecessor. Removing it closes the unintended whoami-probe RPC surface and aligns the file with its src/shared/utils/api/ siblings (responses.mjs, methodNotAllowed.mjs, validateApiKey.mjs), none of which has the directive. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…mples
Both validators are z.object({...}) instances — calling them as functions throws TypeError: isValidX is not a function. The doc narrative just above the snippet correctly says "Zod safeParse," but the code sample used the wrong call form. A reader copy-pasting the example would hit a runtime error immediately. Switched both occurrences to the .safeParse(fetchedData) form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bump to 0.51.7 (patch) and move 15 bugfixes from this branch's commits into the new CHANGELOG section. No new behavior; only edge cases get better. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…e review (0.51.7) PR #411 (db/queries split) + PR #412 (validator unification) went through a 5-angle code review that surfaced 23 candidates; verification confirmed 23, ranked the 15 most severe, and this branch fixes all 15. Highlights: - Critical: admin Revoke API key button was completely broken (form-action arity mismatch) - 3x TOCTOU races wrapped in db.\$transaction with Serializable isolation - validateApiKey DB outages no longer surface as 401 Unauthorized (now 503) - 4 UI consumers now handle result.errors envelopes explicitly - deleteUserAccount: Zod-validates input, reorders revoke/delete, fires revalidatePath - authGuards.mjs: dropped unnecessary 'use server' RPC surface - docs/data-flow: code samples now use correct .safeParse() form Verification: lint clean, typecheck clean, 1419/1419 unit tests pass, build compiles.
…ext method The dev-only `window.__ministry_test__.forceHijack` useEffect was orphaned after the Playwright spec it served was replaced with Vitest smoke tests (commit 4ef57c3). Repurpose the existing logic as a useCallback on the MinistryContext value (memoized against warTone) and delete the dead window hook entirely. - MinistryProvider.jsx: forceHijack(predicate?) — picks first eligible descriptor matching the predicate (default any), fires onHijack with a propaganda string from the existing content pools, resets idle after CYCLE_MS. Same behavior as the old window hook; no environment gate. - MinistryContext.mjs: JSDoc documents the new method on the published context value shape. - MinistryProvider.test.jsx: five new tests cover success path, predicate filtering, no-match, warTone:null, and scope rejection. No user-visible behavior change yet — public consumer arrives in the next commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…egg testing Admins can now reproduce the Ministry Interference hijack effect on any page without waiting on the 2-5 min random scheduler or juggling tabs. - src/features/admin/MinistryTriggerWidget.jsx: new fixed-position client widget (bottom-right, clears the mobile BottomNav). Returns null when !isAdmin. Click calls MinistryContext.forceHijack() and toasts the outcome (success, no-eligible-element, or disabled). Five new RTL tests cover all four toast paths plus the no-provider edge case. - src/app/layout.jsx: server-side session lookup gates the widget to admins. Auth-disabled deploys (BETTER_AUTH_SECRET unset) get auth === null so the lookup short-circuits and isAdmin stays false — the widget renders nothing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Minor bump for the new admin-only floating trigger. Moves Unreleased entries (Features + Changes) into the new 0.52.0 section. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…(0.52.0) Adds a floating admin-only "Trigger Ministry" button to every page so the Ministry Interference easter egg can be reproduced on demand. Repurposes the dead-since-commit-4ef57c3c window.__ministry_test__ debug hook as a first-class MinistryContext.forceHijack() method. - New: src/features/admin/MinistryTriggerWidget.jsx (RTL tests included) - Changed: MinistryContext.mjs gains forceHijack on the published context value - Changed: MinistryProvider.jsx — useCallback wraps the forceHijack logic; dead window hook removed - Changed: src/app/layout.jsx — session lookup added, widget mounted inside MinistryProvider Verification: lint clean, typecheck clean, 1430/1430 unit tests pass, build compiles.
…content + overlay effects Supersedes the 2026-05-23 v1 spec. Replaces the full-sentence text-swap hijack with three overlay effects (scribble, margin marker, message banner) declared per-component; truth text is never mutated visually. Random scheduler stays for non-admin users but now viewport-filters via IntersectionObserver. The 0.52.0 floating admin-trigger widget is replaced by per-Hijackable icon-button strips. Whole subsystem lazy-loaded via next/dynamic so truth text renders SSR but no Ministry JS is in the critical-path bundle. Spec brainstormed via /superpowers:brainstorming; 6 sections approved one at a time before commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…t/cache
The Chainguard runner (cgr.dev/chainguard/node:latest) has no `nonroot` entry
in /etc/passwd — uid 65532 is named `node` — so the runner-stage
`COPY --chown=nonroot:nonroot` lines silently fell back to root (0:0), leaving
/app/.next root-owned. The container runs as uid 65532, so the Next.js image
optimizer's mkdir('.next/cache/images') failed with EACCES and flooded logs
with unhandledRejection on every remote-avatar optimization.
Switch all three runner COPY lines to numeric --chown=65532:65532 (numeric IDs
need no passwd lookup). Verified by reproducing the exact production state
(/app/.next uid=0, mkdir FAILED:EACCES) in a synthetic build, then confirming a
real Dockerfile.app build yields /app/.next uid=65532 and a successful cache
write.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Runner-stage `COPY --chown=nonroot:nonroot` silently fell back to root because the Chainguard runtime has no `nonroot` passwd entry (uid 65532 is `node`), leaving /app/.next root-owned and breaking the Next.js image optimizer's runtime mkdir as uid 65532. Switched the three runner COPY lines to numeric --chown=65532:65532. Bumps version to 0.52.1 and moves the CHANGELOG entry from Unreleased into 0.52.1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Promotes the accumulated
developline to production.mainwas last tagged v0.47.9 (server running v0.47.7); this brings production current to v0.52.1, intentionally skipping the untagged intermediate releases (0.48–0.52.0) per the consolidation decision.Headline fix (0.52.1)
EACCES: permission denied, mkdir '/app/.next/cache'flood fixed. The Chainguard runtime has nononrootpasswd entry (uid 65532 isnode), soCOPY --chown=nonroot:nonrootsilently fell back to root, leaving/app/.nextroot-owned and breaking the Next.js image optimizer's runtime cachemkdir. Switched the three runner COPY lines to numeric--chown=65532:65532. Verified by reproducing the prod state and confirming a real build yields/app/.nextuid=65532+ successful cache write.Scope
CHANGELOG.mdfrom 0.48.x through 0.52.1 (admin Ministry trigger, code-review bug sweep, db/queries split, validator/type-safety work, rebroadcast consolidation, etc.).main— the migrate container applies nothing new on this jump.Deploy after tagging (
v0.52.1)Server
docker-compose.yml:helldiversbot+migrateimages tov0.52.1curlhealthcheck with the node-based probe (Chainguard ships nocurl)then
docker compose pull && docker compose up -d.🤖 Generated with Claude Code