LF & Cross Platform Content Process Doc, 29/07/2026 update
What this page is for. Fully documents how /backdata/lf-cross-platform-content works. Architecture, data model, behavior of the 3 views, synchronization rules, files involved, endpoints. Read by Claude at the start of any session that touches this page. AL refers to it to understand how it works / decide on new features.
Table of contents
A — Architecture (single root)· non-negotiable principle
BOARD is the single root. Everything shown in the 3 views derives from BOARD (in JS memory, persisted to D1). No duplication.
┌──────────────────────────┐
│ BOARD (in-memory) │
│ phases[] · steps[] │
│ (steps → subActions[]) │
└──────────┬───────────────┘
│ derives
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ View 1 │ │ View 2 │ │ View 3 │
│ Edit │ │ Run │ │ Gantt │
└───────────┘ └───────────┘ └───────────┘
struct editing check in visual chain
(who/what) parallel + typed arrows
D1 persistence — 2 separate stores under intranet_state:
gantt_long_v2— Long Form phases + steps (Script · Film · Edit · Post).cross_platform_content_process_v2— Cross Platform phases + steps (Edit · Post · etc).
On display, BOARD merges the 2 stores in memory (origin tagged via step.originBoard). On save, we split on output and PUT separately. The old pages /backdata/long-process and /backdata/cross-platform-content still run on the same stores → everything stays consistent.
User-only overlays that are NOT in BOARD:
gantt_v3_triggers— custom arrows added manually in View 3 (right-click). Additive only, on top of the default chain derived from BOARD.gantt_shared_tags— Owners / Apps / Arrows palette shared across the 3 pages (long-process, cross-platform-content, unified).
Golden sync rule. Every data change goes through BOARD (or a documented user-only store). No view stores a duplicated state. Any action in one view is reflected in the other 2 because they re-render from the same source.
B — Data model· JSON schema
B.1 BOARD (in-memory)
{
version: 8,
phases: [
{
id: 'p_script',
label: 'Script',
originBoard: 'long', // 'long' or 'cross'
originLabel: 'LF', // editable badge (LF / CROSS / custom)
color: '#a855f7', // canonical phase color (PHASE_PALETTE)
criticalPath: [], // step IDs marked critical
wave: 1, // WAVE 1|2|3 Edit-view grouping, 4 = SBY parking. Lives on the PHASE.
// Run + Gantt ignore 1-3 and fully exclude 4 (SBY).
textColor: undefined // 'red' | 'yellow' | 'green' | 'wave1' (legacy yellow marker rows)
},
…
],
steps: [
{
id: 's_l1',
phaseId: 'p_script',
label: 'Script + prompt template (9-bullet skeleton)',
shortLabel: 'Script + prompt',// summary for the right SVG bar
shortLabelManual: false, // true = user edited shortLabel by hand
subActions: [
{
label: 'fill BANK',
owners: [], tools: [],
execModes: [], // ['auto', 'manual'] in the chosen order
linksTo: [], // sub-action → sub-action (cross-task within the phase)
textColor: undefined // 'red' | 'yellow' | undefined (= white)
// subs also carry: execModes, execManual, prompt, url, finder, overlay, strike
}, …
],
owners: ['AL'], // labels (references TAGS.owners[].label)
tools: ['ClaudeCode', 'ClaudeLLM'],
color: '#a855f7', // = phase.color (option A chosen, overwritten on load)
parallelTo: null, // or step.id = parallel to this anchor
execModes: ['auto'], // derived from owners, displayed as AUTO / MANUAL / SEMI
execManual: false, // true = AL cycled the badge by hand, freezes the derivation
prompt: { enabled, title, url, target }, // red PROMPT button
url: { enabled, title, url, target }, // purple URL button
finder: { enabled, title, url, target }, // gold FINDER button, config separate from URL since 28/07
overlay:{ enabled, title, url, target }, // cyan OVERLAY safe-zone marker
strike: true, // optional strikethrough flag (absent = false)
originBoard: 'long',
weekOffset: 0, // 0 for LF, 3 for Cross (Run view)
textColor: undefined, // 'red' | 'yellow' | undefined
linksTo: [] // task → task (manual via right-click)
},
…
]
}
B.2 Run (D1 table video_runs)
{
id: 'r_xxxxx',
label: 'Trauma Map LF',
week_ref: '2026-W20',
created_at: 'ISO',
completed_at: 'ISO' | null,
duration_seconds: number | null,
archived: false,
step_completions: {
's_l1': { checked_at: ISO, started_at: ISO|null },
…
},
sub_completions: {
's_l1:0': { checked_at: ISO, started_at: ISO|null },
…
}
}
v2 format stored in the JSON column. The legacy v1 (flat {stepId: ISO}) is migrated in memory on read. started_at = timestamp of the last check BEFORE this one in the run (computed server-side on PATCH).
B.3 V3 custom chain (D1 key gantt_v3_triggers)
{
version: 1,
firstCell: 'phase:p_script', // entry point of the V3 Gantt
chain: {
'phase:p_script': ['step:s_l1'],
'step:s_l1': ['step:s_l2'],
…
}
}
cellId format:
phase:<phaseId>(e.g.phase:p_script)step:<stepId>(e.g.step:s_l1)sub:<stepId>:<saIdx>(e.g.sub:s_l1:0)
This store holds only the custom arrows the user added on top of the default chain (which is derived on the fly from BOARD at each render).
B.4 Authoring conventions (28-29/07)
- Owners order: dominant first. The first owner is the one who actually DOES the work; helpers come after. Example: "Shorts configuration & posting prompt" is
['CCode', 'AL']because Claude Code runs it and AL only steps in. - Task tools = union of its sub-task tools. The tools chips on a task list every tool used by at least one of its sub-actions, nothing more.
- Finder folders: one folder per BATCH. A batch of assets lives in ONE folder with its descriptions file sitting next to it (e.g.
ready/plusV{N}-descriptions.txt). Never one folder per file, never 200 mixed files in a flat dump.
Schedules live ONLY in content-system. Posting times and days are stored exclusively in /backdata/content-system. The board never stores a schedule, and neither do the grammars or prompts. Any step that posts content points to content-system for its timing.
C — View 1 (Edit)· source of structural changes
Purpose. Edit the production method: tasks, sub-actions, phases, owners, apps, parallelTo, colors, prompts. This is WHERE you change the structure.
C.1 Layout
- 4 top-level dropdowns: WAVE 1 / 2 / 3 (yellow pill identity: black italic title on
#e8d265, same look as the legacy "Wave view 1" rows) + SBY (gray pill, last position). Each phase renders inside the body of its wave. A WAVE selector on every phase summary assignsphase.wave(persisted, default 1). WAVE 1-3 are presentational only; SBY (wave 4) is a parking lot fully excluded from Run, Gantt and run totals. - SBY parking (temporary feature): anything AL doubts goes there. Right-click a task or sub-action → yellow "Move to SBY" item; a phase goes via its WAVE selector (SBY option). Moved items land in a gray parking phase (one per origin board, label SBY); a sub-action becomes its own step there. Not counted as to-check anywhere.
- Phases stacked vertically (each an accordion).
- Within each phase: 2 columns per task → left = name + sub-actions + chips; right = SVG Gantt bar with columns 01/02/...
- Left/right ratio =
3fr / 1fr(left takes 75% to give the sub-action text room to breathe). - Current phase strip (29/07): a one-line indicator under the sticky menu (Edit view only) showing the number, color dot and label of the phase currently under the topbar (
#current-phase-strip, scroll-spy, rAF-throttled, refreshed on scroll, on every render and on view switch). - Phase number in the round pill (29/07): the round phase badge shows the phase number in bold (
.phase-badge-num) next to the origin label LF/CROSS. Re-injected after the 24/07 deploy regression removed it.
C.2 Sub-actions (narrow left column)
Vertical order stacked in .chip-sub-stack:
⋮⋮drag handle1Anumber (phase idx + letter within the step)⊙run dot (visible only if a run is selected)
The sub-action text takes all remaining width (inline editing).
Execution badge, big on the Edit view (28-29/07). Every task and sub-action shows a large MANUAL / AUTO / SEMI badge (execModesHtml(modes, big)) on its own line: under the task name row for tasks, under the text for sub-actions. AUTO = robot alone, MANUAL = AL alone, SEMI = both (Claude runs it, AL steps in for validation, choice or manual finish). Derived from owners; clicking the badge cycles derive, MANUAL, AUTO, SEMI and sets a manual override (execManual) that freezes the derivation until the cycle returns to derive.
C.3 Editor modal (Edit ▼ button)
On each task, opens a modal that exposes:
- long label + shortLabel (with manual flag)
- subActions (CRUD)
- owners + tools (via tag picker)
- execModes (derived from owners by default; the 29/07 toggle sets a manual override, see
execManualin B.1 and G.3) - bar color (auto-set to phase.color)
- parallelTo (dropdown of the phase's other steps)
- prompt (enabled, title, url, target)
- NEW — Previous actions: read-only list of the cells that point TO this step (derived from the inverse of
V3_CHAIN.chain). × per item to remove the arrow. - NEW — Next actions: read-only list of the cells this step points to (
V3_CHAIN.chain[step:id]). × per item. Adding only from V3 (drag arrow or right-click).
Same in a sub-action's modal: Previous actions and Next actions sections.
C.4 Config buttons: PROMPT / URL / FINDER / OVERLAY
- Four button families, each a per-task or per-sub config
{ enabled, title, url, target }: PROMPT (red#e08585), URL (purple#a855f7), FINDER (gold), OVERLAY (cyan#06b6d4). - URL and FINDER are two separate configs since 28/07 (AL request):
step.finder/sa.finderwith their own gold FINDER button, their own "Configure FINDER link" modal and their own right-click entry. FINDER no longer piggybacks on the URL field. Backward compat: a legacy URL config whose url is a local path (/Users/…) still renders as a FINDER button until the migration has passed everywhere. - FINDER opens the folder in Finder via the
eofinder://scheme (helper applet~/Applications/EOFinder.app), because Chrome blocks plainfile://links from an https page. - confirmCfgLoss guard (28/07): saving a config that erases or empties an already-filled PROMPT / URL / FINDER / OVERLAY opens an explicit confirmation modal first. No silent loss of a filled config.
- Drag & drop: any config button dragged onto another task or sub-action MOVES it there (never copies). The one-place rule applies to PROMPT only; URL / FINDER / OVERLAY may legitimately appear on several steps.
D — View 2 (Run)· execution batch
Purpose. See at a glance what launches in parallel and check items off as you go.
D.1 Layout
- Horizontal batches (= packs of parallelizable tasks). 1 column = 1 batch. UI label is Batch N (renamed from "Wave N" on 2026-07-24 to kill the collision with the Edit view's WAVE 1/2/3 grouping; internal code identifiers still say wave).
- Batch N = projection of the V3 columns: all steps whose V3 column equals N, merged cross-swimlane (see F.4). V2 no longer computes its batches: it derives from V3.
- A Phase N+1 batch only appears once the target phase's trigger is satisfied (full phase / specific step / specific sub, defined by the V3 cross-phase modal).
- Batch 1 = phase jalon cards (col 0, not draggable, showing PHASE N + origin), Batch 2+ = step cards (col 1+). SBY phases (
wave = 4) are excluded from the batches and from the done totals. - The word "wave" now lives on the PHASE only:
phase.wave(WAVE 1/2/3 Edit grouping, 4 = SBY parking). Run-view columns are Batches, a separate concept derived from the V3 columns. Internal code identifiers still say wave.
D.2 Cards
- Step card: header (kind + origin + check), editable name, owners + apps + execModes chips, mini-list of sub-actions stacked below.
- Sub-card (inside the step card): check dot + editable name + chips. Click toggles done.
- V2 drag-drop (new model): drag a card to another wave → writes an intra-phase custom arrow in
V3_CHAIN.chainthat pushes the step to the target column. V3 and V2 re-render with the step at its new position. If the target is in another phase, a cross-phase modal asks for the trigger kind. - Gold dot on the cards of a parallel wave (≥ 2 cards).
- Check cascade (29/07): checking a task visually renders ALL its sub-cards as done/gold on screen. The D1
sub_completionsblob stays intact (display-only cascade).
D.3 Run header
Run label + week_ref + done count + global % (computed over BOARD's total steps, SBY excluded).
Validation and run duration (29/07): checking the last pending step opens the "Validate & close run" modal (notes field, 5000 chars). It now shows the run duration, computed client-side from run.created_at (completed_at does not exist yet at that point; the server sets it on the PATCH that follows). Cancel = the step is not checked. Per-step duration badges are intentionally disabled per AL: durations are still tracked in D1 (every check records checked_at + started_at) but never displayed in any view.
E — View 3 (Gantt)· MS Project-style cell chain
Purpose. Visualize the sequencing of all cells (phases, tasks, sub-actions) in a single Gantt with arrows.
E.1 Cells
- All phases + all steps of BOARD are shown by default.
- The sub-actions live inside their parent step cell (mini-list with checkbox).
- A cell contains: kind badge (PHASE / STEP / SUB), origin (LONG / CROSS), editable name, owners + apps + execModes chips.
- The "first" cell is framed in gold (configurable via ⊕ Set first cell).
E.2 Layout (per-swimlane)
- Each phase = a horizontal swimlane (band tinted with the phase color). Top→bottom order =
BOARD.phases[]order. - X axis per swimlane (independent column space):
- Phase cell at column 0
- Steps at column 1 by default (= all parallel within the swimlane)
- Intra-phase
parallelTo+ intra-phase V3 custom arrows can push a step to col 2, 3, …
- Default chain derived from BOARD: only
phase → its steps(intra-swimlane visual grouping). No auto cross-phase arrow. - User chain (
V3_CHAIN.chain):- Intra-phase = pushes target by 1 column within its swimlane
- Cross-phase = semantic trigger, does not move the target on X
E.3 Arrows
Orthogonal path (right angles, MS Project style). Style determined by the TARGET'S TYPE:
| Target | Style | Color |
|---|---|---|
| phase | thick 3px solid | #e8d5b0 |
| step | thin 1.8px solid | #f2f2f5 |
| sub | thin 1.5px dashed (6/4) | #9a9aa4 |
E.4 Right-click + arrow drag (V3)
Right-click on a cell → unified Previous / Next modal: ⬅ previous actions (trigger this cell) · ➡ next actions (triggered by this cell). × pills to remove + a + button to add a custom arrow via a tree (phases > steps > subs).
Drag from a cell handle → pulls an arrow to another cell. The hovered cell is highlighted during the drag. On release: if a cell is under the cursor, the arrow is created; otherwise the drag is cancelled cleanly.
If the target is in another phase (cross-phase) → a mini-modal asks for the trigger kind:
☐ Full phase complete (= wait for all actions of the source phase to finish)
☐ Specific step [dropdown of the source phase's steps]
☐ Specific sub-action [dropdown of the source phase's subs]
→ stored in V3_CHAIN.triggers[srcCellId], V2 gates the start of the target wave.
On an internal sub-action (mini-list) → mini-menu: text color (green/red/yellow/white), ∥ set parallel, ⚙ configure prompt, duplicate, link to another sub.
E.5 Phase brackets (MS Project style)
Above each phase, a thin horizontal SVG line spanning from the phase's first step to its last step, with the phase label above the line. Color = phase.color. Opacity 0.55 so it doesn't clutter visually.
E.6 PROMPT button (top-right of cell)
On each step or sub cell whose prompt is enabled: a PROMPT button in absolute top-right. Empty style (dashed) if no URL, solid if URL. Left-click opens the URL, right-click opens the config modal.
F — Inter-view synchronization· reference contract (full matrix)
Non-negotiable principle. BOARD + V3_CHAIN + run completions = 3 stores. The 3 views read these stores and compute nothing on their own. V3 is the visual source-of-truth for column (X) positioning; V2 derives its waves directly from the V3 columns.
F.1 Storage — where each piece of data lives
| Data | Store | Key / field | Note |
|---|---|---|---|
| Phase / step / sub label | BOARD | phases[].label · steps[].label · steps[].subActions[].label | Editable from V1, V2, or V3 |
| step shortLabel | BOARD | steps[].shortLabel | Auto-derived from label unless shortLabelManual=true |
| Phase color | BOARD | phases[].color | Inherited by all steps of the phase + tints the V3 swimlane |
| Text color (red/yellow/green/white) | BOARD | steps[].textColor · steps[].subActions[].textColor | Applied across the 3 views |
| Owners + apps + execModes | BOARD | steps[].owners[] .tools[] .execModes[] (same for subs) | Tags shared via gantt_shared_tags |
| Prompt config | BOARD | steps[].prompt = { enabled, title, url, target } · same for sub | target = 'task' or sub index. Red #c45c5c. |
| URL config | BOARD | steps[].url = { enabled, title, url, target } · same for sub | Exact mirror of Prompt. Purple #a855f7. Visible if enabled. |
| FINDER config | BOARD | steps[].finder = { enabled, title, url, target } · same for sub | Separate from URL since 28/07. Gold button, opens Finder via eofinder://. |
| OVERLAY config | BOARD | steps[].overlay = { enabled, title, url, target } · same for sub | Cyan safe-zone marker. URL defaults to content-system Safe Zones. |
| Run notes | video_runs (D1) | notes TEXT column | Entered via the "Validate & close run" modal on the last pending step. |
| parallelTo (intra-phase pairing) | BOARD | steps[].parallelTo | Pushes 2 steps into the same V3 column |
| Phase / step ordering BOARD | BOARD | Order of the phases[] and steps[] arrays | Determines V3 swimlane order (top→bottom) |
| Custom arrows (X positioning) | V3_CHAIN | V3_CHAIN.chain[srcCellId] = [tgtCellId, …] | Intra-phase = pushes target by 1 col. Cross-phase = no push, visual only. |
| Trigger kind of a cross-phase arrow | V3_CHAIN | V3_CHAIN.triggers[srcCellId] = { kind: 'phase'|'step'|'sub', refId } | Semantic: what must be complete to start the target phase. To be implemented. |
| V3 firstCell | V3_CHAIN | V3_CHAIN.firstCell | Gold frame + BFS anchor (residual, dispensable since per-swimlane depth) |
| Run completions (steps + subs) | video_runs (D1) | completions = { steps: {id: {checked_at}}, subs: {…} } | Per run, not per BOARD |
| Run history list | video_runs (D1) | Entire table | Shown in the "Tracked runs" widget |
F.2 Visibility — where each piece of data appears
| Data | V1 (Edit) | V2 (Run) | V3 (Gantt) |
|---|---|---|---|
| Phase label | Phase header (editable) | Mention in card meta | Phase cell + left lane head |
| Step label | Left row + right SVG bar (editable) | Card name (editable) | Step cell name (editable) |
| Sub-action label | Chip-sub (editable) | Sub-card name (editable) | Mini-row in step cell (editable) |
| Phase color | Phase border + chips | Card border + dot | Cell border + swimlane tint + lane head |
| Text color | On the name (row + bar) | On the card + sub-card name | On the cell name + sub mini-row |
| Owners + apps + execModes | Step-line chips + chips in chip-sub | Chips in card + sub-card | Chips in cell |
| Prompt button | Button in row (task or sub) | Button in card / sub-card | Top-right button of cell |
| parallelTo | Bilateral orange ∥ chip | Cards aligned on the same wave + gold dot | Cells stacked in the same intra-swimlane column |
| V3 custom arrow | NEW: Edit step modal → "Previous actions" + "Next actions" section (read-only, × to remove) | Implicit via wave shift (= consequence of the column push) | Drawn arrow (orthogonal path) |
| Trigger kind (cross-phase) | Edit step modal → "triggers phase X" mention with kind | The target phase's wave appears only once the trigger is satisfied (checking = advances the wave) | Modal on draw + mini chip on the cross-phase arrow (phase / step / sub icon) |
| Run completions step | — | ⊙ check on card | ⊙ check on cell |
| Run completions sub | ⊙ check on chip-sub | ⊙ check on sub-card | ⊙ check on mini-row |
F.3 Action → Cross-view consequences
| Action | Done in | Consequence in the other views |
|---|---|---|
| Rename (label) a cell | V1 · V2 · V3 | BOARD.label mutated → all views re-render with the new name |
| Check / uncheck a step | V2 · V3 | video_runs mutated → V2 + V3 ⊙ check sync (V1 does not show the run) |
| Check / uncheck a sub | V1 · V2 · V3 | video_runs mutated → V1 ⊙ + V2 sub-card check + V3 mini-row check sync |
| Change text color (green/red/yellow/white) | V1 · V3 (right-click) | BOARD.textColor mutated → 3 views re-render with color |
| Set parallelTo (right-click ∥) | V1 · V3 | BOARD.parallelTo mutated → V1 bilateral ∥ chip + V2 cards same wave + V3 cells same intra-swimlane column |
| Drag-drop a step in V2 from one wave to another | V2 | NEW: writes an intra-phase custom arrow in V3_CHAIN.chain that pushes the step by N columns → V3 shows the cell at the new column + V2 recomputes its waves from V3 |
| Draw a custom arrow in V3 (intra-phase) | V3 | V3_CHAIN.chain mutated → V3 target cell pushed by 1 col + V2 adds a wave + V1 Edit step modal shows it in Previous/Next actions |
| Draw a custom arrow in V3 (cross-phase) | V3 | Modal opens: "Trigger by: ☐ Full phase complete ☐ Specific step ☐ Specific sub". Choice → V3_CHAIN.triggers + visible arrow. V2 gates the start of the target phase's wave on the trigger being satisfied. |
| Add / remove a phase / step / sub | V1 (toolbar or modal) | BOARD mutated + V3_CHAIN cleaned of orphaned refs → 3 views re-render |
| Set first cell V3 | V3 toolbar | V3_CHAIN.firstCell mutated → V3 gold frame (V1 and V2 unchanged) |
| Reorder phases / steps in BOARD | V1 drag-drop | BOARD order mutated → V3 swimlane order (top→bottom) + BOARD-list order in V2 |
F.4 Positioning model (the core)
- Y axis: V3 swimlane order =
BOARD.phases[]order. One phase = one swimlane = one horizontal band. - Intra-swimlane X axis: phase cell at col 0, steps at col 1 by default. Intra-phase
parallelTo+ intra-phase V3 custom arrows can push a step to col 2+. - Cross-phase: no auto arrow. Cross-phase V3 custom arrows do NOT move the target on X (each swimlane stays independent). They only add a semantic trigger.
- V2 waves: derived strictly from the V3 columns, merged cross-swimlane. Wave N = all steps of all phases whose V3 column equals N, AND whose phase trigger is satisfied (or phase already in progress).
- V1 Edit step modal: the "Previous actions" section lists the cells pointing to this step (read from the inverse of V3_CHAIN.chain). The "Next actions" section lists the cells this step points to.
F.5 Save guard: atomic 409 + 3-way merge + undo barrier
- Every save echoes the version token via
X-If-Updated-At;/api/admin/state/[key]checks-and-writes in a single conditional UPDATE and answers409if the row moved since load. Version tokens are millisecond timestamps, unique per write, so two saves in the same second cannot cross silently. - On 409 the page re-reads the server blob and runs a 3-way merge per phase and per step: edited locally wins over untouched, creations on either side are kept, a local delete holds unless the other side edited the item (the edit wins over the delete, both directions). Comparisons use canonical stringify. Status shows "merged with newer changes"; the blocking overlay only appears if the merge itself fails.
- UNDO_BARRIER (28/07, replaces the total history flush): after an auto-merge the earlier undo snapshots stay restorable, but crossing the barrier (undoing to a state from BEFORE the merge) opens a confirm modal, because it may overwrite the other author's work. Once AL confirms, the barrier is lifted for the session.
- Cmd+Z coverage: every mutation that goes through
saveBoard()is snapshot into the undo history, including the strikethrough toggle on tasks and sub-actions.
F.6 Per-view position memory (localStorage)
lfx_pos_v1: on reload the page restores the same view (edit/run/gantt), the same WAVE groups open, the same phases open and the same scroll Y. Saved on scroll (debounced 250ms), on visibilitychange and on beforeunload.lfx_view_scroll(VIEW_SCROLL): per-view scroll memory{ edit: {y}, run: {y, x}, gantt: {y, x} }.run.x(horizontal Batch rail) added 29/07 so the Run view X position survives a reload.- 29/07 fix: the only restorer at load is
restoreLfxPos(end ofload()); the old fixed-600ms restore raced with it on slow networks and lost the scroll. View switches restore viasetViewModethenrestoreScrollForafter render.
G — Color code· phase + text + execMode
G.1 Phase palette (PHASE_PALETTE)
| Index | Color | Hex |
|---|---|---|
| 0 | purple | #a855f7 |
| 1 | gold | #e8d5b0 |
| 2 | cyan | #06b6d4 |
| 3 | green | #22c55e |
| 4 | pink | #ec4899 |
| 5 | orange | #f59e0b |
| 6 | blue | #3b82f6 |
| 7 | light green | #84cc16 |
Option A: all steps of a phase automatically take phase.color on load (per-step custom colors are overwritten). Sub-actions visually inherit their parent phase's color with a subtle variation (lighten 35%).
G.2 Text color (per task/sub override)
| Value | Hex | Preview |
|---|---|---|
'red' | #e08585 | Red text |
'yellow' | #e8d265 | Yellow text |
undefined (= white) | #ffffff | White text |
'green' (phase label only) | #5ed191 | Green text |
'wave1' (phase label only, legacy) | #e8d265 | Yellow pill |
Editable via right-click (V1 on task or sub, V3 on an internal sub-action). Synchronized across the 3 views. Since 2026-07-24 the yellow pill identity ('wave1') is carried by the WAVE dropdown headers themselves; the value stays accepted for legacy rows.
G.3 ExecModes (AUTO / MANUAL / SEMI)
- Array of at most 2 values:
['auto'],['manual'],['auto', 'manual']or['manual', 'auto']. Order is meaningful (= execution sequence). - Display (since 28/07): one explicit textual badge instead of two pictograms to decode:
AUTOwith the gear icon (robot alone) ·MANUALwith the hand icon (AL alone) ·SEMIwith both icons (Claude runs it, AL steps in for validation, choice or manual finish). Big variant on the Edit view, on its own line (see C.2). - Derived from owners on every save. Clicking the badge (29/07) cycles derive, MANUAL, AUTO, SEMI and sets an
execManualoverride that freezes the derivation; cycling back to derive clears the override. The V1 Edit modal toggle sets the same override.
H — Context menus (right-click)· full V1/V3 parity
H.1 On a sub-action (V1 .chip-sub-text · V3 mini-row)
- Set text green
- Set text red
- Set text yellow
- Set text white
- ⌐ Toggle strikethrough
- ∥ Set parallel to… (another sub-action in the same task, orange)
- ⚙ Configure prompt… (red)
- ⚙ Configure URL… (purple)
- ⚙ Configure FINDER link… (gold)
- ⚙ Configure OVERLAY… (cyan)
- ⎘ Duplicate sub-action
- ↪ Link to another sub-action…
- ◪ Move to SBY (yellow)
H.2 On a main task (V1 .task-row-name)
- Set text green
- Set text red
- Set text yellow
- Set text white
- ⌐ Toggle strikethrough
- ∥ Set parallel to… (another task in the SAME phase, ergonomic equivalent of the V2 drag-drop)
- ⚙ Configure prompt… (red)
- ⚙ Configure URL… (purple)
- ⚙ Configure FINDER link… (gold)
- ⚙ Configure OVERLAY… (cyan)
- ⎘ Duplicate task
- ↪ Link to another task… (cross-phase)
- ⇄ Move to another phase…
- ◪ Move to SBY (yellow)
H.3 On a V3 cell (outside an internal sub-action)
- Tree modal to add a custom arrow to another cell.
- List of existing triggers at the top, remove via
× cell.
I — Tracked runs (videos in production)· shared widget
1 run = 1 video that goes through the 2 boards (LF then Cross ~3 weeks later).
I.1 "Active runs" widget
- At the top of the page (below the Tags managers). Lists the runs
archived=0, completed_at IS NULL. - Click a run → selection (persisted in localStorage). Decorating V1 + V2 + V3 activates the checkboxes.
- + New run → label modal + week_ref (auto = current ISO week).
- Right-click → rename.
×→ Archive / Delete permanently modal.
I.2 Toggling a step / sub
Click the dot → PATCH /api/admin/video-runs/:id with {step, done, total_steps} or {sub, done}. The server computes started_at = max(checked_at) among the checks already done. checked_at = ISO now. When done count ≥ total_steps → automatic completed_at.
I.3 Cross-tab sync
BroadcastChannel('video_runs_updated') notifies the other open tabs. The page listens and re-renders.
J — Files, endpoints, D1 stores· where to look
J.1 Source files
| Path | Role |
|---|---|
public/backdata/lf-cross-platform-content.html | Main page (~6500 lines as of 29/07). Inline HTML + CSS + JS. |
public/backdata/_video-runs.js | Shared tracked-runs widget. Active runs section, decorateBoard with checkboxes. |
public/backdata/_board-ui.js | Shared behaviors: collapse, step drag-drop, context menu (sub + task). |
public/backdata/_tune.js | Live layout tuner ?tune=1 (dev-only). |
public/backdata/long-process.html | Legacy LF page (still active, writes the same D1 store). |
public/backdata/cross-platform-content.html | Legacy Cross page (still active, writes the same D1 store). |
J.2 API endpoints
| Path | Methods | Role |
|---|---|---|
/api/admin/state/[key] | GET / PUT | Read/write a JSON blob in intranet_state. Used for gantt_long_v2, cross_platform_content_process_v2, gantt_shared_tags, gantt_v3_triggers, lfx_change_log. Conditional write: GET returns the updated_at token, the client echoes it via X-If-Updated-At, and a mismatch answers 409 (see F.5). Board keys are versioned in intranet_state_history on every save. |
/api/admin/video-runs | GET (list) / POST (create) | Lists runs filtered by status, creates a new run. |
/api/admin/video-runs/[id] | GET / PATCH / DELETE | Run detail, toggle step/sub, rename label, archive. |
J.3 D1 stores
| Key / Table | Schema | Canonical source |
|---|---|---|
gantt_long_v2 (intranet_state) | BOARD LF (phases + steps Script · Film · Edit · Post) | BOARD (split on save) |
cross_platform_content_process_v2 (intranet_state) | BOARD Cross | BOARD (split on save) |
gantt_shared_tags (intranet_state) | Owners + Apps + Arrows tags | TAGS (shared across the 3 pages) |
gantt_v3_triggers (intranet_state) | {firstCell, chain} | V3_CHAIN (additive only, not in BOARD) |
video_runs (dedicated table) | 1 row per run | API /api/admin/video-runs |
lfx_change_log (intranet_state) | {entries[]} readable diff lines, last 400 | Written on every save (diffBoards), read via the "≡ Change log" toolbar button |
intranet_state_history (dedicated table) | 1 row per save per board key (cap 200/key) | Server-side versioning, includes direct D1 patches done by Claude |
K — Deployment workflow· non-negotiable
No GitHub auto-deploy. On this repo (eliteoutsiders-tools), git push ALONE deploys nothing. The prod page stays frozen on the last npm run deploy:prod.
Claude (and AL) workflow after any edit:
cd ~/Projects/eliteoutsiders-tools
# 1. Inline JS syntax validation (anti page-stuck-at-loading)
node -e "const fs=require('fs');const html=fs.readFileSync('public/backdata/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. Commit
git add -A
git commit -m "..."
# 3. DEPLOY (= wrangler pages deploy + smoke tests)
npm run deploy:prod
The deploy:prod script chains: inject-meta.js (canonical + robots) → wrangler pages deploy public --project-name=eliteoutsidersintranet --commit-dirty=true → smoke-test.sh (51 URLs) → auto-commit-after-deploy.sh (creates the commit deploy: prod ship YYYY-MM-DD HH:MM).
L — Recent changelog· updated at each iteration
2026-08-02 update (Run view phase-follows-steps + links + Cmd+B + Gantt zoom/fullscreen)
- Run view — a phase-jalon card follows its steps: it displays in the batch of its EARLIEST step (min column over its steps) instead of a fixed col+1. Rationale: AL's custom arrows point at steps, never at phases, so every phase card without an incoming arrow piled up in Batch 1 while its steps lived several batches later. A phase with no steps keeps the old col+1. See D.1.
- Run view — phase cards framed solid: 3px solid border in the phase colour on all four sides (was dashed 1px). Same thickness as the left accent bar of step cards.
- Run view — PROMPT / URL / FINDER buttons on task cards and sub-cards, same colour language as the Edit view, read-only: left-click opens (via
openUrlSmart, so local paths go througheofinder://). Config (right-click, drag) stays an Edit-view affair. Rendered only when the config is enabled AND has a URL, and only on the element it targets (task or sub index). - Run view — Cmd+B bolds the selected text of a card name. The rich version lives in
step.labelRich(sanitized, b/strong only);step.labelstays plain text so Edit/Gantt/exports are untouched.labelRichis only trusted when its textContent still matches the current label — an edit from the Edit view invalidates it. Card name fields switched fromplaintext-onlyto full contenteditable for this; pasted HTML is flattened by the sanitizer. - Gantt — zoom + fullscreen: − / % / + / Fit buttons plus Ctrl/Cmd+wheel anchored on the pointer (10%–800%, persisted in
localStorage lfx-gantt-zoom). The SVG keeps its viewBox; only the rendered size is multiplied. ⛶ Full screen puts#gantt-view-rootin native fullscreen (Esc leaves). See E. - Known data quirk (not fixed, needs AL's go): one dead arrow in
gantt_v3_triggers—step:s_wc2v60 → phase:p_pin7k2misses thep_x_namespace so its target does not exist; it has no effect. All other Pinterest positioning comes from real arrows (s_a1kaaj, s_mu033v, s_wevufz chains).
2026-07-29 update (FINDER split + exec badges + undo barrier + phase strip + position memory + content resync 28-29/07)
- URL and FINDER split into two configs: FINDER is its own per-task / per-sub config (
step.finder,sa.finder) with its own gold button, its own "Configure FINDER link" modal, its own right-click entry and drag & drop. Legacy fallback: a URL config holding a local path still renders as FINDER until migrated. See C.4. - Big MANUAL / AUTO / SEMI badges on the Edit view, on their own line for both tasks and sub-actions (
execModesHtml(modes, big)). SEMI = auto and manual owners together. Clicking the badge cycles derive, MANUAL, AUTO, SEMI; a manual choice setsexecManualwhich freezes the owner derivation. See C.2 and G.3. - Undo merge barrier (
UNDO_BARRIER, replaces the post-merge history flush): undoing past an auto-merge asks for an explicit confirmation instead of being impossible. confirmCfgLoss: erasing or emptying a filled PROMPT / URL / FINDER / OVERLAY config asks first. Cmd+Z also covers the strikethrough toggle. See F.5. - Current phase strip under the sticky menu (Edit view): number + color dot + label of the phase under the topbar, scroll-spy. Phase number re-injected in bold inside the round phase pill (24/07 deploy regression). See C.1.
- Position memory: on reload the page restores the view, the open WAVE groups, the open phases and the scroll; the Run view horizontal Batch rail X is persisted too (
VIEW_SCROLL.run.x). Load-time restore race fixed (single restorer). See F.6. - Run view: columns are labeled Batch and the word wave now only refers to
phase.wave(WAVE 1/2/3 + SBY, carried by the PHASE). Batch 1 = phase jalon cards. The "Validate & close run" modal shows the run duration since creation. Checking a task renders all its subs done visually (D1 sub blob intact). Per-step duration badges stay disabled: tracked in D1, never displayed. See D. - Content resync 28-29/07 (D1 boards): LF_QUOTE phase deleted (quote extraction + production steps live in LF_POST_Manual) · QUOTES phase deleted and "Pick best quote" parked in SBY · duplicate newsletter step removed (one "Run Prompt Weekly Newsletter" left, on LF_Newsletter) · Lifestyle task rebuilt as 5 sub-tasks (planner 63 rows filled, restock from 4 categories, Canva export, captions via prompt, robot INACTIVE until validated repetitions) · TikTok removed from the shorts auto-posting prompt (IG + FB draft, YouTube shorts manual via YouTube Studio; the Thursday TikTok manual routine keeps its own task with covers by hand) · "Trash sweep post-editing (2-week delta)" step added in the Claudecode finder cleaning phase (move to Trash anything published or scheduled more than 2 weeks ago, keep anything younger or pending) · "Quotes runway check (weekly tick)" reminder added: all quotes scheduled until 07/11/2026, re-run extraction early November.
- Authoring conventions written down (B.4): owners ordered dominant first (the one who DOES the work), task tools = union of its sub-task tools, Finder batches = one folder per batch with its descriptions file next to it, and schedules live ONLY in content-system (never on the board, never in grammars or prompts).
2026-07-28 (atomic save guard + newsletter wiring + shorts resync + sub-tags rows)
- Merge holes closed (audit F3-F5, later same day): comparisons now use canonical stringify (key order can no longer fake an edit and silently overwrite server changes) · an item deleted on one side but EDITED on the other is resurrected (the edit wins over the delete, both directions) · after an auto-merge, the undo history is flushed (Cmd+Z can no longer restore the pre-merge board and erase the other author's work). 8/8 scenario tests pass on the extracted functions.
- Architecture keys protected (audit F13, part 1): notes, up-to-date flags (and the new content-types table cells) refetch-and-merge the server blob before every save, with tombstones for deletions — solo windows and multiple tabs no longer clobber each other's notes.
- Atomic overwrite guard (audit F1): the state endpoint now checks-and-writes in a single conditional UPDATE, and version tokens are millisecond timestamps (unique per write). Two saves in the same second can no longer cross silently. No client change: pages keep echoing the token via
X-If-Updated-At. - Per-save history live: board keys are versioned in
intranet_state_history(cap 200/key) on every save, including direct D1 patches done by Claude (which insert their own history row). - Newsletter task wired (cross board): "Run Prompt Weekly Newsletter" got its PROMPT (new Prompt Weekly Newsletter v0.1) and 3 sub-actions (Launch prompt / Re-reading corrections / Validation flip). The URL button already pointed at the Newsletter Per Long-Form Stack DB.
- Shorts resync (planner 25/07 = truth): task "Shorts configuration & posting prompt" PROMPT wired to the resynced Auto-Publisher prompt; its sub now reads IG + FB + TikTok draft with YouTube manual via YouTube Studio (was listing YouTube as auto-scheduled). Grammar + Prompt Notion pages resynced same day; deployed n8n workflow still legacy (VPS down), do not Execute before its update.
- Sub-action tags layout: owners and apps now render on two separate rows inside a sub-action when both exist (AL request), same box, no other layout change.
2026-07-26 (FINDER one-click + mapping pass + overwrite guard + sync cron)
- One prompt, one place (hard rule): a prompt lives on exactly ONE task or sub-action in the whole workflow. Enforced at assignment time: the page looks the prompt up everywhere else and offers "MOVE HERE" (which clears the previous holder) or cancels. Data deduped on 26/07 (5 duplicates removed, thumbnail TT/IG/FB prompt restored to its own task). URLs are exempt — the same Finder folder, Gling app or social profile legitimately appears on several steps.
- Drag & drop of PROMPT / URL / FINDER buttons: drag a button onto another task or sub-action (both directions) to MOVE it there. It is always a move, never a copy, so the one-place rule holds by construction. Drop zones highlight in gold.
- Change log: every save diffs the previous board against the new one and writes readable lines to D1 (
lfx_change_log, last 400). Read them from the "≡ Change log" toolbar button. Covers task/phase add-rename-delete-move, sub add/remove, and every PROMPT/URL set, changed or removed. - Per-video Finder convention:
_template/and the current video now carry01-rushes/(camera input),02-audio/(raw + Adobe Podcast cleaned),03-gling/(Gling exports + transcript),visuals/,final/,trash/. The Gling-Claude-FCPX prompt reads from 03-gling and writes to final. Every LF_EDIT step carries a FINDER button pointing atvideos/long-form/ongoing/(stable across videos). Documented in the Notion Finder tree page. - OVERLAY marker (cyan): third button family next to PROMPT and URL/FINDER, on tasks and sub-actions. Means "a safe-zone overlay must be respected at this moment". Title field = which overlay / when (shown in tooltip); URL defaults to content-system Safe Zones. Full parity: right-click Configure OVERLAY, drag & drop to move, change-logged. Exempt from the one-place rule (a shared reference, like Finder paths).
- English UI sweep: the last French strings in the page UI were translated (Enregistrer → SAVE, Retirer → Remove, all prompt() dialogs).
- Centered confirm dialogs: the 9 native
confirm()boxes (which Chrome pins to the top of the window) are replaced by a centered in-page modal matching the board style, Enter to confirm, Esc to cancel. - FINDER one-click: a URL that is a local path (
/Users/…) now renders as a gold FINDER button instead of the purple URL one, and opens the folder in Finder via theeofinder://scheme (helper applet at~/Applications/EOFinder.app). Chrome blocks plainfile://links from an https page, hence the custom scheme. First click shows a one-time Chrome permission prompt. - Mapping pass (D1): 48 PROMPT/URL/app assignments posted across both boards from the 95-page Notion inventory, plus 20 apps auto-derived on sub-actions from their own text (Canva, Notion, Finder, Gling, FCPX, PostFast, GPT, CCode, n8n). Existing values were never overwritten; no task, sub-action or phase was created, deleted or renamed except the stale-dated phase
Post — 22/05/26_update→Post — 26/07/26_update. - Overwrite guard + auto-merge: the page sends
X-If-Updated-Aton every save and/api/admin/state/[key]answers409when the row moved since load. On 409 the page now merges instead of blocking: it re-reads the server board and runs a 3-way merge per phase and per step (edited locally → local wins · untouched locally → server wins · created on either side → kept · deleted locally → stays deleted · locally edited beats remote deletion), then saves the merged board and re-renders with a "merged with newer changes" status. Same spirit as alsboard's per-cell sync, applied per entity since this board is stored as one blob. The blocking overlay only appears if the merge itself fails, so nothing is ever silently overwritten. Merge logic covered by 7 unit scenarios. - Seed-overwrite data loss — fixed:
load()used to fall back toSEED_BOARD_LONGwhenever a fetch failed (expired CF Access session, network blip) and then write that seed to D1. This destroyed the real long-form board (24 steps, 4 phases) between 14/07 and 26/07; it was restored from the R2 monthly backup of 19/07, with the 13 seed steps parked in SBY. Now a failed fetch shows "BOARD COULD NOT LOAD", disables saving and writes nothing; the seed is only written when the read succeeds and the key is genuinely empty. - Weekly sync cron: see CRON12 — Content sync check.
2026-07-24 late (SBY parking + menu colors + config defaults + wave data migration)
- SBY parking wave (temporary feature): 4th gray dropdown at the bottom of Edit view.
phase.wave = 4; excluded from Run batches, Gantt swimlanes, V3 columns and run totals. Right-click "Move to SBY" (yellow) on tasks and sub-actions (a sub becomes its own step in the gray SBY parking phase, one per origin board); phases park via the SBY option of their WAVE selector. - Context menu colors: "Configure URL…" purple
#a855f7, "Configure prompt…" red#e08585. Root cause fixed: the generic button styling loop was overwriting per-item inline colors (parallel orange included); it now preserves them. - Config modals default checked: "Prompt active" and "URL active" are pre-checked when the prompt/URL was never configured (no url, no title, not enabled). Existing saved configs keep their stored state.
- Wave data migration: the 3 legacy yellow marker rows ("Cross Platform Content Process WAVE-1/2/3", textColor
wave1, 0 steps) were converted intophase.waveassignments then removed. Backups in repobackups/lfx-wave-migration-2026-07-24/.
2026-07-24 (WAVE dropdowns + Batch rename + Finder tag)
- 3 top-level WAVE dropdowns in Edit view (WAVE 1/2/3), yellow pill identity (black italic on
#e8d265). Newphase.wavefield (1|2|3, default 1) persisted in both D1 stores, assigned via a per-phase WAVE selector. Presentational only: Run + Gantt ignore it. The sequential "phase N" badge is gone (origin tag LF/CROSS kept). - Run view columns renamed "Batch N" (previously "Wave N"): same concept (projection of the V3 columns), new label to avoid the collision with the Edit grouping.
- Finder tag added to the shared Apps palette (
gantt_shared_tags) with the real macOS Finder icon (assets/content-system/logos/finder.png, ICON_MAP entry).
2026-05-19 (D+7 — global phase order persistence)
- Bug: moving a Cross phase before all the LF phases (or vice versa) worked visually but was lost on reload. Cause: 2 separate D1 stores (
gantt_long_v2+cross_platform_content_process_v2), fixed "long then cross" merge on load. - Fix: on save, we persist
phaseOrder=BOARD.phases.map(p => p.id)in the long store. On load, after merge, re-sortBOARD.phasesby this rank. Legacy withoutphaseOrder→ current behavior (long then cross). - In-memory IDs tagged (
p_*long,p_x_*cross) → consistency guaranteed between save and reload.
2026-05-19 (D+6 — V3 dynamic channel widening)
- V3 anti-spaghetti: the channels between columns widen dynamically based on the number of arrows crossing them.
- Step function (a tier every 3 arrows):
- 0-2 arrows → 40 px (base)
- 3-5 arrows → 54 px
- 6-8 arrows → 68 px
- 9-11 arrows → 82 px
- +14 px per tier of 3
- Formula:
gap = 40 + ceil(max(0, n−2) / 3) × 14. Implementation inderiveV3Layout: pre-compute thechannelArrowsmap, then accumulate intocolX[col]instead of the linearcol × (CELL_W + COL_GAP). totalWderived from the final cumulative value, not the linear one.
2026-05-19 (D+5 — V3 task numbering T01/T02)
- The
v3-cell-kindpill now showsT01,T02, … for steps (zero-padded to 2 digits, position within the parent phase). Phases keepP1,P2. Subs keepsub. getCellInforeturnstaskNum=BOARD.steps.filter(phaseId).indexOf(st) + 1.
2026-05-19 (D+4 — fix phase title overlap + Wave view 1 option)
- Overlap bug:
.phase-collapse-toolbar(per-phase EXPAND/COLLAPSE CARDS button) wasposition:absolute; left:50%; transform:translate(-50%,-50%). Long titles ("CROSS PLATFORM CONTENT PROCESS WAVE-2", "UPCOMING WEEK FEED RENDER CHECK", etc.) slid under the button. - Fix:
margin-left:autoon the toolbar (flex flow),.phase-actions { margin-left:0 !important }to avoid conflict. Layout:[▸ badge title color] [Expand/Collapse cards] [×]. - New "Wave view 1" right-click option: black italic title on a solid yellow background (pill). New class
.phase-label-wave1, valuetextColor='wave1'accepted bymigrateBoard.
2026-05-19 (D+3 — kill page-jump on textColor + strike toggle)
- Bug: right-clicking "Set text red/yellow/green/white" on a sub/task triggered a full re-render (
render()+renderRunView()+renderGanttView()) → visual micro-jump + scroll loss. Phases weren't visually affected because their badge stays at the top of the viewport after reflow. - Fix: direct DOM mutation on the CSS class instead of a re-render. Sub: toggle
chip-sub-text-{color}on.chip-sub-text. Step: toggle on.task-row-name+ redraw only the relevant.phase-rightSVG (to update the text fill in the bar). Phase: toggle on.phase-label. - V2/V3 re-rendered ONLY if the view is active (
currentViewMode()). Otherwise sync on the next switch. - Same for
toggleStrike(sub + step): toggle.is-strikeon the existing DOM.
2026-05-19 (D+2 — color picker page-jump diagnosis)
- Investigation: the modal picker
<input type="color">already listened onchange(notinput), so NOT the source of the jump. Real cause =render()on textColor (see D+3).
2026-05-19 (D+1 — fix ownerToMode for renamed tags)
- Bug: owner = CCode (label renamed via TAGS manager instead of ClaudeCode) →
ownerToModematched on a strict string → returned null → empty execMode. - Fix: robust 3-level heuristic in
ownerToMode:- If
TAGS.owners[i].mode === 'auto' | 'manual'→ explicit override. - Otherwise lowercase-label heuristic:
'al'→ manual; containsclaude/llm/gpt/bot/aior starts withc(≤ 12 chars) → auto. - Default → manual (human owner).
- If
- Covers the variants:
CCode,CLLM,ClaudeCode,ClaudeLLM, etc.
2026-05-19 (late night 6 — Execution auto-derived from Owner)
- No more manual ⚙/✋ toggle. The Execution icons (gear / hand) are 100% derived from the Owner field, at both task AND sub-action level.
- Fixed mapping:
AL → ✋ manual,ClaudeCode / ClaudeLLM → ⚙ auto. Owner order is preserved, deduped by mode.[]→+(nothing)['AL']→✋['ClaudeCode']or['ClaudeLLM']→⚙['ClaudeCode','AL']→⚙ ✋(auto first)['AL','ClaudeLLM']→✋ ⚙(manual first)['ClaudeCode','ClaudeLLM']→⚙
- Helpers
deriveExecModes(owners)+syncExecModes(target). Auto-called on every owner mutation: picker checkbox, chip-× (step + sub), drag-reorder, step-pill remove in the modal. - On load,
migrateBoardsystematically overwrites any existingexecModeswith the derived value. Any legacy data where exec was set independently of owner is cleaned up. - UI: Execution modal row → read-only display ("auto-derived from Owner"). Timeline cell .tl-mode → cursor:default + pointer-events:none, click handler removed.
2026-05-19 (late night 5 — CF Access session guard T+23h59)
- Anti work-loss on CF Access session expiry:
- Timestamp
lfx_session_start_msin localStorage on first load (survives tab close/reopen, not reset). - setTimeout set to
T + 23h59mfrom the start. On fire:saveBoard(true)+ flush V3 chain (direct PUT, no debounce) → centered popup "ALL CHANGES SAVED · Your 24h session has expired" with a 3s countdown → redirect/cdn-cgi/access/logout. - After Cloudflare Access re-auth, native CF behavior: return to the same URL.
- localStorage key purged on logout so the next session starts clean.
- If the user is already past T+23h59 on load (rare, e.g. tab dormant overnight) → fire after 1.5s to let the render happen.
- Timestamp
2026-05-19 (late night 4 — Add step moved + short label newlines)
- "+ Add step" button moved from the phase header (next to the title) to the bottom of each phase's left column (
.phase-left-footerinrenderPhaseLeft). No more noise in the header. The existing.phase-add-stephandler in the main loop picks it up unchanged. - Short label: newlines respected in the right Gantt render:
- Before:
wrapLines()didtrim().split(/\s+/)which collapsed\ninto single spaces → the user's line break in the "Short label — Right side" textarea was ignored in the SVG render. - Now: split first on
/\r?\n/to respect user newlines, then word-wrap each line tomaxChars. TotalmaxLinescap kept. - Explicit empty lines preserved if within the quota.
- Before:
2026-05-19 (late night 3 — strikethrough toggle right-click)
- Strikethrough toggle via right-click menu on sub-action AND step:
- New "⌐ Toggle strikethrough" button at the top of the menu (before Set parallel to).
- API:
boardSubActionMenu.toggleStrike(stepId, saIdx)+boardStepMenu.toggleStrike(stepId). Flipssa.strike/step.strike(delete if false to avoid polluting the JSON). - migrateBoard preserves
strike === trueon sub + step. - CSS
.is-strikeapplied to.task-row-name,.chip-sub-text,.run-card-name,.run-subcard-name,.v3-cell-name,.v3-cell-sub-name.text-decoration: line-throughgold rgba(232,213,176,0.85), thickness 1.5px. - Sync V1/V2/V3 via the standard
render*functions.
2026-05-19 (late night 2 — undo glow ping + doc target_blank)
- Doc link opens in a new tab:
target="_blank" rel="noopener"on the Doc link at the top of the V1 page. - Undo / Redo glow ping (Group 4.1):
diffSnapshots(before, after)compares 2 JSON snapshots (board + chain) → list of affected items{ kind:'phase'|'step'|'sub', id, saIdx? }. Covers: add/remove/edit of phase, step, sub-action, and V3 arrows (source + target cells).findAffectedCellEl(item)finds the DOM in the active view: V1 =.task-row[data-step]/.chip-sub[data-step][data-sa-idx]/details.phase[data-phase-id]; V2 =.run-card[data-step]/.run-subcard[data-step][data-sa-idx]/.run-card-phase-jalon[data-phase]; V3 =[data-cell="kind:id[:idx]"].glowPingAffected()called inrequestAnimationFrameafter restore. Finds the 1st visible affected element →scrollIntoViewsmooth (block:center) → 250ms delay → adds class.glow-pingfor 720ms → removes.- CSS animation
@keyframes glowPing: gold box-shadow (#e8d5b0) pulses at 30% then fades. Auto-opens the parent phase<details>if closed. data-phaseadded to V2's.run-card-phase-jalonfor targeting.
2026-05-19 (late night — fix DnD + phase nums + URL/PROMPT lock)
- URL/PROMPT labels locked: the
openUrlConfigModal+openPromptConfigModalmodals remove the "editable title" field. The chip/cell render ignores any storedtitleand ALWAYS shows "URL" / "PROMPT". Save forcestitle: ''. - Phase number visible in V2 + V3 cells:
- V2 milestone card:
PHASE N(N = 1-indexed position inBOARD.phases). - V2 step card:
P{N} · {phase.label}in.run-card-phase. - V3 phase cell: badge
.v3-cell-kind=P{N}instead of "phase". - V3 step/sub cell:
.v3-cell-annotprefixedP{N} ·. getCellInfo()addsphaseNumto info for each kind.
- V2 milestone card:
- PROMPT + URL chip X aligned (task-level): CSS rule
.task-row-line-prompt-row { justify-content: flex-end !important; gap: 8px }. The 2 chips now share the same right-aligned X in their dedicated row. - KNOWN LIMITATION — PROMPT/URL alignment cross-target: the CSS rule above aligns the chips when both are
target='task'. If one is task-level and the other sub-level (target=N), they live in different parents (.task-row-line-prompt-rowvs.chip-sub) → impossible to align via CSS without breaking the sub layout. To resolve later: either force task-level by default, or refactor to put all PROMPT/URL buttons in a shared dedicated row under the step. - Drag phase DOWN bug fixed:
- Cause 1:
.phase-dragginglackedpointer-events: none. An open source phase with a large body blocked thedragoverof the phases below → impossible to drop DOWN. Fix:pointer-events: noneon the source, re-enable on.phase-drag-handlesodragendfires. - Cause 2: splice math refactored. Before: splice src then findIndex tgt (which had shifted). Now: indexOf src + indexOf tgt before splice,
insertAt = before ? tgtIdx : tgtIdx+1, adjustmentif (srcIdx < insertAt) insertAt -= 1. Mathematically equivalent but readable and robust.
- Cause 1:
2026-05-19 (cont. — end of day)
- URL button (purple) exact mirror of Prompt:
- Model:
step.urlandsubActions[].url= { enabled, title, url, target }, preserved in migrateBoard. - V1
.chip-url+.chip-url-sub(#a855f7) — visible ifurl.enabled. If Prompt is also configured, shown side by side. - V3
.v3-cell-urltop-right of cell. If Prompt is present, URL shifted toright: 72px. - Context menu: ⚙ Configure URL… in purple, just under Configure prompt.
- Left-click = open URL, right-click = configure.
openUrlConfigModaltwin ofopenPromptConfigModal.
- Model:
- V3 + sub button in each step cell to add a sub-action directly from V3. Re-render V1+V2+V3.
- V1 drop sub on empty task:
.task-row[data-step]= drop zone. Dragging a sub-action onto a task without sub-actions → append to the end of tgtStep.subActions..row-sub-drop-targetgold dashed highlight. - Validate & close run modal when checking the last pending step:
- D1 migration
0048_video_runs_notes.sqladds anotescolumn. - API
PATCH /api/admin/video-runs/[id]+GET+ listing exposenotes. - Modal on the last step: Notes field (5000 chars max), Cancel / Validate & close run. Cancel → step not checked. Validate → step checked + notes saved + run archived.
- D1 migration
- MAJOR BUG fix step.textColor:
migrateBoardstrippedtextColor+linksToon every save → red/yellow/green customizations that vanished. Now preserved explicitly.setTextColorre-renders V1+V2+V3 together. - PROMPT button only shows if
prompt.enabled === true. No more empty placeholder auto-injected on every Add Step. - Phase color picker:
- Right-click phase → "Set phase color…" → palette modal (8 vivid + 8 muted generated by mixing 65% original + 35% gray).
- Per-phase inline "🎨 Color" button in each phase header (swatch + COLOR label).
- Global "Phase colors" button in the V1 topbar → modal listing all phases.
- Click swatch →
phase.color+ all the phase'sstep.colormutated. Real-time V1/V2/V3 sync.
- Phase right-click Set text color: green/red/yellow/white.
phase.textColorpreserved in migrate. Visual V1 (.phase-label-{red,yellow,green}) + V2 (inline phase card) + V3 (via info.ref.textColor). - Cross-phase drag-drop:
- Drop on a phase summary (header) = accepted. Auto-opens if the phase is closed. The step joins the end of the phase's tasks.
- Drop on an EMPTY phase (no step yet):
dragover+dropon the wholephaseEl..phase-row-drop-targetgold dashed highlight. - Sub drop on an empty task: drop zone on the entire
.task-row.
- Top bar restructure: 3-column grid [LEFT = Add ▾ + Expand/Collapse + Phase colors] [CENTER = view toggle] [RIGHT = Reload + Runs history].
.topbar-innerrespects.containermax-width 1600px + 4% padding → automatic alignment with H1. Full-viewport background extended viabox-shadow: -100vw 0 0, 100vw 0 0(doesn't affect layout, no more page horizontal-scroll bug). - V2 + V3 at 100vw full viewport:
width: 100vw + left/right: 50% + margin: -50vw.body { overflow-x: hidden }belt+suspenders. V3 swimlanes and V2 waves span the whole viewport. Border-top + border-bottom + subtle background in active mode (gray frame). - Enriched scroll memory:
VIEW_SCROLL = { edit, run, gantt }with{ y, x }per mode (page Y + canvas X for V3 / waves X for V2). Persisted inlocalStorage. Auto-migration from the old number format.beforeunloadforces a final save. Restore viacaptureScrollFor+restoreScrollFor. - Min scroll = content start:
contentMinYFor(mode)= offsetTop of#phases-root/#run-view-root/#gantt-view-root. Restore =max(min, saved_y)→ never scrolls above the view's content. - Scroll preservation on re-render:
renderRunViewandrenderGanttViewsnapshot page Y + canvas/waves X at the start, restore viarequestAnimationFrameat the end. Fixes the page jump on dropping a custom arrow + all other re-renders that lost position. - Undo Cmd+Z contenteditable focus fix: if focus is on an EMPTY contenteditable (= label of a just-created new phase), blur + state-level undo. If content is present, native text undo.
- Undo initial snap:
snapBoardForUndo()called after loading BOARD + V3_CHAIN._lastSnapinitialized before the first mutation → the first Add Phase is undoable, and V3_CHAIN is no longer overwritten by an empty initial snap. - Floating bars alignment:
.tag-managers-floating(Owners panel) +#undo-redo-barboth atbottom: 18px, height 38px, box-sizing border-box → horizontally aligned. - Owners panel auto-close on outside click (panel.open = false).
- Tags panel 3x width: 320px collapsed, min(1100px, viewport-270) open.
.tm-tag+.tm-labelinwhite-space: nowrap→ end of vertical char-by-char rendering. - Arrows legend auto-synced: 2 distinct sections:
- "Other types": Custom (gold)
- "Phases (default arrows)": 1 line per phase with its real color
render(). - Active runs visuals: selected run = green border (#5ed191) + blinking green ● dot (vrPulse 1.4s animation). Consistent with the /trauma-map "activation" style.
- Active runs sub simplified: "Long 0/16 · Cross 0/34" → "X/Y done" (total sum).
.vr-sectionwithout left border/padding → alignment with H1. - Per-phase inline Color button:
.phase-color-inlinein the phase header, swatch + COLOR. Click opens the picker directly for that phase. - Doc link: 📖 emoji removed, color
var(--text)white (before: gold with emoji). - '+3 weeks · wait LF distribution' legacy column removed in V2 (unified Model B).
- .v1-link "Original V1 Gantt archived" hidden in V2/V3 modes.
- Tight margins:
.containerpadding-bottom 80→24, topbar margin/padding reduced, H1 margin 24→16, V2/V3 padding 16→12. - V3 SVG width =
max(800, totalW, hostEl.clientWidth)→ black margin on the right eliminated. - Anchored memory:
feedback_tools_pages_english_only.md: all /tools pages in English (except /architecture). Grep check before every commit.
2026-05-19
- Model B validated: everything is parallel by default.
computeColumns()is the ONLY function that decides each cell's X column. V3 + V2 derive from this single function. - V2 waves = projection of the V3 columns (including col 0 = phases in Wave 1). Wave N = all cells at column N. No more phase notion in V2 (cross-cutting waves).
- Cross-phase arrow to a phase cell = pushes the phase to col(source)+1 + all its steps to col(phase)+1. Strict V3 ↔ V2 consistency.
- V3 right-click parity with V1: identical menu (colors / ∥ parallel / ⚙ prompt / ⎘ duplicate / ↪ link / ⇄ move to phase). Handled by
_board-ui.jswhich also catches.v3-cell+.v3-cell-sub. Double-click a V3 cell = Previous/Next picker. - V1 cross-phase + cross-task drag:
- Tasks (rows): dragging a task from one phase to another → adopts the target's phaseId + color + originBoard + weekOffset.
parallelToinvalidated if the anchor is no longer in the same phase. - Sub-actions: drag between tasks (hence between phases). prompt.target recomputed on both steps.
- Auto-sync V1 + V2 + V3 (render + renderRunView + renderGanttView).
- Tasks (rows): dragging a task from one phase to another → adopts the target's phaseId + color + originBoard + weekOffset.
- V1 phase drag-drop: drag the "phase N" badge to reorder the phases. Confirm modal "Move phase X before/after phase Y?". Splice into BOARD.phases + auto-sync V2/V3.
- Dynamic V3 cell height: each swimlane computes its height from the max sub count. All subs visible, no more internal scroll.
- V3 default arrows: source phase color, thin (1px), solid, opacity 0.45. Custom arrows: thick gold 2.5px, focal.
- Right-click arrow: confirm + delete (custom only). Default arrows = non-deletable alert.
- Arrow anti-overlap: Y offset of sy/ty + midX per target index when a source has multiple arrows.
- Auto-scroll during V3 arrow drag: cursor near an edge → continuous vertical / horizontal canvas scroll. Speed proportional to proximity.
- + Add Phase: direct creation with an unused PHASE_PALETTE color. No more "which board?" modal. Push to the end of BOARD.phases, inherits originBoard from the last phase. Mirror button at the bottom of the page (Edit mode only).
- Sticky top bar: view toggle + actions on 1 line at the top (position:sticky). "+ Add ▾" cascade groups Add Phase / Step / Sub. Reload + Runs history at the end of the line.
- Scroll memory per view:
VIEW_SCROLLremembers the position in each view, restored on switch. - "+ new wave" drop zone in V2: drag a step to the end of the waves → creates a new wave (column push via V3_CHAIN).
- Unified BOARD + V3_CHAIN undo: bottom-right floating bar (always visible on scroll, !important + 1.5s re-check). Cmd+Z / Cmd+Shift+Z. Initial snap after load so the first mutation is undoable.
- "↺ Reset custom arrows" button in the V3 toolbar.
- French → English: all UI strings translated (Annuler→Cancel, Supprimer→Delete, alerts, confirms, modal titles).
- "Unified editor — LF phases..." section removed from the header.
- "+ Add step" button at the bottom of each phase removed (kept the one in the phase header + top cascade).
- Owners/tools chips at the bottom of V3 cells (margin-top:auto + border-top dashed): frees up text space → end of vertical rendering in narrow cells.