content-tracker.ts
Language TypeScript5.1 KB108 lines
/*** The Content Creation tracker: the Google Sheet where the designers' and* marketing's individual internal work actually lives.** It is a source because most internal tasks live in this spreadsheet and* nowhere else: Ninety carries one rollup row ("Weekly product photo refresh")* for work that is a dozen separate jobs, so without the sheet the designers* read as nearly idle. Reading the sheet directly beats pushing this work into* Ninety (which would crowd it) or back into Bitrix tasks (which nobody works).* The Ninety rollup stays; this supplies the detail underneath it.** How it is read, and what that implies: Google's `gviz` endpoint returns CSV* for a tab with no credential. That is what makes a Cloudflare Worker cron* able to read it at all, and it is only true because the document is shared* "anyone with the link": anyone holding this URL can read the sheet. If that* sharing is ever tightened the source fails closed, loudly, which is correct,* but nobody should be surprised by either state.** No staleness signal (rule 11). The gviz response carries no `Last-Modified` and the* document's `modifiedTime` needs the Drive API, which needs a credential the* Worker does not have. So "how fresh is this" is genuinely unknown for this* source, and renders as not tracked rather than as fresh. A hand-maintained* spreadsheet is the input most likely to go quietly stale and the one where we* can say the least about it, and that combination is stated on the page.*/export const TRACKER_SHEET_ID = 'EXAMPLE_SHEET_ID_REPLACE_ME'export const TRACKER_TITLE = 'Content Creation Tracker'export const TRACKER_URL =`https://docs.google.com/spreadsheets/d/${TRACKER_SHEET_ID}/edit`/*** The tabs that hold assignable work, by gid, as read from the document.** Two tabs are deliberately absent, and that is the reason this is a list* rather than "every tab":** 100000004 Resources : image dimensions and gallery ordering.* Reference material, no owner column, not work.* 100000005 Product matrix : one row per SKU with per-image TRUE/FALSE* coverage flags. It is a checklist about* products, not work assigned to a person, and* folding it in would attribute dozens of rows* to nobody.** A tab added to the sheet later will not appear here, and will therefore not* appear on the board. That is a real gap and `unknownTabs` is not a thing we* can detect without the Drive API, so it is written down here instead: if* someone says their work is missing, check for a new tab first.*/export const TRACKER_TABS: { gid: string; label: string }[] = [{ gid: '0', label: 'Images' },{ gid: '100000001', label: 'Video & Photo' },{ gid: '100000002', label: 'Copy & SEO' },{ gid: '100000003', label: 'Newsletter' },]/*** What a Status cell means for workload.** Mapped onto the vocabulary the board already uses, not a new one. `ours` is* work someone owes a move on; `waiting` is finished by them and blocked on a* step somebody else takes; `terminal` is done. This is the same three-way split* `classifyOrder()` applies to Plinth statuses, and it exists for the same reason:* counting waiting-on-someone-else as backlog is how a dashboard makes a team* look behind when it is not.** "Ready to Launch" is the judgement call, and it is called out because it is* the one a reader might disagree with. The design work is done; the launch is* not. Read* as `waiting` it says "they finished, the site has not caught up"; read as* `ours` it would add items to a person's board for work they have already* delivered. `waiting` is the reading that does not make someone look* slow for something they finished.** The sheet also has a `Completed` TRUE/FALSE column. It is not used: it* disagrees with Status on live rows ("Ready to Launch" appears with both TRUE* and FALSE) and nothing observable says which one is authoritative. Two* conflicting signals, one of them picked silently, is how you get a* confidently wrong number. Status is the one with meaning in its* values, so Status is the one that decides, and the disagreement is counted and* reported rather than resolved by guess.*/export type TrackerClass = 'ours' | 'waiting' | 'terminal'const STATUS_CLASS: Record<string, TrackerClass> = {'in progress': 'ours','not started': 'ours','in adjustments': 'ours','ready to launch': 'waiting',launched: 'terminal',}/*** Classify a Status cell. Returns null for anything unrecognised, including a* blank one, never a default.** Rule 2 is the reason for the null: a default that stands in for knowledge* must be surfaced every run. Blank and unknown rows are counted as* unclassified and named on the page, not quietly assigned a meaning.*/export function classifyTrackerStatus(status: string | null | undefined): TrackerClass | null {const key = (status ?? '').trim().toLowerCase()if (!key) return nullreturn STATUS_CLASS[key] ?? null}