From e0ede3b175c68bc543520efa807eab211e401112 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 00:07:22 +0000 Subject: [PATCH 1/2] D621: the Help Center's Popular this week and Did this answer it?, wired to their stores (#1280 step 2) D603 built the stores and routes; this wires the page and lifts the two bans the contract test held until they existed (pages/docs/HelpSignals.jsx): - Popular this week (H1): the four most-viewed guides of the last 7 days that the reader can open, in the route's order. A guide they cannot open, or one the manifest no longer has, is left out, never a dead card. - Did this answer it? (H2): opening a guide records the view; Yes saves at once, No opens one field for what was missing (the Worker's 500 cap), and an answer reads back and can be changed. - The answers, for an admin, on the home: per-guide yes and no, most "no" first, and each missing text, with no names. The member note says who reads the answers, which this card makes true; the canvas's "whoever wrote the guide" names an author no guide records. Loading draws no figure; a failed read says so and offers Try again. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018jkNYLq29UXAvcqYxGCSB3 --- documentation/architecture/ROUTE_MAP.md | 2 +- documentation/architecture/decisions/D599.md | 2 + documentation/architecture/decisions/D603.md | 2 + documentation/architecture/decisions/D621.md | 147 ++++++++ frontend/src/pages/docs/DocsLayout.jsx | 17 +- frontend/src/pages/docs/HelpSignals.jsx | 337 +++++++++++++++++++ frontend/src/pages/docs/helpCenter.js | 74 ++++ frontend/test/help_center_contract.test.mjs | 13 +- frontend/test/help_signals_d621.test.mjs | 240 +++++++++++++ 9 files changed, 821 insertions(+), 13 deletions(-) create mode 100644 documentation/architecture/decisions/D621.md create mode 100644 frontend/src/pages/docs/HelpSignals.jsx create mode 100644 frontend/test/help_signals_d621.test.mjs diff --git a/documentation/architecture/ROUTE_MAP.md b/documentation/architecture/ROUTE_MAP.md index 99112b81ef..f727fef212 100644 --- a/documentation/architecture/ROUTE_MAP.md +++ b/documentation/architecture/ROUTE_MAP.md @@ -94,7 +94,7 @@ the root" until 2026-09-07; the root of `design/canvases/` holds only its | GP Application Review | admin | `/admin/lp-applications` | `/admin/lp-applications` | `admin_lp_applications.ts` | UPGRADE | The live page names this handoff and ports the split queue/detail layout. Not built: **bulk approve/decline**, **side-by-side comparison** of 2 applications, **Export CSV**, **median-decision-time**, and the "Qualification signals (derived, not a score)" panel. Keep one deliberate correction: the canvas asserts "Approvals grant reporting access immediately" — the page must not, because approval only unblocks LPA issuance and access follows the countersigned LPA. | | Get Paid & Invoicing | partner (+ founder pay side) | `/services` + `/needs` / `/build/marketplace` | those; `/payouts` redirects to `/referrals` | `needs.ts` (needs → RFP → quotes → accept → engagement → `POST /:id/invoice`, Stripe Connect onboard/refresh), `payments.ts`, `orders.ts`, `services/invoiceEmails.ts` | RESKIN | The whole flow exists on the worker; the UI predates the canvas. Stripe Connect is a `
` of "Charges enabled / Payouts enabled"; invoicing is one button opening a hosted `stripe_invoice_url`. Canvas designs a Get-Paid setup screen with three states, an **in-platform invoice document** (masthead, bill-to, engagement reference, line items carried from the accepted quote, VAT, totals), and a 4-state action rail. **CORRECTION + FIX 2026-08-29.** This row described the FastAPI in `backend/`, which is Replit-dev-only and never deployed. On the production worker `POST /engagements/:id/invoice` set `status = 'invoiced'` and `invoice_id = 'stub-'` and nothing else; D1's `engagements` has no `stripe_invoice_url` and no `stripe_invoice_id` column, and `engagementDto` is a bare row passthrough — so all three UI references were dead: two invoice links that could never render and a guard that never hid the button (a second click 409'd). The engagement was left permanently marked **invoiced with no invoice in existence**, which is worse than a missing feature: the record asserted something untrue about money. **Fixed** by migration 188 (`engagement_invoices`, `invoice_number_seq`), `services/engagementInvoices.ts` and an in-platform invoice document in `NeedsBoardPage.jsx` — which is what the canvas asked for and needs no Stripe, so it does not reverse task #138's removal of Stripe Connect. Amounts are integer minor units throughout; the legacy REAL `quotes.price` is converted at exactly one line and snapshotted, so a later quote edit cannot change an issued invoice. Numbers come from a dedicated counter, not from parsing the last number back, and a void invoice keeps its number. **Not built:** payment collection. There is no payment rail; `paid` records that the partner says they were paid out of band, and the document says so to both sides. Still to do from the canvas: the Get-Paid setup screen with its three states. **The row conflated two different Stripe surfaces; they are not the same and only one exists.** (a) WELLBEING EXPERTS have REAL Stripe Connect: `wellbeing.ts` `POST /experts/me/stripe/connect` creates an Express account, requests transfers + card_payments, returns an account-onboarding link and stores `stripe_account_id`, and the `experts` table carries real `stripe_charges_enabled` / `stripe_payouts_enabled` columns — so the Charges/Payouts `
` on `/wellbeing/expert-dashboard` is backed by live data, not a placeholder. The canvas's three-state Get-Paid setup screen IS buildable there, and the three states map onto the real ones (no account / onboarding incomplete / enabled). (b) MARKETPLACE PROVIDERS have none: `needs.ts` `GET /providers/me/stripe` returns `connected: false, detail: 'stripe_connect_not_configured'` and the onboard route 503s with the same reason. Those are HONEST stubs — they name their own absence — which is why they are not on the defect list above; the invoicing path was the one that claimed something untrue. A Get-Paid screen for marketplace providers cannot be built until someone decides whether Connect returns for them at all (task #138 removed the payouts backend deliberately). | | Graduation Certificate | founder | `/spinout-lab/certificate` | `/spinout-lab/certificate`; public verify at `/verify/:token` | `spinout_certificates.ts` | OUT OF SCOPE | Lab credential. Issue, revoke, list and public verification exist (`spinout_certificates.ts`), and the graduate view is built. **Corrected in D380:** reissue has no route, and `credential_id` is UNIQUE and deterministic, so a same-date reissue would collide; the profile badge has tables (`user_badges`, `assessment_badges`) but no seed row and no mint; the admin issuance tab has its API methods and no UI; nothing records whether the certificate was emailed or downloaded, and nothing sends `spinout_graduated`. **UPDATE (D382):** the admin tab is built (`AdminSpinoutLab.jsx` → Certificates, `AdminSpinoutCertificates.jsx`): KPIs, the issuance table with Issue / Revoke / Preview, "Issue all eligible" (the backfill), an activity log from the registry's own timestamps and the state counts. Issue now runs the graduation path attributed to the admin; revoke requires a reason; both are in the admin log. Emailed, Downloaded, Resend, the badge and template events render Not recorded with their reasons, and Reissue is refused (`reissue_blocked`) until the credential id stops colliding. | -| Help Center | shared | `/help` | `/help`, `/help/:area/:guide`, `/help/tickets`, `/help/tickets/:id`, `/help/admin/*`, `/help/:id`, `/docs`, `/docs/admin/*`, `/support` (the last four redirect) | — (none; static JS modules under `pages/docs/sections/`, client-side fuse.js search) | CURRENT | Live ships 14 journey-organised sections with search, per-subsection how-to/tips/pitfalls, related links, on-this-page rail. New: persona-aware grouping and per-article surface tags, suggested-search chips, a **"Popular this week"** ranked list, a "Still stuck?" block with a live status line to `/status`; on the article page a "Where this lives" block, a worked example in mono, a warning callout, an "Applies to" persona list, an "Open the surface" deep-link, and **"Did this answer it? Yes / No"**. Popularity and feedback both need a backend that does not exist. **Wave 3 shipped** the "Still stuck?" block with its live status line — `GET /api/public/status` was already serving `/status`, and the docs footer was a static sentence. The roll-up rule is now shared (`lib/statusOverall.js`) so the two pages cannot disagree mid-incident; extracting it also fixed an inline `[].every()` that reported a confident "Operational" on an empty probe list, which is why StatusPage's `unknown` pill had never been reachable. **Blocked, not skipped:** "Popular this week" and "Did this answer it?" each need a store that does not exist (view counts, article feedback). "Where this lives" / "Open the surface" / per-article surface tags need a `surface` route on each of ~98 subsections — content authoring, and a wrong deep link is worse than none. "Applies to" would render "everyone" on 97 of 98: only `admin.js` carries a `roles` array today. **This canvas governs `/docs`, not `/help`** — recorded 2026-09-07, after the artifact was re-reported as a mismatch against `/help`. D39 renamed the menu label "Support" → "Help Center" and moved `/tickets` → `/help`; the component behind `/help` is untouched `TicketsPage` (`App.jsx:1919`), a GitHub-Issues-backed ticket tracker that happens to render `

Help Center

` at `:300`. So the design and the address never met: anyone comparing this canvas to `/help` is comparing it to a different page. The 2026-09-07 artifact re-export decodes byte-identical to `canvases/integrated/Help Center.dc.html`, so nothing about the design changed either. Decision taken on it: **`/help` becomes the Help Center composed over this corpus, and `/docs` redirects into it** the way D39 handled `/tickets` — no article store is being added, because the content is already structured and role-filtered and markdown would lose that. Task #103. **Task #103 shipped 2026-09-07 — composition and routing, no article store.** `/help` mounts this corpus (`HelpCenterPage`, formerly `DocsPage`); the ticket tracker that used to hold `/help` moved to `/help/tickets` and its `

` changed from "Help Center" to "Support tickets", because two pages claiming the same name is what made this canvas look unbuilt. `/docs` redirects with **hash and query preserved** — the hash IS the article, and eight deep links across six components address one that way. **Built:** the hero ("How can we help?"), the search moved into it with five suggested queries, a "Browse by what you are doing" grid built from `visibleSections` so a new section file appears without a second list to keep, an in-column result list, and a "Still stuck?" contact block that now offers three channels. **Still refused, same reasons:** "Popular this week" (no view counts), "Did this answer it?" (no feedback store), "Where this lives" / "Open the surface" (no per-article `surface` route on ~98 subsections), "Applies to" (**zero** sections carry a `roles` array, not 1 of 98 — `sections/admin.js` is the only file that has one and it was deliberately dropped from the manifest by `2c38e60b3`, 2026-05-22, so `adminOnlyAnchors()` returns an empty set and the `/…/admin/*` guard is inert; left as found, since re-importing it reverses someone's decision). `frontend/test/help_center_contract.test.mjs` pins all four absences. **Five live defects closed in the same pass, every one an address nothing declared or a route nothing read:** `/support?topic=` was a 404 from four pages' error states (now `/help?q=`); the advisor's `exploreDocs` built `/docs/
/`, a path segment where the corpus wants a hash, so every "Read " CTA 404'd (now `/help#`); `TicketsPage` never read `useParams`, so `/help/` — emitted into Slack by `tickets.ts:147` — silently showed the list; three `ticket_update` notifications linked `/help` for a notification naming one ticket (now `/help/tickets/`); and the pre-KYC allow list was an exact match on `/help`, so an un-KYC'd investor clicking "Open ticket" was bounced to `/kyc` — cut off from the one channel that could unblock them (now the whole `/help` subtree). **`CustomerChatWidget` is mounted at last**: built under Task #7 (IG) with a complete Slack round-trip and referenced nowhere, because its docblock delegated the tier check to "the Help Center help panel" that was never written. `lib/customerChat.js` is that gate, mirroring the worker's `isEligible` with both copies pinned by the contract test; `/auth/me` now returns `investor_tier` alongside `subscription_tier`, without which every investor read as `free` on the client — including `institutional`, the one investor tier that qualifies. **UPDATE 2026-10-06 (D599, #1280): built to the canvas, and the canvas grew.** The owner's new export adds a "Your tickets" card and a four-action "Still stuck?" to H1, and the ticket screens H3 to H7 through an imported `Help Ticket Screen` component; both files are in `canvases/integrated/`. `/help` is now H1: a lavender search band, the areas as two-column cards with tinted icons and four guides each, the reader's open tickets (`GET /api/tickets`; an admin sees every member's), and "Still stuck?" with Open a ticket (`/help/tickets?new=1`, which opens the form), Message the team (chat plans only), Email support and What we can see (`/trust`). Every guide has its own page at `/help//` (H2): breadcrumb, reading time from the guide's own words, how-to, the Tips and Common pitfalls callouts, Related, and an "On this page" rail. The rail that listed all 96 guides is gone, and no guide renders on the home. `#/` on `/help` opens that guide's page, so every old address still lands; `AdminDocsPathGuard` now renders an admin guide at `/help/admin/` instead of redirecting to the hash, which would loop. Still out, each for want of a store: the view-ranked list and the yes/no answer (#1286, S10, D603), and the per-guide surface and licence lines (#1280 step 3). The ticket screens H3 to H7 are #1280 step 4. **UPDATE 2026-10-06 (D603, #1286): the two stores exist, Worker first.** Migration 408 adds `help_guide_views` (anchor, UTC day, count; no reader identity), `help_guide_view_marks` (one counted view per member per guide per day, purged after two days) and `help_guide_feedback` (one yes/no per member per guide, the last wins, an optional "what was missing" with a no). `routes/help_center.ts` serves `POST /api/help/guides/:area/:guide/view`, `GET /api/help/popular`, and `GET`/`PUT /api/help/guides/:area/:guide/feedback` (signed in), plus `GET /api/admin/help/feedback` (admin; counts and texts, no member). The client is `lib/api/helpCenter.js`. The page is not wired yet: Session 1 does that under #1280, and the contract test keeps both off `/help` until then. **UPDATE 2026-10-06 (D604, #1280 step 4): the ticket screens, built to their canvas, Worker first.** `GET /api/tickets` rows carry `review` (a feature request's D544 review: awaiting, approved, declined with its reason, or unreadable) and, for an admin, `sla` (tier, HQ's hours, band; the clock stops at resolved or closed); `POST /api/tickets/sync` sends the same rows. `GET /api/tickets/:id` returns `thread` by side instead of `comments` by GitHub login: a reply StudioOS posted is the member's or the team's by D600's signature, read only at the very end; a comment from anyone outside the repository's team is `outside`, with its login, never the Axal team. `POST /api/tickets/:id/mirror` (admin) retries one ticket's mirror. `/help/tickets` is H3 (a member's tickets: status tabs, Type and Category filters, every chip) or H6 (the admin queue: requester, SLA, tracker, title search); `?new=1` is H4 (Title and Type required, Type with no default, the guides that may answer it); `/help/tickets/` is H5 or H7 (the thread by side, replies open only once on the tracker, the bounty or the review, and for an admin status, Assign to me and the mirror's Retry). Not drawn: the canvas's promise that a failed mirror is retried on its own (nothing does), an assignee picker (no roster exists), and the link to the feature review queue (#1107 waits on its design). | +| Help Center | shared | `/help` | `/help`, `/help/:area/:guide`, `/help/tickets`, `/help/tickets/:id`, `/help/admin/*`, `/help/:id`, `/docs`, `/docs/admin/*`, `/support` (the last four redirect) | — (none; static JS modules under `pages/docs/sections/`, client-side fuse.js search) | CURRENT | Live ships 14 journey-organised sections with search, per-subsection how-to/tips/pitfalls, related links, on-this-page rail. New: persona-aware grouping and per-article surface tags, suggested-search chips, a **"Popular this week"** ranked list, a "Still stuck?" block with a live status line to `/status`; on the article page a "Where this lives" block, a worked example in mono, a warning callout, an "Applies to" persona list, an "Open the surface" deep-link, and **"Did this answer it? Yes / No"**. Popularity and feedback both need a backend that does not exist. **Wave 3 shipped** the "Still stuck?" block with its live status line — `GET /api/public/status` was already serving `/status`, and the docs footer was a static sentence. The roll-up rule is now shared (`lib/statusOverall.js`) so the two pages cannot disagree mid-incident; extracting it also fixed an inline `[].every()` that reported a confident "Operational" on an empty probe list, which is why StatusPage's `unknown` pill had never been reachable. **Blocked, not skipped:** "Popular this week" and "Did this answer it?" each need a store that does not exist (view counts, article feedback). "Where this lives" / "Open the surface" / per-article surface tags need a `surface` route on each of ~98 subsections — content authoring, and a wrong deep link is worse than none. "Applies to" would render "everyone" on 97 of 98: only `admin.js` carries a `roles` array today. **This canvas governs `/docs`, not `/help`** — recorded 2026-09-07, after the artifact was re-reported as a mismatch against `/help`. D39 renamed the menu label "Support" → "Help Center" and moved `/tickets` → `/help`; the component behind `/help` is untouched `TicketsPage` (`App.jsx:1919`), a GitHub-Issues-backed ticket tracker that happens to render `

Help Center

` at `:300`. So the design and the address never met: anyone comparing this canvas to `/help` is comparing it to a different page. The 2026-09-07 artifact re-export decodes byte-identical to `canvases/integrated/Help Center.dc.html`, so nothing about the design changed either. Decision taken on it: **`/help` becomes the Help Center composed over this corpus, and `/docs` redirects into it** the way D39 handled `/tickets` — no article store is being added, because the content is already structured and role-filtered and markdown would lose that. Task #103. **Task #103 shipped 2026-09-07 — composition and routing, no article store.** `/help` mounts this corpus (`HelpCenterPage`, formerly `DocsPage`); the ticket tracker that used to hold `/help` moved to `/help/tickets` and its `

` changed from "Help Center" to "Support tickets", because two pages claiming the same name is what made this canvas look unbuilt. `/docs` redirects with **hash and query preserved** — the hash IS the article, and eight deep links across six components address one that way. **Built:** the hero ("How can we help?"), the search moved into it with five suggested queries, a "Browse by what you are doing" grid built from `visibleSections` so a new section file appears without a second list to keep, an in-column result list, and a "Still stuck?" contact block that now offers three channels. **Still refused, same reasons:** "Popular this week" (no view counts), "Did this answer it?" (no feedback store), "Where this lives" / "Open the surface" (no per-article `surface` route on ~98 subsections), "Applies to" (**zero** sections carry a `roles` array, not 1 of 98 — `sections/admin.js` is the only file that has one and it was deliberately dropped from the manifest by `2c38e60b3`, 2026-05-22, so `adminOnlyAnchors()` returns an empty set and the `/…/admin/*` guard is inert; left as found, since re-importing it reverses someone's decision). `frontend/test/help_center_contract.test.mjs` pins all four absences. **Five live defects closed in the same pass, every one an address nothing declared or a route nothing read:** `/support?topic=` was a 404 from four pages' error states (now `/help?q=`); the advisor's `exploreDocs` built `/docs/
/`, a path segment where the corpus wants a hash, so every "Read " CTA 404'd (now `/help#`); `TicketsPage` never read `useParams`, so `/help/` — emitted into Slack by `tickets.ts:147` — silently showed the list; three `ticket_update` notifications linked `/help` for a notification naming one ticket (now `/help/tickets/`); and the pre-KYC allow list was an exact match on `/help`, so an un-KYC'd investor clicking "Open ticket" was bounced to `/kyc` — cut off from the one channel that could unblock them (now the whole `/help` subtree). **`CustomerChatWidget` is mounted at last**: built under Task #7 (IG) with a complete Slack round-trip and referenced nowhere, because its docblock delegated the tier check to "the Help Center help panel" that was never written. `lib/customerChat.js` is that gate, mirroring the worker's `isEligible` with both copies pinned by the contract test; `/auth/me` now returns `investor_tier` alongside `subscription_tier`, without which every investor read as `free` on the client — including `institutional`, the one investor tier that qualifies. **UPDATE 2026-10-06 (D599, #1280): built to the canvas, and the canvas grew.** The owner's new export adds a "Your tickets" card and a four-action "Still stuck?" to H1, and the ticket screens H3 to H7 through an imported `Help Ticket Screen` component; both files are in `canvases/integrated/`. `/help` is now H1: a lavender search band, the areas as two-column cards with tinted icons and four guides each, the reader's open tickets (`GET /api/tickets`; an admin sees every member's), and "Still stuck?" with Open a ticket (`/help/tickets?new=1`, which opens the form), Message the team (chat plans only), Email support and What we can see (`/trust`). Every guide has its own page at `/help//` (H2): breadcrumb, reading time from the guide's own words, how-to, the Tips and Common pitfalls callouts, Related, and an "On this page" rail. The rail that listed all 96 guides is gone, and no guide renders on the home. `#/` on `/help` opens that guide's page, so every old address still lands; `AdminDocsPathGuard` now renders an admin guide at `/help/admin/` instead of redirecting to the hash, which would loop. Still out, each for want of a store: the view-ranked list and the yes/no answer (#1286, S10, D603), and the per-guide surface and licence lines (#1280 step 3). The ticket screens H3 to H7 are #1280 step 4. **UPDATE 2026-10-06 (D603, #1286): the two stores exist, Worker first.** Migration 408 adds `help_guide_views` (anchor, UTC day, count; no reader identity), `help_guide_view_marks` (one counted view per member per guide per day, purged after two days) and `help_guide_feedback` (one yes/no per member per guide, the last wins, an optional "what was missing" with a no). `routes/help_center.ts` serves `POST /api/help/guides/:area/:guide/view`, `GET /api/help/popular`, and `GET`/`PUT /api/help/guides/:area/:guide/feedback` (signed in), plus `GET /api/admin/help/feedback` (admin; counts and texts, no member). The client is `lib/api/helpCenter.js`. The page is not wired yet: Session 1 does that under #1280, and the contract test keeps both off `/help` until then. **UPDATE 2026-10-06 (D604, #1280 step 4): the ticket screens, built to their canvas, Worker first.** `GET /api/tickets` rows carry `review` (a feature request's D544 review: awaiting, approved, declined with its reason, or unreadable) and, for an admin, `sla` (tier, HQ's hours, band; the clock stops at resolved or closed); `POST /api/tickets/sync` sends the same rows. `GET /api/tickets/:id` returns `thread` by side instead of `comments` by GitHub login: a reply StudioOS posted is the member's or the team's by D600's signature, read only at the very end; a comment from anyone outside the repository's team is `outside`, with its login, never the Axal team. `POST /api/tickets/:id/mirror` (admin) retries one ticket's mirror. `/help/tickets` is H3 (a member's tickets: status tabs, Type and Category filters, every chip) or H6 (the admin queue: requester, SLA, tracker, title search); `?new=1` is H4 (Title and Type required, Type with no default, the guides that may answer it); `/help/tickets/` is H5 or H7 (the thread by side, replies open only once on the tracker, the bounty or the review, and for an admin status, Assign to me and the mirror's Retry). Not drawn: the canvas's promise that a failed mirror is retried on its own (nothing does), an assignee picker (no roster exists), and the link to the feature review queue (#1107 waits on its design). **UPDATE 2026-10-07 (D621, #1280 step 2): the two stores are on the page.** `/help` shows "Popular this week": the four most-viewed guides of the last 7 days that the reader can open (`GET /api/help/popular`). Each guide page records its view and asks "Did this answer it?": yes, or no with an optional "what was missing" (`GET`/`PUT .../feedback`). An admin's `/help` adds the answers card: per-guide yes and no, and each missing text, no names (`GET /api/admin/help/feedback`). Both bans in `help_center_contract.test.mjs` are lifted. The surface line, "Open the surface" and "Applies to" stay out (#1280 step 3). | | Incorporate | founder | `/spinout-lab/incorporate` | `/spinout-lab/incorporate`; separate platform surface at `/incorporate`, `/incorporate/success`, `/incorporate/83b`, `/incorporate/cofounder-agreement` | `legal.ts`, `legal_83b.ts`, `company_ein.ts`, `compliance.ts`, `spinout_lab.ts`, `payments.ts` | OUT OF SCOPE | Lab tool ("Unlocked · Wk 4"). **Not** the same surface as the platform's `/incorporate`. Do not collapse them. Price from `GET /legal/incorporation/quote` (the catalog), paid from the server's order only, jurisdiction from the Lab application (D362). A Wyoming C-Corp (`us_wy_ccorp`) is formable and priced from the catalog only, `catalog_price_missing` until the owner adds its Stripe price; its Articles of Incorporation are generated only from a counsel-approved template body (D586). The company's EIN is stored encrypted (`company_ein.ts`, migration 397): the founders, company editors and admins read the last four, the founder's edit field the number, and the investor preview says Not shared (D579). After payment an Axal admin records "Packet ready" (a project document) and "Filed with the state" (its date) on the Spin-Out Lab's "Formation orders" tab (`admin_incorporations.ts`, migration 411, D601). | ## Graduated from `design/incoming/` — part 5 (21 canvases) diff --git a/documentation/architecture/decisions/D599.md b/documentation/architecture/decisions/D599.md index 52d966c215..047cb587bc 100644 --- a/documentation/architecture/decisions/D599.md +++ b/documentation/architecture/decisions/D599.md @@ -103,6 +103,8 @@ The owner's new export replaces the old one in place. Its route is live, as views or stores answers yet. #1286 (S10, D603, migration 408) builds both stores, and Session 1 wires them here afterwards. Until then the contract test keeps them banned. + **Since D621 (#1280 step 2),** both are built: popular cards on the home, + the yes/no on each guide, and the answers for an admin. - **The per-guide surface tag, "Where this lives", "Open the surface" and "Applies to" stay out.** No guide records its surface or its licences. Adding them is #1280 step 3, content authoring. diff --git a/documentation/architecture/decisions/D603.md b/documentation/architecture/decisions/D603.md index 5ded2b8808..28dd9d9f9a 100644 --- a/documentation/architecture/decisions/D603.md +++ b/documentation/architecture/decisions/D603.md @@ -6,6 +6,8 @@ member per guide, for "Did this answer it?".** Slot S10, issue #1286, step 2 of #1280, 2026-10-06. Migration 408. This PR builds the stores, the routes and the client. Session 1 wires the page under #1280; until then `frontend/test/help_center_contract.test.mjs` keeps both off it. +**D621 (#1280 step 2) wires the page**, and shows the admin read on the Help +Center home. ### The anchor diff --git a/documentation/architecture/decisions/D621.md b/documentation/architecture/decisions/D621.md new file mode 100644 index 0000000000..943b889ac4 --- /dev/null +++ b/documentation/architecture/decisions/D621.md @@ -0,0 +1,147 @@ +## D621 — Help Center: "Popular this week" and "Did this answer it?", wired to their stores, with the answers shown to an admin + +**Issue #1280, step 2, slot S01, 2026-10-07.** Frontend only: no route, no +migration. D603 (#1286, migration 408) built the stores, the routes and the +client. This wires the page to them and lifts the two bans the contract test +held until then. + +### What was true on main + +- **The canvas** (`design/canvases/integrated/Help Center.dc.html`) draws: + - "Popular this week" on H1, the home: four ranked cards, "By views, last 7 + days"; + - "Did this answer it?" on H2, each guide: Yes and No, "A no opens one + field asking what was missing". +- **D599** built H1 and H2 without them, for want of a store, and + `help_center_contract.test.mjs` banned both phrases from the layout. +- **D603** built the stores and the routes: + - guide views by day, one counted per member per guide per day; + - one yes/no answer per member per guide, the last one wins; + - an admin read of every answer, with no member identity. + + Nothing on the page called them, and no screen showed the admin read. + +### The change (`pages/docs/HelpSignals.jsx`, mounted by `DocsLayout.jsx`) + +**Popular this week, on the home** +- It reads `GET /api/help/popular?days=7&limit=10` and draws the first four + guides this reader can open, in the route's order (most views, then the + anchor). +- **A guide the reader cannot open, or one the manifest no longer has, is + left out,** never shown as a dead card. The page asks for the route's + maximum, 10, so a dropped guide leaves room for the next. +- **Each card** has a rank (the first violet, as drawn), the guide's title, + and "1,240 views · Capital", with the area's name. A view is one member + reading a guide on one day (D603). +- **It sits under the areas,** and steps aside while a search is shown. +- **States:** + - loading draws four skeleton cards and no figure; + - a failed read says "The most-read guides could not be read. This is not + a sign nobody reads them.", with Try again; + - a week with no views for the reader's guides says so. + +**Did this answer it?, on each guide** +- **Opening a guide records the view** (`POST .../view`). The Worker counts + one per member per guide per day. +- **The card reads the reader's own answer:** + - with none, it offers Yes and No; + - with one, it says "You said this answered it." or "You said this did not + answer it.", quotes what was missing, and offers "Change your answer". +- **No opens one field,** "What was missing?": + - it is optional; + - it is capped at the Worker's 500 characters, with a counter; + - Send saves the no, with the text. +- **Yes saves at once.** +- **States:** + - while the answer loads, there are no buttons; + - an answer that could not be read still lets the reader answer, and says + a new answer replaces it; + - a failed save says "Your answer was not saved. Try again.". +- **The card sits before Related,** as drawn. + +**The answers, for an admin, on the home** +- **One card,** "Did this answer it? · every member", under the tickets card, + for an admin only: `GET /api/admin/help/feedback` is `requireAdmin`. +- **For each guide,** the yes and no counts, most "no" first (the route's + order), linked to the guide. A guide the manifest no longer has shows its + anchor. +- **Then each "What was missing",** newest first, with its guide and date. +- **The first five of each show,** then Show all. +- **The meta line totals the answers** and says "no names": the route sends + none. + +### Where the build departs from the canvas, and why + +- **The note says who reads the answers.** The canvas says a no "goes to + whoever wrote the guide". No guide records its author, so that would be a + promise nobody keeps. The note says "Axal's admins read the answers, + without your name", and the admin card makes that true. +- **The admin card is not drawn.** Without it, no screen would show a single + answer, and "Did this answer it?" would collect text nobody reads. It is + one card on the home the admin already uses, built from the canvas's own + card and list styles. +- **States the canvas does not draw:** + - loading; + - a failed read; + - an empty week; + - a changed answer; + - an unreadable earlier answer; + - a failed save. + +### Still out (#1280 step 3) + +Two canvas elements still have no store, and the contract test still bans +them: +- the per-guide surface line and "Open the surface", since no guide records + its surface; +- "Applies to", since no guide records the licences it applies to. + +### Tests + +**`frontend/test/help_signals_d621.test.mjs` (12):** +- **The constants against the Worker's:** the route's maximum limit and + window, and `MISSING_MAX`. +- **The popular cards:** + - the route's order; + - guides the reader cannot open left out; + - zero views dropped; + - four at most; + - no list read as a failed read. +- **The reader's answer:** none; yes; no, with and without text; and + unreadable. +- **The admin view:** names, a removed guide, the totals, and that nothing + about a member passes through. +- **Every rendered state** of the three parts. +- **The wiring:** + - where each part mounts, and that the admin card shows only for an admin; + - each store read through the module file; + - each failure kept apart from an empty answer. + +**`frontend/test/help_center_contract.test.mjs`:** the two bans lifted, on +purpose; its header now names only the two that remain. + +**Mutation checks: 24 of 24 caught.** Each undid one behaviour, and every file +was restored and checked by sha256: +- **the cards:** + - a guide the reader cannot open, or with no views, shown; + - no cap at four, or asking the route for only four; + - a failed read as an empty week; + - "1 views"; + - two violet ranks; +- **the answer:** + - the field's cap off the Worker's, or no cap on the field; + - a reply without the field read as no answer; + - a yes keeping stray text; + - the note promising the guide's author; + - a no saved at once with no field; + - an unreadable answer blocking a new one; + - a failed save left silent; + - the view not recorded; +- **the admin card:** + - a member field passed through; + - every row at once; + - a failed read silent, or read as none; +- **the layout:** + - popular under search results; + - the admin card for everyone; + - no answer card on a guide. diff --git a/frontend/src/pages/docs/DocsLayout.jsx b/frontend/src/pages/docs/DocsLayout.jsx index 5f683908ee..31cea8778f 100644 --- a/frontend/src/pages/docs/DocsLayout.jsx +++ b/frontend/src/pages/docs/DocsLayout.jsx @@ -17,6 +17,7 @@ import BountyBadge from '../../components/tickets/BountyBadge'; import ReviewBadge from '../../components/tickets/ReviewBadge'; import { statusChip } from '../../lib/ticketScreens'; import { parseGuidePath, guideHref, hashTarget, readingMinutes, openTicketsByActivity } from './helpCenter'; +import { GuideAnswersAdmin, GuideFeedback, PopularGuides } from './HelpSignals'; // --------------------------------------------------------------------------- // D599 (#1280) — the Help Center, built to its canvas. @@ -37,11 +38,13 @@ import { parseGuidePath, guideHref, hashTarget, readingMinutes, openTicketsByAct // notifications, the advisor's tools and bookmarks, so a hash on `/help` now // opens that guide's page (see `DocsLayout` below). // -// Four canvas elements stay out, each for want of a store, and -// `help_center_contract.test.mjs` pins their absence: the view-ranked list -// (nothing counts views), the yes/no answer (nothing stores it), the per-guide -// surface line and jump (no guide records its surface), and the licence line -// (no guide records the licences it applies to). #1280 steps 2 and 3 add them. +// D621 (#1280 step 2) wires two canvas elements to D603's stores, in +// `HelpSignals.jsx`: "Popular this week" on the home, and "Did this answer +// it?" on each guide, with the answers shown to an admin on the home. Two +// stay out, for want of a store, and `help_center_contract.test.mjs` pins +// their absence: the per-guide surface line and jump (no guide records its +// surface), and the licence line (no guide records the licences it applies +// to). #1280 step 3 adds them. // --------------------------------------------------------------------------- // Wrap the pure-JS split helper into a JSX-friendly highlighter. Kept @@ -583,6 +586,8 @@ function HelpArticle({ sections, area, guide }) { )} + + {related.length > 0 && (