@storyshelf/core 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -8
  3. package/dist/adapter/auth.mjs.map +1 -1
  4. package/dist/adapter/capture-runner.d.mts +4 -0
  5. package/dist/adapter/git-host/helpers.d.mts +1 -0
  6. package/dist/adapter/git-host/helpers.mjs +2 -1
  7. package/dist/adapter/git-host/helpers.mjs.map +1 -1
  8. package/dist/adapter/storage.d.mts +1 -0
  9. package/dist/adapters/capture-queue.d.mts +2 -0
  10. package/dist/adapters/webhook-events.mjs +8 -0
  11. package/dist/adapters/webhook-events.mjs.map +1 -1
  12. package/dist/capture/adapter.d.mts +11 -1
  13. package/dist/capture/adapter.mjs +19 -1
  14. package/dist/capture/adapter.mjs.map +1 -1
  15. package/dist/capture/orchestrator.d.mts +7 -0
  16. package/dist/capture/orchestrator.mjs +43 -10
  17. package/dist/capture/orchestrator.mjs.map +1 -1
  18. package/dist/capture/pipeline.d.mts +5 -3
  19. package/dist/capture/pipeline.mjs +18 -6
  20. package/dist/capture/pipeline.mjs.map +1 -1
  21. package/dist/capture/queue.d.mts +2 -0
  22. package/dist/capture/queue.mjs +2 -1
  23. package/dist/capture/queue.mjs.map +1 -1
  24. package/dist/capture/status-fanout.mjs +1 -0
  25. package/dist/capture/status-fanout.mjs.map +1 -1
  26. package/dist/capture/storybook.d.mts +1 -0
  27. package/dist/capture/storybook.mjs +12 -3
  28. package/dist/capture/storybook.mjs.map +1 -1
  29. package/dist/config.d.mts +27 -20
  30. package/dist/config.mjs +16 -2
  31. package/dist/config.mjs.map +1 -1
  32. package/dist/ddl.d.mts +2 -1
  33. package/dist/ddl.mjs +3 -0
  34. package/dist/ddl.mjs.map +1 -1
  35. package/dist/diff/engine.d.mts +8 -0
  36. package/dist/diff/engine.mjs +10 -2
  37. package/dist/diff/engine.mjs.map +1 -1
  38. package/dist/index.d.mts +7 -4
  39. package/dist/index.mjs +10 -3
  40. package/dist/index.mjs.map +1 -1
  41. package/dist/logger.d.mts +3 -3
  42. package/dist/logger.mjs.map +1 -1
  43. package/dist/middleware/csrf.mjs +2 -1
  44. package/dist/middleware/csrf.mjs.map +1 -1
  45. package/dist/middleware/rate-limit.mjs +1 -0
  46. package/dist/middleware/rate-limit.mjs.map +1 -1
  47. package/dist/models/baseline.d.mts +1 -0
  48. package/dist/models/baseline.mjs +3 -1
  49. package/dist/models/baseline.mjs.map +1 -1
  50. package/dist/models/build.d.mts +19 -1
  51. package/dist/models/build.mjs +22 -2
  52. package/dist/models/build.mjs.map +1 -1
  53. package/dist/models/comment.d.mts +2 -0
  54. package/dist/models/comment.mjs +3 -1
  55. package/dist/models/comment.mjs.map +1 -1
  56. package/dist/models/label.d.mts +6 -0
  57. package/dist/models/label.mjs +13 -1
  58. package/dist/models/label.mjs.map +1 -1
  59. package/dist/models/member.mjs +2 -1
  60. package/dist/models/member.mjs.map +1 -1
  61. package/dist/models/project.d.mts +1 -0
  62. package/dist/models/project.mjs +3 -2
  63. package/dist/models/project.mjs.map +1 -1
  64. package/dist/models/snapshot.d.mts +1 -0
  65. package/dist/models/snapshot.mjs +2 -1
  66. package/dist/models/snapshot.mjs.map +1 -1
  67. package/dist/models/status-config.mjs +2 -1
  68. package/dist/models/status-config.mjs.map +1 -1
  69. package/dist/models/token.mjs +2 -1
  70. package/dist/models/token.mjs.map +1 -1
  71. package/dist/models/webhook.d.mts +1 -0
  72. package/dist/models/webhook.mjs +2 -1
  73. package/dist/models/webhook.mjs.map +1 -1
  74. package/dist/pages/build-detail.mjs +2 -1
  75. package/dist/pages/build-detail.mjs.map +1 -1
  76. package/dist/pages/build-diff.mjs +1 -0
  77. package/dist/pages/build-diff.mjs.map +1 -1
  78. package/dist/pages/compute-jobs.mjs +2 -0
  79. package/dist/pages/compute-jobs.mjs.map +1 -1
  80. package/dist/pages/label-detail.mjs +227 -0
  81. package/dist/pages/label-detail.mjs.map +1 -0
  82. package/dist/pages/login.mjs +1 -0
  83. package/dist/pages/login.mjs.map +1 -1
  84. package/dist/pages/project-builds.mjs +1 -0
  85. package/dist/pages/project-builds.mjs.map +1 -1
  86. package/dist/pages/project-create.mjs +1 -0
  87. package/dist/pages/project-create.mjs.map +1 -1
  88. package/dist/pages/project-settings.mjs +33 -42
  89. package/dist/pages/project-settings.mjs.map +1 -1
  90. package/dist/pages/projects.mjs +1 -0
  91. package/dist/pages/projects.mjs.map +1 -1
  92. package/dist/pages/root.mjs +1 -0
  93. package/dist/pages/root.mjs.map +1 -1
  94. package/dist/pages/settings-general.mjs +1 -0
  95. package/dist/pages/settings-general.mjs.map +1 -1
  96. package/dist/pages/settings-labels.mjs +1 -0
  97. package/dist/pages/settings-labels.mjs.map +1 -1
  98. package/dist/pages/settings-members.mjs +1 -0
  99. package/dist/pages/settings-members.mjs.map +1 -1
  100. package/dist/pages/settings-status.mjs +1 -0
  101. package/dist/pages/settings-status.mjs.map +1 -1
  102. package/dist/pages/settings-tests.mjs +149 -0
  103. package/dist/pages/settings-tests.mjs.map +1 -0
  104. package/dist/pages/settings-tokens.mjs +1 -0
  105. package/dist/pages/settings-tokens.mjs.map +1 -1
  106. package/dist/pages/settings-webhooks.mjs +1 -0
  107. package/dist/pages/settings-webhooks.mjs.map +1 -1
  108. package/dist/pages/storybook.mjs +59 -0
  109. package/dist/pages/storybook.mjs.map +1 -0
  110. package/dist/retention/purge.d.mts +3 -0
  111. package/dist/retention/purge.mjs +2 -1
  112. package/dist/retention/purge.mjs.map +1 -1
  113. package/dist/routers/admin.mjs +1 -0
  114. package/dist/routers/admin.mjs.map +1 -1
  115. package/dist/routers/assets.mjs +1 -0
  116. package/dist/routers/assets.mjs.map +1 -1
  117. package/dist/routers/auth.mjs +1 -0
  118. package/dist/routers/auth.mjs.map +1 -1
  119. package/dist/routers/builds.handlers.mjs +15 -3
  120. package/dist/routers/builds.handlers.mjs.map +1 -1
  121. package/dist/routers/builds.mjs +5 -2
  122. package/dist/routers/builds.mjs.map +1 -1
  123. package/dist/routers/comments.mjs +1 -0
  124. package/dist/routers/comments.mjs.map +1 -1
  125. package/dist/routers/helpers.mjs +7 -1
  126. package/dist/routers/helpers.mjs.map +1 -1
  127. package/dist/routers/htmx.mjs +2 -0
  128. package/dist/routers/htmx.mjs.map +1 -1
  129. package/dist/routers/labels.mjs +34 -1
  130. package/dist/routers/labels.mjs.map +1 -1
  131. package/dist/routers/media.mjs +1 -0
  132. package/dist/routers/media.mjs.map +1 -1
  133. package/dist/routers/members.mjs +1 -0
  134. package/dist/routers/members.mjs.map +1 -1
  135. package/dist/routers/projects.mjs +1 -0
  136. package/dist/routers/projects.mjs.map +1 -1
  137. package/dist/routers/schemas.mjs +40 -4
  138. package/dist/routers/schemas.mjs.map +1 -1
  139. package/dist/routers/settings.mjs +22 -0
  140. package/dist/routers/settings.mjs.map +1 -1
  141. package/dist/routers/snapshots.mjs +1 -0
  142. package/dist/routers/snapshots.mjs.map +1 -1
  143. package/dist/routers/status-configs.mjs +1 -0
  144. package/dist/routers/status-configs.mjs.map +1 -1
  145. package/dist/routers/storybook.mjs +113 -0
  146. package/dist/routers/storybook.mjs.map +1 -0
  147. package/dist/routers/tokens.mjs +1 -0
  148. package/dist/routers/tokens.mjs.map +1 -1
  149. package/dist/routers/ui.mjs +25 -2
  150. package/dist/routers/ui.mjs.map +1 -1
  151. package/dist/routers/webhooks.mjs +1 -0
  152. package/dist/routers/webhooks.mjs.map +1 -1
  153. package/dist/schema-tables.mjs +166 -0
  154. package/dist/schema-tables.mjs.map +1 -0
  155. package/dist/schema.d.mts +194 -4352
  156. package/dist/schema.mjs +26 -136
  157. package/dist/schema.mjs.map +1 -1
  158. package/dist/store.mjs +2 -0
  159. package/dist/store.mjs.map +1 -1
  160. package/dist/types.d.mts +14 -1
  161. package/dist/types.mjs +8 -0
  162. package/dist/types.mjs.map +1 -1
  163. package/dist/ui/components.mjs +5 -0
  164. package/dist/ui/components.mjs.map +1 -1
  165. package/dist/ui/document.d.mts +1 -0
  166. package/dist/ui/document.mjs +10 -0
  167. package/dist/ui/document.mjs.map +1 -1
  168. package/dist/ui/styles.mjs +1 -0
  169. package/dist/ui/styles.mjs.map +1 -1
  170. package/dist/ui/theme.mjs +2 -0
  171. package/dist/ui/theme.mjs.map +1 -1
  172. package/dist/urls.d.mts +8 -0
  173. package/dist/urls.mjs +7 -0
  174. package/dist/urls.mjs.map +1 -1
  175. package/dist/utils/hash.mjs +3 -0
  176. package/dist/utils/hash.mjs.map +1 -1
  177. package/dist/utils/paths.d.mts +5 -0
  178. package/dist/utils/paths.mjs +5 -0
  179. package/dist/utils/paths.mjs.map +1 -1
  180. package/dist/utils/ulid.d.mts +2 -0
  181. package/dist/utils/ulid.mjs +5 -3
  182. package/dist/utils/ulid.mjs.map +1 -1
  183. package/package.json +76 -67
@@ -1 +1 @@
1
- {"version":3,"file":"project.mjs","names":[],"sources":["../../src/models/project.ts"],"sourcesContent":["import { eq } from \"drizzle-orm\";\n\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { projects, type Project } from \"../schema.ts\";\nimport { slugify, ulid } from \"../utils/ulid.ts\";\n\nexport interface ProjectCreateInput {\n name: string;\n gitRepository?: string;\n gitDefaultBranch?: string;\n storybookMeta?: unknown;\n}\n\n/** Data operations for project records. */\nexport class ProjectModel {\n /**\n * @param db - Database adapter.\n */\n constructor(private readonly db: DatabaseAdapter) {}\n\n /**\n * Create a project with a unique slug.\n *\n * @param input - Project creation input.\n * @returns The created project.\n */\n async create(input: ProjectCreateInput): Promise<Project> {\n const now = new Date().toISOString();\n const slug = await this.uniqueSlug(input.name);\n return this.db.insert(projects, {\n id: ulid(),\n name: input.name,\n slug,\n gitRepository: input.gitRepository,\n gitDefaultBranch: input.gitDefaultBranch ?? \"main\",\n storybookMeta: input.storybookMeta ? JSON.stringify(input.storybookMeta) : null,\n createdAt: now,\n updatedAt: now,\n });\n }\n\n /** Fetch a project by id, or null if not found. */\n async get(id: string): Promise<Project | null> {\n return await this.db.get(projects, id);\n }\n\n /** Fetch a project by its slug, or null if not found. */\n async getBySlug(slug: string): Promise<Project | null> {\n const rows = await this.db.list(projects, { where: eq(projects.slug, slug), limit: 1 });\n return rows[0] ?? null;\n }\n\n /** List all projects. */\n async list(): Promise<Project[]> {\n return await this.db.list(projects);\n }\n\n /** Update mutable fields of a project. */\n async update(\n id: string,\n patch: Partial<Omit<Project, \"storybookMeta\">> & {\n storybookMeta?: unknown;\n },\n ): Promise<Project> {\n const normalized: Record<string, unknown> = { ...patch };\n if (patch.storybookMeta !== undefined && patch.storybookMeta !== null && typeof patch.storybookMeta !== \"string\") {\n normalized[\"storybookMeta\"] = JSON.stringify(patch.storybookMeta);\n }\n // oxlint-disable-next-line typescript/no-unnecessary-type-assertion\n return await this.db.update(projects, id, {\n ...normalized,\n updatedAt: new Date().toISOString(),\n } as unknown as Partial<Project>);\n }\n\n /** Delete a project by id. */\n async remove(id: string): Promise<void> {\n await this.db.remove(projects, id);\n }\n\n private async uniqueSlug(name: string): Promise<string> {\n const base = slugify(name) || \"project\";\n const existing = await this.db.list(projects, { where: eq(projects.slug, base) });\n if (existing.length === 0) {\n return base;\n }\n const taken = new Set(existing.map((p) => p.slug));\n let suffix = 2;\n while (taken.has(`${base}-${suffix}`)) {\n suffix += 1;\n }\n return `${base}-${suffix}`;\n }\n}\n\nexport type { Project };\n"],"mappings":";;;;;;;;AAcA,IAAa,eAAb,MAA0B;CAIK;;;;CAA7B,YAAY,IAAsC;EAArB,KAAA,KAAA;CAAsB;;;;;;;CAQnD,MAAM,OAAO,OAA6C;EACxD,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;EACnC,MAAM,OAAO,MAAM,KAAK,WAAW,MAAM,IAAI;EAC7C,OAAO,KAAK,GAAG,OAAO,UAAU;GAC9B,IAAI,KAAK;GACT,MAAM,MAAM;GACZ;GACA,eAAe,MAAM;GACrB,kBAAkB,MAAM,oBAAoB;GAC5C,eAAe,MAAM,gBAAgB,KAAK,UAAU,MAAM,aAAa,IAAI;GAC3E,WAAW;GACX,WAAW;EACb,CAAC;CACH;;CAGA,MAAM,IAAI,IAAqC;EAC7C,OAAO,MAAM,KAAK,GAAG,IAAI,UAAU,EAAE;CACvC;;CAGA,MAAM,UAAU,MAAuC;EAErD,QAAO,MADY,KAAK,GAAG,KAAK,UAAU;GAAE,OAAO,GAAG,SAAS,MAAM,IAAI;GAAG,OAAO;EAAE,CAAC,EAAA,CAC1E,MAAM;CACpB;;CAGA,MAAM,OAA2B;EAC/B,OAAO,MAAM,KAAK,GAAG,KAAK,QAAQ;CACpC;;CAGA,MAAM,OACJ,IACA,OAGkB;EAClB,MAAM,aAAsC,EAAE,GAAG,MAAM;EACvD,IAAI,MAAM,kBAAkB,KAAA,KAAa,MAAM,kBAAkB,QAAQ,OAAO,MAAM,kBAAkB,UACtG,WAAW,mBAAmB,KAAK,UAAU,MAAM,aAAa;EAGlE,OAAO,MAAM,KAAK,GAAG,OAAO,UAAU,IAAI;GACxC,GAAG;GACH,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;EACpC,CAAgC;CAClC;;CAGA,MAAM,OAAO,IAA2B;EACtC,MAAM,KAAK,GAAG,OAAO,UAAU,EAAE;CACnC;CAEA,MAAc,WAAW,MAA+B;EACtD,MAAM,OAAO,QAAQ,IAAI,KAAK;EAC9B,MAAM,WAAW,MAAM,KAAK,GAAG,KAAK,UAAU,EAAE,OAAO,GAAG,SAAS,MAAM,IAAI,EAAE,CAAC;EAChF,IAAI,SAAS,WAAW,GACtB,OAAO;EAET,MAAM,QAAQ,IAAI,IAAI,SAAS,KAAK,MAAM,EAAE,IAAI,CAAC;EACjD,IAAI,SAAS;EACb,OAAO,MAAM,IAAI,GAAG,KAAK,GAAG,QAAQ,GAClC,UAAU;EAEZ,OAAO,GAAG,KAAK,GAAG;CACpB;AACF"}
1
+ {"version":3,"file":"project.mjs","names":[],"sources":["../../src/models/project.ts"],"sourcesContent":["/** Project records and slug management. */\nimport { eq } from \"drizzle-orm\";\n\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { projects } from \"../schema-tables.ts\";\nimport type { Project } from \"../schema.ts\";\nimport { slugify, ulid } from \"../utils/ulid.ts\";\n\n/** Input for creating a project. */\nexport interface ProjectCreateInput {\n name: string;\n gitRepository?: string;\n gitDefaultBranch?: string;\n storybookMeta?: unknown;\n}\n\n/** Data operations for project records. */\nexport class ProjectModel {\n /**\n * @param db - Database adapter.\n */\n constructor(private readonly db: DatabaseAdapter) {}\n\n /**\n * Create a project with a unique slug.\n *\n * @param input - Project creation input.\n * @returns The created project.\n */\n async create(input: ProjectCreateInput): Promise<Project> {\n const now = new Date().toISOString();\n const slug = await this.uniqueSlug(input.name);\n return this.db.insert(projects, {\n id: ulid(),\n name: input.name,\n slug,\n gitRepository: input.gitRepository,\n gitDefaultBranch: input.gitDefaultBranch ?? \"main\",\n storybookMeta: input.storybookMeta ? JSON.stringify(input.storybookMeta) : null,\n createdAt: now,\n updatedAt: now,\n });\n }\n\n /** Fetch a project by id, or null if not found. */\n async get(id: string): Promise<Project | null> {\n return await this.db.get(projects, id);\n }\n\n /** Fetch a project by its slug, or null if not found. */\n async getBySlug(slug: string): Promise<Project | null> {\n const rows = await this.db.list(projects, { where: eq(projects.slug, slug), limit: 1 });\n return rows[0] ?? null;\n }\n\n /** List all projects. */\n async list(): Promise<Project[]> {\n return await this.db.list(projects);\n }\n\n /** Update mutable fields of a project. */\n async update(\n id: string,\n patch: Partial<Omit<Project, \"storybookMeta\">> & {\n storybookMeta?: unknown;\n },\n ): Promise<Project> {\n const normalized: Partial<Project> & Record<string, unknown> = { ...patch } as unknown as Partial<Project> & Record<string, unknown>;\n if (patch.storybookMeta !== undefined && patch.storybookMeta !== null && typeof patch.storybookMeta !== \"string\") {\n normalized.storybookMeta = JSON.stringify(patch.storybookMeta);\n }\n return await this.db.update(projects, id, {\n ...normalized,\n updatedAt: new Date().toISOString(),\n });\n }\n\n /** Delete a project by id. */\n async remove(id: string): Promise<void> {\n await this.db.remove(projects, id);\n }\n\n private async uniqueSlug(name: string): Promise<string> {\n const base = slugify(name) || \"project\";\n const existing = await this.db.list(projects, { where: eq(projects.slug, base) });\n if (existing.length === 0) {\n return base;\n }\n const taken = new Set(existing.map((p) => p.slug));\n let suffix = 2;\n while (taken.has(`${base}-${suffix}`)) {\n suffix += 1;\n }\n return `${base}-${suffix}`;\n }\n}\n\nexport type { Project };\n"],"mappings":";;;;;;;;;AAiBA,IAAa,eAAb,MAA0B;CAIK;;;;CAA7B,YAAY,IAAsC;EAArB,KAAA,KAAA;CAAsB;;;;;;;CAQnD,MAAM,OAAO,OAA6C;EACxD,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;EACnC,MAAM,OAAO,MAAM,KAAK,WAAW,MAAM,IAAI;EAC7C,OAAO,KAAK,GAAG,OAAO,UAAU;GAC9B,IAAI,KAAK;GACT,MAAM,MAAM;GACZ;GACA,eAAe,MAAM;GACrB,kBAAkB,MAAM,oBAAoB;GAC5C,eAAe,MAAM,gBAAgB,KAAK,UAAU,MAAM,aAAa,IAAI;GAC3E,WAAW;GACX,WAAW;EACb,CAAC;CACH;;CAGA,MAAM,IAAI,IAAqC;EAC7C,OAAO,MAAM,KAAK,GAAG,IAAI,UAAU,EAAE;CACvC;;CAGA,MAAM,UAAU,MAAuC;EAErD,QAAO,MADY,KAAK,GAAG,KAAK,UAAU;GAAE,OAAO,GAAG,SAAS,MAAM,IAAI;GAAG,OAAO;EAAE,CAAC,EAAA,CAC1E,MAAM;CACpB;;CAGA,MAAM,OAA2B;EAC/B,OAAO,MAAM,KAAK,GAAG,KAAK,QAAQ;CACpC;;CAGA,MAAM,OACJ,IACA,OAGkB;EAClB,MAAM,aAAyD,EAAE,GAAG,MAAM;EAC1E,IAAI,MAAM,kBAAkB,KAAA,KAAa,MAAM,kBAAkB,QAAQ,OAAO,MAAM,kBAAkB,UACtG,WAAW,gBAAgB,KAAK,UAAU,MAAM,aAAa;EAE/D,OAAO,MAAM,KAAK,GAAG,OAAO,UAAU,IAAI;GACxC,GAAG;GACH,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;EACpC,CAAC;CACH;;CAGA,MAAM,OAAO,IAA2B;EACtC,MAAM,KAAK,GAAG,OAAO,UAAU,EAAE;CACnC;CAEA,MAAc,WAAW,MAA+B;EACtD,MAAM,OAAO,QAAQ,IAAI,KAAK;EAC9B,MAAM,WAAW,MAAM,KAAK,GAAG,KAAK,UAAU,EAAE,OAAO,GAAG,SAAS,MAAM,IAAI,EAAE,CAAC;EAChF,IAAI,SAAS,WAAW,GACtB,OAAO;EAET,MAAM,QAAQ,IAAI,IAAI,SAAS,KAAK,MAAM,EAAE,IAAI,CAAC;EACjD,IAAI,SAAS;EACb,OAAO,MAAM,IAAI,GAAG,KAAK,GAAG,QAAQ,GAClC,UAAU;EAEZ,OAAO,GAAG,KAAK,GAAG;CACpB;AACF"}
@@ -4,6 +4,7 @@ import { SnapshotStatus } from "../types.mjs";
4
4
  import { DatabaseAdapter } from "../adapter/database.mjs";
5
5
  import { Snapshot } from "../schema.mjs";
6
6
  //#region src/models/snapshot.d.ts
7
+ /** Input for creating a snapshot. */
7
8
  interface SnapshotCreateInput {
8
9
  storyId: string;
9
10
  storyName: string;
@@ -1,10 +1,11 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- import { snapshots } from "../schema.mjs";
4
+ import { snapshots } from "../schema-tables.mjs";
5
5
  import { ulid } from "../utils/ulid.mjs";
6
6
  import { eq } from "drizzle-orm";
7
7
  //#region src/models/snapshot.ts
8
+ /** Snapshot records for captured stories within a build. */
8
9
  /** Data operations for snapshot records. */
9
10
  var SnapshotModel = class {
10
11
  db;
@@ -1 +1 @@
1
- {"version":3,"file":"snapshot.mjs","names":[],"sources":["../../src/models/snapshot.ts"],"sourcesContent":["import { eq } from \"drizzle-orm\";\n\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { snapshots, type Snapshot } from \"../schema.ts\";\nimport type { SnapshotStatus } from \"../types.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\n\nexport interface SnapshotCreateInput {\n storyId: string;\n storyName: string;\n storyTitle: string;\n storyImportPath?: string;\n viewportName: string;\n viewportWidth: number;\n viewportHeight: number;\n screenshotPath: string;\n}\n\n/** Data operations for snapshot records. */\nexport class SnapshotModel {\n /**\n * @param db - Database adapter.\n */\n constructor(private readonly db: DatabaseAdapter) {}\n\n /**\n * Create a snapshot for a story within a build.\n *\n * @param projectId - Project ID.\n * @param buildId - Build ID.\n * @param input - Snapshot creation input.\n * @returns The created snapshot.\n */\n async create(projectId: string, buildId: string, input: SnapshotCreateInput): Promise<Snapshot> {\n const now = new Date().toISOString();\n return await this.db.insert(snapshots, {\n id: ulid(),\n projectId,\n buildId,\n storyId: input.storyId,\n storyName: input.storyName,\n storyTitle: input.storyTitle,\n storyImportPath: input.storyImportPath,\n viewportName: input.viewportName,\n viewportWidth: input.viewportWidth,\n viewportHeight: input.viewportHeight,\n screenshotPath: input.screenshotPath,\n status: \"pending\",\n createdAt: now,\n updatedAt: now,\n });\n }\n\n /** List all snapshots belonging to a build. */\n async listByBuild(buildId: string): Promise<Snapshot[]> {\n return await this.db.list(snapshots, { where: eq(snapshots.buildId, buildId) });\n }\n\n /** Fetch a snapshot by id, or null if not found. */\n async get(id: string): Promise<Snapshot | null> {\n return await this.db.get(snapshots, id);\n }\n\n /** Update mutable fields of a snapshot. */\n async update(id: string, patch: Partial<Snapshot>): Promise<Snapshot> {\n return await this.db.update(snapshots, id, { ...patch, updatedAt: new Date().toISOString() });\n }\n\n /** Set the status of a snapshot. */\n async setStatus(id: string, status: SnapshotStatus): Promise<Snapshot> {\n return await this.update(id, { status });\n }\n\n /** Record a reviewer's decision on a snapshot. */\n async review(id: string, status: SnapshotStatus, userId: string): Promise<Snapshot> {\n return await this.update(id, { status, reviewedBy: userId, reviewedAt: new Date().toISOString() });\n }\n}\n\nexport type { Snapshot };\n"],"mappings":";;;;;;;;AAmBA,IAAa,gBAAb,MAA2B;CAII;;;;CAA7B,YAAY,IAAsC;EAArB,KAAA,KAAA;CAAsB;;;;;;;;;CAUnD,MAAM,OAAO,WAAmB,SAAiB,OAA+C;EAC9F,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;EACnC,OAAO,MAAM,KAAK,GAAG,OAAO,WAAW;GACrC,IAAI,KAAK;GACT;GACA;GACA,SAAS,MAAM;GACf,WAAW,MAAM;GACjB,YAAY,MAAM;GAClB,iBAAiB,MAAM;GACvB,cAAc,MAAM;GACpB,eAAe,MAAM;GACrB,gBAAgB,MAAM;GACtB,gBAAgB,MAAM;GACtB,QAAQ;GACR,WAAW;GACX,WAAW;EACb,CAAC;CACH;;CAGA,MAAM,YAAY,SAAsC;EACtD,OAAO,MAAM,KAAK,GAAG,KAAK,WAAW,EAAE,OAAO,GAAG,UAAU,SAAS,OAAO,EAAE,CAAC;CAChF;;CAGA,MAAM,IAAI,IAAsC;EAC9C,OAAO,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE;CACxC;;CAGA,MAAM,OAAO,IAAY,OAA6C;EACpE,OAAO,MAAM,KAAK,GAAG,OAAO,WAAW,IAAI;GAAE,GAAG;GAAO,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;EAAE,CAAC;CAC9F;;CAGA,MAAM,UAAU,IAAY,QAA2C;EACrE,OAAO,MAAM,KAAK,OAAO,IAAI,EAAE,OAAO,CAAC;CACzC;;CAGA,MAAM,OAAO,IAAY,QAAwB,QAAmC;EAClF,OAAO,MAAM,KAAK,OAAO,IAAI;GAAE;GAAQ,YAAY;GAAQ,6BAAY,IAAI,KAAK,EAAA,CAAE,YAAY;EAAE,CAAC;CACnG;AACF"}
1
+ {"version":3,"file":"snapshot.mjs","names":[],"sources":["../../src/models/snapshot.ts"],"sourcesContent":["/** Snapshot records for captured stories within a build. */\nimport { eq } from \"drizzle-orm\";\n\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { snapshots } from \"../schema-tables.ts\";\nimport type { Snapshot } from \"../schema.ts\";\nimport type { SnapshotStatus } from \"../types.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\n\n/** Input for creating a snapshot. */\nexport interface SnapshotCreateInput {\n storyId: string;\n storyName: string;\n storyTitle: string;\n storyImportPath?: string;\n viewportName: string;\n viewportWidth: number;\n viewportHeight: number;\n screenshotPath: string;\n}\n\n/** Data operations for snapshot records. */\nexport class SnapshotModel {\n /**\n * @param db - Database adapter.\n */\n constructor(private readonly db: DatabaseAdapter) {}\n\n /**\n * Create a snapshot for a story within a build.\n *\n * @param projectId - Project ID.\n * @param buildId - Build ID.\n * @param input - Snapshot creation input.\n * @returns The created snapshot.\n */\n async create(projectId: string, buildId: string, input: SnapshotCreateInput): Promise<Snapshot> {\n const now = new Date().toISOString();\n return await this.db.insert(snapshots, {\n id: ulid(),\n projectId,\n buildId,\n storyId: input.storyId,\n storyName: input.storyName,\n storyTitle: input.storyTitle,\n storyImportPath: input.storyImportPath,\n viewportName: input.viewportName,\n viewportWidth: input.viewportWidth,\n viewportHeight: input.viewportHeight,\n screenshotPath: input.screenshotPath,\n status: \"pending\",\n createdAt: now,\n updatedAt: now,\n });\n }\n\n /** List all snapshots belonging to a build. */\n async listByBuild(buildId: string): Promise<Snapshot[]> {\n return await this.db.list(snapshots, { where: eq(snapshots.buildId, buildId) });\n }\n\n /** Fetch a snapshot by id, or null if not found. */\n async get(id: string): Promise<Snapshot | null> {\n return await this.db.get(snapshots, id);\n }\n\n /** Update mutable fields of a snapshot. */\n async update(id: string, patch: Partial<Snapshot>): Promise<Snapshot> {\n return await this.db.update(snapshots, id, { ...patch, updatedAt: new Date().toISOString() });\n }\n\n /** Set the status of a snapshot. */\n async setStatus(id: string, status: SnapshotStatus): Promise<Snapshot> {\n return await this.update(id, { status });\n }\n\n /** Record a reviewer's decision on a snapshot. */\n async review(id: string, status: SnapshotStatus, userId: string): Promise<Snapshot> {\n return await this.update(id, { status, reviewedBy: userId, reviewedAt: new Date().toISOString() });\n }\n}\n\nexport type { Snapshot };\n"],"mappings":";;;;;;;;;AAsBA,IAAa,gBAAb,MAA2B;CAII;;;;CAA7B,YAAY,IAAsC;EAArB,KAAA,KAAA;CAAsB;;;;;;;;;CAUnD,MAAM,OAAO,WAAmB,SAAiB,OAA+C;EAC9F,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;EACnC,OAAO,MAAM,KAAK,GAAG,OAAO,WAAW;GACrC,IAAI,KAAK;GACT;GACA;GACA,SAAS,MAAM;GACf,WAAW,MAAM;GACjB,YAAY,MAAM;GAClB,iBAAiB,MAAM;GACvB,cAAc,MAAM;GACpB,eAAe,MAAM;GACrB,gBAAgB,MAAM;GACtB,gBAAgB,MAAM;GACtB,QAAQ;GACR,WAAW;GACX,WAAW;EACb,CAAC;CACH;;CAGA,MAAM,YAAY,SAAsC;EACtD,OAAO,MAAM,KAAK,GAAG,KAAK,WAAW,EAAE,OAAO,GAAG,UAAU,SAAS,OAAO,EAAE,CAAC;CAChF;;CAGA,MAAM,IAAI,IAAsC;EAC9C,OAAO,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE;CACxC;;CAGA,MAAM,OAAO,IAAY,OAA6C;EACpE,OAAO,MAAM,KAAK,GAAG,OAAO,WAAW,IAAI;GAAE,GAAG;GAAO,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;EAAE,CAAC;CAC9F;;CAGA,MAAM,UAAU,IAAY,QAA2C;EACrE,OAAO,MAAM,KAAK,OAAO,IAAI,EAAE,OAAO,CAAC;CACzC;;CAGA,MAAM,OAAO,IAAY,QAAwB,QAAmC;EAClF,OAAO,MAAM,KAAK,OAAO,IAAI;GAAE;GAAQ,YAAY;GAAQ,6BAAY,IAAI,KAAK,EAAA,CAAE,YAAY;EAAE,CAAC;CACnG;AACF"}
@@ -1,11 +1,12 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- import { projectStatusConfigs } from "../schema.mjs";
4
+ import { projectStatusConfigs } from "../schema-tables.mjs";
5
5
  import { ulid } from "../utils/ulid.mjs";
6
6
  import { decrypt, encrypt } from "../utils/encrypt.mjs";
7
7
  import { eq } from "drizzle-orm";
8
8
  //#region src/models/status-config.ts
9
+ /** Per-project git-provider status check configurations. */
9
10
  /** Data operations for per-project status provider configs. */
10
11
  var StatusConfigModel = class {
11
12
  db;
@@ -1 +1 @@
1
- {"version":3,"file":"status-config.mjs","names":[],"sources":["../../src/models/status-config.ts"],"sourcesContent":["/* oxlint-disable eslint/no-await-in-loop, typescript/promise-function-async, eslint/require-await */\nimport { eq } from \"drizzle-orm\";\n\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { projectStatusConfigs, type ProjectStatusConfig } from \"../schema.ts\";\nimport { decrypt, encrypt } from \"../utils/encrypt.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\n\nexport interface StatusConfigCreateInput {\n provider: string;\n config: unknown;\n token: string;\n}\n\n/** Data operations for per-project status provider configs. */\nexport class StatusConfigModel {\n constructor(\n private readonly db: DatabaseAdapter,\n private readonly secret: string | undefined,\n ) {}\n\n /** List all status configs for a project. */\n async list(projectId: string): Promise<ProjectStatusConfig[]> {\n return await this.db.list(projectStatusConfigs, { where: eq(projectStatusConfigs.projectId, projectId) });\n }\n\n /** Fetch a config by id scoped to a project, or null. */\n async get(projectId: string, id: string): Promise<ProjectStatusConfig | null> {\n const rows = await this.db.list(projectStatusConfigs, { where: eq(projectStatusConfigs.id, id), limit: 1 });\n const found = rows[0] ?? null;\n return found?.projectId === projectId ? found : null;\n }\n\n /** Decrypt the token for a stored row. */\n decryptToken(row: ProjectStatusConfig): string {\n return decrypt(this.secret, row.tokenEncrypted);\n }\n\n /** Parse the JSON config column. */\n static parseConfig(row: ProjectStatusConfig): unknown {\n return JSON.parse(row.config) as unknown;\n }\n\n async create(projectId: string, input: StatusConfigCreateInput): Promise<ProjectStatusConfig> {\n const now = new Date().toISOString();\n return await this.db.insert(projectStatusConfigs, {\n id: ulid(),\n projectId,\n provider: input.provider,\n config: JSON.stringify(input.config),\n tokenEncrypted: encrypt(this.secret, input.token),\n createdAt: now,\n updatedAt: now,\n });\n }\n\n async remove(projectId: string, id: string): Promise<void> {\n const existing = await this.get(projectId, id);\n if (existing) {\n await this.db.remove(projectStatusConfigs, existing.id);\n }\n }\n\n async removeByProject(projectId: string): Promise<void> {\n const rows = await this.list(projectId);\n for (const row of rows) {\n await this.db.remove(projectStatusConfigs, row.id);\n }\n }\n}\n\nexport type { ProjectStatusConfig };\n"],"mappings":";;;;;;;;;AAeA,IAAa,oBAAb,MAA+B;CAEV;CACA;CAFnB,YACE,IACA,QACA;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;;CAGH,MAAM,KAAK,WAAmD;EAC5D,OAAO,MAAM,KAAK,GAAG,KAAK,sBAAsB,EAAE,OAAO,GAAG,qBAAqB,WAAW,SAAS,EAAE,CAAC;CAC1G;;CAGA,MAAM,IAAI,WAAmB,IAAiD;EAE5E,MAAM,SAAQ,MADK,KAAK,GAAG,KAAK,sBAAsB;GAAE,OAAO,GAAG,qBAAqB,IAAI,EAAE;GAAG,OAAO;EAAE,CAAC,EAAA,CACvF,MAAM;EACzB,OAAO,OAAO,cAAc,YAAY,QAAQ;CAClD;;CAGA,aAAa,KAAkC;EAC7C,OAAO,QAAQ,KAAK,QAAQ,IAAI,cAAc;CAChD;;CAGA,OAAO,YAAY,KAAmC;EACpD,OAAO,KAAK,MAAM,IAAI,MAAM;CAC9B;CAEA,MAAM,OAAO,WAAmB,OAA8D;EAC5F,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;EACnC,OAAO,MAAM,KAAK,GAAG,OAAO,sBAAsB;GAChD,IAAI,KAAK;GACT;GACA,UAAU,MAAM;GAChB,QAAQ,KAAK,UAAU,MAAM,MAAM;GACnC,gBAAgB,QAAQ,KAAK,QAAQ,MAAM,KAAK;GAChD,WAAW;GACX,WAAW;EACb,CAAC;CACH;CAEA,MAAM,OAAO,WAAmB,IAA2B;EACzD,MAAM,WAAW,MAAM,KAAK,IAAI,WAAW,EAAE;EAC7C,IAAI,UACF,MAAM,KAAK,GAAG,OAAO,sBAAsB,SAAS,EAAE;CAE1D;CAEA,MAAM,gBAAgB,WAAkC;EACtD,MAAM,OAAO,MAAM,KAAK,KAAK,SAAS;EACtC,KAAK,MAAM,OAAO,MAChB,MAAM,KAAK,GAAG,OAAO,sBAAsB,IAAI,EAAE;CAErD;AACF"}
1
+ {"version":3,"file":"status-config.mjs","names":[],"sources":["../../src/models/status-config.ts"],"sourcesContent":["/* oxlint-disable eslint/no-await-in-loop, typescript/promise-function-async, eslint/require-await */\n/** Per-project git-provider status check configurations. */\nimport { eq } from \"drizzle-orm\";\n\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { projectStatusConfigs } from \"../schema-tables.ts\";\nimport type { ProjectStatusConfig } from \"../schema.ts\";\nimport { decrypt, encrypt } from \"../utils/encrypt.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\n\n/** Input for creating a git-provider status config. */\nexport interface StatusConfigCreateInput {\n provider: string;\n config: unknown;\n token: string;\n}\n\n/** Data operations for per-project status provider configs. */\nexport class StatusConfigModel {\n constructor(\n private readonly db: DatabaseAdapter,\n private readonly secret: string | undefined,\n ) {}\n\n /** List all status configs for a project. */\n async list(projectId: string): Promise<ProjectStatusConfig[]> {\n return await this.db.list(projectStatusConfigs, { where: eq(projectStatusConfigs.projectId, projectId) });\n }\n\n /** Fetch a config by id scoped to a project, or null. */\n async get(projectId: string, id: string): Promise<ProjectStatusConfig | null> {\n const rows = await this.db.list(projectStatusConfigs, { where: eq(projectStatusConfigs.id, id), limit: 1 });\n const found = rows[0] ?? null;\n return found?.projectId === projectId ? found : null;\n }\n\n /** Decrypt the token for a stored row. */\n decryptToken(row: ProjectStatusConfig): string {\n return decrypt(this.secret, row.tokenEncrypted);\n }\n\n /** Parse the JSON config column. */\n static parseConfig(row: ProjectStatusConfig): unknown {\n return JSON.parse(row.config) as unknown;\n }\n\n async create(projectId: string, input: StatusConfigCreateInput): Promise<ProjectStatusConfig> {\n const now = new Date().toISOString();\n return await this.db.insert(projectStatusConfigs, {\n id: ulid(),\n projectId,\n provider: input.provider,\n config: JSON.stringify(input.config),\n tokenEncrypted: encrypt(this.secret, input.token),\n createdAt: now,\n updatedAt: now,\n });\n }\n\n async remove(projectId: string, id: string): Promise<void> {\n const existing = await this.get(projectId, id);\n if (existing) {\n await this.db.remove(projectStatusConfigs, existing.id);\n }\n }\n\n async removeByProject(projectId: string): Promise<void> {\n const rows = await this.list(projectId);\n for (const row of rows) {\n await this.db.remove(projectStatusConfigs, row.id);\n }\n }\n}\n\nexport type { ProjectStatusConfig };\n"],"mappings":";;;;;;;;;;AAkBA,IAAa,oBAAb,MAA+B;CAEV;CACA;CAFnB,YACE,IACA,QACA;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;;CAGH,MAAM,KAAK,WAAmD;EAC5D,OAAO,MAAM,KAAK,GAAG,KAAK,sBAAsB,EAAE,OAAO,GAAG,qBAAqB,WAAW,SAAS,EAAE,CAAC;CAC1G;;CAGA,MAAM,IAAI,WAAmB,IAAiD;EAE5E,MAAM,SAAQ,MADK,KAAK,GAAG,KAAK,sBAAsB;GAAE,OAAO,GAAG,qBAAqB,IAAI,EAAE;GAAG,OAAO;EAAE,CAAC,EAAA,CACvF,MAAM;EACzB,OAAO,OAAO,cAAc,YAAY,QAAQ;CAClD;;CAGA,aAAa,KAAkC;EAC7C,OAAO,QAAQ,KAAK,QAAQ,IAAI,cAAc;CAChD;;CAGA,OAAO,YAAY,KAAmC;EACpD,OAAO,KAAK,MAAM,IAAI,MAAM;CAC9B;CAEA,MAAM,OAAO,WAAmB,OAA8D;EAC5F,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;EACnC,OAAO,MAAM,KAAK,GAAG,OAAO,sBAAsB;GAChD,IAAI,KAAK;GACT;GACA,UAAU,MAAM;GAChB,QAAQ,KAAK,UAAU,MAAM,MAAM;GACnC,gBAAgB,QAAQ,KAAK,QAAQ,MAAM,KAAK;GAChD,WAAW;GACX,WAAW;EACb,CAAC;CACH;CAEA,MAAM,OAAO,WAAmB,IAA2B;EACzD,MAAM,WAAW,MAAM,KAAK,IAAI,WAAW,EAAE;EAC7C,IAAI,UACF,MAAM,KAAK,GAAG,OAAO,sBAAsB,SAAS,EAAE;CAE1D;CAEA,MAAM,gBAAgB,WAAkC;EACtD,MAAM,OAAO,MAAM,KAAK,KAAK,SAAS;EACtC,KAAK,MAAM,OAAO,MAChB,MAAM,KAAK,GAAG,OAAO,sBAAsB,IAAI,EAAE;CAErD;AACF"}
@@ -1,10 +1,11 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- import { tokens } from "../schema.mjs";
4
+ import { tokens } from "../schema-tables.mjs";
5
5
  import { ulid } from "../utils/ulid.mjs";
6
6
  import { and, eq } from "drizzle-orm";
7
7
  //#region src/models/token.ts
8
+ /** CI tokens for project-scoped API access. */
8
9
  /** Data operations for CI tokens. */
9
10
  var TokenModel = class {
10
11
  db;
@@ -1 +1 @@
1
- {"version":3,"file":"token.mjs","names":[],"sources":["../../src/models/token.ts"],"sourcesContent":["import { and, eq } from \"drizzle-orm\";\n\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { tokens, type Token } from \"../schema.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\n\n/** Data operations for CI tokens. */\nexport class TokenModel {\n /**\n * @param db - Database adapter.\n */\n constructor(private readonly db: DatabaseAdapter) {}\n\n /**\n * Create a token record storing its hash.\n *\n * @param projectId - Project ID.\n * @param name - Token name.\n * @param hash - Hashed token value.\n * @returns The created token.\n */\n async create(projectId: string, name: string, hash: string): Promise<Token> {\n return await this.db.insert(tokens, {\n id: ulid(),\n projectId,\n name,\n hash,\n createdAt: new Date().toISOString(),\n });\n }\n\n /** List all tokens for a project. */\n async list(projectId: string): Promise<Token[]> {\n return await this.db.list(tokens, { where: eq(tokens.projectId, projectId) });\n }\n\n /** Fetch a token by id within a project, or null if not found. */\n async get(projectId: string, id: string): Promise<Token | null> {\n const rows = await this.db.list(tokens, {\n where: and(eq(tokens.projectId, projectId), eq(tokens.id, id)),\n limit: 1,\n });\n return rows[0] ?? null;\n }\n\n /** Fetch a token by its hashed value, or null if not found. */\n async findByHash(hash: string): Promise<Token | null> {\n const rows = await this.db.list(tokens, { where: eq(tokens.hash, hash), limit: 1 });\n return rows[0] ?? null;\n }\n\n /** Delete a token by id. */\n async remove(id: string): Promise<void> {\n await this.db.remove(tokens, id);\n }\n}\n\nexport type { Token };\n"],"mappings":";;;;;;;;AAOA,IAAa,aAAb,MAAwB;CAIO;;;;CAA7B,YAAY,IAAsC;EAArB,KAAA,KAAA;CAAsB;;;;;;;;;CAUnD,MAAM,OAAO,WAAmB,MAAc,MAA8B;EAC1E,OAAO,MAAM,KAAK,GAAG,OAAO,QAAQ;GAClC,IAAI,KAAK;GACT;GACA;GACA;GACA,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;EACpC,CAAC;CACH;;CAGA,MAAM,KAAK,WAAqC;EAC9C,OAAO,MAAM,KAAK,GAAG,KAAK,QAAQ,EAAE,OAAO,GAAG,OAAO,WAAW,SAAS,EAAE,CAAC;CAC9E;;CAGA,MAAM,IAAI,WAAmB,IAAmC;EAK9D,QAAO,MAJY,KAAK,GAAG,KAAK,QAAQ;GACtC,OAAO,IAAI,GAAG,OAAO,WAAW,SAAS,GAAG,GAAG,OAAO,IAAI,EAAE,CAAC;GAC7D,OAAO;EACT,CAAC,EAAA,CACW,MAAM;CACpB;;CAGA,MAAM,WAAW,MAAqC;EAEpD,QAAO,MADY,KAAK,GAAG,KAAK,QAAQ;GAAE,OAAO,GAAG,OAAO,MAAM,IAAI;GAAG,OAAO;EAAE,CAAC,EAAA,CACtE,MAAM;CACpB;;CAGA,MAAM,OAAO,IAA2B;EACtC,MAAM,KAAK,GAAG,OAAO,QAAQ,EAAE;CACjC;AACF"}
1
+ {"version":3,"file":"token.mjs","names":[],"sources":["../../src/models/token.ts"],"sourcesContent":["/** CI tokens for project-scoped API access. */\nimport { and, eq } from \"drizzle-orm\";\n\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { tokens } from \"../schema-tables.ts\";\nimport type { Token } from \"../schema.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\n\n/** Data operations for CI tokens. */\nexport class TokenModel {\n /**\n * @param db - Database adapter.\n */\n constructor(private readonly db: DatabaseAdapter) {}\n\n /**\n * Create a token record storing its hash.\n *\n * @param projectId - Project ID.\n * @param name - Token name.\n * @param hash - Hashed token value.\n * @returns The created token.\n */\n async create(projectId: string, name: string, hash: string): Promise<Token> {\n return await this.db.insert(tokens, {\n id: ulid(),\n projectId,\n name,\n hash,\n createdAt: new Date().toISOString(),\n });\n }\n\n /** List all tokens for a project. */\n async list(projectId: string): Promise<Token[]> {\n return await this.db.list(tokens, { where: eq(tokens.projectId, projectId) });\n }\n\n /** Fetch a token by id within a project, or null if not found. */\n async get(projectId: string, id: string): Promise<Token | null> {\n const rows = await this.db.list(tokens, {\n where: and(eq(tokens.projectId, projectId), eq(tokens.id, id)),\n limit: 1,\n });\n return rows[0] ?? null;\n }\n\n /** Fetch a token by its hashed value, or null if not found. */\n async findByHash(hash: string): Promise<Token | null> {\n const rows = await this.db.list(tokens, { where: eq(tokens.hash, hash), limit: 1 });\n return rows[0] ?? null;\n }\n\n /** Delete a token by id. */\n async remove(id: string): Promise<void> {\n await this.db.remove(tokens, id);\n }\n}\n\nexport type { Token };\n"],"mappings":";;;;;;;;;AASA,IAAa,aAAb,MAAwB;CAIO;;;;CAA7B,YAAY,IAAsC;EAArB,KAAA,KAAA;CAAsB;;;;;;;;;CAUnD,MAAM,OAAO,WAAmB,MAAc,MAA8B;EAC1E,OAAO,MAAM,KAAK,GAAG,OAAO,QAAQ;GAClC,IAAI,KAAK;GACT;GACA;GACA;GACA,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;EACpC,CAAC;CACH;;CAGA,MAAM,KAAK,WAAqC;EAC9C,OAAO,MAAM,KAAK,GAAG,KAAK,QAAQ,EAAE,OAAO,GAAG,OAAO,WAAW,SAAS,EAAE,CAAC;CAC9E;;CAGA,MAAM,IAAI,WAAmB,IAAmC;EAK9D,QAAO,MAJY,KAAK,GAAG,KAAK,QAAQ;GACtC,OAAO,IAAI,GAAG,OAAO,WAAW,SAAS,GAAG,GAAG,OAAO,IAAI,EAAE,CAAC;GAC7D,OAAO;EACT,CAAC,EAAA,CACW,MAAM;CACpB;;CAGA,MAAM,WAAW,MAAqC;EAEpD,QAAO,MADY,KAAK,GAAG,KAAK,QAAQ;GAAE,OAAO,GAAG,OAAO,MAAM,IAAI;GAAG,OAAO;EAAE,CAAC,EAAA,CACtE,MAAM;CACpB;;CAGA,MAAM,OAAO,IAA2B;EACtC,MAAM,KAAK,GAAG,OAAO,QAAQ,EAAE;CACjC;AACF"}
@@ -3,6 +3,7 @@ import __tsdown_shims_url from 'node:url';
3
3
  import { DatabaseAdapter } from "../adapter/database.mjs";
4
4
  import { Webhook } from "../schema.mjs";
5
5
  //#region src/models/webhook.d.ts
6
+ /** Input for creating a webhook subscription. */
6
7
  interface WebhookCreateInput {
7
8
  url: string;
8
9
  events?: string[];
@@ -1,10 +1,11 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- import { webhooks } from "../schema.mjs";
4
+ import { webhooks } from "../schema-tables.mjs";
5
5
  import { ulid } from "../utils/ulid.mjs";
6
6
  import { eq } from "drizzle-orm";
7
7
  //#region src/models/webhook.ts
8
+ /** Webhook subscriptions for project events. */
8
9
  /** Data operations for webhook subscriptions. */
9
10
  var WebhookModel = class {
10
11
  db;
@@ -1 +1 @@
1
- {"version":3,"file":"webhook.mjs","names":[],"sources":["../../src/models/webhook.ts"],"sourcesContent":["import { eq } from \"drizzle-orm\";\n\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { webhooks, type Webhook } from \"../schema.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\n\nexport interface WebhookCreateInput {\n url: string;\n events?: string[];\n secret: string;\n}\n\n/** Data operations for webhook subscriptions. */\nexport class WebhookModel {\n /**\n * @param db - Database adapter.\n */\n constructor(private readonly db: DatabaseAdapter) {}\n\n /** List all webhooks for a project. */\n async list(projectId: string): Promise<Webhook[]> {\n return await this.db.list(webhooks, { where: eq(webhooks.projectId, projectId) });\n }\n\n /**\n * Create a webhook subscription for a project.\n *\n * @param projectId - Project ID.\n * @param input - Webhook creation input.\n * @returns The created webhook.\n */\n async create(projectId: string, input: WebhookCreateInput): Promise<Webhook> {\n const now = new Date().toISOString();\n return await this.db.insert(webhooks, {\n id: ulid(),\n projectId,\n url: input.url,\n secret: input.secret,\n events: input.events && input.events.length > 0 ? JSON.stringify(input.events) : null,\n createdAt: now,\n updatedAt: now,\n });\n }\n\n /** Fetch a webhook by id scoped to a project, or null if not found. */\n async get(projectId: string, id: string): Promise<Webhook | null> {\n const rows = await this.db.list(webhooks, { where: eq(webhooks.id, id), limit: 1 });\n const found = rows[0] ?? null;\n return found?.projectId === projectId ? found : null;\n }\n\n /** Remove a webhook if it belongs to the given project. */\n async remove(projectId: string, id: string): Promise<void> {\n const existing = await this.get(projectId, id);\n if (existing) {\n await this.db.remove(webhooks, existing.id);\n }\n }\n\n /** Decode the JSON-serialized event list of a webhook. */\n static eventsOf(webhook: Webhook): string[] {\n if (!webhook.events) {\n return [];\n }\n try {\n const parsed: unknown = JSON.parse(webhook.events);\n return Array.isArray(parsed) ? parsed.filter((event): event is string => typeof event === \"string\") : [];\n } catch {\n return [];\n }\n }\n}\n\nexport type { Webhook };"],"mappings":";;;;;;;;AAaA,IAAa,eAAb,MAA0B;CAIK;;;;CAA7B,YAAY,IAAsC;EAArB,KAAA,KAAA;CAAsB;;CAGnD,MAAM,KAAK,WAAuC;EAChD,OAAO,MAAM,KAAK,GAAG,KAAK,UAAU,EAAE,OAAO,GAAG,SAAS,WAAW,SAAS,EAAE,CAAC;CAClF;;;;;;;;CASA,MAAM,OAAO,WAAmB,OAA6C;EAC3E,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;EACnC,OAAO,MAAM,KAAK,GAAG,OAAO,UAAU;GACpC,IAAI,KAAK;GACT;GACA,KAAK,MAAM;GACX,QAAQ,MAAM;GACd,QAAQ,MAAM,UAAU,MAAM,OAAO,SAAS,IAAI,KAAK,UAAU,MAAM,MAAM,IAAI;GACjF,WAAW;GACX,WAAW;EACb,CAAC;CACH;;CAGA,MAAM,IAAI,WAAmB,IAAqC;EAEhE,MAAM,SAAQ,MADK,KAAK,GAAG,KAAK,UAAU;GAAE,OAAO,GAAG,SAAS,IAAI,EAAE;GAAG,OAAO;EAAE,CAAC,EAAA,CAC/D,MAAM;EACzB,OAAO,OAAO,cAAc,YAAY,QAAQ;CAClD;;CAGA,MAAM,OAAO,WAAmB,IAA2B;EACzD,MAAM,WAAW,MAAM,KAAK,IAAI,WAAW,EAAE;EAC7C,IAAI,UACF,MAAM,KAAK,GAAG,OAAO,UAAU,SAAS,EAAE;CAE9C;;CAGA,OAAO,SAAS,SAA4B;EAC1C,IAAI,CAAC,QAAQ,QACX,OAAO,CAAC;EAEV,IAAI;GACF,MAAM,SAAkB,KAAK,MAAM,QAAQ,MAAM;GACjD,OAAO,MAAM,QAAQ,MAAM,IAAI,OAAO,QAAQ,UAA2B,OAAO,UAAU,QAAQ,IAAI,CAAC;EACzG,QAAQ;GACN,OAAO,CAAC;EACV;CACF;AACF"}
1
+ {"version":3,"file":"webhook.mjs","names":[],"sources":["../../src/models/webhook.ts"],"sourcesContent":["/** Webhook subscriptions for project events. */\nimport { eq } from \"drizzle-orm\";\n\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { webhooks } from \"../schema-tables.ts\";\nimport type { Webhook } from \"../schema.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\n\n/** Input for creating a webhook subscription. */\nexport interface WebhookCreateInput {\n url: string;\n events?: string[];\n secret: string;\n}\n\n/** Data operations for webhook subscriptions. */\nexport class WebhookModel {\n /**\n * @param db - Database adapter.\n */\n constructor(private readonly db: DatabaseAdapter) {}\n\n /** List all webhooks for a project. */\n async list(projectId: string): Promise<Webhook[]> {\n return await this.db.list(webhooks, { where: eq(webhooks.projectId, projectId) });\n }\n\n /**\n * Create a webhook subscription for a project.\n *\n * @param projectId - Project ID.\n * @param input - Webhook creation input.\n * @returns The created webhook.\n */\n async create(projectId: string, input: WebhookCreateInput): Promise<Webhook> {\n const now = new Date().toISOString();\n return await this.db.insert(webhooks, {\n id: ulid(),\n projectId,\n url: input.url,\n secret: input.secret,\n events: input.events && input.events.length > 0 ? JSON.stringify(input.events) : null,\n createdAt: now,\n updatedAt: now,\n });\n }\n\n /** Fetch a webhook by id scoped to a project, or null if not found. */\n async get(projectId: string, id: string): Promise<Webhook | null> {\n const rows = await this.db.list(webhooks, { where: eq(webhooks.id, id), limit: 1 });\n const found = rows[0] ?? null;\n return found?.projectId === projectId ? found : null;\n }\n\n /** Remove a webhook if it belongs to the given project. */\n async remove(projectId: string, id: string): Promise<void> {\n const existing = await this.get(projectId, id);\n if (existing) {\n await this.db.remove(webhooks, existing.id);\n }\n }\n\n /** Decode the JSON-serialized event list of a webhook. */\n static eventsOf(webhook: Webhook): string[] {\n if (!webhook.events) {\n return [];\n }\n try {\n const parsed: unknown = JSON.parse(webhook.events);\n return Array.isArray(parsed) ? parsed.filter((event): event is string => typeof event === \"string\") : [];\n } catch {\n return [];\n }\n }\n}\n\nexport type { Webhook };"],"mappings":";;;;;;;;;AAgBA,IAAa,eAAb,MAA0B;CAIK;;;;CAA7B,YAAY,IAAsC;EAArB,KAAA,KAAA;CAAsB;;CAGnD,MAAM,KAAK,WAAuC;EAChD,OAAO,MAAM,KAAK,GAAG,KAAK,UAAU,EAAE,OAAO,GAAG,SAAS,WAAW,SAAS,EAAE,CAAC;CAClF;;;;;;;;CASA,MAAM,OAAO,WAAmB,OAA6C;EAC3E,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;EACnC,OAAO,MAAM,KAAK,GAAG,OAAO,UAAU;GACpC,IAAI,KAAK;GACT;GACA,KAAK,MAAM;GACX,QAAQ,MAAM;GACd,QAAQ,MAAM,UAAU,MAAM,OAAO,SAAS,IAAI,KAAK,UAAU,MAAM,MAAM,IAAI;GACjF,WAAW;GACX,WAAW;EACb,CAAC;CACH;;CAGA,MAAM,IAAI,WAAmB,IAAqC;EAEhE,MAAM,SAAQ,MADK,KAAK,GAAG,KAAK,UAAU;GAAE,OAAO,GAAG,SAAS,IAAI,EAAE;GAAG,OAAO;EAAE,CAAC,EAAA,CAC/D,MAAM;EACzB,OAAO,OAAO,cAAc,YAAY,QAAQ;CAClD;;CAGA,MAAM,OAAO,WAAmB,IAA2B;EACzD,MAAM,WAAW,MAAM,KAAK,IAAI,WAAW,EAAE;EAC7C,IAAI,UACF,MAAM,KAAK,GAAG,OAAO,UAAU,SAAS,EAAE;CAE9C;;CAGA,OAAO,SAAS,SAA4B;EAC1C,IAAI,CAAC,QAAQ,QACX,OAAO,CAAC;EAEV,IAAI;GACF,MAAM,SAAkB,KAAK,MAAM,QAAQ,MAAM;GACjD,OAAO,MAAM,QAAQ,MAAM,IAAI,OAAO,QAAQ,UAA2B,OAAO,UAAU,QAAQ,IAAI,CAAC;EACzG,QAAQ;GACN,OAAO,CAAC;EACV;CACF;AACF"}
@@ -10,8 +10,9 @@ import { CommentModel } from "../models/comment.mjs";
10
10
  import { Badge, statusTone } from "../ui/components.mjs";
11
11
  import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
12
12
  //#region src/pages/build-detail.tsx
13
+ /** Build overview page: snapshot grid, bulk actions, and build comments. */
13
14
  async function renderBuildDetailPage(buildId) {
14
- const db = getStore().db;
15
+ const { db } = getStore();
15
16
  const build = await new BuildModel(db).get(buildId);
16
17
  if (!build) return null;
17
18
  const project = await new ProjectModel(db).get(build.projectId);
@@ -1 +1 @@
1
- {"version":3,"file":"build-detail.mjs","names":[],"sources":["../../src/pages/build-detail.tsx"],"sourcesContent":["import { BuildModel } from \"../models/build.ts\";\nimport { CommentModel } from \"../models/comment.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { SnapshotModel } from \"../models/snapshot.ts\";\nimport { getStore } from \"../store.ts\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { Badge, statusTone } from \"../ui/components.tsx\";\n\nexport async function renderBuildDetailPage(buildId: string): Promise<RenderedContent | null> {\n const db = getStore().db;\n const build = await new BuildModel(db).get(buildId);\n if (!build) {\n return null;\n }\n const project = await new ProjectModel(db).get(build.projectId);\n if (!project) {\n return null;\n }\n const snapshots = await new SnapshotModel(db).listByBuild(build.id);\n const comments = await new CommentModel(db).listByBuild(build.id);\n const canReview = !getStore().authEnabled || Boolean(getStore().user);\n\n const grouped = new Map<string, typeof snapshots>();\n for (const snap of snapshots) {\n const key = snap.status;\n const list = grouped.get(key) ?? [];\n list.push(snap);\n grouped.set(key, list);\n }\n\n return (\n <DocumentLayout title={`Build ${build.gitBranch}`} nav={{ active: \"builds\", projectSlug: project.slug, projectName: project.name }}>\n <div class=\"page-header\">\n <nav class=\"breadcrumbs\" aria-label=\"Breadcrumb\">\n <ol>\n <li>\n <a href=\"/projects\">Projects</a>\n </li>\n <li>\n <a href={`/projects/${project.slug}/builds`}>{project.name}</a>\n </li>\n <li>\n <span aria-current=\"page\">\n {build.gitBranch} · {build.gitSha.slice(0, 7)}\n </span>\n </li>\n </ol>\n </nav>\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">\n {build.gitBranch} <span style=\"color:var(--text-secondary); font-weight:400;\">· {build.gitSha.slice(0, 7)}</span> <Badge tone={statusTone(build.status)}>{build.status}</Badge>\n </h1>\n <p class=\"page-header__desc\">\n {build.message ?? \"No commit message\"} {build.authorName ? `· ${build.authorName}${build.authorEmail ? ` <${build.authorEmail}>` : \"\"}` : \"\"} · {new Date(build.createdAt).toLocaleString()}\n </p>\n </div>\n <div class=\"page-header__actions\">\n <a class=\"btn btn--primary\" href={`/projects/${project.slug}/builds/${build.id}/diff`}>\n Review diffs\n </a>\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/retry`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/retry`} hx-target=\"body\">\n <button class=\"btn btn--secondary\" type=\"submit\">\n Retry capture\n </button>\n </form>\n </div>\n </div>\n </div>\n\n <div class=\"grid grid--3\" style=\"margin-bottom:1rem;\">\n <div class=\"card card--padded stat\">\n <div class=\"stat__value\">{snapshots.length}</div>\n <div class=\"stat__label\">Snapshots</div>\n </div>\n <div class=\"card card--padded stat\">\n <div class=\"stat__value\" style=\"color: var(--status-new);\">\n {build.changedCount}\n </div>\n <div class=\"stat__label\">Changed / new</div>\n </div>\n <div class=\"card card--padded stat\">\n <div class=\"stat__value\" style=\"color: var(--status-approved);\">\n {build.approvedCount}\n </div>\n <div class=\"stat__label\">Approved / unchanged</div>\n </div>\n </div>\n\n {canReview && (build.status === \"reviewing\" || build.status === \"comparing\") ? (\n <div class=\"card card--padded\" style=\"display:flex; gap:.5rem; flex-wrap:wrap; align-items:center; margin-bottom:1rem;\">\n <span class=\"field__hint\">Bulk actions:</span>\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/approve-all`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/approve-all`} hx-target=\"body\">\n <button class=\"btn btn--primary\" type=\"submit\">\n Approve all\n </button>\n </form>\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/reject-all`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/reject-all`} hx-target=\"body\">\n <button class=\"btn btn--danger\" type=\"submit\">\n Reject all\n </button>\n </form>\n <span class=\"field__hint\">Or review individually in the diff view.</span>\n </div>\n ) : null}\n\n {snapshots.length === 0 ? (\n <div class=\"empty\">\n <h2 class=\"empty__title\">No snapshots yet</h2>\n <p class=\"empty__desc\">Capture is pending or failed. Try retrying capture.</p>\n </div>\n ) : (\n <div class=\"snapshot-grid\">\n {snapshots.map(\n // eslint-disable-next-line promise-function-async -- JSX.Element includes Promise<HtmlEscapedString>\n (snap) => (\n <div key={snap.id} class=\"snapshot-card\">\n <div class=\"snapshot-card__head\">\n <Badge tone={statusTone(snap.status)}>{snap.status}</Badge>\n <span class=\"snapshot-card__meta\">\n {snap.viewportName} · {snap.viewportWidth}×{snap.viewportHeight}\n </span>\n </div>\n <div class=\"snapshot-card__body\">\n <div style=\"font-weight:650; font-size:.95rem; line-height:1.2;\">\n {snap.storyTitle}\n <span style=\"color:var(--text-secondary); font-weight:400;\"> / {snap.storyName}</span>\n </div>\n <div class=\"field__hint\" style=\"font-size:.8rem;\">\n {snap.storyId}\n {snap.diffRatio !== null && snap.diffRatio !== undefined ? ` · diff ${(snap.diffRatio * 100).toFixed(2)}%` : \"\"}\n {snap.diffPixels === null ? \"\" : ` · ${snap.diffPixels} px`}\n </div>\n <div style=\"display:flex; gap:.4rem; flex-wrap:wrap; margin-top:.2rem;\">\n <a class=\"btn btn--secondary\" href={`/projects/${project.slug}/builds/${build.id}/diff?snapshot=${snap.id}`}>\n Review\n </a>\n {canReview && (snap.status === \"new\" || snap.status === \"changed\") ? (\n <>\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/approve`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/approve`} hx-target=\"body\">\n <button class=\"btn btn--ghost\" type=\"submit\">\n Approve\n </button>\n </form>\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/reject`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/reject`} hx-target=\"body\">\n <button class=\"btn btn--ghost\" type=\"submit\">\n Reject\n </button>\n </form>\n </>\n ) : null}\n </div>\n </div>\n </div>\n ))}\n </div>\n )}\n\n <div class=\"card card--padded\" style=\"margin-top:1rem;\">\n <h2 style=\"margin:0 0 .5rem;\">Comments</h2>\n {comments.length === 0 ? <p class=\"field__hint\">No comments. Add one in the diff review page for a specific snapshot.</p> : null}\n <div style=\"display:grid; gap:.6rem;\">\n {comments.map(\n // eslint-disable-next-line promise-function-async -- JSX.Element includes Promise<HtmlEscapedString>\n (comment) => (\n <div key={comment.id} class=\"comment\">\n <div class=\"comment__head\">\n <strong>{comment.userId}</strong>\n <span>· {new Date(comment.createdAt).toLocaleString()}</span>\n {comment.snapshotId ? <Badge tone=\"neutral\">{snapshots.find((snapshot) => snapshot.id === comment.snapshotId)?.storyName ?? comment.snapshotId.slice(0, 6)}</Badge> : <Badge tone=\"neutral\">build</Badge>}\n {comment.resolved ? <Badge tone=\"success\">resolved</Badge> : null}\n </div>\n <p class=\"comment__body\">{comment.body}</p>\n </div>\n ))}\n </div>\n\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/comments`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/comments`} hx-target=\"body\" style=\"margin-top:1rem; display:grid; gap:.5rem; max-width:640px;\">\n <label class=\"field__label\" for=\"comment-body\">\n Add build comment\n </label>\n <textarea class=\"field__input field__input--textarea\" id=\"comment-body\" name=\"body\" rows={3} required placeholder=\"Leave a comment on this build…\" />\n <div>\n <button class=\"btn btn--primary\" type=\"submit\">\n Comment\n </button>\n </div>\n </form>\n </div>\n </DocumentLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAQA,eAAsB,sBAAsB,SAAkD;CAC5F,MAAM,KAAK,SAAS,CAAC,CAAC;CACtB,MAAM,QAAQ,MAAM,IAAI,WAAW,EAAE,CAAC,CAAC,IAAI,OAAO;CAClD,IAAI,CAAC,OACH,OAAO;CAET,MAAM,UAAU,MAAM,IAAI,aAAa,EAAE,CAAC,CAAC,IAAI,MAAM,SAAS;CAC9D,IAAI,CAAC,SACH,OAAO;CAET,MAAM,YAAY,MAAM,IAAI,cAAc,EAAE,CAAC,CAAC,YAAY,MAAM,EAAE;CAClE,MAAM,WAAW,MAAM,IAAI,aAAa,EAAE,CAAC,CAAC,YAAY,MAAM,EAAE;CAChE,MAAM,YAAY,CAAC,SAAS,CAAC,CAAC,eAAe,QAAQ,SAAS,CAAC,CAAC,IAAI;CAEpE,MAAM,0BAAU,IAAI,IAA8B;CAClD,KAAK,MAAM,QAAQ,WAAW;EAC5B,MAAM,MAAM,KAAK;EACjB,MAAM,OAAO,QAAQ,IAAI,GAAG,KAAK,CAAC;EAClC,KAAK,KAAK,IAAI;EACd,QAAQ,IAAI,KAAK,IAAI;CACvB;CAEA,OACE,qBAAC,gBAAD;EAAgB,OAAO,SAAS,MAAM;EAAa,KAAK;GAAE,QAAQ;GAAU,aAAa,QAAQ;GAAM,aAAa,QAAQ;EAAK;EAAjI,UAAA;GACE,qBAAC,OAAD;IAAK,OAAM;IAAX,UAAA,CACE,oBAAC,OAAD;KAAK,OAAM;KAAc,cAAW;KAClC,UAAA,qBAAC,MAAD,EAAA,UAAA;MACE,oBAAC,MAAD,EAAA,UACE,oBAAC,KAAD;OAAG,MAAK;OAAY,UAAA;MAAW,CAAA,EAC7B,CAAA;MACJ,oBAAC,MAAD,EAAA,UACE,oBAAC,KAAD;OAAG,MAAM,aAAa,QAAQ,KAAK;OAAW,UAAA,QAAQ;MAAQ,CAAA,EAC5D,CAAA;MACJ,oBAAC,MAAD,EAAA,UACE,qBAAC,QAAD;OAAM,gBAAa;OAAnB,UAAA;QACG,MAAM;QAAU;QAAI,MAAM,OAAO,MAAM,GAAG,CAAC;OACxC;MACJ,CAAA,EAAA,CAAA;KACF,EAAA,CAAA;IACD,CAAA,GACL,qBAAC,OAAD;KAAK,OAAM;KAAX,UAAA,CACE,qBAAC,OAAD,EAAA,UAAA,CACE,qBAAC,MAAD;MAAI,OAAM;MAAV,UAAA;OACG,MAAM;OAAU;OAAC,qBAAC,QAAD;QAAM,OAAM;QAAZ,UAAA,CAA4D,MAAG,MAAM,OAAO,MAAM,GAAG,CAAC,CAAQ;;OAAC;OAAC,oBAAC,OAAD;QAAO,MAAM,WAAW,MAAM,MAAM;QAAI,UAAA,MAAM;OAAc,CAAA;MAC5K;KACJ,CAAA,GAAA,qBAAC,KAAD;MAAG,OAAM;MAAT,UAAA;OACG,MAAM,WAAW;OAAoB;OAAE,MAAM,aAAa,KAAK,MAAM,aAAa,MAAM,cAAc,KAAK,MAAM,YAAY,KAAK,OAAO;OAAG;OAAI,IAAI,KAAK,MAAM,SAAS,CAAC,CAAC,eAAe;MACzL;KACA,CAAA,CAAA,EAAA,CAAA,GACL,qBAAC,OAAD;MAAK,OAAM;MAAX,UAAA,CACE,oBAAC,KAAD;OAAG,OAAM;OAAmB,MAAM,aAAa,QAAQ,KAAK,UAAU,MAAM,GAAG;OAAQ,UAAA;MAEpF,CAAA,GACH,oBAAC,QAAD;OAAM,QAAO;OAAO,QAAQ,oBAAoB,QAAQ,KAAK,UAAU,MAAM,GAAG;OAAS,WAAS,oBAAoB,QAAQ,KAAK,UAAU,MAAM,GAAG;OAAS,aAAU;OACvK,UAAA,oBAAC,UAAD;QAAQ,OAAM;QAAqB,MAAK;QAAS,UAAA;OAEzC,CAAA;MACJ,CAAA,CACH;KACF,CAAA,CAAA;IACF,CAAA,CAAA;;GAEL,qBAAC,OAAD;IAAK,OAAM;IAAe,OAAM;IAAhC,UAAA;KACE,qBAAC,OAAD;MAAK,OAAM;MAAX,UAAA,CACE,oBAAC,OAAD;OAAK,OAAM;OAAe,UAAA,UAAU;MAAY,CAAA,GAChD,oBAAC,OAAD;OAAK,OAAM;OAAc,UAAA;MAAc,CAAA,CACpC;;KACL,qBAAC,OAAD;MAAK,OAAM;MAAX,UAAA,CACE,oBAAC,OAAD;OAAK,OAAM;OAAc,OAAM;OAC5B,UAAA,MAAM;MACJ,CAAA,GACL,oBAAC,OAAD;OAAK,OAAM;OAAc,UAAA;MAAkB,CAAA,CACxC;;KACL,qBAAC,OAAD;MAAK,OAAM;MAAX,UAAA,CACE,oBAAC,OAAD;OAAK,OAAM;OAAc,OAAM;OAC5B,UAAA,MAAM;MACJ,CAAA,GACL,oBAAC,OAAD;OAAK,OAAM;OAAc,UAAA;MAAyB,CAAA,CAC/C;;IACF;;GAEJ,cAAc,MAAM,WAAW,eAAe,MAAM,WAAW,eAC9D,qBAAC,OAAD;IAAK,OAAM;IAAoB,OAAM;IAArC,UAAA;KACE,oBAAC,QAAD;MAAM,OAAM;MAAc,UAAA;KAAmB,CAAA;KAC7C,oBAAC,QAAD;MAAM,QAAO;MAAO,QAAQ,oBAAoB,QAAQ,KAAK,UAAU,MAAM,GAAG;MAAe,WAAS,oBAAoB,QAAQ,KAAK,UAAU,MAAM,GAAG;MAAe,aAAU;MACnL,UAAA,oBAAC,UAAD;OAAQ,OAAM;OAAmB,MAAK;OAAS,UAAA;MAEvC,CAAA;KACJ,CAAA;KACN,oBAAC,QAAD;MAAM,QAAO;MAAO,QAAQ,oBAAoB,QAAQ,KAAK,UAAU,MAAM,GAAG;MAAc,WAAS,oBAAoB,QAAQ,KAAK,UAAU,MAAM,GAAG;MAAc,aAAU;MACjL,UAAA,oBAAC,UAAD;OAAQ,OAAM;OAAkB,MAAK;OAAS,UAAA;MAEtC,CAAA;KACJ,CAAA;KACN,oBAAC,QAAD;MAAM,OAAM;MAAc,UAAA;KAA8C,CAAA;IACrE;GACH,CAAA,IAAA;GAEH,UAAU,WAAW,IACpB,qBAAC,OAAD;IAAK,OAAM;IAAX,UAAA,CACE,oBAAC,MAAD;KAAI,OAAM;KAAe,UAAA;IAAoB,CAAA,GAC7C,oBAAC,KAAD;KAAG,OAAM;KAAc,UAAA;IAAsD,CAAA,CAC1E;GAEL,CAAA,IAAA,oBAAC,OAAD;IAAK,OAAM;IACR,UAAA,UAAU,KAER,SACD,qBAAC,OAAD;KAAmB,OAAM;KAAzB,UAAA,CACE,qBAAC,OAAD;MAAK,OAAM;MAAX,UAAA,CACE,oBAAC,OAAD;OAAO,MAAM,WAAW,KAAK,MAAM;OAAI,UAAA,KAAK;MAAc,CAAA,GAC1D,qBAAC,QAAD;OAAM,OAAM;OAAZ,UAAA;QACG,KAAK;QAAa;QAAI,KAAK;QAAc;QAAE,KAAK;OAC7C;MACH,CAAA,CAAA;KACL,CAAA,GAAA,qBAAC,OAAD;MAAK,OAAM;MAAX,UAAA;OACE,qBAAC,OAAD;QAAK,OAAM;QAAX,UAAA,CACG,KAAK,YACN,qBAAC,QAAD;SAAM,OAAM;SAAZ,UAAA,CAA4D,OAAI,KAAK,SAAgB;QAClF,CAAA,CAAA;;OACL,qBAAC,OAAD;QAAK,OAAM;QAAc,OAAM;QAA/B,UAAA;SACG,KAAK;SACL,KAAK,cAAc,QAAQ,KAAK,cAAc,KAAA,IAAY,YAAY,KAAK,YAAY,IAAA,CAAK,QAAQ,CAAC,EAAE,KAAK;SAC5G,KAAK,eAAe,OAAO,KAAK,MAAM,KAAK,WAAW;QACpD;;OACL,qBAAC,OAAD;QAAK,OAAM;QAAX,UAAA,CACE,oBAAC,KAAD;SAAG,OAAM;SAAqB,MAAM,aAAa,QAAQ,KAAK,UAAU,MAAM,GAAG,iBAAiB,KAAK;SAAM,UAAA;QAE1G,CAAA,GACF,cAAc,KAAK,WAAW,SAAS,KAAK,WAAW,aACtD,qBAAA,UAAA,EAAA,UAAA,CACE,oBAAC,QAAD;SAAM,QAAO;SAAO,QAAQ,oBAAoB,QAAQ,KAAK,UAAU,MAAM,GAAG,aAAa,KAAK,GAAG;SAAW,WAAS,oBAAoB,QAAQ,KAAK,UAAU,MAAM,GAAG,aAAa,KAAK,GAAG;SAAW,aAAU;SACrN,UAAA,oBAAC,UAAD;UAAQ,OAAM;UAAiB,MAAK;UAAS,UAAA;SAErC,CAAA;QACJ,CAAA,GACN,oBAAC,QAAD;SAAM,QAAO;SAAO,QAAQ,oBAAoB,QAAQ,KAAK,UAAU,MAAM,GAAG,aAAa,KAAK,GAAG;SAAU,WAAS,oBAAoB,QAAQ,KAAK,UAAU,MAAM,GAAG,aAAa,KAAK,GAAG;SAAU,aAAU;SACnN,UAAA,oBAAC,UAAD;UAAQ,OAAM;UAAiB,MAAK;UAAS,UAAA;SAErC,CAAA;QACJ,CAAA,CACN,EAAA,CAAA,IACA,IACD;;MACF;KACF,CAAA,CAAA;IArCK,GAAA,KAAK,EAqCV,CACN;GACE,CAAA;GAGP,qBAAC,OAAD;IAAK,OAAM;IAAoB,OAAM;IAArC,UAAA;KACE,oBAAC,MAAD;MAAI,OAAM;MAAoB,UAAA;KAAY,CAAA;KACzC,SAAS,WAAW,IAAI,oBAAC,KAAD;MAAG,OAAM;MAAc,UAAA;KAAwE,CAAA,IAAI;KAC5H,oBAAC,OAAD;MAAK,OAAM;MACR,UAAA,SAAS,KAEP,YACD,qBAAC,OAAD;OAAsB,OAAM;OAA5B,UAAA,CACE,qBAAC,OAAD;QAAK,OAAM;QAAX,UAAA;SACE,oBAAC,UAAD,EAAA,UAAS,QAAQ,OAAe,CAAA;SAChC,qBAAC,QAAD,EAAA,UAAA,CAAM,MAAG,IAAI,KAAK,QAAQ,SAAS,CAAC,CAAC,eAAe,CAAQ,EAAA,CAAA;SAC3D,QAAQ,aAAa,oBAAC,OAAD;UAAO,MAAK;UAAW,UAAA,UAAU,MAAM,aAAa,SAAS,OAAO,QAAQ,UAAU,CAAC,EAAE,aAAa,QAAQ,WAAW,MAAM,GAAG,CAAC;SAAS,CAAA,IAAI,oBAAC,OAAD;UAAO,MAAK;UAAU,UAAA;SAAY,CAAA;SACvM,QAAQ,WAAW,oBAAC,OAAD;UAAO,MAAK;UAAU,UAAA;SAAe,CAAA,IAAI;QAC1D;OACL,CAAA,GAAA,oBAAC,KAAD;QAAG,OAAM;QAAiB,UAAA,QAAQ;OAAQ,CAAA,CACvC;MARK,GAAA,QAAQ,EAQb,CACN;KACE,CAAA;KAEL,qBAAC,QAAD;MAAM,QAAO;MAAO,QAAQ,oBAAoB,QAAQ,KAAK,UAAU,MAAM,GAAG;MAAY,WAAS,oBAAoB,QAAQ,KAAK,UAAU,MAAM,GAAG;MAAY,aAAU;MAAO,OAAM;MAA5L,UAAA;OACE,oBAAC,SAAD;QAAO,OAAM;QAAe,KAAI;QAAe,UAAA;OAExC,CAAA;OACP,oBAAC,YAAD;QAAU,OAAM;QAAsC,IAAG;QAAe,MAAK;QAAO,MAAM;QAAG,UAAA;QAAS,aAAY;OAAkC,CAAA;OACpJ,oBAAC,OAAD,EAAA,UACE,oBAAC,UAAD;QAAQ,OAAM;QAAmB,MAAK;QAAS,UAAA;OAEvC,CAAA,EACL,CAAA;MACD;;IACH;;EACS;;AAEpB"}
1
+ {"version":3,"file":"build-detail.mjs","names":[],"sources":["../../src/pages/build-detail.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\n\nimport { BuildModel } from \"../models/build.ts\";\nimport { CommentModel } from \"../models/comment.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { SnapshotModel } from \"../models/snapshot.ts\";\nimport { getStore } from \"../store.ts\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { Badge, statusTone } from \"../ui/components.tsx\";\n\n/** Build overview page: snapshot grid, bulk actions, and build comments. */\nexport async function renderBuildDetailPage(buildId: string): Promise<RenderedContent | null> {\n const {db} = getStore();\n const build = await new BuildModel(db).get(buildId);\n if (!build) {\n return null;\n }\n const project = await new ProjectModel(db).get(build.projectId);\n if (!project) {\n return null;\n }\n const snapshots = await new SnapshotModel(db).listByBuild(build.id);\n const comments = await new CommentModel(db).listByBuild(build.id);\n const canReview = !getStore().authEnabled || Boolean(getStore().user);\n\n const grouped = new Map<string, typeof snapshots>();\n for (const snap of snapshots) {\n const key = snap.status;\n const list = grouped.get(key) ?? [];\n list.push(snap);\n grouped.set(key, list);\n }\n\n return (\n <DocumentLayout title={`Build ${build.gitBranch}`} nav={{ active: \"builds\", projectSlug: project.slug, projectName: project.name }}>\n <div class=\"page-header\">\n <nav class=\"breadcrumbs\" aria-label=\"Breadcrumb\">\n <ol>\n <li>\n <a href=\"/projects\">Projects</a>\n </li>\n <li>\n <a href={`/projects/${project.slug}/builds`}>{project.name}</a>\n </li>\n <li>\n <span aria-current=\"page\">\n {build.gitBranch} · {build.gitSha.slice(0, 7)}\n </span>\n </li>\n </ol>\n </nav>\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">\n {build.gitBranch} <span style=\"color:var(--text-secondary); font-weight:400;\">· {build.gitSha.slice(0, 7)}</span> <Badge tone={statusTone(build.status)}>{build.status}</Badge>\n </h1>\n <p class=\"page-header__desc\">\n {build.message ?? \"No commit message\"} {build.authorName ? `· ${build.authorName}${build.authorEmail ? ` <${build.authorEmail}>` : \"\"}` : \"\"} · {new Date(build.createdAt).toLocaleString()}\n </p>\n </div>\n <div class=\"page-header__actions\">\n <a class=\"btn btn--primary\" href={`/projects/${project.slug}/builds/${build.id}/diff`}>\n Review diffs\n </a>\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/retry`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/retry`} hx-target=\"body\">\n <button class=\"btn btn--secondary\" type=\"submit\">\n Retry capture\n </button>\n </form>\n </div>\n </div>\n </div>\n\n <div class=\"grid grid--3\" style=\"margin-bottom:1rem;\">\n <div class=\"card card--padded stat\">\n <div class=\"stat__value\">{snapshots.length}</div>\n <div class=\"stat__label\">Snapshots</div>\n </div>\n <div class=\"card card--padded stat\">\n <div class=\"stat__value\" style=\"color: var(--status-new);\">\n {build.changedCount}\n </div>\n <div class=\"stat__label\">Changed / new</div>\n </div>\n <div class=\"card card--padded stat\">\n <div class=\"stat__value\" style=\"color: var(--status-approved);\">\n {build.approvedCount}\n </div>\n <div class=\"stat__label\">Approved / unchanged</div>\n </div>\n </div>\n\n {canReview && (build.status === \"reviewing\" || build.status === \"comparing\") ? (\n <div class=\"card card--padded\" style=\"display:flex; gap:.5rem; flex-wrap:wrap; align-items:center; margin-bottom:1rem;\">\n <span class=\"field__hint\">Bulk actions:</span>\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/approve-all`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/approve-all`} hx-target=\"body\">\n <button class=\"btn btn--primary\" type=\"submit\">\n Approve all\n </button>\n </form>\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/reject-all`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/reject-all`} hx-target=\"body\">\n <button class=\"btn btn--danger\" type=\"submit\">\n Reject all\n </button>\n </form>\n <span class=\"field__hint\">Or review individually in the diff view.</span>\n </div>\n ) : null}\n\n {snapshots.length === 0 ? (\n <div class=\"empty\">\n <h2 class=\"empty__title\">No snapshots yet</h2>\n <p class=\"empty__desc\">Capture is pending or failed. Try retrying capture.</p>\n </div>\n ) : (\n <div class=\"snapshot-grid\">\n {snapshots.map(\n (snap): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <div key={snap.id} class=\"snapshot-card\">\n <div class=\"snapshot-card__head\">\n <Badge tone={statusTone(snap.status)}>{snap.status}</Badge>\n <span class=\"snapshot-card__meta\">\n {snap.viewportName} · {snap.viewportWidth}×{snap.viewportHeight}\n </span>\n </div>\n <div class=\"snapshot-card__body\">\n <div style=\"font-weight:650; font-size:.95rem; line-height:1.2;\">\n {snap.storyTitle}\n <span style=\"color:var(--text-secondary); font-weight:400;\"> / {snap.storyName}</span>\n </div>\n <div class=\"field__hint\" style=\"font-size:.8rem;\">\n {snap.storyId}\n {snap.diffRatio !== null && snap.diffRatio !== undefined ? ` · diff ${(snap.diffRatio * 100).toFixed(2)}%` : \"\"}\n {snap.diffPixels === null ? \"\" : ` · ${snap.diffPixels} px`}\n </div>\n <div style=\"display:flex; gap:.4rem; flex-wrap:wrap; margin-top:.2rem;\">\n <a class=\"btn btn--secondary\" href={`/projects/${project.slug}/builds/${build.id}/diff?snapshot=${snap.id}`}>\n Review\n </a>\n {canReview && (snap.status === \"new\" || snap.status === \"changed\") ? (\n <>\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/approve`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/approve`} hx-target=\"body\">\n <button class=\"btn btn--ghost\" type=\"submit\">\n Approve\n </button>\n </form>\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/reject`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/reject`} hx-target=\"body\">\n <button class=\"btn btn--ghost\" type=\"submit\">\n Reject\n </button>\n </form>\n </>\n ) : null}\n </div>\n </div>\n </div>\n ))}\n </div>\n )}\n\n <div class=\"card card--padded\" style=\"margin-top:1rem;\">\n <h2 style=\"margin:0 0 .5rem;\">Comments</h2>\n {comments.length === 0 ? <p class=\"field__hint\">No comments. Add one in the diff review page for a specific snapshot.</p> : null}\n <div style=\"display:grid; gap:.6rem;\">\n {comments.map(\n (comment): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <div key={comment.id} class=\"comment\">\n <div class=\"comment__head\">\n <strong>{comment.userId}</strong>\n <span>· {new Date(comment.createdAt).toLocaleString()}</span>\n {comment.snapshotId ? <Badge tone=\"neutral\">{snapshots.find((snapshot) => snapshot.id === comment.snapshotId)?.storyName ?? comment.snapshotId.slice(0, 6)}</Badge> : <Badge tone=\"neutral\">build</Badge>}\n {comment.resolved ? <Badge tone=\"success\">resolved</Badge> : null}\n </div>\n <p class=\"comment__body\">{comment.body}</p>\n </div>\n ))}\n </div>\n\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/comments`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/comments`} hx-target=\"body\" style=\"margin-top:1rem; display:grid; gap:.5rem; max-width:640px;\">\n <label class=\"field__label\" for=\"comment-body\">\n Add build comment\n </label>\n <textarea class=\"field__input field__input--textarea\" id=\"comment-body\" name=\"body\" rows={3} required placeholder=\"Leave a comment on this build…\" />\n <div>\n <button class=\"btn btn--primary\" type=\"submit\">\n Comment\n </button>\n </div>\n </form>\n </div>\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
@@ -8,6 +8,7 @@ import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
8
8
  function imageUrl(project, build, snapshot, kind) {
9
9
  return `/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snapshot.id}/${kind}`;
10
10
  }
11
+ /** Three-up diff review page: baseline, current, and diff with keyboard review. */
11
12
  function renderBuildDiffPage(data) {
12
13
  const { project, build, snapshots, comments, selectedId, canReview, hasBaseline } = data;
13
14
  const selected = snapshots.find((s) => s.id === selectedId) ?? snapshots.find((s) => s.status === "changed" || s.status === "new") ?? snapshots[0];
@@ -1 +1 @@
1
- {"version":3,"file":"build-diff.mjs","names":[],"sources":["../../src/pages/build-diff.tsx"],"sourcesContent":["import type { Build, Comment, Project, Snapshot } from \"../schema.ts\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { Badge, statusTone } from \"../ui/components.tsx\";\n\nexport interface BuildDiffData {\n project: Project;\n build: Build;\n snapshots: Snapshot[];\n comments: Comment[];\n selectedId?: string;\n canReview: boolean;\n hasBaseline: Record<string, boolean>;\n}\n\nfunction imageUrl(project: Project, build: Build, snapshot: Snapshot, kind: \"image\" | \"diff\" | \"baseline\"): string {\n return `/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snapshot.id}/${kind}`;\n}\n\nexport function renderBuildDiffPage(data: BuildDiffData): RenderedContent {\n const { project, build, snapshots, comments, selectedId, canReview, hasBaseline } = data;\n const selected = snapshots.find((s) => s.id === selectedId) ?? snapshots.find((s) => s.status === \"changed\" || s.status === \"new\") ?? snapshots[0];\n const pending = snapshots.filter((s) => s.status === \"new\" || s.status === \"changed\");\n\n return (\n <DocumentLayout title={`Review · ${build.gitBranch}`} nav={{ active: \"builds\", projectSlug: project.slug, projectName: project.name }}>\n <div class=\"page-header\">\n <nav class=\"breadcrumbs\" aria-label=\"Breadcrumb\">\n <ol>\n <li>\n <a href=\"/projects\">Projects</a>\n </li>\n <li>\n <a href={`/projects/${project.slug}/builds`}>{project.name}</a>\n </li>\n <li>\n <a href={`/projects/${project.slug}/builds/${build.id}`}>Build {build.gitBranch}</a>\n </li>\n <li>\n <span aria-current=\"page\">Review</span>\n </li>\n </ol>\n </nav>\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">\n {build.gitBranch} <span style=\"color:var(--text-secondary); font-weight:400;\">· {build.gitSha.slice(0, 7)}</span>\n </h1>\n <p class=\"page-header__desc\">\n {build.message ?? \"No message\"} {build.authorName ? `· ${build.authorName}` : \"\"} · <Badge tone={statusTone(build.status)}>{build.status}</Badge>\n </p>\n </div>\n <div class=\"page-header__actions\">\n <a class=\"btn btn--secondary\" href={`/projects/${project.slug}/builds/${build.id}`}>\n Build overview\n </a>\n {canReview && pending.length > 0 ? (\n <>\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/approve-all`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/approve-all`} hx-target=\"body\">\n <button class=\"btn btn--primary\" type=\"submit\">\n Approve all ({pending.length})\n </button>\n </form>\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/reject-all`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/reject-all`} hx-target=\"body\">\n <button class=\"btn btn--danger\" type=\"submit\">\n Reject all\n </button>\n </form>\n </>\n ) : null}\n </div>\n </div>\n </div>\n\n <div class=\"card card--padded\" style=\"margin-bottom:1rem;\">\n <div class=\"grid grid--3\" style=\"text-align:center;\">\n <div class=\"stat\">\n <div class=\"stat__value\">{snapshots.length}</div>\n <div class=\"stat__label\">Snapshots</div>\n </div>\n <div class=\"stat\">\n <div class=\"stat__value\" style=\"color: var(--status-new);\">\n {pending.length}\n </div>\n <div class=\"stat__label\">Needs review</div>\n </div>\n <div class=\"stat\">\n <div class=\"stat__value\" style=\"color: var(--status-approved);\">\n {build.approvedCount}\n </div>\n <div class=\"stat__label\">Approved</div>\n </div>\n </div>\n </div>\n\n {snapshots.length === 0 ? (\n <div class=\"empty\">\n <h2 class=\"empty__title\">No snapshots</h2>\n <p class=\"empty__desc\">This build has no snapshots yet. Capture may still be running.</p>\n </div>\n ) : (\n <div class=\"grid\" style=\"gap:1rem;\">\n <div style=\"display:flex; gap:1rem; align-items:stretch;\">\n <div style=\"flex: 0 0 320px; max-width: 36%; min-width: 260px;\">\n <div class=\"card\">\n <div style=\"padding:.6rem .75rem; border-bottom:1px solid var(--border); display:flex; justify-content:space-between; align-items:center;\">\n <strong>Snapshots</strong>\n <span class=\"field__hint\">{snapshots.length} total</span>\n </div>\n <div data-diff-nav data-current={selected?.id} style=\"max-height: 70vh; overflow:auto;\">\n {snapshots.map(\n // eslint-disable-next-line promise-function-async -- JSX.Element includes Promise<HtmlEscapedString>\n (snap) => (\n <a\n key={snap.id}\n href={`/projects/${project.slug}/builds/${build.id}/diff?snapshot=${snap.id}`}\n data-snapshot-link\n data-snapshot-id={snap.id}\n class={`snapshot-nav ${selected?.id === snap.id ? \"snapshot-nav--active\" : \"\"}`}\n style={`display:flex; flex-direction:column; gap:.2rem; padding:.6rem .75rem; border-bottom:1px solid var(--border); text-decoration:none; color:inherit; background:${selected?.id === snap.id ? \"var(--surface-muted)\" : \"transparent\"};`}\n hx-get={`/projects/${project.slug}/builds/${build.id}/diff?snapshot=${snap.id}`}\n hx-target=\"body\"\n hx-push-url=\"true\"\n >\n <span style=\"display:flex; gap:.4rem; align-items:center; flex-wrap:wrap;\">\n <Badge tone={statusTone(snap.status)}>{snap.status}</Badge>\n <span style=\"font-weight:600; font-size:.9rem;\">{snap.storyTitle} / {snap.storyName}</span>\n </span>\n <span class=\"field__hint\" style=\"font-size:.8rem;\">\n {snap.viewportName} · {snap.viewportWidth}×{snap.viewportHeight}\n {snap.diffRatio !== null && snap.diffRatio !== undefined ? ` · ${(snap.diffRatio * 100).toFixed(2)}%` : \"\"}\n </span>\n </a>\n ))}\n </div>\n </div>\n </div>\n\n <div style=\"flex:1; min-width:0; display:grid; gap:1rem;\">\n {selected ? (\n <>\n <div class=\"card card--padded\">\n <div style=\"display:flex; justify-content:space-between; gap:.5rem; align-items:center; flex-wrap:wrap;\">\n <div>\n <h2 style=\"margin:0; font-size:1.05rem;\">\n {selected.storyTitle} — {selected.storyName}\n </h2>\n <p class=\"field__hint\" style=\"margin:.2rem 0 0;\">\n {selected.viewportName} · {selected.viewportWidth}×{selected.viewportHeight} · <Badge tone={statusTone(selected.status)}>{selected.status}</Badge>\n {selected.diffPixels === null ? \"\" : ` · ${selected.diffPixels} px`}\n </p>\n </div>\n {canReview && (selected.status === \"new\" || selected.status === \"changed\") ? (\n <div style=\"display:flex; gap:.5rem;\">\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${selected.id}/approve`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${selected.id}/approve`} hx-target=\"body\">\n <button class=\"btn btn--primary\" type=\"submit\" accesskey=\"a\" title=\"Approve (a)\">\n Approve\n </button>\n </form>\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${selected.id}/reject`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${selected.id}/reject`} hx-target=\"body\">\n <button class=\"btn btn--danger\" type=\"submit\" accesskey=\"r\" title=\"Reject (r)\">\n Reject\n </button>\n </form>\n </div>\n ) : null}\n </div>\n\n <div class=\"diff-grid\" style=\"margin-top:1rem;\">\n <div class=\"diff-pane\">\n <div class=\"diff-pane__label\">Baseline</div>\n {hasBaseline[selected.id] ? (\n <img class=\"diff-pane__img\" src={imageUrl(project, build, selected, \"baseline\")} alt={`Baseline for ${selected.storyTitle} / ${selected.storyName}`} loading=\"lazy\" />\n ) : (\n <div style=\"aspect-ratio: 16/9; display:grid; place-items:center; background:var(--surface-muted); color:var(--text-secondary); font-size:.85rem;\">\n No baseline\n <br />\n <span class=\"field__hint\">first capture for this story</span>\n </div>\n )}\n </div>\n <div class=\"diff-pane\">\n <div class=\"diff-pane__label\">Current</div>\n <img class=\"diff-pane__img\" src={imageUrl(project, build, selected, \"image\")} alt={`Current for ${selected.storyTitle} / ${selected.storyName}`} loading=\"lazy\" />\n </div>\n <div class=\"diff-pane\">\n <div class=\"diff-pane__label\">Diff {selected.diffRatio === null ? \"\" : `· ${(selected.diffRatio * 100).toFixed(2)}%`}</div>\n {selected.diffPath ? (\n <img class=\"diff-pane__img\" src={imageUrl(project, build, selected, \"diff\")} alt={`Diff for ${selected.storyTitle} / ${selected.storyName}`} loading=\"lazy\" />\n ) : (\n <div style=\"aspect-ratio: 16/9; display:grid; place-items:center; color:var(--text-secondary);\">\n {selected.status === \"unchanged\" || selected.status === \"approved\" ? \"No diff — within threshold\" : \"No diff yet\"}\n </div>\n )}\n </div>\n </div>\n <p class=\"field__hint\" style=\"margin-top:.6rem;\">\n Keyboard: <kbd>←</kbd> <kbd>→</kbd> navigate · <kbd>a</kbd> approve · <kbd>r</kbd> reject · <kbd>?</kbd> help\n </p>\n </div>\n\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .5rem;\">Comments</h3>\n <div style=\"display:grid; gap:.75rem;\">\n {comments\n .filter((comment) => !comment.snapshotId || comment.snapshotId === selected.id)\n .map(\n // eslint-disable-next-line promise-function-async -- JSX.Element includes Promise<HtmlEscapedString>\n (comment) => (\n <div key={comment.id} class=\"comment\">\n <div class=\"comment__head\">\n <strong>{comment.userId}</strong>\n <span>· {new Date(comment.createdAt).toLocaleString()}</span>\n {comment.resolved ? <Badge tone=\"success\">resolved</Badge> : null}\n </div>\n <p class=\"comment__body\">{comment.body}</p>\n {!comment.resolved && canReview ? (\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/comments/${comment.id}/resolve`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/comments/${comment.id}/resolve`} hx-target=\"body\" style=\"margin-top:.5rem;\">\n <button class=\"btn btn--ghost\" type=\"submit\">\n Mark resolved\n </button>\n </form>\n ) : null}\n </div>\n ))}\n {comments.filter((c) => !c.snapshotId || c.snapshotId === selected.id).length === 0 ? <p class=\"field__hint\">No comments on this snapshot.</p> : null}\n </div>\n\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/comments`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/comments`} hx-target=\"body\" style=\"margin-top:1rem; display:grid; gap:.5rem;\">\n <input type=\"hidden\" name=\"snapshotId\" value={selected.id} />\n <label class=\"field__label\" for=\"comment-body\">\n Add comment\n </label>\n <textarea class=\"field__input field__input--textarea\" id=\"comment-body\" name=\"body\" rows={3} required placeholder=\"Leave feedback on this snapshot…\" />\n <div>\n <button class=\"btn btn--primary\" type=\"submit\">\n Comment\n </button>\n </div>\n </form>\n </div>\n </>\n ) : (\n <div class=\"empty\">\n <p class=\"empty__desc\">Select a snapshot to review.</p>\n </div>\n )}\n </div>\n </div>\n </div>\n )}\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"build-diff.mjs","names":[],"sources":["../../src/pages/build-diff.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\nimport type { Build, Comment, Project, Snapshot } from \"../schema.ts\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { Badge, statusTone } from \"../ui/components.tsx\";\n\n/** Data required to render the three-up build diff review page. */\nexport interface BuildDiffData {\n project: Project;\n build: Build;\n snapshots: Snapshot[];\n comments: Comment[];\n selectedId?: string;\n canReview: boolean;\n hasBaseline: Record<string, boolean>;\n}\n\nfunction imageUrl(project: Project, build: Build, snapshot: Snapshot, kind: \"image\" | \"diff\" | \"baseline\"): string {\n return `/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snapshot.id}/${kind}`;\n}\n\n/** Three-up diff review page: baseline, current, and diff with keyboard review. */\nexport function renderBuildDiffPage(data: BuildDiffData): RenderedContent {\n const { project, build, snapshots, comments, selectedId, canReview, hasBaseline } = data;\n const selected = snapshots.find((s) => s.id === selectedId) ?? snapshots.find((s) => s.status === \"changed\" || s.status === \"new\") ?? snapshots[0];\n const pending = snapshots.filter((s) => s.status === \"new\" || s.status === \"changed\");\n\n return (\n <DocumentLayout title={`Review · ${build.gitBranch}`} nav={{ active: \"builds\", projectSlug: project.slug, projectName: project.name }}>\n <div class=\"page-header\">\n <nav class=\"breadcrumbs\" aria-label=\"Breadcrumb\">\n <ol>\n <li>\n <a href=\"/projects\">Projects</a>\n </li>\n <li>\n <a href={`/projects/${project.slug}/builds`}>{project.name}</a>\n </li>\n <li>\n <a href={`/projects/${project.slug}/builds/${build.id}`}>Build {build.gitBranch}</a>\n </li>\n <li>\n <span aria-current=\"page\">Review</span>\n </li>\n </ol>\n </nav>\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">\n {build.gitBranch} <span style=\"color:var(--text-secondary); font-weight:400;\">· {build.gitSha.slice(0, 7)}</span>\n </h1>\n <p class=\"page-header__desc\">\n {build.message ?? \"No message\"} {build.authorName ? `· ${build.authorName}` : \"\"} · <Badge tone={statusTone(build.status)}>{build.status}</Badge>\n </p>\n </div>\n <div class=\"page-header__actions\">\n <a class=\"btn btn--secondary\" href={`/projects/${project.slug}/builds/${build.id}`}>\n Build overview\n </a>\n {canReview && pending.length > 0 ? (\n <>\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/approve-all`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/approve-all`} hx-target=\"body\">\n <button class=\"btn btn--primary\" type=\"submit\">\n Approve all ({pending.length})\n </button>\n </form>\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/reject-all`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/reject-all`} hx-target=\"body\">\n <button class=\"btn btn--danger\" type=\"submit\">\n Reject all\n </button>\n </form>\n </>\n ) : null}\n </div>\n </div>\n </div>\n\n <div class=\"card card--padded\" style=\"margin-bottom:1rem;\">\n <div class=\"grid grid--3\" style=\"text-align:center;\">\n <div class=\"stat\">\n <div class=\"stat__value\">{snapshots.length}</div>\n <div class=\"stat__label\">Snapshots</div>\n </div>\n <div class=\"stat\">\n <div class=\"stat__value\" style=\"color: var(--status-new);\">\n {pending.length}\n </div>\n <div class=\"stat__label\">Needs review</div>\n </div>\n <div class=\"stat\">\n <div class=\"stat__value\" style=\"color: var(--status-approved);\">\n {build.approvedCount}\n </div>\n <div class=\"stat__label\">Approved</div>\n </div>\n </div>\n </div>\n\n {snapshots.length === 0 ? (\n <div class=\"empty\">\n <h2 class=\"empty__title\">No snapshots</h2>\n <p class=\"empty__desc\">This build has no snapshots yet. Capture may still be running.</p>\n </div>\n ) : (\n <div class=\"grid\" style=\"gap:1rem;\">\n <div style=\"display:flex; gap:1rem; align-items:stretch;\">\n <div style=\"flex: 0 0 320px; max-width: 36%; min-width: 260px;\">\n <div class=\"card\">\n <div style=\"padding:.6rem .75rem; border-bottom:1px solid var(--border); display:flex; justify-content:space-between; align-items:center;\">\n <strong>Snapshots</strong>\n <span class=\"field__hint\">{snapshots.length} total</span>\n </div>\n <div data-diff-nav data-current={selected?.id} style=\"max-height: 70vh; overflow:auto;\">\n {snapshots.map(\n (snap): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <a\n key={snap.id}\n href={`/projects/${project.slug}/builds/${build.id}/diff?snapshot=${snap.id}`}\n data-snapshot-link\n data-snapshot-id={snap.id}\n class={`snapshot-nav ${selected?.id === snap.id ? \"snapshot-nav--active\" : \"\"}`}\n style={`display:flex; flex-direction:column; gap:.2rem; padding:.6rem .75rem; border-bottom:1px solid var(--border); text-decoration:none; color:inherit; background:${selected?.id === snap.id ? \"var(--surface-muted)\" : \"transparent\"};`}\n hx-get={`/projects/${project.slug}/builds/${build.id}/diff?snapshot=${snap.id}`}\n hx-target=\"body\"\n hx-push-url=\"true\"\n >\n <span style=\"display:flex; gap:.4rem; align-items:center; flex-wrap:wrap;\">\n <Badge tone={statusTone(snap.status)}>{snap.status}</Badge>\n <span style=\"font-weight:600; font-size:.9rem;\">{snap.storyTitle} / {snap.storyName}</span>\n </span>\n <span class=\"field__hint\" style=\"font-size:.8rem;\">\n {snap.viewportName} · {snap.viewportWidth}×{snap.viewportHeight}\n {snap.diffRatio !== null && snap.diffRatio !== undefined ? ` · ${(snap.diffRatio * 100).toFixed(2)}%` : \"\"}\n </span>\n </a>\n ))}\n </div>\n </div>\n </div>\n\n <div style=\"flex:1; min-width:0; display:grid; gap:1rem;\">\n {selected ? (\n <>\n <div class=\"card card--padded\">\n <div style=\"display:flex; justify-content:space-between; gap:.5rem; align-items:center; flex-wrap:wrap;\">\n <div>\n <h2 style=\"margin:0; font-size:1.05rem;\">\n {selected.storyTitle} — {selected.storyName}\n </h2>\n <p class=\"field__hint\" style=\"margin:.2rem 0 0;\">\n {selected.viewportName} · {selected.viewportWidth}×{selected.viewportHeight} · <Badge tone={statusTone(selected.status)}>{selected.status}</Badge>\n {selected.diffPixels === null ? \"\" : ` · ${selected.diffPixels} px`}\n </p>\n </div>\n {canReview && (selected.status === \"new\" || selected.status === \"changed\") ? (\n <div style=\"display:flex; gap:.5rem;\">\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${selected.id}/approve`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${selected.id}/approve`} hx-target=\"body\">\n <button class=\"btn btn--primary\" type=\"submit\" accesskey=\"a\" title=\"Approve (a)\">\n Approve\n </button>\n </form>\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${selected.id}/reject`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${selected.id}/reject`} hx-target=\"body\">\n <button class=\"btn btn--danger\" type=\"submit\" accesskey=\"r\" title=\"Reject (r)\">\n Reject\n </button>\n </form>\n </div>\n ) : null}\n </div>\n\n <div class=\"diff-grid\" style=\"margin-top:1rem;\">\n <div class=\"diff-pane\">\n <div class=\"diff-pane__label\">Baseline</div>\n {hasBaseline[selected.id] ? (\n <img class=\"diff-pane__img\" src={imageUrl(project, build, selected, \"baseline\")} alt={`Baseline for ${selected.storyTitle} / ${selected.storyName}`} loading=\"lazy\" />\n ) : (\n <div style=\"aspect-ratio: 16/9; display:grid; place-items:center; background:var(--surface-muted); color:var(--text-secondary); font-size:.85rem;\">\n No baseline\n <br />\n <span class=\"field__hint\">first capture for this story</span>\n </div>\n )}\n </div>\n <div class=\"diff-pane\">\n <div class=\"diff-pane__label\">Current</div>\n <img class=\"diff-pane__img\" src={imageUrl(project, build, selected, \"image\")} alt={`Current for ${selected.storyTitle} / ${selected.storyName}`} loading=\"lazy\" />\n </div>\n <div class=\"diff-pane\">\n <div class=\"diff-pane__label\">Diff {selected.diffRatio === null ? \"\" : `· ${(selected.diffRatio * 100).toFixed(2)}%`}</div>\n {selected.diffPath ? (\n <img class=\"diff-pane__img\" src={imageUrl(project, build, selected, \"diff\")} alt={`Diff for ${selected.storyTitle} / ${selected.storyName}`} loading=\"lazy\" />\n ) : (\n <div style=\"aspect-ratio: 16/9; display:grid; place-items:center; color:var(--text-secondary);\">\n {selected.status === \"unchanged\" || selected.status === \"approved\" ? \"No diff — within threshold\" : \"No diff yet\"}\n </div>\n )}\n </div>\n </div>\n <p class=\"field__hint\" style=\"margin-top:.6rem;\">\n Keyboard: <kbd>←</kbd> <kbd>→</kbd> navigate · <kbd>a</kbd> approve · <kbd>r</kbd> reject · <kbd>?</kbd> help\n </p>\n </div>\n\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .5rem;\">Comments</h3>\n <div style=\"display:grid; gap:.75rem;\">\n {comments\n .filter((comment) => !comment.snapshotId || comment.snapshotId === selected.id)\n .map(\n (comment): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <div key={comment.id} class=\"comment\">\n <div class=\"comment__head\">\n <strong>{comment.userId}</strong>\n <span>· {new Date(comment.createdAt).toLocaleString()}</span>\n {comment.resolved ? <Badge tone=\"success\">resolved</Badge> : null}\n </div>\n <p class=\"comment__body\">{comment.body}</p>\n {!comment.resolved && canReview ? (\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/comments/${comment.id}/resolve`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/comments/${comment.id}/resolve`} hx-target=\"body\" style=\"margin-top:.5rem;\">\n <button class=\"btn btn--ghost\" type=\"submit\">\n Mark resolved\n </button>\n </form>\n ) : null}\n </div>\n ))}\n {comments.filter((c) => !c.snapshotId || c.snapshotId === selected.id).length === 0 ? <p class=\"field__hint\">No comments on this snapshot.</p> : null}\n </div>\n\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/comments`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/comments`} hx-target=\"body\" style=\"margin-top:1rem; display:grid; gap:.5rem;\">\n <input type=\"hidden\" name=\"snapshotId\" value={selected.id} />\n <label class=\"field__label\" for=\"comment-body\">\n Add comment\n </label>\n <textarea class=\"field__input field__input--textarea\" id=\"comment-body\" name=\"body\" rows={3} required placeholder=\"Leave feedback on this snapshot…\" />\n <div>\n <button class=\"btn btn--primary\" type=\"submit\">\n Comment\n </button>\n </div>\n </form>\n </div>\n </>\n ) : (\n <div class=\"empty\">\n <p class=\"empty__desc\">Select a snapshot to review.</p>\n </div>\n )}\n </div>\n </div>\n </div>\n )}\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
@@ -8,6 +8,7 @@ import { DocumentLayout } from "../ui/document.mjs";
8
8
  import { Badge, statusTone } from "../ui/components.mjs";
9
9
  import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
10
10
  //#region src/pages/compute-jobs.tsx
11
+ /** Live-refreshing partial showing the currently queued and running captures. */
11
12
  function renderActiveQueue(slug, queueView) {
12
13
  return /* @__PURE__ */ jsxs("div", {
13
14
  class: "card card--padded",
@@ -50,6 +51,7 @@ function renderActiveQueue(slug, queueView) {
50
51
  })]
51
52
  });
52
53
  }
54
+ /** Compute jobs page: live capture queue plus recent build history with retry. */
53
55
  async function renderComputeJobsPage(slug, queueView, canRetry) {
54
56
  const project = (await new ProjectModel(getStore().db).list()).find((item) => item.slug === slug);
55
57
  if (!project) return null;
@@ -1 +1 @@
1
- {"version":3,"file":"compute-jobs.mjs","names":[],"sources":["../../src/pages/compute-jobs.tsx"],"sourcesContent":["import { BuildModel } from \"../models/build.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { getStore } from \"../store.ts\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { Badge, statusTone } from \"../ui/components.tsx\";\n\ninterface QueueView {\n buildId: string;\n status: string;\n queuedAt: string;\n startedAt?: string;\n finishedAt?: string;\n error?: string;\n}\n\nexport function renderActiveQueue(slug: string, queueView: QueueView[]): RenderedContent {\n return (\n <div class=\"card card--padded\" id=\"active-queue\" hx-get={`/projects/${slug}/jobs?partial=queue`} hx-trigger=\"every 5s\" hx-swap=\"outerHTML\" hx-target=\"#active-queue\">\n <h2 style=\"margin:0 0 .5rem;\">Active queue</h2>\n {queueView.length === 0 ? (\n <p class=\"field__hint\">No captures are currently queued or running.</p>\n ) : (\n <div class=\"table-wrap\">\n <table>\n <thead>\n <tr>\n <th>Build</th>\n <th>Status</th>\n <th>Queued</th>\n <th>Started</th>\n </tr>\n </thead>\n <tbody>\n {queueView.map(\n // eslint-disable-next-line promise-function-async -- JSX.Element includes Promise<HtmlEscapedString>\n (job) => (\n <tr key={job.buildId}>\n <td>\n <a href={`/projects/${slug}/builds/${job.buildId}`}>{job.buildId.slice(0, 8)}</a>\n </td>\n <td>\n <Badge tone={statusTone(job.status)}>{job.status}</Badge>\n </td>\n <td class=\"field__hint\">{new Date(job.queuedAt).toLocaleTimeString()}</td>\n <td class=\"field__hint\">{job.startedAt ? new Date(job.startedAt).toLocaleTimeString() : \"—\"}</td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </div>\n )}\n </div>\n );\n}\n\nexport async function renderComputeJobsPage(slug: string, queueView: QueueView[], canRetry: boolean): Promise<RenderedContent | null> {\n const projects = await new ProjectModel(getStore().db).list();\n const project = projects.find((item) => item.slug === slug);\n if (!project) {\n return null;\n }\n const builds = await new BuildModel(getStore().db).list(project.id);\n const recentBuilds = builds.slice(0, 20);\n const queueByBuild = new Map(queueView.map((job) => [job.buildId, job]));\n\n return (\n <DocumentLayout title={`${project.name} · Compute jobs`} nav={{ active: \"builds\", projectSlug: project.slug, projectName: project.name }}>\n <div class=\"page-header\">\n <nav class=\"breadcrumbs\" aria-label=\"Breadcrumb\">\n <ol>\n <li>\n <a href=\"/projects\">Projects</a>\n </li>\n <li>\n <a href={`/projects/${project.slug}/builds`}>{project.name}</a>\n </li>\n <li>\n <span aria-current=\"page\">Compute jobs</span>\n </li>\n </ol>\n </nav>\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">Compute jobs</h1>\n <p class=\"page-header__desc\">Capture jobs run on this server. Queued and running jobs refresh live; recent history is below.</p>\n </div>\n <div class=\"page-header__actions\">\n <a class=\"btn btn--secondary\" href={`/projects/${project.slug}/builds`}>\n Back to builds\n </a>\n </div>\n </div>\n </div>\n\n {renderActiveQueue(project.slug, queueView)}\n\n <div class=\"card card--padded\">\n <h2 style=\"margin:0 0 .3rem;\">Recent builds</h2>\n <p class=\"field__hint\">Capture history for {project.name}. Failed jobs can be retried.</p>\n <div class=\"table-wrap\" style=\"margin-top:.75rem;\">\n <table>\n <thead>\n <tr>\n <th>Branch</th>\n <th>Status</th>\n <th>Snapshots</th>\n <th>Created</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n {recentBuilds.map(\n // eslint-disable-next-line promise-function-async -- JSX.Element includes Promise<HtmlEscapedString>\n (build) => (\n <tr key={build.id}>\n <td>\n <div style=\"font-weight:600;\">{build.gitBranch}</div>\n <div class=\"field__hint\" style=\"font-family: ui-monospace, monospace;\">\n {build.gitSha.slice(0, 7)}\n </div>\n </td>\n <td>\n <Badge tone={statusTone(build.status)}>{build.status}</Badge>\n </td>\n <td class=\"field__hint\">{build.snapshotCount} total · {build.changedCount} changed</td>\n <td class=\"field__hint\">{new Date(build.createdAt).toLocaleString()}</td>\n <td style=\"white-space:nowrap;\">\n <a class=\"btn btn--secondary\" href={`/projects/${project.slug}/builds/${build.id}`}>\n View\n </a>\n {canRetry && (build.status === \"failed\" || build.status === \"pending\") ? (\n <>\n <span style=\"margin-left:.35rem;\" />\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/retry`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/retry`} hx-target=\"body\" style=\"display:inline;\">\n <button class=\"btn btn--ghost\" type=\"submit\">\n Retry\n </button>\n </form>\n </>\n ) : null}\n {queueByBuild.get(build.id)?.error ? (\n <div class=\"field__hint\" style=\"margin-top:.25rem; color: var(--status-rejected);\">\n {queueByBuild.get(build.id)?.error}\n </div>\n ) : null}\n </td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </div>\n {recentBuilds.length === 0 ? <p class=\"field__hint\" style=\"margin-top:.5rem;\">No builds yet.</p> : null}\n </div>\n </DocumentLayout>\n );\n}"],"mappings":"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"}
1
+ {"version":3,"file":"compute-jobs.mjs","names":[],"sources":["../../src/pages/compute-jobs.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\n\nimport { BuildModel } from \"../models/build.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { getStore } from \"../store.ts\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { Badge, statusTone } from \"../ui/components.tsx\";\n\ninterface QueueView {\n buildId: string;\n status: string;\n queuedAt: string;\n startedAt?: string;\n finishedAt?: string;\n error?: string;\n}\n\n/** Live-refreshing partial showing the currently queued and running captures. */\nexport function renderActiveQueue(slug: string, queueView: QueueView[]): RenderedContent {\n return (\n <div class=\"card card--padded\" id=\"active-queue\" hx-get={`/projects/${slug}/jobs?partial=queue`} hx-trigger=\"every 5s\" hx-swap=\"outerHTML\" hx-target=\"#active-queue\">\n <h2 style=\"margin:0 0 .5rem;\">Active queue</h2>\n {queueView.length === 0 ? (\n <p class=\"field__hint\">No captures are currently queued or running.</p>\n ) : (\n <div class=\"table-wrap\">\n <table>\n <thead>\n <tr>\n <th>Build</th>\n <th>Status</th>\n <th>Queued</th>\n <th>Started</th>\n </tr>\n </thead>\n <tbody>\n {queueView.map(\n (job): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={job.buildId}>\n <td>\n <a href={`/projects/${slug}/builds/${job.buildId}`}>{job.buildId.slice(0, 8)}</a>\n </td>\n <td>\n <Badge tone={statusTone(job.status)}>{job.status}</Badge>\n </td>\n <td class=\"field__hint\">{new Date(job.queuedAt).toLocaleTimeString()}</td>\n <td class=\"field__hint\">{job.startedAt ? new Date(job.startedAt).toLocaleTimeString() : \"—\"}</td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </div>\n )}\n </div>\n );\n}\n\n/** Compute jobs page: live capture queue plus recent build history with retry. */\nexport async function renderComputeJobsPage(slug: string, queueView: QueueView[], canRetry: boolean): Promise<RenderedContent | null> {\n const projects = await new ProjectModel(getStore().db).list();\n const project = projects.find((item) => item.slug === slug);\n if (!project) {\n return null;\n }\n const builds = await new BuildModel(getStore().db).list(project.id);\n const recentBuilds = builds.slice(0, 20);\n const queueByBuild = new Map(queueView.map((job) => [job.buildId, job]));\n\n return (\n <DocumentLayout title={`${project.name} · Compute jobs`} nav={{ active: \"builds\", projectSlug: project.slug, projectName: project.name }}>\n <div class=\"page-header\">\n <nav class=\"breadcrumbs\" aria-label=\"Breadcrumb\">\n <ol>\n <li>\n <a href=\"/projects\">Projects</a>\n </li>\n <li>\n <a href={`/projects/${project.slug}/builds`}>{project.name}</a>\n </li>\n <li>\n <span aria-current=\"page\">Compute jobs</span>\n </li>\n </ol>\n </nav>\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">Compute jobs</h1>\n <p class=\"page-header__desc\">Capture jobs run on this server. Queued and running jobs refresh live; recent history is below.</p>\n </div>\n <div class=\"page-header__actions\">\n <a class=\"btn btn--secondary\" href={`/projects/${project.slug}/builds`}>\n Back to builds\n </a>\n </div>\n </div>\n </div>\n\n {renderActiveQueue(project.slug, queueView)}\n\n <div class=\"card card--padded\">\n <h2 style=\"margin:0 0 .3rem;\">Recent builds</h2>\n <p class=\"field__hint\">Capture history for {project.name}. Failed jobs can be retried.</p>\n <div class=\"table-wrap\" style=\"margin-top:.75rem;\">\n <table>\n <thead>\n <tr>\n <th>Branch</th>\n <th>Status</th>\n <th>Snapshots</th>\n <th>Created</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n {recentBuilds.map(\n (build): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={build.id}>\n <td>\n <div style=\"font-weight:600;\">{build.gitBranch}</div>\n <div class=\"field__hint\" style=\"font-family: ui-monospace, monospace;\">\n {build.gitSha.slice(0, 7)}\n </div>\n </td>\n <td>\n <Badge tone={statusTone(build.status)}>{build.status}</Badge>\n </td>\n <td class=\"field__hint\">{build.snapshotCount} total · {build.changedCount} changed</td>\n <td class=\"field__hint\">{new Date(build.createdAt).toLocaleString()}</td>\n <td style=\"white-space:nowrap;\">\n <a class=\"btn btn--secondary\" href={`/projects/${project.slug}/builds/${build.id}`}>\n View\n </a>\n {canRetry && (build.status === \"failed\" || build.status === \"pending\") ? (\n <>\n <span style=\"margin-left:.35rem;\" />\n <form method=\"post\" action={`/api/v1/projects/${project.slug}/builds/${build.id}/retry`} hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/retry`} hx-target=\"body\" style=\"display:inline;\">\n <button class=\"btn btn--ghost\" type=\"submit\">\n Retry\n </button>\n </form>\n </>\n ) : null}\n {queueByBuild.get(build.id)?.error ? (\n <div class=\"field__hint\" style=\"margin-top:.25rem; color: var(--status-rejected);\">\n {queueByBuild.get(build.id)?.error}\n </div>\n ) : null}\n </td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </div>\n {recentBuilds.length === 0 ? <p class=\"field__hint\" style=\"margin-top:.5rem;\">No builds yet.</p> : null}\n </div>\n </DocumentLayout>\n );\n}"],"mappings":"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"}