Actions board · process doc
What this page is for. Fully documents how /lf-cross-platform-content (the byForja Actions board) works. Architecture, data model, the single view, synchronization rules, files involved, endpoints. Read by Claude at the start of any session that touches this page. Sections that exist on the EO board but not here say so, so the doc reads the same on the three intranets.
Table of contents
A — Architecture (single root)· non-negotiable principle
PHASES is the single root. The board content (phases, tasks, owner, app, link) lives in one constant inside the page script. D1 stores nothing but the ticks. render() derives everything shown from PHASES plus the tick map.
┌──────────────────────────────┐ ┌──────────────────────────┐
│ PHASES (page constant) │ │ D1 board_state │
│ phases[] · tasks[] │ │ board_key = 'actions' │
│ id · name · owner · app │ │ item_key = task id │
│ link · label │ │ value_json = {done} │
└──────────────┬───────────────┘ └────────────┬─────────────┘
│ derives │ ticks
└────────────────┬────────────────────┘
▼
┌─────────────┐
│ View 1 │
│ Board │
└─────────────┘
D1 persistence: one row per item under board_key = 'actions' in board_state, written through /api/admin/board-state. Content changes (a new task, a renamed phase) are code changes deployed with the page; only ticks are data.
Golden sync rule. A tick goes to D1 first, the screen follows. No localStorage fallback: a tick that only lives in one browser is a lie. The server is authoritative on every reload and every tab focus.
B — Data model· JSON schema
B.1 PHASES (page constant)
[
{
num: '01', name: 'Find prospects', want: 'I want new names in the pipe.',
tasks: [
{ id: 'find-01', // D1 primary key, stable, never renamed without a migration
name: 'Source new prospects',
owner: 'Claude' | 'AL' | 'Robot',
app: 'Notion', // where the task happens
link: 'https://…' | '/page', // what to open
label: 'Prompt 01' }, // chip text
…
]
},
…
]
Wired on byForja today: five phases, nineteen tasks.
B.2 Tick (D1 table board_state)
{ board_key: 'actions', item_key: 'find-01', value_json: '{"done":true}', updated_at: 'YYYY-MM-DD HH:MM:SS' }
value_json is coerced server-side to { done: bool }: the endpoint rejects any other shape so the column never becomes a junk drawer.
B.3 Custom chain
None. Order on the board is the order of PHASES and of each tasks array. No arrows, no triggers.
B.4 Authoring conventions
- Ids are keys.
<phase>-<nn>, lowercase, url-safe (KEY_REin the endpoint). Renaming one orphans its tick. - One intent per line. A task answers "I want X" with what to open, who does it, where.
- Owner is the one who acts. AL runs it, Claude is asked to run it, Robot means a cron does it.
C — View 1 (Board)· the only view
Purpose. See the whole pipeline in one column and tick items off. Structure edits happen in the page source, not in the UI.
C.1 Layout
- Phases stacked vertically (
section.bf-phaseon byForja,section.bc-phaseon byCaliber), head = number, name, "I want" line, done counter. - One row per task: round checkbox, name, owner chip, app chip, link chip (external links open in a new tab).
- A phase without tasks shows "No task wired yet for this phase." (shared JS, 2026-09-10).
C.2 Sub-actions
None. A task is the smallest unit.
C.3 Editor modal
None. Edit PHASES in public/lf-cross-platform-content.html and deploy.
C.4 Config buttons
One link chip per task (link + label). No PROMPT / URL / FINDER / OVERLAY families.
D — View 2 (Run)· not present
Not present on this board. The board is its own run view: the ticks are the run, there is no batch or wave projection.
E — View 3 (Gantt)· not present
Not present on this board. No cell chain, no arrows, no swimlanes.
F — Synchronization· reference contract
F.1 Storage
| Data | Where | Written by |
|---|---|---|
| Phases, tasks, owners, apps, links | PHASES in the page | Code deploy |
| Ticks | D1 board_state, board actions | POST /api/admin/board-state, one row per tick |
| In-flight writes | inFlight map in memory | The page, cleared when D1 answers |
F.2 Visibility
Everything is on the one view. The status line (top right) shows done / total and the last sync word: saved, synced, or Not saved · reason in red.
F.3 Action → consequences
- Tick: optimistic paint, checkbox disabled while the POST is pending, revert + red status if D1 refuses.
- Reload / tab focus / visibility change:
GET ?board=actions, state rebuilt from scratch so a tick removed in another tab disappears here too; pending writes are preserved.
F.4 Positioning model
None. Order = source order.
F.5 Save guard
Per-item rows make the merge happen server-side: tab A ticking find-01 and tab B ticking close-02 touch different primary keys, both survive. No version token, no 409 needed.
F.6 Per-view memory
None (no localStorage by design).
G — Color code· owner + link
- Owner chip: AL = brand accent, Claude = violet, Robot = grey (
ownerClass()). - App chip: ink, no fill.
- Link chip: amber, "↗" appended on external links.
- Done: name struck through, ink faint; filled dot on the checkbox.
H — Context menus (right-click)· not present
None on this board.
I — Tracked runs (videos in production)· shared widget
/videos tracks runs in D1 video_runs (same API as EO: /api/admin/video-runs). On byForja no process board feeds the run steps yet (BOARD_SOURCES is empty in the page), so a run tracks its label, ISO week and A→Z duration and shows "No process board wired on this brand".
J — Files, endpoints, D1 stores· where to look
J.1 Source files
| Path | Role |
|---|---|
public/lf-cross-platform-content.html | The board: HTML + CSS + JS inline, PHASES constant. |
public/lf-cross-platform-content-doc.html | This doc. |
public/videos.html | Tracked runs (EO template). |
J.2 API endpoints
| Path | Methods | Role |
|---|---|---|
/api/admin/board-state | GET ?board= / POST | Read all ticks of a board, upsert one tick. Coerces the value to {done}. |
/api/admin/annotations | GET / POST / DELETE | Same table, board architecture_annotations, used by /architecture only (allowlist). |
/api/admin/video-runs, /api/admin/video-runs/[id] | GET / POST · GET / PATCH / DELETE | Runs list, create, detail, toggle, rename, archive, delete. |
J.3 D1 stores
| Key / Table | Schema | Canonical source |
|---|---|---|
board_state (board actions) | 1 row per ticked item | API /api/admin/board-state |
board_state (board architecture_annotations) | 1 row per annotation | API /api/admin/annotations |
video_runs (dedicated table) | 1 row per run | API /api/admin/video-runs |
K — Deployment workflow· non-negotiable
No GitHub auto-deploy. On this repo (byforja-tools), git push ALONE deploys nothing. The prod page stays frozen on the last npm run deploy:prod.
cd ~/Projects/byforja-tools
# 1. Inline JS syntax validation (anti page-stuck-at-loading)
node -e "const fs=require('fs');const html=fs.readFileSync('public/lf-cross-platform-content.html','utf8');const scripts=[...html.matchAll(/<script>([\s\S]*?)<\/script>/g)];scripts.forEach((m,i)=>{try{new Function(m[1]);console.log('script',i,'OK')}catch(e){console.log('ERR:',e.message)}})"
# 2. DEPLOY (repo guard → mobile check → pages inventory → stamps → wrangler → smoke → auto-commit)
npm run deploy:prod
The deploy:prod script chains: scripts/repo-guard.sh → scripts/mobile-check.py → scripts/pages-inventory.js → scripts/stamp-pages.js → wrangler pages deploy public --project-name=byforja-tools --commit-dirty=true → scripts/smoke-test.sh → scripts/auto-commit-after-deploy.sh. D1 migrations are not part of the chain: apply 0011 / 0012 by hand.
L — Recent changelog· updated at each iteration
2026-09-10 (rule 15 parity)
- This doc rebuilt on the EO structure (A to L). Phases without tasks now say so on the board (shared JS tweak, no effect on byForja which has tasks everywhere).
- Migrations 0011 (weekly_steering, steering_notes) and 0012 (video_runs) added for /weekly-business-steering and /videos; apply with
wrangler d1 migrations apply byforja --remotebefore first use.
2026-08-09 (board_state)
- Migration 0008: one D1 row per item so two open tabs never clobber each other. localStorage banned on purpose: a tick that only lives in one browser is a lie.