Elite Outsiders ← Tools menu → Open the board

LF Scripts Board Doc, 30/07/2026 update

Documentation source-of-truth · last updated 2026-07-30 · for AL and Claude

What this page is for. The full reference for /script-board (the LF Scripts Board): why it exists, how it works, and where it sits in the content system. Read by Claude at the start of any session that touches this page. The WHY sections at the top are not decoration: they are the design constraints every future change must respect.

Table of contents

  1. 1. The problem this tool solves
  2. 2. The answer: a spatial canvas per script
  3. 3. How it works technically
  4. 4. Where it sits in the system
  5. 5. Changelog

1. The problem this tool solves

AL writes a YouTube long-form script every day, continuously. An idea comes, he notes it. All week, notes aggregate. At the last moment he reorganizes everything, turns the camera on, and films. That is the real workflow, and it collides with a tension he has carried through every previous scripting method:

The board resolves the tension: capture stays loose and continuous (improv survives), while the last-moment spatial reorganization produces just enough structure to protect the value, without ever producing a linear script to read from.

2. The answer: a spatial canvas per script

In AL's words, the board is "a mix of Miro, Notion, and a piece of paper". One board per video. Two zones:

Design consequences that follow from part 1 and must not be regressed:

3. How it works technically

3.1 Data model

One JSON object per board, stored as a single opaque blob (see 3.2). Shape:

{
  title: string,                         // human label, NOT the URL id (see 3.3)
  sidebar: {
    tt: string, ttHistory: [{text,t}], ttAlts: [{id,text}],   // title + history (30) + alternates (5)
    tb: { url, key }, tbHistory: [{url,key,t}],               // thumbnail (R2) + history (20)
    hook: string,
    s: [ { text, pct } x3 ],             // S1 Storytelling, S2 Shifts, S3 Systems
    checks: [ { id, text, checked } ],   // LEGACY per-board list, migration source only (3.1.1)
    checksDone: { globalItemId: true },  // per-board checked state over the GLOBAL checks list
    videoDone: { field: bool x7 },       // per-field "shot in video" checkboxes, drives the Filmed bar
    manualPct: 0-50,                     // AL's manual judgment half of the Ready bar, steps of 10
    custom: [ { id, label, text } ]      // ad-hoc extra sidebar fields, max 20
  },
  elements: [                            // the freeform canvas
    { id, type: 'text',   x, y, w, h, title, body, color, fontSize, align, border, done },
    { id, type: 'image',  x, y, w, h, url, key },
    { id, type: 'mirror', x, y, w, h, field }
  ],
  arrows: [ { id, from, to, color } ],   // canvas connectors, endpoints = element ids (3.7)
  dismissedMirrors: [ 'tt', ... ],       // mirrors deliberately removed from the canvas
  notion: { pageId, url, title, status } // planner interconnection (3.4), empty pageId = not linked
}

S1 / S2 / S3 semantics (AL, 30/07/2026). AL's three personal script variables: S1 = Storytelling, S2 = Shifts, S3 = Systems. Each video weights them differently and one usually dominates. The three percentages can never exceed 100 in total: the slider being moved is capped at whatever the other two leave available (enforced on interaction only; a legacy board already past 100 is left untouched until a slider next moves). The dominant percentage renders gold everywhere it appears (sidebar and canvas combo box), the others muted; all three equal means all three gold.

3.1.1 The GLOBAL checks list

Since 30/07/2026 the check items (text and order) are global across all boards: one shared list at its own D1 key, script_board_checks ([{id, text}], plain read-modify-write like the registry). Same mental model as LFCPCP's process template: edit an item on any board and it changes on every board, including future ones. Only the checked state is per-board (sidebar.checksDone, keyed by global item id).

Why this changed: the "disappearing checks" episode. AL typed check items on one board (V9), later opened another (V8) and found them "gone", twice. They were never lost: the list was per-board by design, so each board started empty. That design contradicted AL's mental model (the sidebar inputs apply to all videos), so the list went global. A guard ships with it: an empty server value can never clobber a non-empty in-memory list (protects the one-time migration seed against a fetch race). Migration: the first board opened that still carries a legacy per-board sidebar.checks list seeds the global list, and its checked flags become that board's checksDone. CTAs and Legit were separate checklists until 30/07/2026; sbNormalizeState() still folds old ctas/legit arrays in so nothing typed there is lost.

3.2 Persistence: D1, anti-clobber guard, per-save history

Three layers, so that "a lost script = a week of thinking lost" cannot happen:

  1. D1 cross-device sync. Each board is one row in intranet_state (key script_board_<boardId>), written through the shared endpoint /api/admin/state/[key]. sbSyncedStore() is the same last-write-wins algorithm as /alsboard's bpSyncedStore (payload {v, t}, client-ms timestamp, 10s poll skipped while typing or dragging, 400ms network debounce, richer-tie escape hatch at t=0), kept as an independent copy so a change to one page can never break the other. localStorage doubles as an instant-paint cache per board.
  2. X-If-Updated-At anti-clobber guard (added 30/07/2026). The same optimistic-concurrency pattern as /backdata/lf-cross-platform-content: every GET memorizes the row's updated_at, every PUT sends it back as X-If-Updated-At, and the server only writes if the row has not moved (atomic conditional UPDATE, 409 otherwise). On 409 the store refetches, adopts the remote if it wins on t, then retries once with the fresh token. Net effect: a stale tab can no longer blind-overwrite a newer save it never saw. Before this, LWW alone left a window (a tab that had not polled yet could clobber another device's newer write).
  3. Per-save history (added 30/07/2026). Every PUT on a script_board_* key (boards, registry, global checks) is also versioned into intranet_state_history, cap 200 versions per key (the endpoint's existing audit-F7 mechanism, extended by prefix). Any future loss incident becomes a one-minute restore from history instead of a monthly R2 dump being the only recovery floor.

3.3 Multi-boards: registry, template, create, rename, archive

3.4 Notion planner interconnection (v1, no new Notion column)

The YouTube Long Form Content Planner database stays exactly as it is (no added columns). The interconnection is deliberately thin:

DirectionMechanism
Board to NotionThe Planner button (switch bar) opens a selector listing the planner rows (id, video number, title, status), fed by GET /api/scriptboard/planner-rows. Picking a row stores state.notion = {pageId, url, title, status} on the board. The linked row's title then shows in the switch bar as a link that opens the Notion row, next to its status chip.
Notion to boardLinking fires one idempotent write-back via POST /api/scriptboard/link-row: a paragraph block "Script board: <board title>" (the title linking to /script-board?video=<id>) inserted at the head of the row's page body. The function scans the first 100 blocks first and never duplicates the block for the same board URL. Notion API limitation: exact head position is only possible on an empty page; otherwise the block lands right after the first block.
Status displayThe board shows the row's title and status read at load time (a small "status read at load time" note sits next to it). Rows are fetched once per page session, refreshed on each board load, never synced in realtime. Unlink is available in the same Planner selector.

Both functions live outside the /api/admin/ middleware prefix, so they replicate the exact same admin gate internally (getAuthenticatedEmail + env.ADMIN_EMAIL allowlist, fail-closed), on top of the zone-wide Cloudflare Access gate. Secret: env.NOTION_API_KEY, with fallback to the older env.NOTION_TOKEN already used by /api/notion/planner.

3.5 The mirror system (bidirectional sidebar and canvas)

A mirror is a canvas element (type: 'mirror') that renders and edits the exact same underlying sidebar.<field> data as the fixed sidebar, not a copy. MIRROR_FIELDS = tt, tb, hook, sx, checks. sx is the combined S1/S2/S3 box: three columns, each label plus its percentage with dominance coloring, no textbox or slider (editing stays in the sidebar). One shared render + bind function pair per field kind is called identically for the sidebar mount (bound once) and any mirror mount (bound on each canvas render); a bug fix in one of those functions fixes the sidebar and every mirror at once.

Auto-seeding and dismissal. sbEnsureMirrors() runs on every normalize and adds exactly one mirror per missing field, skipping anything in dismissedMirrors. Deleting a mirror only removes that view (the field stays editable in the sidebar) and it never resurrects. Live-sync rule: every field mutation funnels through sbAfterFieldSave() (save, re-render sidebar, re-render canvas unless focus is inside it), so edits propagate both ways within ~300ms and an actively-edited element never gets its DOM ripped out from under the cursor.

3.6 Progress: Ready and Filmed

BarQuestion it answersFormula
ReadyIs the script written and good enough to shoot?Auto half (0-50): 7 booleans (tt/tb/hook/s1/s2/s3 non-empty + ALL global checks ticked on this board). Manual half (0-50): sidebar.manualPct, AL's own judgment set with a minus/plus stepper in steps of 10. Field presence cannot see script quality; the manual half exists for that.
FilmedHow much has actually been shot?7 per-field "shot in video" checkboxes (sidebar.videoDone), checked/7. Shown in the sidebar label rows and on mirrors, both writing the same object.

Active runs lists every registered board with both bars read-only, sorted least-Ready first; only the currently open board's dot pulses green, toggled instantly on switch. The editable stepper and checkboxes live only on the open board itself.

3.7 Canvas interactions

3.8 Files, endpoints, D1 keys

WhatWhere
Pagepublic/script-board.html (single file, inline CSS/JS)
Doc (this page)public/backdata/script-board-doc.html
State endpoint (shared with alsboard)functions/api/admin/state/[key].js, D1 intranet_state + intranet_state_history
Board keysscript_board_<boardId>, script_board_registry, script_board_checks
Planner rows proxyfunctions/api/scriptboard/planner-rows.js (GET, admin-gated, Notion DB 04428224657182ee8f3d81fc0fbe50b0)
Planner write-backfunctions/api/scriptboard/link-row.js (POST, admin-gated, idempotent link block)
Image upload / servefunctions/api/admin/upload-image.js / functions/api/admin/board-image/[[path]].js, R2 bucket eo-user-toolkit
Homepage tilepublic/index.html tile 08; doc tile 16 on /backdata
Smoke entriesscripts/smoke-test.sh: /script-board, doc, upload, serve, planner-rows (all 302 unauthenticated)

3.9 Hard rules

4. Where it sits in the system

Divide of responsibilities, agreed 30/07/2026:

SurfaceRole
LF Scripts BoardThe real state of a script's progress. Everything that happens between "idea" and "camera on" lives here: continuous notes, the template fields, the reorganization, the Ready/Filmed truth.
Notion planner (YouTube Long Form Content Planner)Archive and outputs base. Kept exactly as it is (no new columns, no restyle). Scheduling, statuses, and the durable record of what was posted live there. The two are linked per video (3.4) so each side is one click from the other.

The day of the shoot: open the board, reorganize the canvas into the shape of the video, film from it (camera on, board on screen, no linear script). After the shoot, the recording goes through the classic circuit: Gling transcript, then the transcript feeds the downstream content pipeline (cross-platform derivatives, per the LF cross platform content doc). The board's Filmed bar tracks which template fields made it into the recording; the planner row's status records the video's lifecycle.

The 200th-piece test: the system must hold at content piece 200, not just piece 10. That is why boards are one D1 row each (cheap), archived boards disappear from the list without deleting data, every save is versioned (cap 200 per key), and the Active runs list stays a short operational view (registered boards only) rather than an archive.

5. Changelog