{
  "for_llm": "This page documents ONE unit of LeadLift's Editing Pipeline: coded OS (Modal + Supabase + the app) plus 4 Make scenarios that do the Google Drive work. To change it, open the repo in code.repo, read the stage file and the files in code.files. change_policy: green = the app can write it (os/app/src/lib/writes.ts and the column grants); amber = a named business rule with a test (change the rule and its test together, merge through the gate); red = a contract other parts call (routes, events, tables, stage names), a setting or secret, or Make's operations budget. known_issues includes drift found between the Loom, the stage file, the ops notes and main.",
  "system": "Editing Pipeline",
  "client": "LeadLift Marketing",
  "unit": {
    "index": 1,
    "of": 1,
    "node": "Editing pipeline: footage to live ad"
  },
  "platform": "code (Modal) + make.com",
  "code": {
    "repo": "https://github.com/Icarus-Growth/leadlift-os",
    "ref": "main, commit e0a8709",
    "stage": "os/process/ll-editing-pipeline.md",
    "boxes": [
      "1",
      "2",
      "3",
      "4",
      "5",
      "6",
      "7",
      "8",
      "10",
      "11",
      "12"
    ],
    "files": [
      "os/api/footage.py (start_scan, run_scan, new_files, pick_waiting, move_batch, finish_move, merge, request_edited_folder, check_callback)",
      "os/api/editing.py (moved, messages_for_move, refuse_hand_in, tick_launch, empty_feedback, new_card, card_for_script, sweep_scripts, review_page, answer, report_error, launch_date_for)",
      "deploy/handlers/editing.py (routes under /editing)",
      "deploy/app.py (footage_scan_schedule, run_footage_scan, watch_footage_scan, weekly_client_report)",
      "automations/make/footage-scan/, footage-move/, footage-merge/, edited-folder/ (README.md + blueprint.json each)",
      "automations/jobs/weekly_report.py (redo rounds, first-round pass rate, MOVE_BACK_SECONDS)",
      "os/app/src/areas/records/boardPanels.tsx (EditPanel, BriefModal, Set Ads Manager URL)",
      "os/app/src/areas/records/Footage.tsx (MergeModal)",
      "os/app/src/areas/boards/Scan.tsx (Scan for new footage)",
      "os/app/src/areas/client/index.tsx (Send Edits To Client?, Ads Manager URL)",
      "os/app/src/lib/writes.ts (columns the app may write)",
      ".claude/skills/scan-raw-footage/",
      "tests/editing/test_scan.py, tests/editing/test_moves.py"
    ]
  },
  "scenarios": [
    {
      "id": 6583520,
      "team": 438268,
      "zone": "us2",
      "name": "Scan Requested >> List Raw Footage Files",
      "modules": 10,
      "setting": "MAKE_HOOK_FOOTAGE_SCAN",
      "answers_to": "POST /editing/footage"
    },
    {
      "id": 6585510,
      "team": 438268,
      "zone": "us2",
      "name": "Footage Found >> Move Into A Dated Folder",
      "modules": 19,
      "setting": "MAKE_HOOK_FOOTAGE_MOVE",
      "answers_to": "POST /editing/footage-moved"
    },
    {
      "id": 6585511,
      "team": 438268,
      "zone": "us2",
      "name": "Creatives Merged >> Move Footage Into The Kept Folder",
      "modules": 12,
      "setting": "MAKE_HOOK_FOOTAGE_MERGE",
      "answers_to": "POST /editing/merged"
    },
    {
      "id": 6585512,
      "team": 438268,
      "zone": "us2",
      "name": "Creative Sent To Editor >> Make Edited Folder",
      "modules": 12,
      "setting": "MAKE_HOOK_EDITED_FOLDER",
      "answers_to": "POST /editing/edited-folder"
    }
  ],
  "trigger": {
    "scan": [
      "Modal cron footage_scan_schedule 0 6,18 * * * America/Chicago",
      "POST /editing/scan from the 'Scan for new footage' button (signed-in Active team member or app_access address) or the scan-raw-footage skill (bearer ONBOARDING_LEG_SECRET); on demand at most once per 60 s (MIN_GAP)"
    ],
    "moves": "database trigger editing_pipeline_moved (function notify_edit_moved) -> POST /editing/moved on every status change",
    "script": "POST /editing/script-approved from the app; every scan also runs sweep_scripts()",
    "client": "GET/POST /editing/review (the card's client_review_token is the only key; no sign-in)",
    "merge": "POST /editing/merge from the card's 'Merge another creative in' button"
  },
  "reads": [
    {
      "table": "clients",
      "why": "drive_raw_footage_folder_id, drive_edited_folder_id, slack_channel_id, client_reviews_edits, ads_manager_url; scanned only when status in (Onboarding, Active)"
    },
    {
      "table": "team",
      "why": "Slack ids to tag"
    },
    {
      "table": "scripts",
      "why": "100 - Approved scripts get a waiting card"
    },
    {
      "table": "launches",
      "why": "the launch to tick at 40"
    },
    {
      "table": "raw_ads",
      "why": "Drive file ids already seen"
    }
  ],
  "writes": [
    {
      "table": "editing_pipeline",
      "why": "the card: status, links, editor_id, reviewer_id, footage_received_at, due_at, launch_date, asset_type, feedback_notes"
    },
    {
      "table": "raw_ads",
      "why": "one row per Drive file, drive_file_id unique"
    },
    {
      "table": "launches",
      "why": "creative_approved; launch_date when empty"
    },
    {
      "table": "stage_events",
      "why": "database trigger log_status_change on each move"
    },
    {
      "modal_dict": "leadlift-once",
      "why": "once-only move keys, scan and job tokens (hashed, 1 hour), footage-seen lists per folder, last scan time"
    }
  ],
  "notifies": [
    {
      "slack": "#new-creative",
      "bot": "Raw Footage Scanner",
      "when": "each scan; one thread reply per client ('New Content: <client>')"
    },
    {
      "slack": "#editor-updates",
      "bot": "New Video for Editing",
      "when": "10 - To Edit"
    },
    {
      "slack": "#editor-updates",
      "bot": "Hand-In Refused",
      "when": "20 without a Drive folder or file link; card moved back"
    },
    {
      "slack": "#creative-review",
      "bot": "New Content For Review",
      "when": "20 - Internal Review; tags the reviewer"
    },
    {
      "slack": "#amendments",
      "bot": "Needs Amendments",
      "when": "19 - Redo Requested or 29 - Client Changes; one thread reply per note"
    },
    {
      "slack": "client channel (clients.slack_channel_id)",
      "bot": "Your Ad Is Ready To Review",
      "when": "30 - Client Review; review link"
    },
    {
      "slack": "#approved-creative",
      "bot": "Approved!",
      "when": "40 - Ready To Launch; launch date + Ads Manager link"
    },
    {
      "slack": "#pipeline-errors",
      "bot": "<automation name>",
      "when": "every planned failure; tags Fidel; silent if this post fails"
    },
    {
      "slack": "#sentry",
      "when": "no Make answer within 10 minutes (scan, move); faults only a person can fix"
    }
  ],
  "schedules": [
    {
      "function": "footage_scan_schedule",
      "cron": "0 6,18 * * *",
      "timezone": "America/Chicago",
      "timeout_s": 120
    },
    {
      "function": "run_footage_scan",
      "every": "after each scan callback",
      "timeout_s": 600
    },
    {
      "function": "watch_footage_scan",
      "every": "once, 10 minutes after each scan or Make job"
    },
    {
      "function": "weekly_client_report",
      "cron": "0 8 * * 1",
      "timezone": "UTC",
      "note": "Active clients only; assembles the report, a person sends it"
    }
  ],
  "edges": {
    "upstream": [
      {
        "node": "Onboarding",
        "site": "../onboarding/",
        "via": "Make STEP 1 makes the client folder (Raw Footage, Edited, Lead Sheet, Contract, Brief); the Slack leg makes the client channel and bookmarks Raw Footage"
      },
      {
        "node": "Scripts board (OS Walkthrough)",
        "site": "../os-walkthrough/",
        "via": "POST /editing/script-approved; scan sweep"
      }
    ],
    "downstream": [
      {
        "node": "Board digest (stage Box 9)",
        "unit": null,
        "via": "reads editing_pipeline; pending, no page"
      },
      {
        "node": "Launches board",
        "via": "tick_launch sets creative_approved and an empty launch_date"
      },
      {
        "node": "Weekly client report",
        "via": "stage_events moves into 19 or 29"
      }
    ]
  },
  "capabilities": [
    "Scans every Onboarding or Active client's Raw Footage folder through Make; one card per client upload per scan; never two cards for one Drive file",
    "Fills a card waiting for footage first (name match, else oldest); otherwise names a new card 'Footage <d Mon>' (with the time on a second batch that Central day)",
    "Sets due_at = scan time + 48 h, launch_date = Central day of due + 24 h, editor Paulo, reviewer Fidel, asset_type Video Creative or Static",
    "Moves each upload into a dated Raw Footage subfolder, then the card to 10 - To Edit; makes the card's Edited folder + Trash at 10",
    "Merges two cards of one client at 0 or 10; the second goes to Rejected with 'Merged into <name>.'",
    "Refuses a hand-in without a Drive folder or file link; routes approval to 30 or 40 from client_reviews_edits and skip_client_review",
    "Client review page with no sign-in; approve -> 40 + client_approved; notes -> 29",
    "At 40: posts #approved-creative, ticks the launch, empties feedback_notes"
  ],
  "boundaries": [
    "Does not upload to Meta or Ads Manager; a person does it and sets 100 - Live In Ad Manager",
    "Does not scan a client with no drive_raw_footage_folder_id or a status other than Onboarding or Active",
    "The app cannot write drive_raw_footage_folder_id or drive_edited_folder_id (not in writes.ts); onboarding writes them",
    "No redo reasons, counts or caps on the card; no per-card deadline reminders (board digest instead)",
    "Make decides nothing; the server picks files, names and cards"
  ],
  "change_policy": {
    "green": [
      "editing_pipeline: edit_name, editor_id, reviewer_id, due_at, launch_date, ad_count, asset_type, script_id, raw_footage_link, edited_footage_link, feedback_notes, skip_client_review, client_approved, status (card panel)",
      "clients: client_reviews_edits (client page, Delivery), ads_manager_url (client page or Set Ads Manager URL on a card)"
    ],
    "amber": [
      "editing.PAULO / editing.FIDEL in new_card (Sheni, 2026-10-10, until Fidel hires); the app's 'Log new footage' seeds the same pair (store.ts defaultEditorId 'Paulo', defaultReviewerId 'Fidel', found by first name)",
      "editing.DUE_HOURS = 48 (Sheni, 2026-10-10)",
      "editing.LAUNCH_HOURS = 24 plus database rule editing_set_launch_date (amendments, 2026-10-11)",
      "footage.FIRST_SCAN_DAYS = 14 (Jack's suggestion); the pending first-scan change before real folders are linked",
      "scan cron 0 6,18 * * * America/Chicago (editing plan 2026-10-10: every 12 hours)",
      "editing.FOLDER_RE hand-in rule: Drive folder, or file link for one ad (Sheni, 2026-10-10)",
      "Slack message wording in editing.py and footage.py (amendments 2026-10-11)",
      "footage.MIN_GAP = 60 s between on-demand scans"
    ],
    "red": [
      "routes: POST /editing/moved, POST/GET /editing/scan, POST /editing/footage, POST /editing/footage-moved, POST /editing/merge, POST /editing/merged, POST /editing/edited-folder, POST /editing/script-approved, GET/POST /editing/review, POST /editing/digest",
      "stage names: 0 - Footage Received, 10 - To Edit, 19 - Redo Requested, 20 - Internal Review, 29 - Client Changes, 30 - Client Review, 40 - Ready To Launch, 100 - Live In Ad Manager, Rejected, Blocked",
      "tables: editing_pipeline, raw_ads, clients, launches, scripts, stage_events, team",
      "events: evt.raw_footage_received, evt.creative_ready_to_launch",
      "settings: LEADLIFT_OS_API_URL, LEADLIFT_OS_URL, ONBOARDING_LEG_SECRET, MAKE_HOOK_FOOTAGE_SCAN, MAKE_HOOK_FOOTAGE_MOVE, MAKE_HOOK_FOOTAGE_MERGE, MAKE_HOOK_EDITED_FOLDER, SLACK_CHANNEL_NEW_CREATIVE, SLACK_CHANNEL_EDITOR_UPDATES, SLACK_CHANNEL_CREATIVE_REVIEW, SLACK_CHANNEL_AMENDMENTS, SLACK_CHANNEL_APPROVED_CREATIVE, SLACK_CHANNEL_PIPELINE_ERRORS",
      "database trigger editing_pipeline_moved holds the server address; moving or renaming the Modal app stops every move post with no error",
      "Make's Google Drive connection (Fidel's account, expires 2027-04-11)",
      "Make Core plan budget: 10,000 operations a month; a scan costs about 5 plus 1 per folder"
    ]
  },
  "known_issues": [
    "as of 2026-10-12: no real client has drive_raw_footage_folder_id; all 7 cards are test cards",
    "first scan of a folder turns files created in the last 14 days into cards and Slack posts; change before linking real folders",
    "dated folder, Edited folder and Merge ran only on test data",
    "a Modal job stopped by its own timeout reports nothing",
    "a hired editor gets no new cards until the defaults change",
    "database column default (migration-2026-09-24-editing-slack.sql) sets reviewer_id to Paulo; it applies only to a row inserted with no reviewer, because the server and the app both set Fidel",
    "Slack posts go by channel id: renaming a channel is safe; archiving or deleting it, or removing the bot, stops posts and reports to #sentry",
    "no client has ads_manager_url; #approved-creative says 'not set yet'",
    "a malformed review link shows the client a raw error",
    "a missing Trash folder is not reported; moving a card to 10 twice before Make answers makes two Edited folders; two scans at once can both post in #new-creative",
    "weekly report runs for Active clients only; no client is Active (2026-10-11)",
    "doc drift: stage file Box 3 says one card per video file named after the file, videos only, raw link = the file; main makes one card per client upload named 'Footage <date>', counts images too, and links the Raw Footage folder then the dated subfolder",
    "doc drift: stage file Box 5 refuses a single-file link and posts a refused hand-in in #pipeline-errors; main accepts a file link for one ad and posts in #editor-updates",
    "doc drift: stage file Box 7 sends the review link to the reviewer when the client has no channel; main posts it in #pipeline-errors (tags Fidel)",
    "doc drift: stage file Box 7 client wording differs from main ('Your ads are ready.' + Batch Name + [LINK])",
    "doc drift: Loom and its description say 'Paola'; the code's default editor is Paulo"
  ],
  "recovery": {
    "git": "every change is a commit on main, merged through the gate",
    "rollback": "modal app rollback leadlift-os",
    "blueprint_backups": [
      "backups/bp_6583520.json",
      "backups/bp_6585510.json",
      "backups/bp_6585511.json",
      "backups/bp_6585512.json"
    ],
    "blueprints_in_repo": "automations/make/<footage-scan|footage-move|footage-merge|edited-folder>/blueprint.json"
  }
}