Skip to content

fix: The /divan raporlar and decisions panes are unreachable by URL, so review-ui can never render them - #9423

Open
usirin wants to merge 1 commit into
mainfrom
build/8776-divan-raporlar-url-a5248e27
Open

usirin wants to merge 1 commit into
mainfrom
build/8776-divan-raporlar-url-a5248e27

Conversation

@usirin

@usirin usirin commented Sep 17, 2026

Copy link
Copy Markdown
Member

The /divan section switch lived in useState, so raporlar existed only after an in-page
click. A renderer that navigates and never clicks could not paint .kp-divan__raporlar-pane
or .kp-divan__decisions-pane, and a moderator could not link or reload into them.

The section is a path segment now. App.tsx mounts /divan for the roster and
/divan/raporlar for the reports pane, both off the constants in the new
components/divan/divanSection.ts, and passes the section down as a prop so the page parses
no path itself. The in-page nav and the Subnav zone switcher navigate instead of setting
state.

The moderator gate did not move. visibleDivanSection folds a raporlar URL down to the
roster whenever the server's isModerator is not true. The fold is a render decision rather
than a redirect on purpose: me reads false while it is still unread, so a redirect would
spend a moderator's own URL on the loading frame.

Tests: divanSection.test.ts covers the hrefs and the fold; DivanPage.route.test.tsx
renders the routes and asserts direct navigation for a moderator and a non-moderator, plus
the URL moving on an in-page click. .patterns/frontend-routing.md records the shape.

Reviewer: start at divanSection.ts, then the fold's three call sites in DivanPage.tsx.

Fixes #8776

Deviations

  • Out-of-scope changeSaid: the issue names the divan section's URL only.
    Did: also re-pointed App.tsx's existing divanTo topbar link at the new
    DIVAN_PATH constant. Why: the same module now owns that path string, and leaving one
    literal behind is the drift the constants exist to prevent. Disposition: stated here;
    the string is unchanged.

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Preview deployed

@usirin

usirin commented Sep 17, 2026

Copy link
Copy Markdown
Member Author

review-code: PASS @ 454f3c8 content:ee4532adc204 — merge-ready

Scope read at 454f3c8: 6 changed files, classes code(5) / doc(1) / ui(3). review-ui is a routed row and is not judged here. Governance: not-required. Contract: fixes:8776, 5 criteria, all open at this round; no prior verdicts at this head.

Per-criterion verification

  • AC-1 — direct navigation to the raporlar URL mounts .kp-divan__raporlar-pane and .kp-divan__decisions-pane for a moderator, with no click. [PASS] apps/web/src/App.tsx mounts a second route path={DIVAN_RAPORLAR_PATH} with element={<DivanPage section="raporlar" />}, inside the same divan-zone layout route that already carried /divan, so the Subnav zone is above both. DivanPage takes the section as a prop and parses no path. apps/web/src/pages/DivanPage.route.test.tsx renders at DIVAN_RAPORLAR_PATH with isModerator = true and asserts .kp-divan__raporlar-pane, .kp-divan__decisions-pane and .kp-divan__decisions-title present and .kp-divan__roster-pane absent, with no click in the test.
  • AC-2 — clicking the in-page raporlar nav updates the URL. [PASS] Both in-page tabs and the Subnav zone's onFilterChange now call goToSection, which is navigate(divanSectionHref(next)) rather than setSection. The route test's fourth case clicks divan-nav-raporlar, reads a useLocation probe back as /divan/raporlar, clicks divan-nav-caylaklar and reads /divan back, asserting the matching pane each way.
  • AC-3 — a non-moderator on that URL still lands on caylaklar and never sees either pane; the gate stays the server's isModerator. [PASS] raporlarVisible = me?.isModerator ?? false is unchanged in DivanPage.tsx — no tier read was introduced. visibleDivanSection(routeSection, isModerator) returns raporlar only when both hold, and showRaporlarPane is now just section === "raporlar", so the path cannot widen the gate. The second route-test case asserts roster present and both mod panes plus .kp-divan__nav absent for isModerator = false; divanSection.test.ts pins the fold at the function level for both polarities.
  • AC-4 — .patterns/frontend-routing.md records the shape. [PASS] Graded in review-doc.
  • AC-5 — a test covers direct navigation for both viewers, and the named existing divan tests still pass. [PASS] DivanPage.route.test.tsx (4 cases) and divanSection.test.ts (5 cases) are new. Local subset at head (apps/web, divan dirs only, both vitest projects): 26 client tests over 5 files and 159 unit tests over 8 files, all green, including divanGating.test.ts, divanReads.test.ts and Divan.locale.test.tsx by name.

Execution evidence

review ci at 454f3c8: settle settled, ci green, 45 check runs enumerated (40 success, 5 skipped), 28 of 40 repo-authored workflows produced a run at this head. Not re-derived locally.

Standing checks

  • Test honesty — no pre-existing test or assertion was changed or deleted; the two test files are additions. The new assertions read the rendered DOM, not the implementation's own helpers, except divanSection.test.ts which is a pure-function unit test of the module it names.
  • Release containment — none stated and none owed. /divan was already a plainly-mounted public product route whose fate roots deny an unentitled reader (.patterns/frontend-routing.md, "Public product routes"). The new path adds no reachable surface: a non-moderator on it renders exactly the roster they already got at /divan.
  • Comment discipline — the added comments carry constraints the code cannot show (why the fold is a render decision rather than a redirect, that the section arrives as a prop so no path parsing lives in the page). No separators, no name-restaters. These files are outside packages/fabrika-cli/, so the #8776 citations are in-repo and correct here.
  • Staleness traps — none. The section is derived per render from the route prop and the live me; nothing is cached across a boundary that can move.

Deviations

Entry Substance Verdict
Out-of-scope change: App.tsx's divanTo topbar link re-pointed at DIVAN_PATH The diff changes exactly that one line, divanTo: showDivan ? DIVAN_PATH : undefined, and DIVAN_PATH is "/divan" — the string is unchanged, as disclosed Disclosed and accurate

Swept the rest of the diff against the section: the only other changes are the route addition, the new module and its test, the page's state-to-navigation swap, and the pattern doc — all inside the issue's stated job. deviation-disclosure: PASS.

Observations, not findings

  • DivanPage.route.test.tsx declares its own two-route table rather than rendering the real App, so it pins the page's behaviour at each path but not that App.tsx mounts /divan/raporlar at all. Both sides import the same constants, and apps/web/src/App.test.tsx already renders the real App at a MemoryRouter path if an app-level pin is ever wanted. The routed review-ui gate navigates the real URL on this PR, which covers the same seam for this round.
  • A moderator deep-linking to the raporlar URL paints the roster for the frame before me resolves, then flips. That is deliberate and documented in divanSection.ts and the PR body; naming it only so the next reader does not read it as a bug.

Verdict: PASS.

Verdict-written: 2026-09-17T19:28:47Z

@usirin

usirin commented Sep 17, 2026

Copy link
Copy Markdown
Member Author

review-doc: PASS @ 454f3c8 content:ee4532adc204 — merge-ready

Doc-class slice at 454f3c8: one file, .patterns/frontend-routing.md, +20 lines adding the section "A product's sections are path segments, not page state" between the visibility-classes list and the per-product Subnav zones section.

Per-criterion verification

  • AC-4 — .patterns/frontend-routing.md records the divan section's URL shape alongside the other per-product zone routes. [PASS] The new section names both paths (/divan roster, /divan/raporlar reports), names App.tsx as the mount point, names the DIVAN_PATH / DIVAN_RAPORLAR_PATH constants and links the module that owns them, and states that the section arrives as a prop so the page parses no path. It sits directly above "Per-product Subnav zones — nested layout routes", which is where the other per-product zone routes are described, so a reader working the route tree meets it in place. The other four criteria are graded in review-code.

Hygiene checklist

  • Right surface. [PASS] This is code-shape material — which path maps to which pane and which module holds the constants — and .patterns/ is this repo's home for how the code is shaped. The why that rides along is the bounded rationale half a pattern doc owns; nothing here is a decision narrative that belongs in the decision corpus.
  • Diátaxis. [PASS] single-mode: reference. Signal: the page prescribes no ordered sequence the reader performs, and the new section is a look-it-up account of the route shape structured to match the code. The three bullets carry the rationale a pattern doc's why-half is entitled to, and they route the reader to divanSection.ts rather than re-arguing the choice, so reference does not tip into explanation. No mix to flag.
  • Supersession. [PASS] Nothing is replaced or contradicted. The existing "Public product routes" bullet already describes /divan as plainly mounted with server-side denial; the new section extends that with the second path and does not restate or overturn it. No second live doc answers this question.
  • Status sanity. [N/A] The file carries no frontmatter or status line, and the body speaks in the present tense about live code.
  • Claims trace. [PASS] Three falsifiable claims, all read back against source at this head:
    • "App.tsx mounts both off the DIVAN_PATH / DIVAN_RAPORLAR_PATH constants ... passing the section down as a prop, so the page parses no path itself" — verified in App.tsx's divanRoutes and in DivanPage, which takes section as a prop and calls no path-reading hook.
    • "Its renderer navigates and never clicks, so a section held in useState is invisible to the design gate" — verified against the rendered gate's own render verb, whose only surface input is a path crossed with a viewport; it has no click affordance, so a click-only pane is genuinely unreachable to it.
    • "me reads false while it is still unread, so a redirect would spend a moderator's own URL on the loading frame" — verified: DivanPage derives raporlarVisible as me?.isModerator ?? false, and me is null until the read lands.
  • Portability. [N/A] No changed file sits under the fabrika plugin tree or the CLI's source tree, so portability-guard has nothing to say here and the in-repo #8776 citation is the correct form on this surface.
  • Prose craft (writing-for-agents). [PASS] "path segments, not page state" is a compact anchor the rest of the section reuses rather than re-explaining. No no-op sentences: every line changes what a reader would otherwise assume. No sprawl at twenty lines. The one prohibition-shaped line, "It buys no entitlement", is immediately paired with the positive mechanism (visibleDivanSection folds the URL down), which is the form that lever asks for. The rationale overlaps divanSection.ts's docblock, which is the sanctioned cache rather than duplication — the constraint is a reason behind a choice that no config confesses, and the doc links the module instead of restating its API.
  • CI-answered checks. Link integrity and machine-local-path leaks are CI gates; review ci settled green at this head with 40 successful check runs, so they are read, not re-derived. The added relative link ../apps/web/src/components/divan/divanSection.ts resolves from .patterns/ to the real file.

Craft note, not a finding

The lead-in "Two things this buys, and one it does not:" asks the reader to count three bullets onto a two-plus-one split. The third bullet self-labels ("It buys no entitlement"), so the mapping survives; naming it only in case the next edit to this section wants the tighter form.

Verdict: PASS.

Verdict-written: 2026-09-17T19:29:57Z

@usirin

usirin commented Sep 17, 2026

Copy link
Copy Markdown
Member Author

CANT-SEE at 454f3c8 — the design gate could not render the
surfaces this PR adds, so no review-ui verdict is posted and the empty namespace fail-closes
the ship gate.

What I ran

  • review-ui render --pr 9423 --surface /divan:auth --surface /divan/raporlar:auth --surface /divan/raporlar:auth-caylak
    refused on exit 11 for every surface: "did not render signed in (the preview answered the
    seeded cookie as a visitor) — the authenticated render is UNKNOWN, never the anonymous one."

    Both tier tokens are set in this session, so what is missing is the test account's session row
    on this PR's own preview D1, not the credential.
  • A control render of the same two routes with no tier state came back captured at 1280x800, so
    the preview deployment is live and reachable. Both anonymous shots are byte-identical
    (sha256 29a897a1de585929c865e0fe42b67d2a6ae7735c111dcea137e328c70089ea02) and paint the
    signed-out divan wall ("yetkin yok") plus the page's own FateRequestError console error.
    Neither .kp-divan__raporlar-pane nor .kp-divan__decisions-pane is in that tree.

Why this is CANT-SEE and not a verdict

The entire rendered delta here is the two moderator-only panes now reachable at
/divan/raporlar. The anonymous pixels are the state the PR did not add, so a PASS off them
would be a judgment over surfaces nobody rendered. Per the gate's own rule, an unseen surface is
never a clean one.

What unblocks it

An operator seeds the yazar+moderator test account and its session onto this PR's preview D1
through the preview-seed package's test-account verb (and the caylak tier for the
non-moderator fold criterion), using the tokens this session already holds. Then review-ui
re-renders /divan:auth, /divan/raporlar:auth and /divan/raporlar:auth-caylak at the live
head and lands the verdict.

Context, not a verdict

  • LAW-SOURCE: manifest-proseui law exits 13 here, so there is no typed prohibition
    registry; the manifest's four pillars are the rubric.
  • CI is green at this head, and the deterministic design seams each ran their own gate there
    (component CSS token seam, the property-based a11y check over @kampus/design, descriptive
    inventory freshness). This gate mints no rival answer on any of them.

@usirin

usirin commented Sep 17, 2026

Copy link
Copy Markdown
Member Author

This gate cannot see the surfaces this PR adds, so it posts no verdict. Head 454f3c8; the blocker is the preview session, cause no-preview-render. The empty review-ui namespace fail-closes the ship gate.

LAW-SOURCE: manifest-prose (fabrika ui law exit 13 - no typed prohibition registry beside the manifest).

What this PR owes a rendered judgment on

The diff raises ui on 3 files and the deliverable is two panes that only a moderator session paints: .kp-divan__raporlar-pane and .kp-divan__decisions-pane at /divan/raporlar, plus the non-moderator fold back to the roster. Neither state is anonymous, and neither is behind a flag, so there is no override that makes them paint for a visitor.

What refused

review-ui render --pr 9423 --surface /divan:auth --surface /divan/raporlar:auth --surface /divan/raporlar:auth-caylak --viewport desktop --viewport mobile refused all six shots on 11:

surface "/divan/raporlar:auth" at desktop did not render signed in (the preview answered the seeded cookie as a visitor) - the authenticated render is UNKNOWN, never the anonymous one.

Both tiers refused identically, :auth and :auth-caylak alike, with PREVIEW_TEST_SESSION_TOKEN, PREVIEW_TEST_CAYLAK_SESSION_TOKEN and BETTER_AUTH_SECRET all set in this session. The preview D1 named in the deploy comment (53ba9937-edf8-43e9-9284-747eb495c500) is the same one the operator seeded, so the accounts should be present.

Two tiers failing the same way points at the signing key rather than at the session rows: a cookie signed with a secret the preview worker did not deploy with is well-formed and the worker answers it as a visitor, which is exactly the observed shape. The ambient BETTER_AUTH_SECRET this session holds is the one that stood in - the contract's --auth-secret-from path (the repo-wide value exported from the ci-credentials stack's alchemy state) is an operator act I hold no route to, and a direct D1 read to confirm the session rows was outside what this shell may run. So the cause is the probable one, not a proven one.

What did render, and what it proves

An anonymous probe of /divan and /divan/raporlar at desktop captured cleanly on the same preview, so the preview is live at this head and the new route resolves rather than 404s: /divan/raporlar mounts the divan page and serves the visitor wall ("bu alani gorme yetkin yok") with the roster's empty-detail column beside it.

That is coverage of the route's existence only. It is not a judgment of the panes, which never entered the tree, so nothing here is a partial PASS.

What unblocks the next round

The BETTER_AUTH_SECRET the pr-9423 preview worker actually deployed with, handed to the gate (or exported to a file for --auth-secret-from). With that, the three surfaces above render and the gate takes its verdict in one run; the panes need no --flag and no admin grant.

@github-actions

Copy link
Copy Markdown
Contributor

heal-ci: ROUTED — PR #9423 @ 454f3c8 → review

Scheduled stall sweep: this pull request classifies as ungated, stranded 113 minute(s) at head 454f3c88ba4c08fa7085419a5823baaac1b7d662.

Detection only — this run merged nothing, re-ran nothing and spawned nothing. What to do about the flag is a driver decision; /fabrika:heal-ci 9423 reads the full diagnosis.

Posted by the heal-ci-sweep workflow (#6146). The suppression key <pr>:<class>:<head> rides in the marker below, matched over the whole comment history: no further note lands here until this pull request changes class or gains a new head commit.

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.

The /divan raporlar and decisions panes are unreachable by URL, so review-ui can never render them

1 participant