2026 10 22 town hall site powers design - xopherdeep/do-it-for-the-xp GitHub Wiki

The Town Hall: the site's posts, managed from the game: design

Date: 2026-10-22. Status: design approved in chat by the owner, section by section (2026-10-21/22); this spec awaits review.

Why

The Town Hall reads doit.forthexp.com (News, Patch Notes, the boards, comments) and heroes send topics, comments and bug reports to it, where they wait as pending. Everything after that happens in wp-admin: publishing a family's topic, approving a held comment, writing the news and the patch notes, reading a bug report. The owner, who is an administrator of the site, wants to do those jobs from inside the game as well, on the phone, in the Town Hall, even though no one but the owner will ever see the tools.

Success: with the owner's family linked on a phone, every pending thing on the site can be published or binned from the Town Hall, a news post or a patch note can be written and published there, and the tools never show for any other family's link.

Scope

In: site powers read from the account's capabilities; drafts, pending topics, held comments and new bug reports shown in place in the Town Hall with Publish, Approve, Resolve, Edit and Bin; a news writer in the compose sheet; a count of what waits on the clerk's button; a topic approved on a device that can publish goes up published.

Out (see section 9): featured images and rich text, scheduling, spam, hard deletes, editing comments, a Desk tab, a plugin or site change of any kind, the Game Master's local approval queue (unchanged), the multiplayer road.

Decisions (owner, 2026-10-21/22)

  • Both jobs, equally: clearing the pending queue and writing News and Patch Notes.
  • In place, on the cards. Pending items sit where they belong (top of their board, under their post, top of News) with their buttons on the card. No sixth tab, nothing in the Admin console.
  • Plain WordPress REST through the link the family already has (approach A). No plugin route, no new backend, no wp-admin in a web view.
  • Publish on approval. On a device whose link can publish, the Game Master's approval is the review: the topic is sent published. Other families' links keep sending pending topics for the owner to publish in the game.
  • The site decides who is an admin, not the game: the linked account's capabilities, read from the site. The Game Master role means nothing here.

1. Site powers

The link check (checkAndStore) already reads GET wp/v2/users/me?context=edit for the display name. The same answer carries capabilities, a map of capability name to true. A pure function in a new module, src/lib/services/townhall/townHallDesk.ts, turns it into the powers the Town Hall cares about:

export interface SitePowers {
  /** Publish, edit and bin posts and topics, including other people's: `publish_posts` and `edit_others_posts`. */
  publish: boolean;
  /** Approve and bin comments: `moderate_comments`. */
  moderate: boolean;
  /** When the site said so (ms). */
  readAt: number;
}
export function powersOf(me: { capabilities?: Record<string, unknown> } | null | undefined, now: number): SitePowers;

An administrator or editor has both; an author has neither (edit_others_posts is missing); a Contributor (every other family) has neither. The two booleans are all the screens read.

  • Stored on the link. TownHallLink gains powers?: SitePowers, written at link time. An older stored link without powers reads as no powers until the first refresh. TownHallDb needs no migration: the link is saved whole.
  • Refreshed on each Town Hall visit. The store gains refreshPowers(): one read of users/me?context=edit, best effort. A good answer replaces the stored powers (and saves the link); a failure of any kind keeps the stored ones and logs. The clerk composable calls it on mount, after init().
  • Hosted inside the site (nonce auth). The same read runs on mount and the powers are held in memory only (there is no link to save them on). So an administrator opening the app inside doit.forthexp.com gets the tools too.
  • Dropped on a refusal. dropPowers() sets both false and saves. Section 6 says when.
  • Exposed as store.powers (a SitePowers with both false when unknown) and store.canPublish / store.canModerate computeds. The Game Master's approvals page and everything else in the game ignore them.

2. In place, tab by tab

Every reader below runs only when the matching power is true; without it the tabs behave exactly as today. All reads go through townHallFetch with store.auth, the same as the feed. townHallFeed.ts gains the readers; the request builders for actions live in townHallDesk.ts (section 4); a new composable src/app/Console/MyPortal/HomeTown/TownHall/components/useTownHallDesk.ts holds the busy flags, the counts and the calls, so the tab components stay thin.

Chips (ion-chip in the card header, the class names is-draft, is-pending, is-held, is-new): Draft, Waiting, Held, New. The chip names the state in the clerk's words; the buttons sit in the card's footer row, the same row the hero's Comment and Reply buttons use, so nothing moves for a hero.

News (ordinary posts outside Patch Notes)

  • Drafts first. GET wp/v2/posts?status=draft,pending&context=edit&_embed=true&per_page=5&categories_exclude={patch} renders above the published news with the Draft chip. Buttons: Publish, Edit, Bin.
  • Published cards get Edit and Bin after the existing buttons.
  • Held comments. See Comments below: the card's comments button shows "(2 held)" when the post has any.

Updates (Patch Notes)

  • The same as News for posts in the Patch Notes category (categories={patch} instead of categories_exclude). While the site has no Patch Notes category the drafts reader is skipped, as the notes reader is today.
  • Bug Net card, between the notes and Game Statistics, for a link that can publish: GET wp/v2/compass_bug?status=pending,publish,draft&context=edit&per_page=10&orderby=date&order=desc, filtered on the device to bug_status not in resolved, closed. Each bug shows its title, date, the New chip (or the status it has), the route and the first lines of the environment. Buttons: Resolve (sets bug_status: "resolved" through Bug Net's REST field), Bin.

Board

  • Pending topics first. GET wp/v2/cafeteria_topic?status=pending&context=edit&cafeteria_board={id}&_embed=true&per_page=10 renders above the published topics of the board being looked at, with the Waiting chip. Buttons: Publish, Edit, Bin.
  • Published topics get Edit and Bin after Reply.
  • The hero's own waiting posts (TownHallWaiting, the local outbox) stay where they are, above everything.

Comments (News posts and topics)

  • A moderator's comments reader asks for everything: GET wp/v2/comments?post={id}&status=all&context=edit&per_page=50&order=asc. Held ones carry the Held chip and the buttons Approve and Bin; approved ones look as today.
  • To find them without opening every post, the desk composable reads the held comments once per visit: GET wp/v2/comments?status=hold&context=edit&per_page=20 and keeps them by post id. A News card or a topic card whose id has held comments shows the held count on its comments button. A held comment on a post that is not on the first page of its tab is reached by scrolling, as any post is; this is accepted.

The count on the clerk's button

The clerk's ion-fab (bottom centre, the building FAB) gets a badge with the number of things waiting, so "is anything waiting?" is answered on arrival without opening the tabs: drafts (news and patch notes together) + pending topics (all boards) + held comments + bug reports not resolved. The desk composable reads them once per visit, all best effort (a failed read counts nothing and shows nothing):

  • drafts: the X-WP-Total header of GET wp/v2/posts?status=draft,pending&context=edit&per_page=1;
  • pending topics: the header of GET wp/v2/cafeteria_topic?status=pending&context=edit&per_page=1;
  • held comments: the header of the held-comments read above (which the cards share);
  • bugs: the Bug Net card's read above (which the card shares), filtered on the device, so at most ten count. A header total would count resolved bugs too, since Resolve changes only bug_status.

The badge vanishes at zero. The per-tab chips appear as each tab loads its items.

3. The writer

The compose sheet (TownHallCompose.vue) gains a kind, news, for a link that can publish:

  • Fields: Title (required, 100 characters) and the body (plain text, NEWS_BODY_MAX = 8000 characters). WordPress renders the paragraphs. Site news carries no hero signature.
  • Bottom bar, where the board picker sits for a topic: a two-way picker, News | Patch Notes, and two sends, Publish (primary) and Draft. Patch Notes is offered only while the site has that category; otherwise the picker is hidden and the post is news.
  • Opened from the clerk. clerkMenuChoices gains, for a link that can publish, Write news and Write a patch note (the FAB menu is where the hero's Post on the board lives; there is no separate Write button). The choices carry a pen icon and sit after the hero's choices.
  • Editing. Edit on a draft, a post, a topic or a patch note reads the item alone in edit context (GET wp/v2/{type}/{id}?context=edit) for title.raw and content.raw, then opens the same sheet with those words, the heading Edit, the board or category picker hidden (neither changes on edit), and one send, Save. Editing a hero's topic keeps the signature line, which is ordinary text in the body. A failed read says the courier is late and opens nothing.
  • Limits and problems go through draftProblem, extended for news (title required, 8,000 characters).
  • Comments are not edited: Approve or Bin.

4. The requests, and publish on approval

townHallDesk.ts builds every admin request, pure, in the shape requestFor already uses ({ path, body } plus a method), with one spec pinning each:

Action Request
Publish a draft or a pending topic POST wp/v2/{posts|cafeteria_topic}/{id} { status: "publish" }
Create news POST wp/v2/posts { title, content, status: "publish" | "draft", categories: [patchId]? }
Save an edit POST wp/v2/{posts|cafeteria_topic}/{id} { title, content }
Bin a post or topic DELETE wp/v2/{posts|cafeteria_topic}/{id} (the site's trash, never force)
Approve a comment POST wp/v2/comments/{id} { status: "approved" }
Bin a comment DELETE wp/v2/comments/{id} (trash)
Resolve a bug POST wp/v2/compass_bug/{id} { bug_status: "resolved" }
Bin a bug DELETE wp/v2/compass_bug/{id}

Admin actions are not queued: each is one call through townHallFetch, the card is busy while it waits, and on success the item changes in place (a published draft moves down into the list, a binned item leaves, an approved comment loses its chip, the FAB count drops by one). Nothing is written to the outbox or the Town Hall database.

Bin asks once. Binning anything published or approved asks the clerk's question first (reveal prompt: "Take it down? It goes to the city's bin; the site can bring it back for thirty days."), with Take it down and Leave it. Binning a draft, a pending topic, a held comment or a bug does not ask. Publish, Approve, Resolve and Save never ask.

Publish on approval. requestFor(post, boardIds, options) gains { canPublish: boolean }: a topic is sent with status: "publish" when canPublish is true, "pending" otherwise. Comments and bug reports are unchanged (the site already approves a moderator's comments on arrival; bugs are never published). flush() passes store.canPublish. postedOutcome gains the outcome published (a topic sent by a device that can publish) and postedNotice the line "It's up on the wall already."; the hero whose post waits for the Game Master still hears the same as today.

5. Copy and look

  • The tools speak in the clerk's classic voice only: the owner is an adult, and no one else sees them. No simple or adventure variants in the catalogue for these lines.
  • Buttons use the Town Hall accent and the Earthbound box; icons from the duotone set (upload Publish, check Approve, pen Edit, trash Bin, flag-checkered Resolve). Every button does something real.
  • Nothing in the bottom nav, the segment or the board chips changes. The FAB badge is the one new thing in the footer area.

6. Errors, in the clerk's voice

Reply What happens
401 on any call As today: the link is forgotten, the clerk asks the family to register again
403 on an admin action or reader The site took the keys back: dropPowers(), the tools and chips vanish, the clerk says "The city has taken back your keys. Register again if that's a mistake." Not retried
Offline, 5xx, timeout The buttons stay but a tap gets the courier-late prompt (retry or never mind); nothing is queued. Readers show nothing extra, never an error card
400 (the site refused the text) The site's message shows on the card, as a hero's failed post does
refreshPowers() fails The stored powers stand; a failed refresh never hides the tools by itself
The edit read fails Courier late; the sheet does not open

Inside the building these are reveal prompts, never Ionic alerts.

7. Testing and rehearsal

  • Pure specs (tests/unit/): townHallDesk.spec.ts (powers from capabilities: admin, editor, author, contributor, no capabilities; each request in the table; the readers' queries; the count sum), townHallPosts.spec.ts (publish on approval; the news draft problems), townHallFeed.spec.ts (drafts, pending topics, held comments, bugs readers; the moderator's comments query), townHallOutbox.spec.ts (the published outcome), townHallStore.spec.ts (powers stored at link time, refreshed on open, kept on a failed refresh, dropped on 403, nonce-hosted powers in memory only).
  • Component specs: TownHallNews, TownHallUpdates, TownHallBoard, the comments block and TownHallCompose show the tools only with the power, emit the action with the id, show the chips, and the news kind's picker and two sends; useTownHallDesk.spec.ts for busy flags, in-place changes and the 403 drop.
  • Cypress (tests/e2e/specs/town-hall.spec.js): an admin run where the users/me?context=edit stub answers with capabilities, the drafts, pending topics, held comments and bugs stubs answer, the chips and the FAB badge show, Publish hits the stubbed write, and the same page with a Contributor's answer shows none of it. Writes stay stubbed (the spec already refuses unstubbed writes with 599).
  • Rehearsal on the local Docker WordPress (http://localhost, Bulletin Board and Bug Net are active there) with the local admin's link through the development site override, after making the Patch Notes category there. Then doit.forthexp.com with the owner's go-ahead. No site change is needed for the feature.

8. Delivery

A feature branch (feat/town-hall-site-powers), a PR opened with auto-merge, the Tests check as the gate. Board tasks from the implementation plan (writing-plans next), in the order: desk module and feed readers; store powers; the outbox change; the compose sheet; the tabs; the FAB count; Cypress and rehearsal.

Files touched: src/lib/services/townhall/townHallDesk.ts (new), townHallTypes.ts, townHallFeed.ts, townHallPosts.ts, townHallOutbox.ts, src/lib/store/stores/townHall.ts, src/lib/services/hometown/hometownPrompts.ts, components/useTownHallDesk.ts (new), useTownHallPosting.ts, useTownHallClerk.ts, TownHallCompose.vue, TownHallNews.vue, TownHallUpdates.vue, TownHallBoard.vue, TownHall.vue/.ts, the specs above.

9. Out of scope, follow-ups

  • Featured images, rich text, scheduling: edit on the site (the open-on-the-site link stays on every card).
  • Marking spam, hard deletes, restoring from the trash, editing comments, replying as the site (the owner's comments already go through the hero flow and are approved on arrival).
  • A second page of drafts or of held comments; a Desk tab; counts per tab in the segment.
  • Other administrators: the tools show for any link whose account has the powers, which today is the owner alone.
  • The game reporting into xp/v1 (the flagship audit) is a separate piece of work.
⚠️ **GitHub.com Fallback** ⚠️