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

history.ts

Language TypeScript4.9 KB118 lines

apps/dashboard/src/history.ts

  1. /**
  2. * Day-over-day deltas.
  3. *
  4. * The briefing mockup shows "▲ 5 vs yesterday" next to the artwork-review count,
  5. * and that arrow is the most useful thing on the page: a queue of 40 means
  6. * nothing, a queue of 40 that grew by 5 overnight means the designers are losing
  7. * ground. But nothing in Plinth or Bitrix records history, so the only way to have
  8. * a yesterday is to keep one.
  9. *
  10. * This writes one small row per run to `data/dashboard-history.json` and reads
  11. * the most recent row from a previous local day back out.
  12. *
  13. * The rule this file exists to enforce: no prior run means no delta. Not a zero,
  14. * not a dash that reads like "unchanged". The arrow is simply absent and the
  15. * page says the comparison is not available yet. An invented "▼ 0" on the first
  16. * morning would be the dashboard's first lie, and the second one is much easier.
  17. *
  18. * Runs are keyed by America/Chicago day, not by UTC, because the 6:00 AM CT sync
  19. * is what people are comparing. Several runs on the same day overwrite each
  20. * other rather than accumulating, so re-running the build at 9:00 does not make
  21. * "yesterday" mean "an hour ago".
  22. */
  23. import { localDay } from '@meridian/ninety-client'
  24. import type { Headline } from './collect.js'
  25. // File I/O lives in history-file.ts, not here. `rowFrom`, `previousRun` and
  26. // `delta` are pure and are needed by the Cloudflare Worker, which stores history
  27. // in KV and has no filesystem. A static `node:fs` import in this module would
  28. // break the Worker build even though it never calls any of it (rule 7).
  29. /**
  30. * Bump this whenever the meaning of a recorded number changes (rule 6).
  31. *
  32. * Comparing rows across a definition change announces a swing that never
  33. * happened, the morning after a code change nobody outside this repo knew
  34. * about. A delta is only meaningful between two rows that counted the same thing.
  35. * Rows with a different `defs` are ignored by previousRun(), so the comparison
  36. * simply restarts and says so, which is the same rule as the first morning.
  37. *
  38. * 1 original
  39. * 2 openLeads excludes terminal stages (Junk, Reorder Completed, Converted
  40. * Lead, Out of Scope, Unconverted Lead); ourMove excludes them too
  41. * 3 the Plinth sweep pages through its whole window rather than stopping
  42. * after the oldest page; every order-derived number (ourMove,
  43. * designQueue, waitingOnClient, atVendor, needsReply) grows
  44. * 4 order lines are classified on the product status, gated on the order
  45. * not having shipped past Part Dispatched (see classifyLine); every
  46. * order-derived number moves, including the per-person workloads and the
  47. * queue
  48. */
  49. export const DEFS_VERSION = 4
  50. /** Kept deliberately small: only the numbers a delta is ever shown for. */
  51. export interface HistoryRow {
  52. /** America/Chicago calendar day, YYYY-MM-DD. */
  53. day: string
  54. at: string
  55. /** Definition version these numbers were computed under. See DEFS_VERSION. */
  56. defs?: number
  57. ourMove: number
  58. designQueue: number
  59. waitingOnClient: number
  60. atVendor: number
  61. needsReply: number
  62. openLeads: number
  63. }
  64. export interface Delta {
  65. value: number
  66. /** 'up' | 'down' | 'flat', from the reader's point of view, not the number's. */
  67. direction: 'up' | 'down' | 'flat'
  68. /** The day being compared against, for the tooltip. */
  69. since: string
  70. }
  71. /** How many rows to keep. Enough for a month of weekdays, small enough to read. */
  72. export const KEEP = 40
  73. export function rowFrom(h: Headline, at: Date = new Date()): HistoryRow {
  74. return {
  75. day: localDay(at),
  76. at: at.toISOString(),
  77. defs: DEFS_VERSION,
  78. ourMove: h.ourMove,
  79. designQueue: h.designQueue,
  80. waitingOnClient: h.waitingOnClient,
  81. atVendor: h.atVendor,
  82. needsReply: h.needsReply,
  83. openLeads: h.realLeads,
  84. }
  85. }
  86. /**
  87. * The most recent row from a day before `today`. Returns null when there is no
  88. * previous day on file, which is the honest answer on the first morning and
  89. * after any gap, and which every caller must render as "no comparison" rather
  90. * than as zero.
  91. */
  92. export function previousRun(history: HistoryRow[], today: string): HistoryRow | null {
  93. // Rows recorded under an older definition are skipped, not compared. A delta
  94. // across a definition change is a fabricated swing (see DEFS_VERSION).
  95. const earlier = history.filter((r) => r.day < today && (r.defs ?? 1) === DEFS_VERSION)
  96. return earlier.length ? earlier[earlier.length - 1]! : null
  97. }
  98. /**
  99. * `up` and `down` describe the number, not whether it is good news. Which
  100. * direction is good depends on the metric, so the caller decides the color:
  101. * our-move falling is good, at-vendor falling may just mean nothing shipped.
  102. */
  103. export function delta(now: number, prev: HistoryRow | null, key: keyof HistoryRow): Delta | null {
  104. if (!prev) return null
  105. const was = prev[key]
  106. if (typeof was !== 'number') return null
  107. const d = now - was
  108. return { value: d, direction: d > 0 ? 'up' : d < 0 ? 'down' : 'flat', since: prev.day }
  109. }