Owner-only admin page: Reader roster plus Poll Lane status #102

Closed
opened 2026-08-16 11:28:19 +07:00 by sulthan · 0 comments
Owner

Blocked by: #101

Problem Statement

As the owner of this backend, I cannot see anything. There is no administrative page: the only operational surface in the whole system is a health endpoint that returns the word "ok", and the only administrative capability is a fold-out Reader roster tucked inside my own reading page, offering one action.

That was survivable while the backend did one simple thing on a fixed schedule. It stops being survivable with Poll Lanes, where every Site has its own pace, can shorten it automatically, can be skipped for refusing, and can be halted entirely by an absent browser. None of that is visible anywhere except a log I have to be watching at the time. I cannot answer the one question that matters — is my Library actually being kept fresh — without reading logs.

It is also about to become a correctness problem. The Sighting work that follows this spec can mark a Reader as untrusted, and a wrong mark can be produced by a broken adapter rather than by a lying Reader. Without a page, clearing that mark means hand-written SQL against production.

Solution

An owner-only administrative page at its own address, built from what already exists: the same server-rendered templates, the same htmx partial swaps, the same Discord session, the same owner test that already guards the Reader roster.

It shows two things. The Reader roster, moved out of the reading page and given the Sighting counters and a control to clear a Reader's marks. And Poll Lane status, one row per Site: how many Series are due, when the Lane last ran, what gap it is using, whether that gap is clamped to the floor, whether the Site is currently refusing, and whether the browser is reachable.

The status block refreshes itself. Nothing else about the reading experience changes, except that the reading page gets simpler when the roster moves off it.

User Stories

  1. As the owner, I want an administrative page at its own address, so that I can bookmark it and return to it rather than hunting for a panel inside my reading page.
  2. As the owner, I want my reading page to stop carrying administrative controls, so that the page I use every day is only about reading.
  3. As the owner, I want the page refused to every Reader who is not me, so that administrative state is not visible to the rest of the guild.
  4. As the owner, I want the refusal to be indistinguishable from the page not existing, so that its address is not confirmed to someone probing for it.
  5. As a Reader who is not the owner, I want no change to anything I can see or do, so that this page's arrival is invisible to me.
  6. As the owner, I want the roster to keep showing each Reader's Discord identity and live session count, so that nothing I have today is lost in the move.
  7. As the owner, I want to keep revoking another Reader's sessions from the roster, so that the one control I have today still works.
  8. As the owner, I want to see each Reader's Sighting counters, so that I can tell an honest Reader from one whose reports keep being contradicted.
  9. As the owner, I want to see clearly which Readers are currently blocked from deferring, so that I do not have to work it out from two numbers.
  10. As the owner, I want to clear a Reader's marks in one action, so that a false mark caused by a broken adapter does not need SQL against production.
  11. As the owner, I want clearing marks to be confirmed before it takes effect, so that I do not wipe a genuine record by mis-clicking.
  12. As the owner, I want to see how many Series are due for each Site, so that I can tell a Lane that is idle from one that is drowning.
  13. As the owner, I want to see when each Lane last ran, so that I can tell a Lane that is quiet because there is no work from one that has stopped.
  14. As the owner, I want to see the gap each Lane is actually using, so that I can see the automatic shortening working rather than infer it.
  15. As the owner, I want a Lane whose gap is clamped to the floor marked plainly, so that I learn my Library has outgrown the hourly promise without reading logs.
  16. As the owner, I want to see which Sites are currently refusing, so that I know a Site has turned hostile before my Latest Chapters go stale.
  17. As the owner, I want to see whether the browser is reachable, so that I know why my comix, kagane and novelfull Series have stopped moving.
  18. As the owner, I want the status to update on its own while I watch it, so that I can observe a run rather than sample it by reloading.
  19. As the owner, I want the status to reflect the running system rather than a stored copy, so that I never have to wonder whether I am looking at something stale.
  20. As the owner, I want the page to be honest immediately after a restart rather than showing confident zeroes, so that an empty page never looks like a broken one.
  21. As the owner, I want an unhealthy Lane to be visually obvious, so that I can find the problem without reading every row.
  22. As the owner, I want that emphasis never to look like a New Chapter, so that the one meaning ember carries in this system stays intact.
  23. As the owner, I want the page to work on my phone, so that I can check the backend without a desktop.
  24. As the owner, I want the page to meet the same accessibility floor as the rest of the system, so that it is not the one screen that regresses it.
  25. As a maintainer, I want the owner test written once and applied at route registration, so that a future administrative route cannot accidentally ship without it.
  26. As a maintainer, I want the two hand-written owner comparisons replaced by that one wrapper, so that there is a single place the rule lives.
  27. As a maintainer, I want one test proving every administrative route refuses a non-owner, so that adding a route does not require remembering to add a test.
  28. As a maintainer, I want the Lane status delivered through one narrow interface, so that the page's tests do not need a running poller.
  29. As a maintainer, I want the page to be a foundation rather than a finished thing, so that adding a future administrative control is a template block and a route.

Implementation Decisions

Its own page, owner-only. A full page at its own address, in the same shape as the existing full pages, rather than another fold-out panel. The Reader roster moves off the reading page and into it. The roster's existing tests move with it.

One owner gate, applied at registration. Today the owner test is a hand-written comparison duplicated inside two handlers. It becomes a single wrapper layered on top of the existing session gate and applied where routes are registered, replacing both copies. This is a security boundary and the reason for centralising it is exactly that: a gate visible in the route list makes a missing gate visible too, and one test can then cover every administrative route rather than one test per route. Non-owners continue to receive a not-found rather than a forbidden, matching the existing behaviour.

Roster contents. Discord identity, live session count, and the existing session-revocation control, all unchanged. Added: the Sighting counters for that Reader, a plain indication of whether they are currently blocked from deferring, and a control to clear their marks. Clearing is confirmed before it acts, following the same confirmation pattern the destructive controls in the reading UI already use. Note that clearing marks is not destruction in the design-system sense — it restores a privilege — so it must not borrow the destruction accent.

Lane status contents. One row per Site: due Series count, time of last run, effective gap in use, whether that gap is clamped to the floor, whether the Site is currently in its post-refusal wait, and whether the browser is reachable. The browser fact is shared by the three browser Sites rather than per Site.

The seam. The web handler accepts one narrow interface that reports the current Lane snapshot. In production the poller satisfies it; in tests a fake with fixed values is injected, exactly as the Discord API is already stubbed through its base address. This is the only new seam in the spec, it sits at the constructor, and it is what keeps the page's tests from needing a running poller.

Snapshot, not storage. Lane state is held in memory by the Lanes and copied out on request. No new table, no write per round, no staleness to reason about. Every figure the page wants is something a Lane already computes each round, so the snapshot is a copy of existing knowledge rather than new work. The accepted cost is that the page is empty for the first few seconds after a restart, until each Lane completes a round; the page must present that as "no data yet" rather than as zeroes, so an empty page is never mistaken for a stopped one.

Refresh. The status block refreshes itself every thirty seconds using the framework already vendored, as one attribute on the fragment. The roster does not refresh on a timer; it re-renders after an action, as it does today.

Visual treatment. A new named accent token is added for a Lane needing attention. It may not be the ember, which means New Chapter and nothing else, and it may not be the destruction accent, which means destruction and nothing else. The proposal is a cool blue-green named for a copper patina — the complement of the palette's warm crimson, distinct from the existing cool blue used for archiving, and a metal name consistent with the existing brass. Values are defined in both colour branches together, as the design system requires, and no surface, ink or wash siblings are added until something needs a surface rather than a mark.

Rendering model. Unchanged from the rest of the system: server-rendered templates parsed as a group, htmx fragments for anything that updates, no additional client framework, and no client-side rendering of values. Everything the page displays is either operator-supplied or server-derived, but the templates must still render through the escaping path like every other page.

Testing Decisions

A good test here asserts what an owner and a non-owner observe over HTTP: status codes, and the presence or absence of specific rendered content. It must not assert template structure, class names, or the shape of the snapshot type.

Everything lands at the existing assembled-router seam. Web handler tests already build the whole router over a real Postgres, mint real sessions, and stub Discord through its base address. That seam carries this spec.

The behaviours to cover:

  • The administrative page renders for the owner.
  • It returns not-found for a signed-in Reader who is not the owner, and unauthorised for a request with no session.
  • One test walks every administrative route and asserts a non-owner is refused by all of them. This is the test that makes the centralised gate worth having.
  • The roster renders each Reader's identity, session count and Sighting counters.
  • A Reader over the mark threshold is rendered as blocked; one under it is not.
  • Clearing a Reader's marks resets their counters, and the roster re-renders showing them unblocked.
  • Session revocation continues to work from its new home, and the owner is still not offered revocation of their own sessions.
  • The Lane status block renders one row per Site with the values supplied by an injected fake snapshot.
  • A Lane reported as clamped, refusing, or browser-unreachable renders its attention state; a healthy Lane does not.
  • An empty snapshot renders as "no data yet" rather than as zeroes.
  • The reading page no longer contains the roster, for the owner or anyone else.

Prior art: the existing tests covering the owner roster panel, owner revocation of another Reader's sessions, session gating across the UI routes, and the index rendering the login page without a session. All of them assert status codes plus rendered content against a real database, which is the pattern to follow.

Out of Scope

  • Any Series browser, search, or a control that forces a Poll on demand. A control that makes the backend fetch is a new outbound path and a security review item, and it has not been asked for.
  • A log viewer.
  • Metrics, history, or any charting. The page shows current state only.
  • Any role model beyond owner and Reader. There is one owner, seeded from configuration, and nothing here needs a third role.
  • Editing Site rest times or gaps from the page. They live in the registry, which is deliberately the one place a Site is described.
  • Live streaming of status. A thirty-second refresh is enough for a page one person opens.
  • Anything that lets an administrator change a Series' Latest Chapter or a Reader's Progress by hand.

Further Notes

This page is deliberately sequenced before the Sighting work rather than after it. The reason is narrow: the Sighting design can mark a Reader, and one of its known failure modes is a false mark produced by a broken Site adapter rather than by a dishonest Reader. Shipping the ability to create that state before the ability to clear it would leave production SQL as the only remedy.

The page ships with the counters and the clear control in place before the feature that fills them, so on delivery those columns will read zero for everyone. That is intentional and preferable to the alternative ordering.

The design system's checklist asks whether a new thing can be a hairline, a small-caps label or a serif line before it earns a colour, and that question was put and answered: a distinct accent was chosen so an unhealthy Lane is findable at a glance rather than read for. The rule it must respect is not "avoid colour" but "never borrow the two colours that already have exactly one meaning each".

Blocked by: #101 ## Problem Statement As the owner of this backend, I cannot see anything. There is no administrative page: the only operational surface in the whole system is a health endpoint that returns the word "ok", and the only administrative capability is a fold-out Reader roster tucked inside my own reading page, offering one action. That was survivable while the backend did one simple thing on a fixed schedule. It stops being survivable with Poll Lanes, where every Site has its own pace, can shorten it automatically, can be skipped for refusing, and can be halted entirely by an absent browser. None of that is visible anywhere except a log I have to be watching at the time. I cannot answer the one question that matters — is my Library actually being kept fresh — without reading logs. It is also about to become a correctness problem. The Sighting work that follows this spec can mark a Reader as untrusted, and a wrong mark can be produced by a broken adapter rather than by a lying Reader. Without a page, clearing that mark means hand-written SQL against production. ## Solution An owner-only administrative page at its own address, built from what already exists: the same server-rendered templates, the same htmx partial swaps, the same Discord session, the same owner test that already guards the Reader roster. It shows two things. The Reader roster, moved out of the reading page and given the Sighting counters and a control to clear a Reader's marks. And Poll Lane status, one row per Site: how many Series are due, when the Lane last ran, what gap it is using, whether that gap is clamped to the floor, whether the Site is currently refusing, and whether the browser is reachable. The status block refreshes itself. Nothing else about the reading experience changes, except that the reading page gets simpler when the roster moves off it. ## User Stories 1. As the owner, I want an administrative page at its own address, so that I can bookmark it and return to it rather than hunting for a panel inside my reading page. 2. As the owner, I want my reading page to stop carrying administrative controls, so that the page I use every day is only about reading. 3. As the owner, I want the page refused to every Reader who is not me, so that administrative state is not visible to the rest of the guild. 4. As the owner, I want the refusal to be indistinguishable from the page not existing, so that its address is not confirmed to someone probing for it. 5. As a Reader who is not the owner, I want no change to anything I can see or do, so that this page's arrival is invisible to me. 6. As the owner, I want the roster to keep showing each Reader's Discord identity and live session count, so that nothing I have today is lost in the move. 7. As the owner, I want to keep revoking another Reader's sessions from the roster, so that the one control I have today still works. 8. As the owner, I want to see each Reader's Sighting counters, so that I can tell an honest Reader from one whose reports keep being contradicted. 9. As the owner, I want to see clearly which Readers are currently blocked from deferring, so that I do not have to work it out from two numbers. 10. As the owner, I want to clear a Reader's marks in one action, so that a false mark caused by a broken adapter does not need SQL against production. 11. As the owner, I want clearing marks to be confirmed before it takes effect, so that I do not wipe a genuine record by mis-clicking. 12. As the owner, I want to see how many Series are due for each Site, so that I can tell a Lane that is idle from one that is drowning. 13. As the owner, I want to see when each Lane last ran, so that I can tell a Lane that is quiet because there is no work from one that has stopped. 14. As the owner, I want to see the gap each Lane is actually using, so that I can see the automatic shortening working rather than infer it. 15. As the owner, I want a Lane whose gap is clamped to the floor marked plainly, so that I learn my Library has outgrown the hourly promise without reading logs. 16. As the owner, I want to see which Sites are currently refusing, so that I know a Site has turned hostile before my Latest Chapters go stale. 17. As the owner, I want to see whether the browser is reachable, so that I know why my comix, kagane and novelfull Series have stopped moving. 18. As the owner, I want the status to update on its own while I watch it, so that I can observe a run rather than sample it by reloading. 19. As the owner, I want the status to reflect the running system rather than a stored copy, so that I never have to wonder whether I am looking at something stale. 20. As the owner, I want the page to be honest immediately after a restart rather than showing confident zeroes, so that an empty page never looks like a broken one. 21. As the owner, I want an unhealthy Lane to be visually obvious, so that I can find the problem without reading every row. 22. As the owner, I want that emphasis never to look like a New Chapter, so that the one meaning ember carries in this system stays intact. 23. As the owner, I want the page to work on my phone, so that I can check the backend without a desktop. 24. As the owner, I want the page to meet the same accessibility floor as the rest of the system, so that it is not the one screen that regresses it. 25. As a maintainer, I want the owner test written once and applied at route registration, so that a future administrative route cannot accidentally ship without it. 26. As a maintainer, I want the two hand-written owner comparisons replaced by that one wrapper, so that there is a single place the rule lives. 27. As a maintainer, I want one test proving every administrative route refuses a non-owner, so that adding a route does not require remembering to add a test. 28. As a maintainer, I want the Lane status delivered through one narrow interface, so that the page's tests do not need a running poller. 29. As a maintainer, I want the page to be a foundation rather than a finished thing, so that adding a future administrative control is a template block and a route. ## Implementation Decisions **Its own page, owner-only.** A full page at its own address, in the same shape as the existing full pages, rather than another fold-out panel. The Reader roster moves off the reading page and into it. The roster's existing tests move with it. **One owner gate, applied at registration.** Today the owner test is a hand-written comparison duplicated inside two handlers. It becomes a single wrapper layered on top of the existing session gate and applied where routes are registered, replacing both copies. This is a security boundary and the reason for centralising it is exactly that: a gate visible in the route list makes a missing gate visible too, and one test can then cover every administrative route rather than one test per route. Non-owners continue to receive a not-found rather than a forbidden, matching the existing behaviour. **Roster contents.** Discord identity, live session count, and the existing session-revocation control, all unchanged. Added: the Sighting counters for that Reader, a plain indication of whether they are currently blocked from deferring, and a control to clear their marks. Clearing is confirmed before it acts, following the same confirmation pattern the destructive controls in the reading UI already use. Note that clearing marks is not destruction in the design-system sense — it restores a privilege — so it must not borrow the destruction accent. **Lane status contents.** One row per Site: due Series count, time of last run, effective gap in use, whether that gap is clamped to the floor, whether the Site is currently in its post-refusal wait, and whether the browser is reachable. The browser fact is shared by the three browser Sites rather than per Site. **The seam.** The web handler accepts one narrow interface that reports the current Lane snapshot. In production the poller satisfies it; in tests a fake with fixed values is injected, exactly as the Discord API is already stubbed through its base address. This is the only new seam in the spec, it sits at the constructor, and it is what keeps the page's tests from needing a running poller. **Snapshot, not storage.** Lane state is held in memory by the Lanes and copied out on request. No new table, no write per round, no staleness to reason about. Every figure the page wants is something a Lane already computes each round, so the snapshot is a copy of existing knowledge rather than new work. The accepted cost is that the page is empty for the first few seconds after a restart, until each Lane completes a round; the page must present that as "no data yet" rather than as zeroes, so an empty page is never mistaken for a stopped one. **Refresh.** The status block refreshes itself every thirty seconds using the framework already vendored, as one attribute on the fragment. The roster does not refresh on a timer; it re-renders after an action, as it does today. **Visual treatment.** A new named accent token is added for a Lane needing attention. It may not be the ember, which means New Chapter and nothing else, and it may not be the destruction accent, which means destruction and nothing else. The proposal is a cool blue-green named for a copper patina — the complement of the palette's warm crimson, distinct from the existing cool blue used for archiving, and a metal name consistent with the existing brass. Values are defined in both colour branches together, as the design system requires, and no surface, ink or wash siblings are added until something needs a surface rather than a mark. **Rendering model.** Unchanged from the rest of the system: server-rendered templates parsed as a group, htmx fragments for anything that updates, no additional client framework, and no client-side rendering of values. Everything the page displays is either operator-supplied or server-derived, but the templates must still render through the escaping path like every other page. ## Testing Decisions A good test here asserts what an owner and a non-owner observe over HTTP: status codes, and the presence or absence of specific rendered content. It must not assert template structure, class names, or the shape of the snapshot type. **Everything lands at the existing assembled-router seam.** Web handler tests already build the whole router over a real Postgres, mint real sessions, and stub Discord through its base address. That seam carries this spec. The behaviours to cover: - The administrative page renders for the owner. - It returns not-found for a signed-in Reader who is not the owner, and unauthorised for a request with no session. - One test walks every administrative route and asserts a non-owner is refused by all of them. This is the test that makes the centralised gate worth having. - The roster renders each Reader's identity, session count and Sighting counters. - A Reader over the mark threshold is rendered as blocked; one under it is not. - Clearing a Reader's marks resets their counters, and the roster re-renders showing them unblocked. - Session revocation continues to work from its new home, and the owner is still not offered revocation of their own sessions. - The Lane status block renders one row per Site with the values supplied by an injected fake snapshot. - A Lane reported as clamped, refusing, or browser-unreachable renders its attention state; a healthy Lane does not. - An empty snapshot renders as "no data yet" rather than as zeroes. - The reading page no longer contains the roster, for the owner or anyone else. Prior art: the existing tests covering the owner roster panel, owner revocation of another Reader's sessions, session gating across the UI routes, and the index rendering the login page without a session. All of them assert status codes plus rendered content against a real database, which is the pattern to follow. ## Out of Scope - Any Series browser, search, or a control that forces a Poll on demand. A control that makes the backend fetch is a new outbound path and a security review item, and it has not been asked for. - A log viewer. - Metrics, history, or any charting. The page shows current state only. - Any role model beyond owner and Reader. There is one owner, seeded from configuration, and nothing here needs a third role. - Editing Site rest times or gaps from the page. They live in the registry, which is deliberately the one place a Site is described. - Live streaming of status. A thirty-second refresh is enough for a page one person opens. - Anything that lets an administrator change a Series' Latest Chapter or a Reader's Progress by hand. ## Further Notes This page is deliberately sequenced before the Sighting work rather than after it. The reason is narrow: the Sighting design can mark a Reader, and one of its known failure modes is a false mark produced by a broken Site adapter rather than by a dishonest Reader. Shipping the ability to create that state before the ability to clear it would leave production SQL as the only remedy. The page ships with the counters and the clear control in place before the feature that fills them, so on delivery those columns will read zero for everyone. That is intentional and preferable to the alternative ordering. The design system's checklist asks whether a new thing can be a hairline, a small-caps label or a serif line before it earns a colour, and that question was put and answered: a distinct accent was chosen so an unhealthy Lane is findable at a glance rather than read for. The rule it must respect is not "avoid colour" but "never borrow the two colours that already have exactly one meaning each".
sulthan added the enhancementready-for-agent labels 2026-08-16 11:28:19 +07:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sulthan/mangaBookmark#102