From design/ia.md in the repository.
On this page (12 sections)
Annoudio information architecture
Status: decision, 2026-10-09. ROADMAP Phase 5c step 1. Phase 5c step 2 builds this file. Read CONTEXT.md first; this file uses its words, including Reply and Topic.
Sources: ia-C-journey-centred.md (the spine: lifecycle, who sees what, mails, member home, system return, no Unpublish), ia-A-task-centred.md (operator navigation: Today, job sidebar, Publish → next story, the 3-step wizard), ia-B-object-centred.md (the project frame on public surfaces, one project overview page). Not taken from B: per-project list scoping, the project cookie, the replaced sidebar, the two-level navigation, the stored project ↔ recording link. Payload facts come from the feasibility sections of A, B and C (checked against 3.90.2 types).
0 Decision summary
- One story lifecycle with six names (Processing, Waiting for review, Returned, Published, Published · change waiting, Removed) on every surface: admin, Annotate page, player note, mail.
- Unpublish goes. A story leaves the player by Return to the member (with a reason) or Remove. A return of a change keeps the published version visible.
- The operator home is Today: a setup checklist on an empty stack, then the work that waits.
- The sidebar names jobs in ≤ 3 words: Today, Review stories, Add a widget, Vocabulary, Members; then Settings: Projects, Recordings, Operators, Your account. (Vocabulary replaced Concepts on 2026-10-09:
design/vocab.md§1.) - Publish opens a dialog with the visitor preview; after it, the next waiting story opens. Next story replaces "Keep as draft".
- Add a widget to a site has 3 steps: Recording, Site (the project), Snippet. A new public route
GET /embed/v1/recordings/{project}/{id}.jsonserves the CMS plugins. - Members get a home, Your stories, and Your account (name, mail choice, sign out). Six mails close every handoff; each has one link.
- The project frame (name, theme, rights line) shows on the Annotate page; the player shows theme and rights line. The theme JSON gets three additive keys:
name,rightsLine,rightsIri. - Schema: five new fields (story
toldOn,changedBy,lastAction,rejection.automatic; membermailNotices; operatorreviewMail). One migration. - Counts split stories and replies in words ("2 stories and 1 reply wait for review"); the catalogue column counts stories only, as the player's
totaldoes.
1 People, jobs, handoffs
| Person | Device | Home | Daily or common jobs |
|---|---|---|---|
| Operator | Laptop | Today (/admin) |
Review stories; put a recording on a site; keep the concepts tidy; look after members; set up projects |
| Member | Phone first | Your stories (/annotate/mine) |
Tell a story; fix a returned story; read the result; reply |
| Visitor | Any | The host page with the player | Listen; read stories and replies; select Tell a story or Reply |
A handoff is the moment when one person's action is the next person's trigger. Each handoff has a signal that reaches the next person when that person is away.
| # | From → to | Action | Signal |
|---|---|---|---|
| H1 | Operator → host site | Copy the snippet (step 3) | Clipboard; the operator pastes it |
| H2 | Host site → visitor | Page loads | The player |
| H3 | Visitor → member | Tell a story / Reply | Annotate page at t |
| H4 | Member → operator | Send for review | Count on Today and the sidebar; M6 |
| H5 | Worker → story | Media ready or failed | State change; the page polls; a failure is a system return (M2) |
| H6 | Operator → member | Return to the member | M2; banner on the story |
| H7 | Operator → member | Publish | M3; badge in Your stories |
| H8 | Operator → visitor | Publish | The player shows it within STORIES_MAX_AGE (60 s) |
| H9 | Replier → parent's member | Reply is published | M4 |
| H10 | Operator → member | Remove | M5 |
2 Story lifecycle
2.1 States
The UI state is derived from stored data. One function, stories.lifecycle(row), derives it. The admin, the Annotate page, the mails and the counts call only this function.
| UI state | Stored as | Badge role |
|---|---|---|
| Writing | Not stored. Annotate page only. | none |
| Processing | draft, no rejection, a media body not ready |
neutral |
| Waiting for review | draft, no rejection, all media ready |
attention |
| Returned | draft with rejection (automatic = operator or system) |
warning |
| Published | published, no newer draft |
success |
| Published · change waiting | published plus a newer draft. The change has its own state: Processing, Waiting for review or Returned. |
success + attention |
| Removed | No row. M5 is the record. | none |
Transitions:
Writing ──Send for review──> Processing ──media ready──> Waiting for review ──Publish──> Published
│ file failed (system) │ Return │ │
v v │ │ member changes
Returned <────────────────────────┘<──── Return ──────────┘ v
│ member: Send for review Published · change waiting
└────> Processing or Waiting (Return or Publish acts on the change)
Remove: from any state → Removed (the story and its replies)
Rules:
publish-at-once: Waiting for review is skipped. Writing → Published, or Processing → Published when the media is ready (themedia.settledconsumer publishes it).- Return of a Published story (no newer draft) makes it a draft with the reason: visitors stop seeing it. This is the take-down path. Return of a change returns the newer draft only: the published version stays visible (Phase 5 decision 5).
- System return: when a media body fails, the
media.settledconsumer setsrejectionwithautomatic: trueand the reason "We could not use your file. Attach a different file." The operator sees "Returned (automatic)". - A member never sees "draft". An operator sees "Draft" only in History.
- A reply has the same states. Its parent must be Published for the reply to show (Phase 5b decision 7).
2.2 Who sees what
| State | Operator sees | Member sees | Visitor sees | Next move (who) | |
|---|---|---|---|---|---|
| Writing | nothing | the form | nothing | none | Send for review (member) |
| Processing | Row in Processing tab: "You can publish when the file is ready." | "Processing your voice note. You can leave this page." | nothing | none | wait (system) |
| Waiting for review | Row in Waiting for review, oldest first, with age ("2 days"); Today count | "Waits for review. We send you a mail when it is published." | nothing | M6 to operators | Publish / Return to the member (operator) |
| Returned | Row in Returned with reason and age; "(automatic)" for a system return | Banner "Returned" with the reason; Change the story | nothing (or the published version, rule 2) | M2 | Send for review (member) |
| Published | Row in Published; Open on the site | "Published"; Listen on the site | the story | M3 (rule M3 below) | none |
| Published · change waiting | Row in Waiting for review marked "Change to a published story" | "Published. Your change waits for review." | the published version | M6 | as Waiting for review |
| Removed | gone from lists | gone from Your stories | gone (≤ 60 s) | M5 | none |
2.3 Mails
Mail rules: plain text plus a simple HTML copy. One link per mail. No operator name in a member mail: the voice is "the River Voices team" (it protects operators; the admin keeps rejection.operator). Mails go after the commit, never inside a hook transaction. A failed send logs and does not undo the action. Sender address: EMAIL_FROM (exists). Sender name: the project name for member mails, "Annoudio" for operator mails.
| ID | To | Sent when | Not sent when | Subject | Body (essentials) | Link |
|---|---|---|---|---|---|---|
| M1 | member | a login code is asked (exists) | never skipped | Your login code for River Voices | Code; "It works for 10 minutes." "Your account works on all sites of this organisation." | none |
| M2 | member | Returned (operator or system) | never skipped (it asks for an action) | Your story needs a change | Recording title, time range 1:02–1:35, the reason, "Visitors do not see the story until it is published." |
Change your story → /annotate?project&recording&story=<id>&return=<toldOn> |
| M3 | member | Published by an operator, or by the system after processing | the save published it at once (publish-at-once, no media wait): the member saw it; mailNotices off |
Your story is published | Title, range, "Visitors see it in the player now." | Listen on the site → toldOn + #annoudio-t=<start>&annoudio-project&annoudio-recording&annoudio-story=<id>; no toldOn → Your stories |
| M4 | parent's member | a reply to their story is published | the replier is the parent's member; mailNotices off |
Someone replied to your story | Replier's name, first 200 characters of the reply | Read the reply → as M3, at the parent story |
| M5 | member (and each reply's member) | Remove | never skipped | Your story is removed | Title, range, the reason if given; for a reply: "The story that you replied to is removed." | Your stories |
| M6 | operators | digest: once a day at 08:00 stack time, only when ≥ 1 story or reply waits; or at each H4 | publish-at-once (nothing waits); operator reviewMail = never |
2 stories and 1 reply wait for review | Count per project; the oldest age | Review the stories → /admin/collections/stories (Waiting tab) |
| M7 | operator | password reset (Payload forgotPassword) |
— | Reset your Annoudio password | Link, expiry | the reset link |
Opt-outs: the member's Your account has one checkbox, "Mail me when my story is published or gets a reply" (mailNotices, default on; M3, M4). The operator's Your account has "Review mail: once a day (default) / for each story / never" (reviewMail). The choice is per person, not per project: the person who gets the mail owns the choice.
3 Object model as each role sees it
| Object | Operator | Member | Visitor |
|---|---|---|---|
| Project | "One for each site". Settings → Projects; Overview tab with counts | Project name above the recording title; theme; rights line in the footer | Theme and rights line only; never the word |
| Recording | Title, audio address, length, snippet, story count | Title and audio at the top of the form | The audio in the player |
| Instance | Never a word. A filter: "Ferry crossing, 1962 in River Voices" | Never | One player |
| Story | Queue row and review screen with the lifecycle badge | A card in Your stories with the same badge | A story in the list and a region |
| Reply | A story marked "Reply to: …"; the parent shows above it | A card "Reply to the story by Asha" | The thread under the open story |
| Concept / Vocabulary | Concept in a tree with story counts | Topic chips | Topic labels (tag part) |
| Body | Text, voice note, photo, video in the review panel | Text field, record button, attach button | story-text, story-media |
| Snippet | Step 3; the recording page | never | never |
| Member | Name, email, stories by state, Block | "Signed in as Asha"; Your account | The name in story-meta |
| Media body | Reached from its story only (not in the sidebar) | Part of the story | Part of the story |
Nobody outside the admin sees Instance, Catalogue, IRI, Container, Body or Media body as words.
4 Navigation
4.1 Admin
┌ Annoudio ─────────────┬──────────────────────────────────────────────┐
│ ● Today │ │
│ Review stories 3 │ (content) │
│ Add a widget │ │
│ Concepts │ │
│ Members │ │
│ SETTINGS │ │
│ Projects │ │
│ Recordings │ │
│ Operators │ │
│ Your account │ │
└───────────────────────┴──────────────────────────────────────────────┘
Rules:
- The badge on Review stories is the Waiting for review count (stories + replies). No other badge.
- Job links come from
beforeNavLinks. Stories, Media bodies, Concepts and Members havegroup: false. Projects, Recordings and Operators are in the group "Settings". The default Payload Nav stays. - Object pages (story, recording, concept, member, project) stay reachable by URL and by links. Breadcrumbs name the job: "Review stories / Story by Asha, 1:02–1:35".
- Every job ends on a screen that says what happens next.
Today, empty stack and normal day:
┌ Today ─────────────────────────────────┐ ┌ Today ─────────────────────────────────────┐
│ Put your first player on a site │ │ 2 stories and 1 reply wait for review. │
│ [x] 1 Make a project for your site │ │ The oldest came in 2 days ago. │
│ [ ] 2 Add a recording │ │ [ Start reviewing ] │
│ [ ] 3 Put the snippet on a site │ │ 1 story is processing. │
│ [ I pasted it ] │ │ 2 stories are returned. Members must │
│ [ ] 4 Publish the first story │ │ change them. 1 is automatic (file). │
│ Optional: add concepts, so members │ │ Other jobs: [ Add a widget ] [ Concepts ] │
│ can choose topics. │ │ Last 7 days: 12 in, 9 published, │
│ [ Add a recording ] ←next │ │ 2 returned, 1 removed. │
└────────────────────────────────────────┘ └────────────────────────────────────────────┘
Checklist rules: a step ticks from data. 1: a project exists. 2: a recording exists. 3: the operator selects I pasted it, or a story arrives with a toldOn on a site of the project. 4: a story is published. The checklist goes when all four tick, or with Hide the list. Empty queue text: "No stories wait for review. The last story came in 3 days ago."
Review screen (Waiting for review) and the Publish dialog:
┌ Review stories / Story by Asha · Ferry crossing, 1962 ─── [Waiting for review · 2 days] ┐
│ River Voices │
│ ▶ 1:02–1:35 ───■────────── │
│ "My father worked the ferry. The horn meant…" │
│ Topics: Ferry, Horn Voice note ▶ 0:41 (ready) │
│ Told on: rivervoices.example.org/ferry [ Open on the site ] │
│ No topic fits? [ Add a concept to River Voices ] │
│ [ Publish ] [ Return to the member ] [ Next story ] ⋮ Remove │
│ Tabs: Story · History │
└─────────────────────────────────────────────────────────────────────────────────────────┘
┌ Publish this story? ────────────────────────────────┐
│ Visitors see it like this, within one minute: │
│ ┌ Asha · 1:02–1:35 ───────────────────────────────┐ │
│ │ My father worked the ferry… [Ferry] [Horn] │ │
│ └─────────────────────────────────────────────────┘ │
│ We mail Asha that it is published. │
│ [ Publish the story ] [ Cancel ] │
└─────────────────────────────────────────────────────┘
After Publish the story: toast "Published. Visitors see it within one minute." and the next waiting story opens; at the end, the empty Waiting tab. In Processing, Publish is off with "The voice note is still processing." For a reply, the parent story shows above, read-only.
Add a widget to a site (/admin/add-widget):
┌ Add a widget to a site ── 1 Recording ─ 2 Site ─ 3 Snippet ─────────┐
│ 1 Recording │
│ ( ) Choose a recording [ search the recordings… ] │
│ (•) New recording Title [Ferry crossing, 1962 ] │
│ Audio address [https://archive.example/f.mp3] │
│ ✓ The audio plays in this browser. Length: 30:34. │
│ [ Next ] │
├───────────────────────────────────────────────────────────────────────┤
│ 2 Site │
│ Which site shows the player? │
│ (•) River Voices rivervoices.example.org │
│ ( ) Kochi Voices kochivoices.example.org │
│ ( ) New project: Name [ ] Website address [https:// ] │
│ [ Next ] │
├───────────────────────────────────────────────────────────────────────┤
│ 3 Snippet │
│ <script async src="https://stories.org.example/embed/v1.js"></script> │
│ <annoudio-player project="river-voices" recording="1" │
│ src="https://archive.example/f.mp3"></annoudio-player> │
│ [ Copy the snippet ] ✓ Copied. │
│ Paste these two lines into the page. A page needs the first line │
│ one time only. Members come back only to rivervoices.example.org. │
│ WordPress or Omeka S: type the recording number 1 in the block. │
│ ┌ preview: the real player with the River Voices theme ──────────────┐│
│ └────────────────────────────────────────────────────────────────────┘│
│ [ Done ] │
└───────────────────────────────────────────────────────────────────────┘
How the project is chosen: step 2 lists the projects. With one project, it is selected and step 2 shows one line, "Site: River Voices. [Change]". With no project, step 2 shows the inline form (name, website address); the other project settings get defaults. The snippet holds the three values: project (step 2 slug), recording (step 1 id), src (step 1 audio address). Done goes to Today. The wizard and the recording page call the same catalogue functions; the Recordings list has no create button of its own: its button is Add a widget.
The recording JSON route GET /embed/v1/recordings/{project}/{id}.json → { id, title, url, duration } (CORS *, anonymous, 404 for an unknown project or recording). Callers: the WordPress block and the Omeka S module (the editor check fills src from the number; Phase 7a request 1), and step 3's line "type the recording number 1", which is true only because of this route. Step 3 itself reads catalogue.recording(id) in process, the same function that the route serves.
4.2 Annotate page (member, phone)
No sidebar. Top bar: project name (small), recording title, "Asha ▾" (menu: Your stories, Your account, Sign out). One way back on every screen: Back to the site (the checked return), else Your stories. Footer: the rights line, for example "Stories here are shared under CC BY 4.0." with the rights link. The page sets the project's theme tokens on its root.
Your stories (phone, 360 px):
┌ Your stories ─────────────── Asha ▾ ┐
│ Returned to you (1) │
│ ┌ Ferry crossing, 1962 · 1:02–1:35 ┐│
│ │ River Voices [Returned] ││
│ │ "Please remove the full name…" ││
│ │ [ Change the story ] ││
│ └──────────────────────────────────┘│
│ Processing (0) │
│ Waiting for review (1) ▸ │
│ Published (4) ▸ │
│ Replies (2) ▸ │
│ ─────────────────────────────────── │
│ Empty: You have no stories yet. On │
│ a site, select "Tell a story". │
└─────────────────────────────────────┘
Groups in this order: Returned to you, Processing, Waiting for review, Published (with "change waiting" on its card), Replies. Each card names its recording, project and range. Your stories has no single project, so it uses the default theme.
Your account (minimum): Email (read-only), Name (Save), checkbox "Mail me when my story is published or gets a reply", Sign out. Delete: open question 1.
4.3 Player (visitor)
No navigation. Exits: Tell a story (the Annotate link) and Reply (per open story). The project frame: theme tokens (exists) and the rights line once, below the story list, in a new part rights. The player does not show the project name: the host page already names the site.
Empty state:
┌ ▶ 0:00 / 30:34 ───────────────────────────── ┐
│ No stories yet. Tell the first one. │ ← message
│ [ Tell a story ] │ ← annotate-link
│ Stories here are shared under CC BY 4.0. │ ← rights (new part)
└──────────────────────────────────────────────┘
4.4 Links between surfaces
| From | Words | To |
|---|---|---|
| Step 3, recording page | Copy the snippet | host site (by paste) |
| Review screen | Open on the site | toldOn + #annoudio-t=<start>&annoudio-project&annoudio-recording&annoudio-story=<id> |
| Player | Tell a story / Reply | /annotate?project&recording&t&return (+ replyTo) |
| Annotate page | Back to the site | return + fragment with annoudio-saved=draft or published |
| M2 | Change your story | /annotate?project&recording&story=<id>&return=<toldOn> |
| M3, M4 | Listen on the site / Read the reply | as Open on the site; no toldOn → /annotate/mine |
| M5 | Your stories | /annotate/mine |
| M6 | Review the stories | the Waiting tab |
5 Screen inventory
Marks: NEW, CHANGED, UNCHANGED against the Phase 5b code.
5.1 Admin
| ID | Screen | States | Primary action, button words | Mark |
|---|---|---|---|---|
| O0 | Create the first operator | form; field errors | Create the first operator | CHANGED (words) |
| O1 | Sign in | form; wrong email or password; locked | Sign in, Forgot your password? | CHANGED (logo, words) |
| O2 | Forgot / set password | request; sent; new password; link expired | Send the link, Set the password | CHANGED (mail works) |
| O3 | Today | setup checklist; work waits; nothing waits; a count failed ("We could not count the stories. Reload the page.") | Start reviewing, or the next checklist step | CHANGED (replaces Dashboard) |
| O4 | Review stories (list) | tabs Waiting for review · Processing · Returned · Published · All, each with a count and its own empty text; search on excerpt and name | Start reviewing (opens the oldest) | CHANGED (Processing tab, empty texts, search) |
| O5 | Review one story | one view per lifecycle state; reply variant (parent above); publish error; the answers in field order as "Label: value", an own answer as "Ravi (new)" (design/vocab.md §8) |
Publish, Return to the member, Next story, ⋮ Remove; Open on the site; Add "Ravi" to People; "No item fits?" Add an item to Places | CHANGED (no Unpublish, no Keep as draft; answers 2026-10-09) |
| O5a | Publish dialog | preview; publishing; error | Publish the story, Cancel | NEW |
| O5b | Return dialog | reason empty ("Write the reason. The member sees it."); filled; for a Published story: "Visitors stop seeing this story." | Return the story, Cancel | CHANGED (text) |
| O5c | Remove dialog | optional reason; with replies: "Its 2 replies are removed too." | Remove the story, Cancel | NEW |
| O6 | History (story tab) | lines "Published by Ravi, 9 Oct 14:02", "Changed by Asha (member)", "Returned by Ravi: reason", "Returned automatically: file failed"; one line | none | NEW |
| O7 | Add a widget to a site, steps 1–3 | step 1: choose / new, checking, plays, cannot play (save allowed), not https; step 2: one project, many, none (inline form), website address missing; step 3: copied, copy refused (select-all fallback) | Next, Copy the snippet, Done | NEW |
| O8 | Recordings (Settings) | empty "No recordings yet."; list: Title, Length, Stories 3 (2) |
Add a widget | CHANGED (button, no create view) |
| O9 | Recording page | edit; snippet panel per project; "Stories on this recording" link | Save, Copy the snippet | UNCHANGED |
| O10 | Lists and the page of one list (/admin/concepts/{project}[?list=]) |
Lists: "Members pick from these lists when they answer a question.", rows with counts and Open; a list: its tree, or rows Label · Same as · stories for an entity list; empty "Places is empty. Members can choose nothing from it yet." | Add a list, Add an item, Download the list (SKOS), Load a SKOS file | CHANGED 2026-10-09 (design/vocab.md §8 O10l) |
| O11 | Item form | new; in use "12 stories use this item"; hidden; Same as | Save, Hide from new stories | CHANGED 2026-10-09 (item words, list, Same as) |
| O12 | SKOS import preview | parsed; refused file | Import these changes | UNCHANGED |
| O13 | Members list | empty "No members yet. Members sign up on the Annotate page."; list | row | CHANGED (empty text) |
| O14 | Member page | active; blocked ("This member cannot sign in. Their published stories stay."); stories by state | Block this member, Unblock | CHANGED (stories by state) |
| O15 | Projects list | empty "No projects yet. A project is one site: its topics, look and review rule."; list | New project | CHANGED (empty text) |
| O16 | Project page | tabs Overview (new, first) · existing tabs; Overview: stories and replies by state, recordings with stories here, concept count, members with stories here, links "Review this project's stories" (a where link) |
Save | CHANGED (Overview tab) |
| O17 | Operators list and form | list; one operator ("You are the only operator. Add a colleague."); mail not set up line | Add an operator | CHANGED (words) |
| O18 | Your account (operator) | password; Review mail choice | Save | CHANGED (reviewMail) |
5.2 Annotate page
| ID | Screen | States | Primary action | Mark |
|---|---|---|---|---|
| P0 | Not found | unknown recording; unknown parent story | Back to the site (when return is valid) |
UNCHANGED |
| P1 | Sign in: email | default; invalid; rate limited; send failed. The moment shows above: "You tell a story about 1:02." | Send the code | CHANGED (frame) |
| P2 | Sign in: code | waiting; wrong (tries left); expired; too many | Sign in, Send a new code, Use a different email | CHANGED (frame) |
| P3 | Choose your name | first sign-in only; empty; too long; has @ | Continue | UNCHANGED |
| P4 | Tell a story | empty; filled; field errors; saving. Start and End shown 1:02, typed m:ss or seconds; Use the player time |
Send for review (review) / Publish the story (publish-at-once) |
CHANGED (words, frame) |
| P4r | Reply | parent shown; text; media | Send for review / Publish the reply | CHANGED (words) |
| P4m | Voice note, photo or video | idle; recording; listen; uploading %; processing; ready; failed; no microphone | Record a voice note, Attach this voice note | CHANGED (words) |
| P5 | Sent (no valid return) |
Processing; Waiting for review; Published | Your stories | CHANGED |
| P6 | Your story (story=<id>) |
Processing; Waiting; Returned (reason banner); Published; change waiting | Change the story | CHANGED |
| P7 | Your stories | empty; groups (4.2) | Change the story on a returned card | NEW |
| P8 | Your account | saved; name error | Save, Sign out | NEW |
| P9 | Session ended | after 30 days or sign-out elsewhere: "Sign in again. Your text stays on this page." | Send the code | CHANGED (text kept in sessionStorage) |
5.3 Player
| ID | State | Shows | Primary | Mark |
|---|---|---|---|---|
| V0 | Before the loader | fallback child content | — | UNCHANGED |
| V1 | Loading | controls, no stories | Play | UNCHANGED |
| V2 | Ready, no stories | message "No stories yet. Tell the first one."; rights |
Tell a story | CHANGED (text, link word) |
| V3 | Ready, stories | regions, stories, tags, rights |
Play | CHANGED (rights) |
| V4 | Story open | story active, thread |
Reply | UNCHANGED |
| V5 | Back from the Annotate page | "Your story is sent. It shows here after review." / "Your story is published. It can take one minute to show." (annoudio-saved=published) |
Play | CHANGED |
| V6 | Opened from a mail or the admin | annoudio-story=<id> opens that story (as annoudio-reply), no note |
Play | NEW |
| V7 | Errors | the six codes of CONTRACT §9 | as §9 | UNCHANGED |
| V8 | annotate="none" |
V1–V6 without links | Play | UNCHANGED |
Mail screens M1–M7: section 2.3 (M1 CHANGED: project name in the subject; M2–M7 NEW).
6 Flows
F1 First operator on an empty stack. 1. /admin → O0 → Create the first operator. 2. O3 shows the checklist. 3. Make a project → O15 → name, website address, rights line → Save; step 1 ticks. 4. Add a recording → O7 → F2.
F2 Recording to live player. 1. O7 step 1: New recording, title, audio address; the probe says "The audio plays in this browser. Length: 30:34." Next. 2. Step 2: River Voices is selected (one project). Next. 3. Step 3: Copy the snippet → "Copied." The operator pastes it on the host page (H1). On WordPress or Omeka S, the editor types the number 1; the block reads the recording route. 4. Done → O3; the operator selects I pasted it; step 3 ticks. 5. A visitor opens the page: V2.
F3 Story to visible. 1. Visitor plays to 1:02 → Tell a story (H3) → P1. 2. P1 → M1 → P2 → P3 (first time) → P4 with Start 1:02. 3. Member sets End, writes, picks topics, records a voice note (P4m). 4. Send for review → host page, V5 "Your story is sent…" (or P5). The story stores toldOn. 5. Worker ends (H5): Processing → Waiting for review. M6 goes the next morning. 6. Operator: O3 Start reviewing → O5 → Publish → O5a → Publish the story; the next story opens. 7. M3 to the member (H7); the visitor sees the story in ≤ 60 s (H8). With publish-at-once: step 4 shows "published", steps 5–6 do not occur, M3 goes only if the media made the publish wait.
F4 Returned and sent again. 1. O5 Return to the member → O5b reason → Return the story. The next story opens; the row moves to Returned. 2. M2 (H6) → Change your story → P6 with the reason banner (P1–P2 first if signed out). 3. Member changes it → Send for review. The rejection clears. 4. O5 shows "Sent again after a return"; History keeps the reason. Publish → F3 step 7. Variant: a change to a Published story; the published version stays visible during the whole flow. Variant: a failed file gives the same flow with "Returned (automatic)" and no operator step 1.
F5 Item added and used. 1. O5: no item fits → Add an item to Places → the page of the list → Add an item → O11 with the project and the list set → label "Strike" → Save. Or O5: an own answer "Ravi (new)" → Add "Ravi" to People (the story keeps its own answer). 2. Members see "Strike" in P4 at the next page load. Operators do not change stories: the operator can return the story with "Choose Strike as the place". 3. After the next publish, the widget shows "Place: Strike". 4. A later rename runs conceptChanged; O11 says "12 stories use this item".
F6 Reply. 1. Visitor opens a story (V4) → Reply → Annotate page with replyTo. 2. Sign in as F3 → P4r → Send for review. 3. O4 row "Reply to: My father worked…"; O5 shows the parent above. Publish. 4. M3 to the replier, M4 to the parent's member (H9). Visitors see the thread under the parent.
F7 Take down. 1. A complaint names a story. O4 All tab, search → O5 (Published). 2. Content can come back after a change → Return to the member (it leaves the player; M2). Content must go → ⋮ Remove → O5c → Remove the story (its replies go too; M5 to each member). 3. The player drops it within 60 s.
7 Naming
| UI word | Where | Forbidden | Reason |
|---|---|---|---|
| Story | all | comment, note, annotation (UI), post, submission | CONTEXT term |
| Reply | all | comment, response | CONTEXT term |
| Question, answer | Annotate page ("About this part"), review panel | field, property, value (code words) | design/vocab.md §1; CONTEXT: Field, Value |
| Widget | all UI text, mails, ARIA labels (concept-map W1) | player (UI), embed | CONTEXT term; the tag stays annoudio-player |
| Item, list | admin (Lists, O10, O11); "Topics" is the name of one list | concept, tag, term (SKOS and code only) | CONTEXT: Item, List; design/vocab.md §1 |
| Tell a story | player link, P4 title | Add a story (today), Annotate, Contribute | the member's own words for the job; no time in the link (the page says the time) |
| Send for review | P4, P4r with review |
Save the story (today), Submit | names what happens next |
| Publish / Publish the story | O5, O5a; P4 with publish-at-once |
Approve, Publish changes, Go live | one verb on both sides |
| Return to the member / Returned / Returned to you | O5, badges, P6, P7, M2 | Reject, Rejected, Decline | says what happens; matches the badge |
| Remove / Removed | O5c, M5 | Delete (UI), Take down, Unpublish | one exit word; Unpublish is gone |
| Next story | O5 | Keep as draft | the button saves nothing; it moves on |
| Processing · Waiting for review · Published · change waiting | all | Pending, Draft (outside History), Waits for review (today), In moderation | one lifecycle (2.1) |
Time range 1:02–1:35 |
all; en dash, m:ss, h:mm:ss over one hour | 42 s to 51 s, t=62,95, "Start (seconds)" |
one format, human time |
| Today · Review stories · Add a widget · Vocabulary · Members | sidebar (≤ 3 words) | Add a player, Put a recording on a site (renamed 2026-10-09, concept-map W2), Tidy the vocabulary, Look after members (A's long verbs) | short and translatable; the page title keeps the full job name |
| Add a widget to a site | O7 title only | Embed wizard, Put a recording on a site | names the whole job once |
| Website address · Audio address · Length | admin | origin, URL, duration | Phase 5 words |
| Rights line | admin field; the text itself on public surfaces | licence field | CONTEXT term |
| Snippet | admin | embed code | CONTEXT term |
| Login code | P1, P2, M1 | OTP, magic link | CONTEXT term |
| Your stories · Your account | member, operator account | My contributions, Dashboard, Profile | one possessive pattern |
| Member · Operator · Name | all | user, admin, staff, annotator, nickname (UI) | CONTEXT terms |
8 Data model and interface changes
| Change | Module | Kind | Migration |
|---|---|---|---|
stories.toldOn (text, operator-only; the checked return of the last save that had one; never in WADM) |
stories |
field + saveDraft input |
yes |
stories.changedBy (text: operator email or "member") and stories.lastAction (select: saved, published, returned, returned-automatically) on each version |
stories |
fields, set in beforeChange |
yes (main and versions tables) |
stories.rejection.automatic (checkbox) |
stories |
field | yes |
members.mailNotices (checkbox, default true) |
membership |
field | yes |
users.reviewMail (select: daily, each, never; default daily) |
operators (Users.ts) |
field | yes |
stories.lifecycle(row), nextWaiting(afterId), ownStories(member), remove(operator, id, reason?); counts gains processing and a stories/replies split |
stories |
interface | no |
stories.unpublish |
stories |
delete when no caller is left (reject covers it) |
no |
media.settled consumer: system return on failure; publish for publish-at-once |
media → stories |
behaviour | no |
Mailer.send(kind, to, data) with M2–M5 templates; send after commit |
membership |
interface | no |
Payload email adapter (@payloadcms/email-nodemailer on SMTP_URL, EMAIL_FROM) for M6, M7 |
config | dependency | no |
Daily digest.due job: the worker schedules it, the app consumes it and sends M6 |
worker, app |
job | no |
Theme JSON adds name, rightsLine, rightsIri (CONTRACT §3.2 rule 6: the stack can add keys) |
projects |
route, additive | no |
GET /embed/v1/recordings/{project}/{id}.json → { id, title, url, duration }, CORS *, cache as the theme route |
catalogue |
new public route | no |
catalogue.register({ title, url, length }), projects.create({ name, origin }) for O7 |
catalogue, projects |
interface | no |
CONTRACT §7.2: annoudio-saved=published; key annoudio-story=<id> (opens the story, no note) |
player |
additive | no |
CONTRACT §3.3: part rights (the project rights line, below stories) |
player |
additive | no |
| Player texts: link "Tell a story"; empty "No stories yet. Tell the first one."; V5 notes | player |
text (CONTRACT §9 rule 2: not contract) | no |
Annotate routes /annotate/mine, /annotate/account |
annotate-page |
new routes | no |
No projects.recordings link: the instance stays derived (CONTEXT). The Overview tab lists recordings with stories in the project only.
9 Feasibility per screen
Payload default = no code beyond labels. Customised = Payload slots and API. Custom page = an admin view (admin.components.views[key] with DefaultTemplate, own operator check). Outside = Next routes or embed/.
| Screen | Path | How | Flag |
|---|---|---|---|
| O0, O1 | customised | i18n, graphics.Logo, beforeLogin |
O0 has no component slot |
| O2, M7 | customised | auth.forgotPassword.generateEmail* + email adapter |
needs SMTP_URL |
| O3 Today | customised | views.dashboard replacement (not experimental widgets) |
reads module interfaces only |
| Sidebar | customised | beforeNavLinks, group: false, group "Settings" |
cache counts 30 s |
| O4 | customised | baseFilter, tab links (exist), beforeListTable empty texts, listSearchableFields |
Payload's global "No Results" text: hide it; test it |
| O5, O5a–c | customised | ui panel (exists), Status, edit.PublishButton (dialog + next story), SaveDraftButton → Next story, editMenuItems (Remove), UnpublishButton off |
verify that the custom PublishButton keeps Payload's error toast |
| O6 | customised | views.edit.versions replacement, tab: { label: 'History' } |
needs changedBy, lastAction |
| O7 | custom page | /admin/add-widget; Server Actions over catalogue, projects; reuses DurationField, SnippetCopy, ThemePreview |
— |
| O8, O9, O11–O15, O17, O18 | default / customised | exist; labels and beforeListTable |
O8 hides Create New |
| O10 | custom page | /admin/concepts/:project? reusing VocabularyPanel, SkosTools |
— |
| O16 Overview | customised | views.edit.overview document tab |
— |
| P0–P9 | outside | (frontend)/annotate; P7, P8 new routes |
P9 sessionStorage with try/catch |
| V0–V8 | outside | embed/ |
contract additions in section 8 |
| M1–M6 | outside | membership Mailer (members), Payload adapter (operators) |
deliverability (SPF, DKIM) is the organisation's setup; the deploy guide says so |
| Recording route | outside | Next route beside the theme route | — |
10 Build order (Phase 5c step 2)
Each slice ≤ 5 files. Each slice keeps test:int, test:e2e, tsc, lint green.
| # | Slice | Files (about) | Done when |
|---|---|---|---|
| 1 | Lifecycle | stories/lifecycle.ts, types.ts, review.ts (counts), index.ts, int spec |
An int test derives all 7 states and the reply variant at the stories interface; counts splits stories and replies and has processing. |
| 2 | Schema | stories/collection.ts, membership/collections.ts, Users.ts, migration, int spec |
Migration up and down pass on an empty database with rows; toldOn is saved from a valid return and never appears in WADM. |
| 3 | Story actions | stories/review.ts (remove, nextWaiting, ownStories), media/settled.ts, types.ts, int spec |
A failed file gives Returned (automatic); publish-at-once with media publishes when ready; remove deletes the replies; unpublish has no caller and is gone. |
| 4 | Mail backbone | payload.config.ts (adapter), membership/mailer.ts, membership/mails.ts (M1–M5 text), stories after-commit sender, int spec |
Memory mailer gets M2 after a return and M3 after a publish; a rolled-back transaction sends nothing; mailNotices off stops M3, M4. |
| 5 | Digest | worker schedule, app consumer, mails.ts (M6), int spec |
A fake clock at 08:00 with 3 waiting sends one M6 per operator with daily; 0 waiting sends none; each sends at H4. |
| 6 | Sidebar and Today | admin/Today.tsx, admin/NavLinks.tsx, payload.config.ts, collection group changes, e2e |
Empty stack shows the 4-step checklist; each step ticks from data; the badge equals the Waiting count. |
| 7 | Review screen | admin/stories/PublishButton.tsx, NextStory.tsx (replaces KeepAsDraft), RemoveItem.tsx, Status.tsx, review.e2e |
Publish shows the preview, then the next story opens; no Unpublish anywhere; Remove removes; Processing tab and empty texts show. |
| 8 | History | admin/stories/History.tsx, collection view config, e2e |
History reads "Published by …", "Returned automatically …" in browser time. |
| 9 | Add a widget to a site | admin/addWidget/View.tsx, actions.ts, catalogue/index.ts, projects/index.ts, e2e |
An e2e test goes Today → 3 steps → copied snippet with the right project, recording, src; the inline project form works with 0 projects. |
| 10 | Concepts page and project Overview | admin/vocabulary/ConceptsView.tsx, admin/projects/Overview.tsx, config, e2e |
Sidebar Concepts opens the tree (picker with 2 projects); Overview counts match stories.counts. |
| 11 | Public routes and contract | theme route, projects/theme.ts, recording route, CONTRACT.md, int spec |
Theme JSON has name, rightsLine, rightsIri; recording JSON gives 200/404 with CORS *; CONTRACT lists every addition with the date. |
| 12 | Player | embed/src/text.ts, player.ts, stories.ts, contract spec, fixture (new, not changed) |
Empty text, "Tell a story", rights part, annoudio-saved=published note and annoudio-story pass in 3 browsers. |
| 13 | Annotate page frame and words | annotate/page.tsx, strings.ts, annotate.css, StoryForm.tsx, ReplyForm.tsx |
Project name, theme and rights line show; buttons say Send for review / Publish the story; ranges show 1:02–1:35; text survives a session end. |
| 14 | Your stories and Your account | annotate/mine/page.tsx, annotate/account/page.tsx, actions.ts, strings.ts, e2e |
Groups show in order; the name change and the mail choice save; Sign out ends the session. |
| 15 | Walkthrough | tests/e2e/lifecycle.e2e.spec.ts, screenshot script |
ROADMAP 5c done-when: F1–F7 pass with axe on every screen; screenshots of every state in design/phase5c-screens/. |
11 Open questions
- Can a member delete their own account? Default: not in 5c. Your account says "To delete your account and your stories, write to {EMAIL_FROM}." An operator then removes the stories and blocks the member. A real delete (with what happens to published stories and replies) goes to Phase 7.
- Does Remove also remove the replies under the story? Default: yes. A reply is served only under a published parent (5b decision 7), so a kept reply is invisible data. Each reply's member gets M5.
- Should the stack tick "Put the snippet on a site" by itself? Default: no new write. The step ticks by I pasted it or by the first story with a
toldOnon that site. A "player seen" record per site origin needs a write on the cached read path, which today writes nothing (proposal C §8.3).