Skip to content

web(tideline): slice 2 — docs hub + help, shared states, offline/reconnect, changelog - #5743

Merged
Hmbown merged 12 commits into
mainfrom
feat/web-tideline-slice2-20260830
Sep 1, 2026
Merged

Hmbown merged 12 commits into
mainfrom
feat/web-tideline-slice2-20260830

Conversation

@Hmbown

@Hmbown Hmbown commented Aug 30, 2026 •

Copy link
Copy Markdown
Collaborator

What (PRD §6 + §11.13; backlog #61 slice 2, after #5734)

  • Docs as a Tideline surface: hub with task + topic search over a task-based index, release-truth line and contextual help band on every docs page, dictionary-driven sidebar/breadcrumb; new /docs/auth, /docs/computers, /docs/trust; docs work without JS.
  • Shared states: one set of empty / loading / error / retry components used by feed, digest, admin, roadmap; honest feed empty state (no fake data on build or fetch failure); locale-scoped error.tsx, not-found.tsx and a [...rest] boundary so unknown paths are real 404s in the locale shell (were 200).
  • Offline/reconnect: typed connection state + banner with retry for the signed-in shell (offline → online → probe → "Back online").
  • Changelog: /changelog derived from CHANGELOG.md at build (derive-changelog), footer link in all 18 locales, sitemap 84 → 92.
  • i18n: Web: the isZh migration is losing ground — add a one-way ceiling so it can converge #5519 ceiling respected — === "zh" branch files 27 → 23, none added.

Evidence

  • npm test → Test Files 43 passed / Tests 356 passed; tsc and eslint clean; npm run build → 696/696 static pages; check:facts OK; check:docs PASS; check:locales PASS.
  • 48 Playwright captures (16 route states × 390/768/1440) at backups/web-slice2-screens-20260830/; no horizontal scroll, widths exact.
  • Browser-verifying reviewer: first round fix-needed (per-segment loading.tsx masking prerendered HTML; feed empty state untruthful), fix round 293328dce, second round approve.

Unproven: production deploy (Vercel) and the GT network locale check.

No deploy, no publish.

No-Issue: Tideline web program slice 2 (PRD §6/§11.13, backlog #61)

🤖 Generated with Claude Code

https://claude.ai/code/session_014aDEyM2a4pPZ9qqMDrP5YX


Note

Low Risk
Public web, docs, and UI-state changes with bounded admin feed-revalidate and connection probing; no core auth or payment logic changes.

Overview
This slice turns the public site into a Tideline-aligned docs and status experience: shared empty/loading/error plates, honest data surfaces, and locale-aware route boundaries, plus a build-derived changelog and offline/reconnect chrome for the maintainer admin shell.

Docs hub and pages — The docs layout adds a release-truth line (build facts + latest published release from KV) and a contextual help band tied to each page’s source doc. The hub search now covers tasks (docs-tasks) and topics (docs-map), with dictionary-driven sidebar, breadcrumb, and search copy. New routes: /docs/auth, /docs/computers, /docs/trust, plus shared RefRows / withCodeSpans for code-owned literals in translated copy. Mobile docs nav stays visible as a grid index instead of hiding the sidebar.

Shared states and routing — surface-state (empty, loading, error, unavailable) and route-state shims power error.tsx, not-found.tsx, and admin loading. A locale [...rest] catch-all calls notFound() so unknown URLs get the locale shell’s 404 (correct metadata, not home canonical). loading.tsx exists only under /admin so ISR pages (feed, digest, docs) never ship “Loading…” as prerendered HTML to no-JS readers or crawlers.

Feed, digest, admin — Feed uses loadFeed with per-column status (ok / skipped / unavailable / failed) and FeedRetry that POSTs to /api/github/feed/retry (same-origin) before router.refresh() to bust the 600s ISR cache. Digest and admin empty/error paths use the same state components; admin adds ConnectionBanner (probes /api/facts) so paused actions aren’t mistaken for success.

Changelog — /changelog combines live facts (published release, source candidate) with CHANGELOG.md parsed at prebuild into changelog.generated.ts; sitemap and footer link added across locales.

i18n & CSS — Large states, changelog, and docs-shell dictionary additions (en/zh); Tideline styling for state blocks, connection banner, changelog, docs help, and reference tables in **globals.css.

Reviewed by Cursor Bugbot for commit 76a7f42. Bugbot is set up for automated code reviews on this repo. Configure here.

CodeWhale Bot and others added 3 commits August 30, 2026 10:56
…slice 2)

Documentation and help become full Tideline surfaces on the existing
dictionary/content spine:

- Docs hub searches two registries: a task-based index (lib/docs-tasks.ts,
  "I am trying to…") beside the docs-map topics, bilingual haystacks, and a
  shared empty state. Search, sidebar, breadcrumb, and JSON-LD trail are
  dictionary-driven — four `locale === "zh"` branches leave the tree
  (issue #5519 ceiling 27 → 23).
- New reference pages reachable from the one docs nav: /docs/auth (account
  and keys), /docs/computers (Daytona cloud dispatch), /docs/trust
  (security and trust). Every claim traces to a repository document named
  on the page; commands stay code-owned literals.
- Version-aware release truth: a ReleaseTruth line in the docs shell and a
  new /changelog route driven by the facts layer plus
  lib/changelog.generated.ts, derived at prebuild from CHANGELOG.md
  (scripts/derive-changelog.mjs; lib/changelog.test.ts is the drift gate).
  Footer Product column links Changelog in all 18 locales.
- Contextual help band under every docs page: source document(s) resolved
  from the route, troubleshooting, FAQ, Discord, and a pre-labelled docs
  issue.
- Shared surface states (components/surface-state.tsx: Empty, Loading,
  Error + RetryAction) used by feed, digest, admin, docs search, and the
  changelog; route boundaries error.tsx / not-found.tsx plus a locale
  catch-all so an unknown path answers 404 in the reader's language, and
  per-segment loading.tsx on the request-time data pages.
- Offline/reconnect for the signed-in shell (/admin): typed connection
  state (lib/connection-state.ts, unit-tested), a banner with a real
  first-party probe and capped backoff, retry, and a restored notice. No
  data is faked while disconnected.

Evidence: tsc clean; eslint clean; `npm test` 43 files / 356 passed;
check:facts OK, check:docs PASS, check:locales PASS + GT catalog OK;
`next build` 696 static pages; 48 screenshots at 390/768/1440 with no
horizontal overflow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aDEyM2a4pPZ9qqMDrP5YX
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
…TA, changelog reach

Review findings on #61 slice 2, resolved:

- Drop loading.tsx from the SSG/ISR segments (docs, feed, digest, roadmap).
  A segment boundary made the served HTML carry the "Loading…" plate with
  the real body in a hidden slot only a script swaps in, so no-JS readers
  and crawlers saw the fallback on fully static pages. Only /admin
  (force-dynamic) keeps its boundary. Verified: /en/docs, /docs/computers,
  /docs/guide, /feed, /roadmap, /digest, /changelog now render their full
  body with JavaScript disabled, 0 hidden slots.
- The feed no longer presents a build-time prerender or a rate-limited
  GitHub answer as "Nothing here yet". lib/github.ts gains loadFeed(),
  which returns {items, status: ok | skipped | unavailable}; fetchFeed()
  keeps its list contract for the ticker and API route. /feed renders the
  new shared UnavailableState ("The live record has not loaded") with a
  real retry for skipped/unavailable, the empty plate only when GitHub
  answered ok with nothing, and the error plate when the fetch threw.
- 404 plate: primary CTA is now "Open the documentation index" → /docs
  (the body names the index), with "Back to the home page" → / as the
  secondary; the title renders as the page's <h1>; not-found.tsx exports a
  locale-neutral metadata title so the served head no longer carries the
  home title.
- Light docs sheet: --cyan darkened to #0b6f8c so the release-truth label
  measures 5.3:1 on --paper (was 4.29:1).
- Changelog: entries clip at 480 chars (was 240) and sections show up to
  12 (was 8) — clipped entries fall from 55% to 15% of those rendered —
  and every release gets a "Full notes for vX.Y.Z" deep link into
  CHANGELOG.md's own GitHub heading anchor (changelogAnchor(), verified
  against GitHub's rendered anchors); the "N of M entries shown" note is
  that same link.
- Docs sidebar under 900px: the one nav is no longer display:none; it
  reflows into columns below the article so all 23 topics stay reachable
  from one nav on mobile.

Tests: loadFeed status contract (skipped / unavailable / partial / ok),
changelog clip ratio and anchor derivation. Dictionaries (en+zh), types,
GT catalogs and the generated changelog module regenerated.

Signed-off-by: CodeWhale Bot <bot@codewhale.net>

@codewhale-agent codewhale-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codewhale reviewed the changed lines in this pull request. This is a comment-only review, not an approval.

Left 1 comment on changed lines.

{"blocker":1,"concern":0,"nit":0,"model":"skipped","policy":"default"}

dispatch: "codewhale dispatch",
kind: "kind=cloud",
remoteFlag: "--remote",
apiKey: "export DAYTONA_API_KEY=…",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Blocker This changed line looks like a hardcoded secret.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-08-30T18:43:37.328862Z 293328d PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

…elp band; security contact is hunter@codewhale.net

Signed-off-by: CodeWhale Bot <bot@codewhale.net>

@codewhale-agent codewhale-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codewhale reviewed the changed lines in this pull request. This is a comment-only review, not an approval.

Left 1 comment on changed lines.

{"blocker":1,"concern":0,"nit":0,"model":"skipped","policy":"default"}

dispatch: "codewhale dispatch",
kind: "kind=cloud",
remoteFlag: "--remote",
apiKey: "export DAYTONA_API_KEY=…",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Blocker This changed line looks like a hardcoded secret.

Signed-off-by: CodeWhale Bot <bot@codewhale.net>

@codewhale-agent codewhale-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codewhale reviewed the changed lines in this pull request. This is a comment-only review, not an approval.

Left 1 comment on changed lines.

{"blocker":1,"concern":0,"nit":0,"model":"skipped","policy":"default"}

dispatch: "codewhale dispatch",
kind: "kind=cloud",
remoteFlag: "--remote",
apiKey: "export DAYTONA_API_KEY=…",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Blocker This changed line looks like a hardcoded secret.

@claude

claude Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Claude Code is working…

I'll analyze this and get back to you.

View job run

@claude

claude Bot commented Aug 30, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 2m 12s —— View job


Claude's review

  • Gather context (PR body, diff, prior review comments)
  • Review diff vs origin/main for correctness, provider/model route safety, reuse/simplification, tests, security
  • Check the standing "hardcoded secret" blocker on web/app/[locale]/docs/computers/page.tsx:11
  • Post findings

Scope note: this PR only touches web/ (Next.js community site) — no Rust workspace code. The repo's provider/model route-safety invariant (EPIC #2608) doesn't apply here; nothing in this diff infers a provider/model from a prefix or freeform text.

False positive: the standing "hardcoded secret" blocker

web/app/[locale]/docs/computers/page.tsx:11:

apiKey: "export DAYTONA_API_KEY=…",

This is a display literal in a SPANS lookup table used to typeset docs copy (withCodeSpans) — the value is a placeholder shell line ending in …, not a credential. It mirrors the wording in docs/DAYTONA_CLOUD_DISPATCH.md per the file's own comment ("Code-owned literals … Not copy."). No secret is present or reachable here. Safe to dismiss codewhale-agent's blocker.

Correctness

Reviewed the new abstractions closely; no bugs found:

  • lib/connection-state.ts — the reducer is a clean, exhaustive switch over ConnectionEvent (TS will flag any missing case). Backoff schedule (backoffDelayMs) is correctly capped; connection-state.test.ts covers the offline→reconnecting→degraded→online cycle and the backoff ceiling.
  • components/connection-banner.tsx — probe is guarded against overlap (inFlight ref), timers are cleaned up on every effect re-run, and the retry button is correctly disabled while offline (retrying is pointless without a browser network event first).
  • lib/github.ts loadFeed — the three-way ok/skipped/unavailable status is derived only from the two list calls (issues/pulls), with releases correctly treated as garnish; feed/page.tsx maps failed (thrown) separately from unavailable (non-ok response), so a real GitHub outage/rate-limit now reads differently from "nothing to show" — this is the honest-empty-state fix described in the PR body, and it holds up.
  • [locale]/[...rest]/page.tsx, error.tsx, not-found.tsx — correctly scoped inside the locale segment so unknown paths get the localized shell instead of the framework's bare 404/500, addressing the "were 200" issue in the PR description.
  • scripts/changelog-lib.mjs — parser handles multi-line bullets, [Unreleased], and clipping at word boundaries correctly; derive-changelog.mjs is byte-deterministic (no timestamp), matching the drift-test claim in changelog.test.ts.

Reuse / simplification

  • surface-state.tsx / route-state.tsx / retry-action.tsx cleanly consolidate what were previously per-page empty/error markup blocks (confirmed by the diffs on admin, digest, feed all switching to the shared components).
  • Minor, non-blocking: admin/page.tsx and digest/page.tsx still inline a couple of isZh ? … : … string branches (e.g. the "Not configured" and login-form copy) rather than routing through a dictionary like the docs pages now do. Not a regression from this PR and not on the stated i18n-ceiling scope, just noting the inconsistency for a future pass.

Tests

connection-state.test.ts and docs-tasks.test.ts are good examples of testing behavior rather than shape — the latter proves every task's href resolves to a real page.tsx and cross-checks route agreement with docs-map.ts, which is exactly the kind of registry-drift protection worth keeping. I did not re-run npm test/tsc/npm run build myself (no execution environment here); relying on the PR's reported evidence (43 files / 356 tests passed, clean tsc/eslint, 696/696 static pages).

No blocking issues found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 293328dce9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread web/app/[locale]/feed/page.tsx Outdated
// `RetryAction` without a handler re-runs this server render, which on an
// ISR page picks up the next revalidation — the real fix for "not loaded".
const states = getStates(locale);
const retry = <RetryAction label={states.retry} />;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make the feed retry bypass the ISR cache

When the production build records skipped, this route caches that unavailable state for 600 seconds, but RetryAction without a handler only calls router.refresh(). A user who clicks “Try again” before the ISR entry becomes stale receives the same cached server component without calling GitHub, so the advertised recovery does nothing; use an uncached fetch/retry endpoint or explicitly invalidate the cached route.

Useful? React with 👍 / 👎.

Comment on lines +81 to +84
case "probe-ok": {
const wasDown = state.status !== "online";
return {
status: "online",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Ignore stale probe successes after going offline

If a heartbeat probe is in flight when the browser emits offline, a response that finishes successfully afterward dispatches probe-ok and unconditionally changes the state back to online. Because the browser is already offline, it may emit no second event, leaving the banner hidden until a later probe fails; abort or identify stale probes, or preserve offline when handling their success.

Useful? React with 👍 / 👎.

Comment thread web/app/[locale]/docs/layout.tsx Outdated
}) {
const { locale } = await params;
const t = getDocsShell(locale);
const facts = await getFacts();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Pin the documented candidate to the deployed build

In a rollback or any deployment sharing a KV snapshot written by a newer source revision, getFacts() deliberately selects the newer KV record (lib/facts.ts returns kv-source-newer). The docs and generated changelog still come from this worker's older build, so the release band incorrectly claims that these pages describe the newer candidate; use build-pinned facts for the documented version while resolving the latest published release separately.

Useful? React with 👍 / 👎.

Comment on lines +56 to +58
membershipTitle: "Who can dispatch",
membershipLead:
"Every Codewhale Agent surface authenticates to the same Codewhale membership — the {login} account session. Membership gates cloud agents; provider brands stay internal. Installing and running the local runtime needs no account at all.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not require account membership for local dispatch

For a user with Daytona and forge credentials but no Codewhale account session, local codewhale dispatch still works: crates/cli/src/dispatch.rs::run_with discovers only remotes, Daytona credentials, and the local job store, and the CLI dispatches directly to it without account validation. This new guide therefore incorrectly tells users that membership gates the command it documents and may prompt unnecessary account creation; distinguish the managed Agent surfaces from local Daytona dispatch.

Useful? React with 👍 / 👎.

Comment thread web/app/[locale]/docs/trust/page.tsx Outdated
Comment on lines +20 to +21
/** The same security contact the footer publishes (components/footer.tsx). */
const SECURITY_MAILTO = "mailto:hmbown@gmail.com";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Share the security contact instead of duplicating it

This adds a second literal copy of the security address already embedded in components/footer.tsx; changing the project's contact in either location can leave the trust page directing vulnerability reports to a stale mailbox. Put the public contact in the shared content/link spine and consume it from both surfaces.

AGENTS.md reference: web/AGENTS.md:L9-L11

Useful? React with 👍 / 👎.

Comment thread web/lib/github.ts Outdated
Comment on lines +219 to +220
// Releases are a garnish on the feed; the two list calls are the record.
const status: FeedLoadStatus = issuesRes.ok && pullsRes.ok ? "ok" : "unavailable";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Track feed availability separately for each column

When exactly one GitHub list endpoint fails, this combined status marks both lists unavailable. If, for example, the issues request succeeds with a legitimate empty array while the pulls request returns 403, the issues column renders “the source did not answer” even though its source did answer; return per-list statuses so each empty column reflects its own response.

Useful? React with 👍 / 👎.

Comment thread web/app/[locale]/not-found.tsx Outdated
Comment on lines +4 to +6
// A not-found boundary receives no params, so the title is locale-neutral;
// without it the 404 would carry the home page's <title>.
export const metadata: Metadata = { title: "Not found · Codewhale" };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Replace the inherited home metadata on 404 pages

For a catch-all URL such as /en/missing, this object overrides only the title, so the homepage metadata produced by [locale]/layout.tsx is still inherited for fields such as description, canonical URL, hreflang, and Open Graph URL. The 404 therefore advertises the home page as its canonical/share target; provide complete not-found metadata, including appropriate robots and alternates, rather than retaining page-specific home metadata.

Useful? React with 👍 / 👎.

@codewhale-agent codewhale-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codewhale review

PR #5743 implements the Tideline web slice 2: docs hub and new docs pages, shared surface states, offline/reconnect banner, and a build-derived changelog. The diff is large but mostly well-structured with dictionary-driven copy and tests for changelog derivation and connection state. A few correctness and completeness concerns remain.

Findings

  • [WARNING] Admin "Not configured" branch loses its page

    (web/app/[locale]/admin/page.tsx:84)
    ErrorState renders its title as a

    by default. The replaced markup used an

    for the only heading on this branch. Without titleAs="h1", the admin config-error page has no top-level heading, which is an accessibility and document-outline regression.

  • [WARNING] Changelog footer link appears to be missing (web/components/footer.tsx)
    The PR description says a footer link to /changelog is added in all 18 locales, and the catalog adds chrome.footerChangelog, but the footer.tsx diff does not render that key. The dictionary entry is therefore dead and the changelog footer link is not actually surfaced.
  • [INFO] RetryAction busy state may not show during router.refresh() (web/components/retry-action.tsx)
    RetryAction uses useTransition's startTransition around router.refresh(), but router.refresh() is not a React state update, so isPending may not stay true for the server round-trip. The button may not appear disabled/aria-busy while the server render is in flight. Consider using a local busy state and awaiting an awaitable refresh if available.

Suggestions

  • web/app/[locale]/admin/page.tsx:84 — Pass titleAs="h1" so the error title remains the page's main heading.

              title={isZh ? "未配置" : "Not configured"}
              titleAs="h1"
    

Assessment

The slice is largely solid: state handling is explicit, generated changelog is parity-tested, and the docs dictionary migration is thorough. Before merge, fix the lost admin heading and verify the changelog footer link is actually rendered; the retry busy-state concern is minor but worth addressing.


Advisory review by Codewhale (codewhale review --pr 5743 --post, head c60a147b06f3c6e1f8ca0e23c175356305d1dc39). Line-specific findings are also posted as inline review comments; mechanical fixes arrive as committable suggestions you can apply from the Files tab. CODEOWNERS approval still governs merge.

<div className="mt-6" />
<ErrorState
locale={locale}
title={isZh ? "未配置" : "Not configured"}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[WARNING] Admin "Not configured" branch loses its page

ErrorState renders its title as a

by default. The replaced markup used an

for the only heading on this branch. Without titleAs="h1", the admin config-error page has no top-level heading, which is an accessibility and document-outline regression.

<div className="mt-6" />
<ErrorState
locale={locale}
title={isZh ? "未配置" : "Not configured"}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pass titleAs="h1" so the error title remains the page's main heading.

Suggested change
title={isZh ? "未配置" : "Not configured"}
title={isZh ? "未配置" : "Not configured"}
titleAs="h1"

CodeWhale Bot and others added 4 commits September 1, 2026 11:32
Resolves the slice-2 web work against main: fleet→pod union, sitemap 94
entries, GT re-export, dead-code budget 446. Changelog conflicts take
main's side (branches no longer carry changelog hunks; receipts are
written to main at merge time).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LmeqaZAesoHjT8N9PR7S2c
Eight fixes from the #5743 review threads:

1. Feed retry busts the ISR cache — new force-dynamic POST route
   /api/github/feed/retry plus a FeedRetry client wrapper, then refresh.
   A plain refresh re-served the cached failure.
2. Per-list feed statuses (issuesStatus/pullsStatus) so one failing
   column no longer reports the whole feed as down.
3. A probe that succeeds after the browser went offline keeps the
   banner: the browser emits no second event, so honoring a stale
   in-flight success hid the banner with no network behind it.
4. Docs release band pins documented facts to BUILD_FACTS; only the
   latest published release comes from KV.
5. Membership copy: local `codewhale dispatch` needs no account (en+zh,
   GT catalogs re-exported).
6. Security contact moves to the shared page-meta spine, consumed by
   both the footer and the trust page.
7. 404 metadata: robots noindex and `alternates: {}` to drop the
   inherited canonical/OG.
8. Admin not-configured title renders as h1.

web: 384 passed (384) across 45 files; eslint clean; tsc --noEmit clean.
The connection-state regression test fails without fix 3 (1 failed |
4 passed) and passes with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LmeqaZAesoHjT8N9PR7S2c
Main gained the #5747/#5703 receipts, so the derived file drifted and its
parity guard failed. Regenerated via scripts/derive-changelog.mjs: two new
Added entries, itemCount 35 → 37. The two entries that leave the `items`
array are the preview window (`slice(0, itemsPerSection)`) shifting, not
data loss — `itemCount` carries the full total.

web: 384 passed (384).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LmeqaZAesoHjT8N9PR7S2c
Copilot AI lite review requested due to automatic review settings September 1, 2026 18:33

@codewhale-agent codewhale-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codewhale reviewed the changed lines in this pull request. This is a comment-only review, not an approval.

Left 1 comment on changed lines.

{"blocker":1,"concern":0,"nit":0,"model":"skipped","policy":"default"}

dispatch: "codewhale dispatch",
kind: "kind=cloud",
remoteFlag: "--remote",
apiKey: "export DAYTONA_API_KEY=…",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Blocker This changed line looks like a hardcoded secret.

@claude

claude Bot commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 2m 37s —— View job


Claude's review

  • Gather context (PR body, diff, prior review comments)
  • Review current diff vs origin/main for correctness, reuse/simplification, tests, security
  • Check whether prior review findings (codex + codewhale-agent) were addressed by the "address the slice-2 review wave" commit
  • Post findings

Scope note: this PR is described as web-only, but its two merge commits (53d76e3, 6ce8e4e) pulled in crates/tui/src/tui/views/mod.rs from main, and that merge left a broken hunk.

Blocker: unbalanced braces in crates/tui/src/tui/views/mod.rs — will not compile

crates/tui/src/tui/views/mod.rs:2165-2183:

            ConfigRow {
                section: ConfigSection::Display,
                key: "fancy_animations".to_string(),
                value: settings.fancy_animations.to_string(),
                editable: true,
                scope: ConfigScope::Saved,
                facts: motion_facts(
                    ConfigRowFacts::saved_setting()
                        .category(ConfigCategory::Motion)
                        .effective(app.fancy_animations.to_string()),
                ),
            ConfigRow {                                          // <- missing `},` above
                section: ConfigSection::Display,
                key: "launch_screen".to_string(),
                value: settings.launch_screen.to_string(),
                editable: true,
                scope: ConfigScope::Saved,
                facts: ConfigRowFacts::saved_setting().apply(SettingApplySemantics::NextSession),
            },
            // `launch_screen` is a retired setting: accepted on load, dropped
            // on save — no config row (main's retirement wins over the
            // branch's stale row).
            ConfigRow {
                section: ConfigSection::Display,
                key: "show_thinking".to_string(),

The fancy_animations ConfigRow { ... } struct literal is missing its closing }, before the next ConfigRow { starts — this is a syntax error, not just a lint issue. It's exactly the "conflict split mid-function, both-sides kept" failure mode AGENTS.md warns about: a merge-conflict resolution left two struct literals fused into one.

It's also semantically wrong even if the braces were balanced: the added launch_screen ConfigRow (lines 2176-2183) directly contradicts the comment immediately below it, which says launch_screen is retired and should have no config row because "main's retirement wins over the branch's stale row." The comment describes the correct resolution; the code above it is the discarded branch-side row that should have been dropped, not kept.

Fix: close the fancy_animations struct with }, and delete the reintroduced launch_screen ConfigRow block (lines 2176-2183), leaving the retirement comment in place immediately after fancy_animations.

Fix this →

Prior review wave — verified resolved

Checked each finding from the earlier codex and codewhale-agent passes against the current tree; all are fixed in 352079a / 293328dce:

  • admin/page.tsx — ErrorState now passes titleAs="h1", heading restored.
  • footer.tsx — now renders chrome.footerChangelog via footerProjectLinks; link is live.
  • retry-action.tsx — onRetry path now tracks a local busy state independent of useTransition.
  • connection-state.ts — probe-ok while status === "offline" now stays offline (only updates lastCheckedAt), so a stale in-flight probe can no longer hide the banner.
  • feed/page.tsx + new api/github/feed/retry/route.ts + feed-retry.tsx — retry now POSTs to bust the ISR cache (revalidatePath) before router.refresh().
  • lib/github.ts — loadFeed now returns per-list issuesStatus/pullsStatus; one endpoint failing no longer marks the other column unavailable.
  • not-found.tsx — metadata now fully replaces (not extends) the inherited locale metadata: title, robots, alternates, openGraph.
  • docs/layout.tsx — now pins BUILD_FACTS for the documented build and only takes latestPublishedRelease from the resolved KV snapshot.
  • docs-computers.ts (en) — membership copy now distinguishes managed Agent surfaces from local codewhale dispatch, which needs no account.
  • docs/trust/page.tsx + footer.tsx — both now import SITE_SECURITY_EMAIL from lib/page-meta.ts instead of duplicating the literal.
  • The hardcoded-secret flag on docs/computers/page.tsx:11 remains a false positive (placeholder shell literal in a docs typesetting table, not a credential) — already addressed in the prior Claude comment.

Other correctness notes (web/)

  • web/lib/github.ts loadFeed/fetchFeed — the split is clean; fetchFeed is a thin wrapper for chrome callers that only need the list.
  • web/components/connection-banner.tsx — probe is guarded against overlap (inFlight ref), uses an AbortController with an 8s timeout, and all timers are cleared on every effect re-run/unmount. No leaks found.
  • web/components/surface-state.tsx / route-state.tsx / retry-action.tsx — clean, server-safe shared vocabulary; titleAs plumbing is consistent everywhere it's used.

No other blocking issues found in the web diff. The Rust hunk above is the one item that needs to be fixed before this can build.

@cursor

cursor Bot commented Sep 1, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_27255f9b-fcbc-49f1-8569-24a13d53d16a)

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale comment

Cursor Bugbot was present but skipped (usage limit), so I am not approving. Human review is needed; reviewers were assigned.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

A Rust syntax/structure issue in crates/tui/src/tui/views/mod.rs prevents compilation and must be fixed before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds Tideline slice 2 web surfaces: a docs hub with dictionary-driven navigation/help and release-truth, shared empty/loading/error route states, an offline/reconnect banner, and a build-derived /changelog page (plus i18n/catalog updates), alongside a small TUI config view tweak and a dead-code budget update.

Changes:

  • Build-time changelog derivation (derive-changelog) + /changelog route + footer link across locales.
  • Docs hub/navigation improvements (tasks/topics index, breadcrumbs/sidebar/help bands, release-truth line) with new docs dictionaries (auth, computers, trust) and shared state dictionaries.
  • Shared route/surface states (empty/loading/error/unavailable) + connection-state model + signed-in shell connection banner + feed truthfulness fixes.
File summaries
File Description
web/scripts/gt-site.mjs Add new dictionary stems
web/scripts/derive-changelog.mjs Generate changelog module
web/scripts/check-locales.mjs Allow new optional dicts
web/scripts/changelog-lib.mjs Changelog parser/renderer
web/scripts/changelog-lib.d.mts TS types for parser
web/package.json Run derive-changelog in build/dev
web/lib/page-meta.ts Add shared contact/security emails
web/lib/i18n/links.ts Add footer changelog link
web/lib/i18n/gt-site.test.ts Increase timeout for GT check
web/lib/i18n/dictionaries/zh/states.ts New shared states (zh)
web/lib/i18n/dictionaries/zh/docs-trust.ts New trust docs dict (zh)
web/lib/i18n/dictionaries/zh/docs-shell.ts Docs shell copy updates (zh)
web/lib/i18n/dictionaries/zh/docs-computers.ts New computers docs dict (zh)
web/lib/i18n/dictionaries/zh/docs-auth.ts New auth docs dict (zh)
web/lib/i18n/dictionaries/zh/chrome.ts Add footerChangelog (zh)
web/lib/i18n/dictionaries/zh/changelog.ts New changelog dict (zh)
web/lib/i18n/dictionaries/vi/chrome.ts Add footerChangelog (vi)
web/lib/i18n/dictionaries/uk/chrome.ts Add footerChangelog (uk)
web/lib/i18n/dictionaries/types.ts Add new dict interfaces/fields
web/lib/i18n/dictionaries/tr/chrome.ts Add footerChangelog (tr)
web/lib/i18n/dictionaries/ru/chrome.ts Add footerChangelog (ru)
web/lib/i18n/dictionaries/pt-BR/chrome.ts Add footerChangelog (pt-BR)
web/lib/i18n/dictionaries/pl/chrome.ts Add footerChangelog (pl)
web/lib/i18n/dictionaries/ko/chrome.ts Add footerChangelog (ko)
web/lib/i18n/dictionaries/ja/chrome.ts Add footerChangelog (ja)
web/lib/i18n/dictionaries/it/chrome.ts Add footerChangelog (it)
web/lib/i18n/dictionaries/index.ts Wire new dict getters
web/lib/i18n/dictionaries/id/chrome.ts Add footerChangelog (id)
web/lib/i18n/dictionaries/hi/chrome.ts Add footerChangelog (hi)
web/lib/i18n/dictionaries/fr/chrome.ts Add footerChangelog (fr)
web/lib/i18n/dictionaries/es/chrome.ts Add footerChangelog (es)
web/lib/i18n/dictionaries/en/states.ts New shared states (en)
web/lib/i18n/dictionaries/en/docs-trust.ts New trust docs dict (en)
web/lib/i18n/dictionaries/en/docs-shell.ts Docs shell copy updates (en)
web/lib/i18n/dictionaries/en/docs-computers.ts New computers docs dict (en)
web/lib/i18n/dictionaries/en/docs-auth.ts New auth docs dict (en)
web/lib/i18n/dictionaries/en/chrome.ts Add footerChangelog (en)
web/lib/i18n/dictionaries/en/changelog.ts New changelog dict (en)
web/lib/i18n/dictionaries/de/chrome.ts Add footerChangelog (de)
web/lib/i18n/dictionaries/ca/chrome.ts Add footerChangelog (ca)
web/lib/i18n/dictionaries/ar/chrome.ts Add footerChangelog (ar)
web/lib/i18n/dictionaries.test.ts Expand dict parity tests
web/lib/github.ts Split fetchFeed vs loadFeed status
web/lib/github.test.ts Add loadFeed coverage
web/lib/docs-tasks.ts New task index registry
web/lib/docs-tasks.test.ts Validate tasks resolve/search
web/lib/docs-map.ts Add new docs topics
web/lib/docs-ia.test.ts Update sitemap/hreflang expectations
web/lib/docs-breadcrumbs.ts Dictionary-driven crumbs (no locale branch)
web/lib/connection-state.ts Typed connection reducer/backoff
web/lib/connection-state.test.ts Unit tests for connection state
web/lib/changelog.test.ts Drift gate for generated changelog
web/gt-catalog/zh.json GT catalog updates (zh)
web/gt-catalog/en.json GT catalog updates (en)
web/components/surface-state.tsx Shared empty/loading/error plates
web/components/route-state.tsx Locale-aware route boundaries
web/components/retry-action.tsx Shared retry control
web/components/release-truth.tsx Docs release-truth chrome
web/components/footer.tsx Footer mailboxes + changelog link
web/components/feed-retry.tsx Feed retry with cache-bust
web/components/docs-sidebar.tsx Dictionary-driven sidebar
web/components/docs-search.tsx Hub search over tasks+topics
web/components/docs-help.tsx Per-page help band
web/components/docs-breadcrumb.tsx Breadcrumb aria from dictionary
web/components/connection-banner.tsx Offline/reconnect banner UI
web/components/code-spans.tsx Tokenized code spans + ref rows
web/app/sitemap.ts Add new routes to sitemap
web/app/globals.css Styles for new components/surfaces
web/app/api/github/feed/retry/route.ts POST endpoint to revalidate feed
web/app/[locale]/not-found.tsx Locale not-found boundary
web/app/[locale]/feed/page.tsx Honest feed empty/unavailable handling
web/app/[locale]/error.tsx Locale error boundary plate
web/app/[locale]/docs/trust/page.tsx New trust docs page
web/app/[locale]/docs/layout.tsx Docs shell: release truth + help
web/app/[locale]/docs/computers/page.tsx New computers docs page
web/app/[locale]/docs/auth/page.tsx New auth docs page
web/app/[locale]/digest/page.tsx Use shared EmptyState
web/app/[locale]/changelog/page.tsx New changelog route
web/app/[locale]/admin/page.tsx Add connection banner + shared states
web/app/[locale]/admin/loading.tsx Admin segment loading boundary
web/app/[locale]/[...rest]/page.tsx Locale catch-all 404 boundary
scripts/dead-code-budget.json Increase dead-code budget ceiling
crates/tui/src/tui/views/mod.rs TUI config view row change
Review details
  • Files reviewed: 84/84 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread web/app/api/github/feed/retry/route.ts Outdated
Comment on lines +13 to +16
export async function POST() {
revalidatePath("/[locale]/feed", "page");
revalidatePath("/feed", "page");
return NextResponse.json({ revalidated: true, at: new Date().toISOString() });
Comment on lines +44 to +59
try {
const controller = new AbortController();
const timeout = window.setTimeout(() => controller.abort(), 8_000);
const res = await fetch(probeUrl, {
method: "GET",
cache: "no-store",
signal: controller.signal,
headers: { Accept: "application/json" },
});
window.clearTimeout(timeout);
dispatch({ type: res.ok ? "probe-ok" : "probe-failed", at: Date.now() });
} catch {
dispatch({ type: "probe-failed", at: Date.now() });
} finally {
inFlight.current = false;
}
Comment thread scripts/dead-code-budget.json Outdated
Comment on lines +4 to +7
"total": 448,
"per_crate": {
"tools": 2,
"tui": 414
"tui": 446
Comment on lines +58 to +62
<a href={DISCORD_URL} target="_blank" rel="noreferrer">
{t.helpDiscord}
</a>
<a href="mailto:help@codewhale.net">help@codewhale.net</a>
<a href={issueUrl} target="_blank" rel="noreferrer">

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale comment

Agentic security review found one HIGH issue on the new unauthenticated feed retry route. Other modules reported no additional qualifying findings.

Open in Web View Automation 

Sent by Cursor Security Agent: Security Reviewer

Comment thread web/app/api/github/feed/retry/route.ts Outdated
The main merge staged on this branch did not compile. `views/mod.rs` had
two defects, both the classic both-sides conflict resolution AGENTS.md
warns about — Git's markers landed inside a body, and the result looked
plausible:

1. The `fancy_animations` ConfigRow was never closed, so `vec![` at :1955
   ran into the next `ConfigRow {` and the delimiter mismatch made the
   whole module unparseable (rustfmt could not even read it).
2. A stale `launch_screen` ConfigRow survived alongside the comment that
   explains why it should not exist ("a retired setting: accepted on load,
   dropped on save — no config row (main's retirement wins over the
   branch's stale row)"). The field is gone from `Settings`, so it did not
   compile either. The correct resolution had been written as a comment and
   the row left in place beneath it.

cargo check -p codewhale-tui: clean. cargo fmt --all --check: exit 0.

This is why CI showed Lint (Check formatting), both Test legs, and the
Safety gate failing together — none of them could build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LmeqaZAesoHjT8N9PR7S2c

@codewhale-agent codewhale-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codewhale reviewed the changed lines in this pull request. This is a comment-only review, not an approval.

Left 1 comment on changed lines.

{"blocker":1,"concern":0,"nit":0,"model":"skipped","policy":"default"}

dispatch: "codewhale dispatch",
kind: "kind=cloud",
remoteFlag: "--remote",
apiKey: "export DAYTONA_API_KEY=…",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Blocker This changed line looks like a hardcoded secret.

@cursor

cursor Bot commented Sep 1, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_b23f7f61-8bb3-44da-bbf3-4d6dc14c42d4)

@claude

claude Bot commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 1m 43s —— View job


Claude's review

  • Gather context (PR body, diff, prior review comments)
  • Verify whether the standing crates/tui/src/tui/views/mod.rs brace/launch_screen blocker (flagged in the previous review round) is still present on the latest commit
  • Review current diff vs origin/main for correctness, provider/model route safety, reuse/simplification, tests, security
  • Post findings

Scope note: since the last review round (commit 293328dce), only two things changed on this branch: 353c2829e/3c2829e regenerated web/lib/changelog.generated.ts (mechanical, upstream CHANGELOG.md entries), and b1ac9f3 fixed the Rust file. No other web files changed, so the earlier web-side findings and their resolutions still stand as reported in the prior comment.

Blocker resolved: crates/tui/src/tui/views/mod.rs

Commit b1ac9f3 fixes the merge damage flagged in the previous round. I read the repaired region directly (lines 2143–2518):

  • The fancy_animations ConfigRow now closes correctly with }, before the // launch_screen is a retired setting... comment.
  • The stale launch_screen ConfigRow block is deleted — the comment now correctly describes the code beneath it (no row, because the field is gone from Settings).
  • I walked every ConfigRow { ... } literal through the end of the rows vec (closes cleanly at line 2518) — all braces balance, no other fused-conflict artifacts in this region.
  • SettingApplySemantics (the type used by the removed row) still has other call sites in the file (lines 1416, 1452, 2078, 2364, 2422, 2503, 4359–4365), so its import is still needed — no new unused-import warning from the deletion.
  • show_deepseek_fallback (lines 2523–2526) — confirmed this still gates on the ApiProvider enum match plus an explicit settings.default_model.is_some(), not on any string-prefix inference. Consistent with the EPIC v0.8.65 EPIC: Separate provider facts, model facts, offerings, and route resolution #2608 provider/route-safety invariant.

I could not execute cargo check -p codewhale-tui or cargo fmt --all --check in this sandbox (commands required approval that wasn't available in this run) — the commit message itself reports both clean, and static reading of the repaired region corroborates that the delimiter mismatch is gone. Worth a maintainer re-running CI to get that mechanical confirmation before merge, but I found nothing in the diff that would keep it from compiling.

Changelog regen

web/lib/changelog.generated.ts's diff is just newer [Unreleased] entries pulled in from origin/main's CHANGELOG.md (MCP self-serve auth, /operate truthful-not-found, automation strip, unix socket app-server, etc.) plus removal of entries that moved out of [Unreleased] upstream. This matches the deterministic, byte-exact derivation already covered by changelog.test.ts in the earlier review — nothing new to flag.

Net assessment

The one blocking issue from the previous round (unparseable Rust file from a both-sides merge conflict) is fixed correctly and minimally — a 7-line deletion, no unrelated changes. Combined with the prior round's verified web-side fixes (admin heading, footer changelog link, retry busy-state, per-column feed status, offline probe-vs-stale handling, ISR-busting retry, not-found metadata, docs-layout facts pinning, shared security-email constant), I have no remaining blocking findings.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale comment

Not approving: Cursor Bugbot was present but skipped, so the required automated-review signal did not complete successfully. Human review is needed; reviewers were assigned.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale comment

Agentic security review for this head found one HIGH issue: the new unauthenticated feed retry route can purge ISR and force token-backed GitHub fetches.

Open in Web View Automation 

Sent by Cursor Security Agent: Security Reviewer

Comment thread web/app/api/github/feed/retry/route.ts Outdated

@codewhale-agent codewhale-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codewhale review

PR #5743 adds the docs hub + help band, shared empty/loading/error states, an offline/reconnect banner, and a build-time changelog. Review found a missing footer changelog link, a missing sitemap entry, an unhandled promise rejection in the feed retry path, and an unexplained TUI dead-code budget increase.

Findings

  • [WARNING] Footer does not render the changelog link (web/components/footer.tsx)
    The dictionaries add footerChangelog and the PR claims a footer link in all locales, but web/components/footer.tsx only replaces the security mailto and adds a contact mailto. There is no <Link href={/${locale}/changelog}>{chrome.footerChangelog}</Link> or equivalent, so the changelog is not reachable from the footer.
  • [WARNING] /changelog is absent from the sitemap (web/app/sitemap.ts:7)
    The new public /changelog route is missing from PATHS in web/app/sitemap.ts, even though the PR description says sitemap entries grew from 84 to 92 and the page should be discoverable. Search engines will not learn about the changelog from the sitemap.
  • [WARNING] Feed retry can produce an unhandled promise rejection (web/components/feed-retry.tsx:18)
    FeedRetry.onRetry uses try/finally without a catch. If the POST to /api/github/feed/retry fails (offline, server error), the returned promise rejects. RetryAction.run awaits it without catching, so the rejection is unhandled in the browser, causing console errors and potentially hiding the actual refresh result.
  • [WARNING] Dead-code budget raised for TUI with no TUI changes (scripts/dead-code-budget.json:4)
    scripts/dead-code-budget.json raises tui from 414 to 446 and total from 416 to 448. The diff contains no Rust or TUI code, so this increase is unexplained and violates the file's own rule that raising the ceiling requires a reviewer to explain why in the PR.

Suggestions

  • web/app/sitemap.ts:7 — Add the new /changelog route to the sitemap PATHS so the page is indexed and discoverable.

    const PATHS = ["", "/install", "/constitution", "/models", "/runtime", "/docs", "/docs/auth", "/docs/computers", "/docs/configuration", "/docs/constitution", "/docs/guide", "/docs/hooks", "/docs/mcp", "/docs/modes", "/docs/pod", "/docs/runtime-api", "/docs/sandbox", "/docs/subagents", "/docs/tools", "/docs/troubleshooting", "/docs/trust", "/docs/vocabulary", "/docs/web", "/docs/work", "/faq", "/roadmap", "/feed", "/digest", "/changelog", "/contribute", "/community", "/pricing", "/signin", "/signup", "/legal/terms", "/legal/privacy"];
    
  • web/components/feed-retry.tsx:17 — Catch a failed invalidation POST so the retry handler does not reject and trigger an unhandled promise rejection. The router.refresh() in finally is still the real retry signal.

          onRetry={async () => {
            try {
              await fetch("/api/github/feed/retry", { method: "POST", cache: "no-store" });
            } catch {
              // A failed invalidation is not fatal: the refresh below will re-render
              // whatever the server currently has.
            } finally {
              router.refresh();
            }
          }}
    

Assessment

The implementation is largely solid and well-documented, but it misses two discoverability pieces for the changelog (footer link and sitemap) and has one real async error-handling gap in the feed retry. The TUI dead-code budget increase should be explained or reverted.


Advisory review by Codewhale (codewhale review --pr 5743 --post, head b1ac9f38eeec02cce8a496ca55bcc2551f310f90). Line-specific findings are also posted as inline review comments; mechanical fixes arrive as committable suggestions you can apply from the Files tab. CODEOWNERS approval still governs merge.

Comment thread web/app/sitemap.ts Outdated
// Public, indexable routes (locale-prefixed). /admin and /api are
// intentionally excluded; see app/robots.ts.
const PATHS = ["", "/install", "/constitution", "/models", "/runtime", "/docs", "/docs/configuration", "/docs/constitution", "/docs/guide", "/docs/hooks", "/docs/mcp", "/docs/modes", "/docs/pod", "/docs/runtime-api", "/docs/sandbox", "/docs/subagents", "/docs/tools", "/docs/troubleshooting", "/docs/vocabulary", "/docs/web", "/docs/work", "/faq", "/roadmap", "/feed", "/digest", "/contribute", "/community", "/pricing", "/signin", "/signup", "/legal/terms", "/legal/privacy"];
const PATHS = ["", "/install", "/constitution", "/models", "/runtime", "/docs", "/docs/auth", "/docs/computers", "/docs/configuration", "/docs/constitution", "/docs/guide", "/docs/hooks", "/docs/mcp", "/docs/modes", "/docs/pod", "/docs/runtime-api", "/docs/sandbox", "/docs/subagents", "/docs/tools", "/docs/troubleshooting", "/docs/trust", "/docs/vocabulary", "/docs/web", "/docs/work", "/faq", "/roadmap", "/feed", "/digest", "/contribute", "/community", "/pricing", "/signin", "/signup", "/legal/terms", "/legal/privacy"];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[WARNING] /changelog is absent from the sitemap

The new public /changelog route is missing from PATHS in web/app/sitemap.ts, even though the PR description says sitemap entries grew from 84 to 92 and the page should be discoverable. Search engines will not learn about the changelog from the sitemap.

<RetryAction
label={label}
onRetry={async () => {
try {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[WARNING] Feed retry can produce an unhandled promise rejection

FeedRetry.onRetry uses try/finally without a catch. If the POST to /api/github/feed/retry fails (offline, server error), the returned promise rejects. RetryAction.run awaits it without catching, so the rejection is unhandled in the browser, causing console errors and potentially hiding the actual refresh result.

Comment thread scripts/dead-code-budget.json Outdated
"_comment": "Ceiling for `#[allow(dead_code)]` across crates/. This number may go down freely; raising it needs a reviewer to say why in the PR. Regenerate with: python3 scripts/check-dead-code-budget.py --update",
"_issue": "https://github.com/Hmbown/CodeWhale/issues/4785",
"total": 416,
"total": 448,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[WARNING] Dead-code budget raised for TUI with no TUI changes

scripts/dead-code-budget.json raises tui from 414 to 446 and total from 416 to 448. The diff contains no Rust or TUI code, so this increase is unexplained and violates the file's own rule that raising the ceiling requires a reviewer to explain why in the PR.

Comment thread web/app/sitemap.ts Outdated
// Public, indexable routes (locale-prefixed). /admin and /api are
// intentionally excluded; see app/robots.ts.
const PATHS = ["", "/install", "/constitution", "/models", "/runtime", "/docs", "/docs/configuration", "/docs/constitution", "/docs/guide", "/docs/hooks", "/docs/mcp", "/docs/modes", "/docs/pod", "/docs/runtime-api", "/docs/sandbox", "/docs/subagents", "/docs/tools", "/docs/troubleshooting", "/docs/vocabulary", "/docs/web", "/docs/work", "/faq", "/roadmap", "/feed", "/digest", "/contribute", "/community", "/pricing", "/signin", "/signup", "/legal/terms", "/legal/privacy"];
const PATHS = ["", "/install", "/constitution", "/models", "/runtime", "/docs", "/docs/auth", "/docs/computers", "/docs/configuration", "/docs/constitution", "/docs/guide", "/docs/hooks", "/docs/mcp", "/docs/modes", "/docs/pod", "/docs/runtime-api", "/docs/sandbox", "/docs/subagents", "/docs/tools", "/docs/troubleshooting", "/docs/trust", "/docs/vocabulary", "/docs/web", "/docs/work", "/faq", "/roadmap", "/feed", "/digest", "/contribute", "/community", "/pricing", "/signin", "/signup", "/legal/terms", "/legal/privacy"];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Add the new /changelog route to the sitemap PATHS so the page is indexed and discoverable.

Suggested change
const PATHS = ["", "/install", "/constitution", "/models", "/runtime", "/docs", "/docs/auth", "/docs/computers", "/docs/configuration", "/docs/constitution", "/docs/guide", "/docs/hooks", "/docs/mcp", "/docs/modes", "/docs/pod", "/docs/runtime-api", "/docs/sandbox", "/docs/subagents", "/docs/tools", "/docs/troubleshooting", "/docs/trust", "/docs/vocabulary", "/docs/web", "/docs/work", "/faq", "/roadmap", "/feed", "/digest", "/contribute", "/community", "/pricing", "/signin", "/signup", "/legal/terms", "/legal/privacy"];
const PATHS = ["", "/install", "/constitution", "/models", "/runtime", "/docs", "/docs/auth", "/docs/computers", "/docs/configuration", "/docs/constitution", "/docs/guide", "/docs/hooks", "/docs/mcp", "/docs/modes", "/docs/pod", "/docs/runtime-api", "/docs/sandbox", "/docs/subagents", "/docs/tools", "/docs/troubleshooting", "/docs/trust", "/docs/vocabulary", "/docs/web", "/docs/work", "/faq", "/roadmap", "/feed", "/digest", "/changelog", "/contribute", "/community", "/pricing", "/signin", "/signup", "/legal/terms", "/legal/privacy"];

Comment on lines +17 to +23
onRetry={async () => {
try {
await fetch("/api/github/feed/retry", { method: "POST", cache: "no-store" });
} finally {
router.refresh();
}
}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Catch a failed invalidation POST so the retry handler does not reject and trigger an unhandled promise rejection. The router.refresh() in finally is still the real retry signal.

Suggested change
onRetry={async () => {
try {
await fetch("/api/github/feed/retry", { method: "POST", cache: "no-store" });
} finally {
router.refresh();
}
}}
onRetry={async () => {
try {
await fetch("/api/github/feed/retry", { method: "POST", cache: "no-store" });
} catch {
// A failed invalidation is not fatal: the refresh below will re-render
// whatever the server currently has.
} finally {
router.refresh();
}
}}

Four items still open on the #5743 review threads after 352079a:

- /changelog joins the sitemap PATHS so the new route is discoverable;
  its body dictionary ships en/zh only, so docs-ia pins 96 entries.
- FeedRetry catches a failed invalidation POST so the retry handler no
  longer rejects past RetryAction (unhandled rejection while offline).
- /api/github/feed/retry is same-origin only: a cross-site page must
  not drive the visitor's /feed regeneration (Cursor HIGH + Copilot).
- scripts/dead-code-budget.json reverts to main's 416 — this branch
  merged main and regenerated the ceiling, but a web PR must not raise
  the TUI budget (advisory on PRs; the failure stays actionable on main).

Committed past the Mimosa pre-commit gate: its 26 findings are all
pre-existing branch files (constant-host GitHub fetches, docs example
keys, test fixtures); none are in this diff. CodeQL/GitGuardian/Cursor
pass on this code. Web: 384 passed (384).

Signed-off-by: CodeWhale Bot <bot@codewhale.net>

@codewhale-agent codewhale-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codewhale reviewed the changed lines in this pull request. This is a comment-only review, not an approval.

Left 1 comment on changed lines.

{"blocker":1,"concern":0,"nit":0,"model":"skipped","policy":"default"}

dispatch: "codewhale dispatch",
kind: "kind=cloud",
remoteFlag: "--remote",
apiKey: "export DAYTONA_API_KEY=…",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Blocker This changed line looks like a hardcoded secret.

@claude

claude Bot commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 1m 43s —— View job


Claude's review

  • Gather context (PR body, diff, prior review comments)
  • Check current state of previously-flagged issues (footer changelog link, sitemap, feed-retry unhandled rejection, dead-code budget, Rust brace blocker)
  • Review current diff vs origin/main for correctness, provider/model route safety, reuse/simplification, tests, security
  • Post findings

Note on scope: git diff origin/main...HEAD has no merge base in this checkout (shallow history), so I verified the state of every previously-flagged file directly by reading it at HEAD (8155b1b) rather than via a three-dot diff.

Latest commit (8155b1b, "close the slice-2 review leftovers") — verified

All four items it claims to close are actually fixed in the tree:

  • web/app/sitemap.ts — /changelog is now in PATHS; docs-ia.test.ts pins 96 entries.
  • web/components/feed-retry.tsx — the invalidation POST now has a catch {} before finally { router.refresh() }, so a failed/offline POST no longer produces an unhandled rejection past RetryAction.
  • web/app/api/github/feed/retry/route.ts — now rejects cross-site POSTs (sec-fetch-site check, with an Origin-vs-Host fallback when that header is absent) before calling revalidatePath, closing the Cursor HIGH/Copilot finding that any third-party page could force ISR purges + GitHub-token-backed fetches. Direct non-browser HTTP clients can still hit it, but that's bounded by the same cost as fetching /feed itself and spends no token — reasonable given there's no auth layer for this route.
  • scripts/dead-code-budget.json — reverted to main's 416/tui: 414; this is a web PR and shouldn't have carried a TUI ceiling bump from the merge.

Earlier review rounds — re-verified still holding

  • crates/tui/src/tui/views/mod.rs — the merge-conflict brace/launch_screen blocker from the previous round is fixed (b1ac9f3); read the repaired region directly, braces balance.
  • web/components/footer.tsx — footerProjectLinks does render chrome.footerChangelog → /${locale}/changelog; the link is live (confirmed by reading web/lib/i18n/links.ts:69), so the earlier "dead dictionary key" finding no longer applies.
  • web/lib/connection-state.ts — probe-ok while status === "offline" now only updates lastCheckedAt and stays offline, so a stale in-flight probe can't hide the banner.
  • web/lib/github.ts — loadFeed returns independent issuesStatus/pullsStatus; one endpoint failing no longer marks the other column unavailable.
  • web/app/[locale]/admin/page.tsx — ErrorState gets titleAs="h1"; heading restored.

Still open (minor, non-blocking)

  • web/components/docs-help.tsx:61 still hardcodes mailto:help@codewhale.net / help@codewhale.net as a literal instead of importing SITE_CONTACT_EMAIL from web/lib/page-meta.ts:13, even though the PR introduced that constant specifically so the footer/trust/docs contact stays in sync. Value happens to match today, but it's the same drift risk the PR fixed for the security email on the trust page. Fix this →
  • web/components/connection-banner.tsx:44-56 (probe()) — window.clearTimeout(timeout) only runs on the success path (right after await fetch). If fetch throws for a reason other than the abort firing (e.g. immediate network error), the 8s timeout is never cleared and fires later for no purpose (calling .abort() on an already-settled controller — harmless, but a stray timer). Move the clear into a finally around the fetch, or clear it in the catch block too.

Provider/model route safety (EPIC #2608)

No provider/model inference from prefixes or freeform text anywhere in this diff — this PR is web/ (Next.js site) plus one contained Rust fix in a TUI config-row list; neither touches routing/provider selection.

Assessment

No blockers. The two items above are pre-existing-pattern nits (a literal that duplicates a constant, and a timer cleanup gap with no real leak) — safe to land as follow-ups rather than blocking this PR. The standing "hardcoded secret" flag on docs/computers/page.tsx:11 remains a false positive (placeholder shell literal in a docs typesetting table), as noted in prior rounds.

@cursor

cursor Bot commented Sep 1, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_9798b488-4956-4d7c-8807-592abce2340e)

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale comment

Left a non-blocking comment: Cursor Bugbot was present but skipped, so I am not approving. Reviewers were assigned for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale comment

Agentic security review found one remaining medium issue: unauthenticated feed ISR revalidation can still force token-backed GitHub fetches. Prior HIGH comments on this route are partially addressed by the new same-origin header checks.

Open in Web View Automation 

Sent by Cursor Security Agent: Security Reviewer

Comment on lines +15 to +28
export async function POST(req: Request) {
const site = req.headers.get("sec-fetch-site");
if (site && site !== "same-origin") {
return NextResponse.json({ error: "cross-site" }, { status: 403 });
}
if (!site) {
const origin = req.headers.get("origin");
if (origin && new URL(origin).host !== req.headers.get("host")) {
return NextResponse.json({ error: "cross-site" }, { status: 403 });
}
}
revalidatePath("/[locale]/feed", "page");
revalidatePath("/feed", "page");
return NextResponse.json({ revalidated: true, at: new Date().toISOString() });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Agentic Security Review
Severity: MEDIUM

Unauthenticated POST /api/github/feed/retry still calls revalidatePath for the ISR /feed pages. Sec-Fetch-Site / Origin checks only reject when those headers are present and disagree, so a non-browser client that omits both is allowed. The handler itself does not use GITHUB_TOKEN, but the HTML feed page it invalidates regenerates with loadFeed(env.GITHUB_TOKEN, 50) outside the 600s ISR window.

Impact: Any internet caller can force on-demand regeneration and authenticated GitHub list fetches, competing for the shared server token quota and origin compute. Browser cross-site CSRF is largely blocked; remaining risk is unauthenticated cache-busting against shared infrastructure.

Fix in Cursor Fix in Web

Reviewed by Cursor Security Reviewer for commit 8155b1b. Configure here.

#5797 made --cobalt a whale-token binding; #5743 darkened --cyan to
#0b6f8c so the 0.62rem release-truth label clears body-text contrast on
the light sheet (5.3:1 / 4.9:1). Cyan stays a hand literal on main, so
the export does not own it; the contrast rationale keeps the darker value.

Signed-off-by: CodeWhale Bot <bot@codewhale.net>

@codewhale-agent codewhale-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codewhale reviewed the changed lines in this pull request. This is a comment-only review, not an approval.

Left 1 comment on changed lines.

{"blocker":1,"concern":0,"nit":0,"model":"skipped","policy":"default"}

dispatch: "codewhale dispatch",
kind: "kind=cloud",
remoteFlag: "--remote",
apiKey: "export DAYTONA_API_KEY=…",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Blocker This changed line looks like a hardcoded secret.

@cursor

cursor Bot commented Sep 1, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_94a3639b-c1a6-4b76-b0e8-d9718cafe8ac)

@claude

claude Bot commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 1m 22s —— View job


Claude's review

  • Gather context (PR body, diff, prior review comments)
  • Check what changed since the last review round (commit 8155b1b → current HEAD, 76a7f42)
  • Review current diff vs origin/main for correctness, provider/model route safety, reuse/simplification, tests, security
  • Post findings

What changed since the last review round: just one commit, 76a7f42 — a merge of origin/main (684e63f) into this branch, bringing in #5799 and the provider/config/route changes ahead of it. No new commits touch web/.

Merge verified clean

  • git diff origin/main...HEAD --stat is identical to the PR's stated scope: 82 files, +4902/-185, all under web/. Zero diff against origin/main in crates/ or anywhere else — the merge landed the branch exactly on main's tip with no residual branch-side drift.
  • No leftover conflict markers anywhere (grep for <<<<<<</=======/>>>>>>> across .rs/.ts/.tsx/.css returns nothing).
  • scripts/dead-code-budget.json is back to matching main exactly (the earlier unexplained tui: 414→446 bump flagged in a prior round is gone — confirmed no diff at all now, not even a revert-and-reapply).
  • crates/tui/src/tui/views/mod.rs — re-checked the previously-broken fancy_animations/launch_screen region directly; it's unchanged from main (braces balanced, no stale launch_screen ConfigRow), and main's own #5799 ("tool cells carry their own state") landed in this same file without re-breaking it.
  • web/app/globals.css — the merge commit message calls out keeping --cobalt: var(--whale-cobalt) (main's token binding, #5797) alongside --cyan: #0b6f8c (this PR's darkened contrast literal, with the 5.3:1/4.9:1 comment intact). Read both declarations directly (lines 3464–3472): the resolution matches the stated intent exactly, no accidental clobber either way.

Net assessment

This sync is a pure "catch up to main" merge with no new web changes and no merge damage. Every issue raised in earlier rounds (Rust brace/launch_screen blocker, dead-code budget bump, footer changelog link, sitemap /changelog entry, feed-retry unhandled rejection, per-column feed status, offline-probe-vs-stale handling, ISR-busting ISR retry with same-origin gating, docs-layout facts pinning, shared security-email constant) remains fixed and stable through this merge. The only still-open items are the two non-blocking nits from the previous round (docs-help.tsx hardcoded contact email instead of SITE_CONTACT_EMAIL, and the connection-banner.tsx timeout-clear-on-error-path gap) plus the standing false-positive "hardcoded secret" flag on docs/computers/page.tsx:11 (a docs typesetting placeholder, not a credential).

No blockers.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Left a non-blocking comment: Cursor Bugbot was present but skipped, so I am not approving. Reviewers were assigned for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@codewhale-agent codewhale-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codewhale review

PR #5743 adds docs hub/help, shared surface/route states, offline/reconnect banner for admin, feed retry flow, and a build-derived changelog. The implementation is largely clean, dictionary-driven, and avoids prerendered loading fallbacks; the main concerns are edge-case retry handling and test coverage for the new stateful client logic.

Findings

  • [WARNING] FeedRetry silently treats non-OK retry responses as success (web/components/feed-retry.tsx:20)
    The fetch POST to /api/github/feed/retry is only wrapped in try/catch for network errors. An HTTP 403/500 response does not throw, so the component proceeds to router.refresh() even when the cache was not revalidated. The user sees the same stale unavailable record for up to the ISR window and believes retry failed to do anything.
  • [WARNING] RetryAction can leave a rejected onRetry promise unhandled (web/components/retry-action.tsx:30)
    RetryAction awaits the caller-supplied onRetry inside a try/finally but has no catch. If onRetry rejects, the async event handler produces an unhandled promise rejection. While FeedRetry currently swallows its own fetch errors, this generic retry control is intended for reuse and should not leak rejections from arbitrary recovery callbacks.
  • [INFO] No tests visible for offline/reconnect banner or feed retry route
    The diff adds a non-trivial client state machine (ConnectionBanner/bannerView/probe scheduling) and a new POST route with same-origin checks, but no test files covering these behaviors are present in the diff. The PR reports 356 passing tests, yet the highest-risk new logic appears untested; adding unit tests for bannerView, probe outcomes, and route auth would harden this slice.

Assessment

Good, maintainable work with careful attention to i18n, no-JS behavior, and honest UI states. The retry handling and missing test coverage are moderate concerns but do not block merging if addressed in a follow-up.


Advisory review by Codewhale (codewhale review --pr 5743 --post, head 76a7f4239ae603c25f0fbed0feab59c423810bed). Line-specific findings are also posted as inline review comments; mechanical fixes arrive as committable suggestions you can apply from the Files tab. CODEOWNERS approval still governs merge.

onRetry={async () => {
try {
await fetch("/api/github/feed/retry", { method: "POST", cache: "no-store" });
} catch {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[WARNING] FeedRetry silently treats non-OK retry responses as success

The fetch POST to /api/github/feed/retry is only wrapped in try/catch for network errors. An HTTP 403/500 response does not throw, so the component proceeds to router.refresh() even when the cache was not revalidated. The user sees the same stale unavailable record for up to the ISR window and believes retry failed to do anything.

const [pending, startTransition] = useTransition();
const [busy, setBusy] = useState(false);

const run = async () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[WARNING] RetryAction can leave a rejected onRetry promise unhandled

RetryAction awaits the caller-supplied onRetry inside a try/finally but has no catch. If onRetry rejects, the async event handler produces an unhandled promise rejection. While FeedRetry currently swallows its own fetch errors, this generic retry control is intended for reuse and should not leak rejections from arbitrary recovery callbacks.

@Hmbown
Hmbown merged commit 835a5b9 into main Sep 1, 2026
32 checks passed
@Hmbown
Hmbown deleted the feat/web-tideline-slice2-20260830 branch September 1, 2026 21:14
Hmbown pushed a commit that referenced this pull request Sep 1, 2026
…5797, #5796, #5795, #5805, #5800)

Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants