mattrguerra/meridian-ops-boardmainPrivate on GitHub, published here in full

content-tracker.ts

Language TypeScript5.1 KB108 lines

config/content-tracker.ts

  1. /**
  2. * The Content Creation tracker: the Google Sheet where the designers' and
  3. * marketing's individual internal work actually lives.
  4. *
  5. * It is a source because most internal tasks live in this spreadsheet and
  6. * nowhere else: Ninety carries one rollup row ("Weekly product photo refresh")
  7. * for work that is a dozen separate jobs, so without the sheet the designers
  8. * read as nearly idle. Reading the sheet directly beats pushing this work into
  9. * Ninety (which would crowd it) or back into Bitrix tasks (which nobody works).
  10. * The Ninety rollup stays; this supplies the detail underneath it.
  11. *
  12. * How it is read, and what that implies: Google's `gviz` endpoint returns CSV
  13. * for a tab with no credential. That is what makes a Cloudflare Worker cron
  14. * able to read it at all, and it is only true because the document is shared
  15. * "anyone with the link": anyone holding this URL can read the sheet. If that
  16. * sharing is ever tightened the source fails closed, loudly, which is correct,
  17. * but nobody should be surprised by either state.
  18. *
  19. * No staleness signal (rule 11). The gviz response carries no `Last-Modified` and the
  20. * document's `modifiedTime` needs the Drive API, which needs a credential the
  21. * Worker does not have. So "how fresh is this" is genuinely unknown for this
  22. * source, and renders as not tracked rather than as fresh. A hand-maintained
  23. * spreadsheet is the input most likely to go quietly stale and the one where we
  24. * can say the least about it, and that combination is stated on the page.
  25. */
  26. export const TRACKER_SHEET_ID = 'EXAMPLE_SHEET_ID_REPLACE_ME'
  27. export const TRACKER_TITLE = 'Content Creation Tracker'
  28. export const TRACKER_URL =
  29. `https://docs.google.com/spreadsheets/d/${TRACKER_SHEET_ID}/edit`
  30. /**
  31. * The tabs that hold assignable work, by gid, as read from the document.
  32. *
  33. * Two tabs are deliberately absent, and that is the reason this is a list
  34. * rather than "every tab":
  35. *
  36. * 100000004 Resources : image dimensions and gallery ordering.
  37. * Reference material, no owner column, not work.
  38. * 100000005 Product matrix : one row per SKU with per-image TRUE/FALSE
  39. * coverage flags. It is a checklist about
  40. * products, not work assigned to a person, and
  41. * folding it in would attribute dozens of rows
  42. * to nobody.
  43. *
  44. * A tab added to the sheet later will not appear here, and will therefore not
  45. * appear on the board. That is a real gap and `unknownTabs` is not a thing we
  46. * can detect without the Drive API, so it is written down here instead: if
  47. * someone says their work is missing, check for a new tab first.
  48. */
  49. export const TRACKER_TABS: { gid: string; label: string }[] = [
  50. { gid: '0', label: 'Images' },
  51. { gid: '100000001', label: 'Video & Photo' },
  52. { gid: '100000002', label: 'Copy & SEO' },
  53. { gid: '100000003', label: 'Newsletter' },
  54. ]
  55. /**
  56. * What a Status cell means for workload.
  57. *
  58. * Mapped onto the vocabulary the board already uses, not a new one. `ours` is
  59. * work someone owes a move on; `waiting` is finished by them and blocked on a
  60. * step somebody else takes; `terminal` is done. This is the same three-way split
  61. * `classifyOrder()` applies to Plinth statuses, and it exists for the same reason:
  62. * counting waiting-on-someone-else as backlog is how a dashboard makes a team
  63. * look behind when it is not.
  64. *
  65. * "Ready to Launch" is the judgement call, and it is called out because it is
  66. * the one a reader might disagree with. The design work is done; the launch is
  67. * not. Read
  68. * as `waiting` it says "they finished, the site has not caught up"; read as
  69. * `ours` it would add items to a person's board for work they have already
  70. * delivered. `waiting` is the reading that does not make someone look
  71. * slow for something they finished.
  72. *
  73. * The sheet also has a `Completed` TRUE/FALSE column. It is not used: it
  74. * disagrees with Status on live rows ("Ready to Launch" appears with both TRUE
  75. * and FALSE) and nothing observable says which one is authoritative. Two
  76. * conflicting signals, one of them picked silently, is how you get a
  77. * confidently wrong number. Status is the one with meaning in its
  78. * values, so Status is the one that decides, and the disagreement is counted and
  79. * reported rather than resolved by guess.
  80. */
  81. export type TrackerClass = 'ours' | 'waiting' | 'terminal'
  82. const STATUS_CLASS: Record<string, TrackerClass> = {
  83. 'in progress': 'ours',
  84. 'not started': 'ours',
  85. 'in adjustments': 'ours',
  86. 'ready to launch': 'waiting',
  87. launched: 'terminal',
  88. }
  89. /**
  90. * Classify a Status cell. Returns null for anything unrecognised, including a
  91. * blank one, never a default.
  92. *
  93. * Rule 2 is the reason for the null: a default that stands in for knowledge
  94. * must be surfaced every run. Blank and unknown rows are counted as
  95. * unclassified and named on the page, not quietly assigned a meaning.
  96. */
  97. export function classifyTrackerStatus(status: string | null | undefined): TrackerClass | null {
  98. const key = (status ?? '').trim().toLowerCase()
  99. if (!key) return null
  100. return STATUS_CLASS[key] ?? null
  101. }