@storyshelf/core 0.1.2 → 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 (170) hide show
  1. package/dist/adapter/auth.mjs.map +1 -1
  2. package/dist/adapter/git-host/helpers.d.mts +1 -0
  3. package/dist/adapter/git-host/helpers.mjs +1 -0
  4. package/dist/adapter/git-host/helpers.mjs.map +1 -1
  5. package/dist/adapters/capture-queue.d.mts +2 -0
  6. package/dist/adapters/webhook-events.mjs +8 -0
  7. package/dist/adapters/webhook-events.mjs.map +1 -1
  8. package/dist/capture/adapter.d.mts +2 -0
  9. package/dist/capture/adapter.mjs +3 -0
  10. package/dist/capture/adapter.mjs.map +1 -1
  11. package/dist/capture/orchestrator.d.mts +7 -0
  12. package/dist/capture/orchestrator.mjs +6 -0
  13. package/dist/capture/orchestrator.mjs.map +1 -1
  14. package/dist/capture/queue.d.mts +2 -0
  15. package/dist/capture/queue.mjs +1 -0
  16. package/dist/capture/queue.mjs.map +1 -1
  17. package/dist/capture/status-fanout.mjs +1 -0
  18. package/dist/capture/status-fanout.mjs.map +1 -1
  19. package/dist/capture/storybook.d.mts +1 -0
  20. package/dist/capture/storybook.mjs +1 -0
  21. package/dist/capture/storybook.mjs.map +1 -1
  22. package/dist/config.d.mts +27 -20
  23. package/dist/config.mjs +14 -0
  24. package/dist/config.mjs.map +1 -1
  25. package/dist/ddl.d.mts +1 -0
  26. package/dist/ddl.mjs +1 -0
  27. package/dist/ddl.mjs.map +1 -1
  28. package/dist/diff/engine.d.mts +8 -0
  29. package/dist/diff/engine.mjs +8 -0
  30. package/dist/diff/engine.mjs.map +1 -1
  31. package/dist/index.d.mts +3 -0
  32. package/dist/index.mjs +6 -1
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/logger.mjs.map +1 -1
  35. package/dist/middleware/csrf.mjs +1 -0
  36. package/dist/middleware/csrf.mjs.map +1 -1
  37. package/dist/middleware/rate-limit.mjs +1 -0
  38. package/dist/middleware/rate-limit.mjs.map +1 -1
  39. package/dist/models/baseline.d.mts +1 -0
  40. package/dist/models/baseline.mjs +3 -1
  41. package/dist/models/baseline.mjs.map +1 -1
  42. package/dist/models/build.d.mts +3 -0
  43. package/dist/models/build.mjs +3 -1
  44. package/dist/models/build.mjs.map +1 -1
  45. package/dist/models/comment.d.mts +2 -0
  46. package/dist/models/comment.mjs +3 -1
  47. package/dist/models/comment.mjs.map +1 -1
  48. package/dist/models/label.mjs +2 -1
  49. package/dist/models/label.mjs.map +1 -1
  50. package/dist/models/member.mjs +2 -1
  51. package/dist/models/member.mjs.map +1 -1
  52. package/dist/models/project.d.mts +1 -0
  53. package/dist/models/project.mjs +2 -1
  54. package/dist/models/project.mjs.map +1 -1
  55. package/dist/models/snapshot.d.mts +1 -0
  56. package/dist/models/snapshot.mjs +2 -1
  57. package/dist/models/snapshot.mjs.map +1 -1
  58. package/dist/models/status-config.mjs +2 -1
  59. package/dist/models/status-config.mjs.map +1 -1
  60. package/dist/models/token.mjs +2 -1
  61. package/dist/models/token.mjs.map +1 -1
  62. package/dist/models/webhook.d.mts +1 -0
  63. package/dist/models/webhook.mjs +2 -1
  64. package/dist/models/webhook.mjs.map +1 -1
  65. package/dist/pages/build-detail.mjs +1 -0
  66. package/dist/pages/build-detail.mjs.map +1 -1
  67. package/dist/pages/build-diff.mjs +1 -0
  68. package/dist/pages/build-diff.mjs.map +1 -1
  69. package/dist/pages/compute-jobs.mjs +2 -0
  70. package/dist/pages/compute-jobs.mjs.map +1 -1
  71. package/dist/pages/login.mjs +1 -0
  72. package/dist/pages/login.mjs.map +1 -1
  73. package/dist/pages/project-builds.mjs +1 -0
  74. package/dist/pages/project-builds.mjs.map +1 -1
  75. package/dist/pages/project-create.mjs +1 -0
  76. package/dist/pages/project-create.mjs.map +1 -1
  77. package/dist/pages/project-settings.mjs +1 -0
  78. package/dist/pages/project-settings.mjs.map +1 -1
  79. package/dist/pages/projects.mjs +1 -0
  80. package/dist/pages/projects.mjs.map +1 -1
  81. package/dist/pages/root.mjs +1 -0
  82. package/dist/pages/root.mjs.map +1 -1
  83. package/dist/pages/settings-general.mjs +1 -0
  84. package/dist/pages/settings-general.mjs.map +1 -1
  85. package/dist/pages/settings-labels.mjs +1 -0
  86. package/dist/pages/settings-labels.mjs.map +1 -1
  87. package/dist/pages/settings-members.mjs +1 -0
  88. package/dist/pages/settings-members.mjs.map +1 -1
  89. package/dist/pages/settings-status.mjs +1 -0
  90. package/dist/pages/settings-status.mjs.map +1 -1
  91. package/dist/pages/settings-tests.mjs +1 -0
  92. package/dist/pages/settings-tests.mjs.map +1 -1
  93. package/dist/pages/settings-tokens.mjs +1 -0
  94. package/dist/pages/settings-tokens.mjs.map +1 -1
  95. package/dist/pages/settings-webhooks.mjs +1 -0
  96. package/dist/pages/settings-webhooks.mjs.map +1 -1
  97. package/dist/retention/purge.d.mts +3 -0
  98. package/dist/retention/purge.mjs +2 -1
  99. package/dist/retention/purge.mjs.map +1 -1
  100. package/dist/routers/admin.mjs +1 -0
  101. package/dist/routers/admin.mjs.map +1 -1
  102. package/dist/routers/assets.mjs +1 -0
  103. package/dist/routers/assets.mjs.map +1 -1
  104. package/dist/routers/auth.mjs +1 -0
  105. package/dist/routers/auth.mjs.map +1 -1
  106. package/dist/routers/builds.handlers.mjs +10 -0
  107. package/dist/routers/builds.handlers.mjs.map +1 -1
  108. package/dist/routers/builds.mjs +1 -0
  109. package/dist/routers/builds.mjs.map +1 -1
  110. package/dist/routers/comments.mjs +1 -0
  111. package/dist/routers/comments.mjs.map +1 -1
  112. package/dist/routers/helpers.mjs +7 -1
  113. package/dist/routers/helpers.mjs.map +1 -1
  114. package/dist/routers/htmx.mjs +2 -0
  115. package/dist/routers/htmx.mjs.map +1 -1
  116. package/dist/routers/labels.mjs +1 -0
  117. package/dist/routers/labels.mjs.map +1 -1
  118. package/dist/routers/media.mjs +1 -0
  119. package/dist/routers/media.mjs.map +1 -1
  120. package/dist/routers/members.mjs +1 -0
  121. package/dist/routers/members.mjs.map +1 -1
  122. package/dist/routers/projects.mjs +1 -0
  123. package/dist/routers/projects.mjs.map +1 -1
  124. package/dist/routers/schemas.mjs +25 -0
  125. package/dist/routers/schemas.mjs.map +1 -1
  126. package/dist/routers/settings.mjs +2 -0
  127. package/dist/routers/settings.mjs.map +1 -1
  128. package/dist/routers/snapshots.mjs +1 -0
  129. package/dist/routers/snapshots.mjs.map +1 -1
  130. package/dist/routers/status-configs.mjs +1 -0
  131. package/dist/routers/status-configs.mjs.map +1 -1
  132. package/dist/routers/storybook.mjs +1 -0
  133. package/dist/routers/storybook.mjs.map +1 -1
  134. package/dist/routers/tokens.mjs +1 -0
  135. package/dist/routers/tokens.mjs.map +1 -1
  136. package/dist/routers/ui.mjs +1 -0
  137. package/dist/routers/ui.mjs.map +1 -1
  138. package/dist/routers/webhooks.mjs +1 -0
  139. package/dist/routers/webhooks.mjs.map +1 -1
  140. package/dist/schema-tables.mjs +166 -0
  141. package/dist/schema-tables.mjs.map +1 -0
  142. package/dist/schema.d.mts +194 -4420
  143. package/dist/schema.mjs +26 -138
  144. package/dist/schema.mjs.map +1 -1
  145. package/dist/store.mjs +2 -0
  146. package/dist/store.mjs.map +1 -1
  147. package/dist/types.d.mts +12 -0
  148. package/dist/types.mjs +8 -0
  149. package/dist/types.mjs.map +1 -1
  150. package/dist/ui/components.mjs +5 -0
  151. package/dist/ui/components.mjs.map +1 -1
  152. package/dist/ui/document.d.mts +1 -0
  153. package/dist/ui/document.mjs +1 -0
  154. package/dist/ui/document.mjs.map +1 -1
  155. package/dist/ui/styles.mjs +1 -0
  156. package/dist/ui/styles.mjs.map +1 -1
  157. package/dist/ui/theme.mjs +2 -0
  158. package/dist/ui/theme.mjs.map +1 -1
  159. package/dist/urls.d.mts +8 -0
  160. package/dist/urls.mjs +7 -0
  161. package/dist/urls.mjs.map +1 -1
  162. package/dist/utils/hash.mjs +3 -0
  163. package/dist/utils/hash.mjs.map +1 -1
  164. package/dist/utils/paths.d.mts +5 -0
  165. package/dist/utils/paths.mjs +5 -0
  166. package/dist/utils/paths.mjs.map +1 -1
  167. package/dist/utils/ulid.d.mts +2 -0
  168. package/dist/utils/ulid.mjs +2 -0
  169. package/dist/utils/ulid.mjs.map +1 -1
  170. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"project-create.mjs","names":[],"sources":["../../src/pages/project-create.tsx"],"sourcesContent":["import { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\n\nexport interface ProjectCreateFormState {\n values?: { name?: string; gitRepository?: string; gitDefaultBranch?: string };\n errors?: Record<string, string>;\n globalError?: string;\n}\n\nexport function renderProjectCreatePage(state: ProjectCreateFormState = {}): RenderedContent {\n const values = state.values ?? {};\n const errors = state.errors ?? {};\n return (\n <DocumentLayout title=\"New project\" nav={{ active: \"projects\" }}>\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 <span aria-current=\"page\">New project</span>\n </li>\n </ol>\n </nav>\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">Create project</h1>\n <p class=\"page-header__desc\">A project corresponds to one Storybook. You can have multiple projects per repository.</p>\n </div>\n </div>\n </div>\n\n {state.globalError ? (\n <div class=\"alert alert--danger\" role=\"alert\">\n <strong class=\"alert__title\">Could not create project</strong>\n <div class=\"alert__body\">{state.globalError}</div>\n </div>\n ) : null}\n\n <div class=\"card card--padded\" style=\"max-width: 640px;\">\n <form\n method=\"post\"\n action=\"/projects/new\"\n hx-post=\"/projects/new\"\n hx-target=\"body\"\n hx-swap=\"outerHTML\"\n novalidate\n >\n <div class=\"field\">\n <label class=\"field__label\" for=\"name\">\n Name <span aria-hidden=\"true\">*</span>\n </label>\n <input\n class={`field__input ${errors[\"name\"] ? \"field__input--error\" : \"\"}`}\n id=\"name\"\n name=\"name\"\n type=\"text\"\n required\n autofocus\n value={values.name ?? \"\"}\n placeholder=\"Design System\"\n aria-invalid={errors[\"name\"] ? \"true\" : undefined}\n aria-describedby={errors[\"name\"] ? \"name-error\" : \"name-hint\"}\n autocomplete=\"off\"\n />\n {errors[\"name\"] ? (\n <p class=\"field__error\" id=\"name-error\" role=\"alert\">\n {errors[\"name\"]}\n </p>\n ) : (\n <p class=\"field__hint\" id=\"name-hint\">\n Human readable name. A URL-friendly slug is generated automatically.\n </p>\n )}\n </div>\n\n <div class=\"field\">\n <label class=\"field__label\" for=\"gitRepository\">\n Git repository\n </label>\n <input\n class={`field__input ${errors[\"gitRepository\"] ? \"field__input--error\" : \"\"}`}\n id=\"gitRepository\"\n name=\"gitRepository\"\n type=\"text\"\n value={values.gitRepository ?? \"\"}\n placeholder=\"acme/design-system\"\n aria-invalid={errors[\"gitRepository\"] ? \"true\" : undefined}\n aria-describedby={errors[\"gitRepository\"] ? \"gitRepository-error\" : \"gitRepository-hint\"}\n />\n {errors[\"gitRepository\"] ? (\n <p class=\"field__error\" id=\"gitRepository-error\" role=\"alert\">\n {errors[\"gitRepository\"]}\n </p>\n ) : (\n <p class=\"field__hint\" id=\"gitRepository-hint\">\n Optional owner/repo for status checks and links.\n </p>\n )}\n </div>\n\n <div class=\"field\">\n <label class=\"field__label\" for=\"gitDefaultBranch\">\n Default branch\n </label>\n <input\n class=\"field__input\"\n id=\"gitDefaultBranch\"\n name=\"gitDefaultBranch\"\n type=\"text\"\n value={values.gitDefaultBranch ?? \"main\"}\n placeholder=\"main\"\n />\n <p class=\"field__hint\">Baselines fall back to this branch when no branch-specific baseline exists.</p>\n </div>\n\n <div style=\"display:flex; gap:.5rem; margin-top:1rem;\">\n <button class=\"btn btn--primary\" type=\"submit\">\n Create project\n </button>\n <a class=\"btn btn--secondary\" href=\"/projects\">\n Cancel\n </a>\n </div>\n </form>\n </div>\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"project-create.mjs","names":[],"sources":["../../src/pages/project-create.tsx"],"sourcesContent":["import { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\n\n/** Form state for the project creation page (values and validation errors). */\nexport interface ProjectCreateFormState {\n values?: { name?: string; gitRepository?: string; gitDefaultBranch?: string };\n errors?: Record<string, string>;\n globalError?: string;\n}\n\n/** New-project form page (one project corresponds to one Storybook). */\nexport function renderProjectCreatePage(state: ProjectCreateFormState = {}): RenderedContent {\n const values = state.values ?? {};\n const errors = state.errors ?? {};\n return (\n <DocumentLayout title=\"New project\" nav={{ active: \"projects\" }}>\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 <span aria-current=\"page\">New project</span>\n </li>\n </ol>\n </nav>\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">Create project</h1>\n <p class=\"page-header__desc\">A project corresponds to one Storybook. You can have multiple projects per repository.</p>\n </div>\n </div>\n </div>\n\n {state.globalError ? (\n <div class=\"alert alert--danger\" role=\"alert\">\n <strong class=\"alert__title\">Could not create project</strong>\n <div class=\"alert__body\">{state.globalError}</div>\n </div>\n ) : null}\n\n <div class=\"card card--padded\" style=\"max-width: 640px;\">\n <form\n method=\"post\"\n action=\"/projects/new\"\n hx-post=\"/projects/new\"\n hx-target=\"body\"\n hx-swap=\"outerHTML\"\n novalidate\n >\n <div class=\"field\">\n <label class=\"field__label\" for=\"name\">\n Name <span aria-hidden=\"true\">*</span>\n </label>\n <input\n class={`field__input ${errors[\"name\"] ? \"field__input--error\" : \"\"}`}\n id=\"name\"\n name=\"name\"\n type=\"text\"\n required\n autofocus\n value={values.name ?? \"\"}\n placeholder=\"Design System\"\n aria-invalid={errors[\"name\"] ? \"true\" : undefined}\n aria-describedby={errors[\"name\"] ? \"name-error\" : \"name-hint\"}\n autocomplete=\"off\"\n />\n {errors[\"name\"] ? (\n <p class=\"field__error\" id=\"name-error\" role=\"alert\">\n {errors[\"name\"]}\n </p>\n ) : (\n <p class=\"field__hint\" id=\"name-hint\">\n Human readable name. A URL-friendly slug is generated automatically.\n </p>\n )}\n </div>\n\n <div class=\"field\">\n <label class=\"field__label\" for=\"gitRepository\">\n Git repository\n </label>\n <input\n class={`field__input ${errors[\"gitRepository\"] ? \"field__input--error\" : \"\"}`}\n id=\"gitRepository\"\n name=\"gitRepository\"\n type=\"text\"\n value={values.gitRepository ?? \"\"}\n placeholder=\"acme/design-system\"\n aria-invalid={errors[\"gitRepository\"] ? \"true\" : undefined}\n aria-describedby={errors[\"gitRepository\"] ? \"gitRepository-error\" : \"gitRepository-hint\"}\n />\n {errors[\"gitRepository\"] ? (\n <p class=\"field__error\" id=\"gitRepository-error\" role=\"alert\">\n {errors[\"gitRepository\"]}\n </p>\n ) : (\n <p class=\"field__hint\" id=\"gitRepository-hint\">\n Optional owner/repo for status checks and links.\n </p>\n )}\n </div>\n\n <div class=\"field\">\n <label class=\"field__label\" for=\"gitDefaultBranch\">\n Default branch\n </label>\n <input\n class=\"field__input\"\n id=\"gitDefaultBranch\"\n name=\"gitDefaultBranch\"\n type=\"text\"\n value={values.gitDefaultBranch ?? \"main\"}\n placeholder=\"main\"\n />\n <p class=\"field__hint\">Baselines fall back to this branch when no branch-specific baseline exists.</p>\n </div>\n\n <div style=\"display:flex; gap:.5rem; margin-top:1rem;\">\n <button class=\"btn btn--primary\" type=\"submit\">\n Create project\n </button>\n <a class=\"btn btn--secondary\" href=\"/projects\">\n Cancel\n </a>\n </div>\n </form>\n </div>\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
@@ -34,6 +34,7 @@ function renderActiveTab(data, formState) {
34
34
  if (activeTab === "status") return renderSettingsStatus(project, data.statusConfigs, data.gitHosts, data.isAdmin);
35
35
  return null;
36
36
  }
37
+ /** Project settings shell: tab navigation plus the active settings tab. */
37
38
  function renderProjectSettingsPage(data, formState) {
38
39
  const { project, activeTab } = data;
39
40
  return /* @__PURE__ */ jsxs(DocumentLayout, {
@@ -1 +1 @@
1
- {"version":3,"file":"project-settings.mjs","names":[],"sources":["../../src/pages/project-settings.tsx"],"sourcesContent":["import type { GitHostProvider } from \"../adapters/git-host/index.ts\";\nimport type { LabelType, Project, Token } from \"../schema.ts\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { renderSettingsGeneral } from \"./settings-general.tsx\";\nimport { renderSettingsLabels } from \"./settings-labels.tsx\";\nimport { renderSettingsMembers, type SettingsMember } from \"./settings-members.tsx\";\nimport { renderSettingsStatus, type SettingsStatusConfig } from \"./settings-status.tsx\";\nimport { renderSettingsTests } from \"./settings-tests.tsx\";\nimport { renderSettingsTokens } from \"./settings-tokens.tsx\";\nimport { renderSettingsWebhooks, type SettingsWebhook } from \"./settings-webhooks.tsx\";\n\nexport type SettingsTab = \"general\" | \"tests\" | \"labels\" | \"tokens\" | \"webhooks\" | \"members\" | \"status\";\n\nexport interface ProjectSettingsData {\n project: Project;\n activeTab: SettingsTab;\n labelTypes: LabelType[];\n tokens: Omit<Token, \"hash\">[];\n members: SettingsMember[];\n webhooks: SettingsWebhook[];\n statusConfigs: SettingsStatusConfig[];\n gitHosts: GitHostProvider[];\n isAdmin: boolean;\n}\n\nexport interface SettingsFormState {\n errors?: Record<string, string>;\n globalError?: string;\n secret?: string;\n}\n\nfunction tabHref(project: Project, tab: SettingsTab): string {\n return tab === \"general\" ? `/projects/${project.slug}/settings` : `/projects/${project.slug}/settings/${tab}`;\n}\n\nfunction renderTabLink(project: Project, activeTab: SettingsTab, tab: SettingsTab, label: string): unknown {\n const active = activeTab === tab;\n return (\n <a class={`tabs__link ${active ? \"tabs__link--active\" : \"\"}`} href={tabHref(project, tab)} aria-current={active ? \"page\" : undefined}>\n {label}\n </a>\n );\n}\n\nfunction renderActiveTab(data: ProjectSettingsData, formState?: SettingsFormState): unknown {\n const { project, activeTab } = data;\n if (activeTab === \"general\") return renderSettingsGeneral(project, formState, data.isAdmin);\n if (activeTab === \"tests\") return renderSettingsTests(project, data.isAdmin, formState);\n if (activeTab === \"labels\") return renderSettingsLabels(project, data.labelTypes, data.isAdmin);\n if (activeTab === \"tokens\") return renderSettingsTokens(project, data.tokens, data.isAdmin);\n if (activeTab === \"webhooks\") return renderSettingsWebhooks(project, data.webhooks, data.isAdmin, formState);\n if (activeTab === \"members\") return renderSettingsMembers(project, data.members, data.isAdmin);\n if (activeTab === \"status\") return renderSettingsStatus(project, data.statusConfigs, data.gitHosts, data.isAdmin);\n return null;\n}\n\nexport function renderProjectSettingsPage(data: ProjectSettingsData, formState?: SettingsFormState): RenderedContent {\n const { project, activeTab } = data;\n return (\n <DocumentLayout title={`${project.name} · Settings`} nav={{ active: \"settings\", 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\">Settings</span>\n </li>\n </ol>\n </nav>\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">Project settings</h1>\n <p class=\"page-header__desc\">Manage general settings, labels, tokens, webhooks and members for {project.name}.</p>\n </div>\n </div>\n </div>\n\n <nav class=\"tabs\" aria-label=\"Settings sections\">\n {renderTabLink(project, activeTab, \"general\", \"General\")}\n {renderTabLink(project, activeTab, \"tests\", \"Tests\")}\n {renderTabLink(project, activeTab, \"labels\", \"Labels\")}\n {renderTabLink(project, activeTab, \"tokens\", \"Tokens\")}\n {renderTabLink(project, activeTab, \"webhooks\", \"Webhooks\")}\n {renderTabLink(project, activeTab, \"members\", \"Members\")}\n {renderTabLink(project, activeTab, \"status\", \"Git status\")}\n </nav>\n\n {renderActiveTab(data, formState)}\n </DocumentLayout>\n );\n}"],"mappings":";;;;;;;;;;;;;AA+BA,SAAS,QAAQ,SAAkB,KAA0B;CAC3D,OAAO,QAAQ,YAAY,aAAa,QAAQ,KAAK,aAAa,aAAa,QAAQ,KAAK,YAAY;AAC1G;AAEA,SAAS,cAAc,SAAkB,WAAwB,KAAkB,OAAwB;CACzG,MAAM,SAAS,cAAc;CAC7B,OACE,oBAAC,KAAD;EAAG,OAAO,cAAc,SAAS,uBAAuB;EAAM,MAAM,QAAQ,SAAS,GAAG;EAAG,gBAAc,SAAS,SAAS,KAAA;EACxH,UAAA;CACA,CAAA;AAEP;AAEA,SAAS,gBAAgB,MAA2B,WAAwC;CAC1F,MAAM,EAAE,SAAS,cAAc;CAC/B,IAAI,cAAc,WAAW,OAAO,sBAAsB,SAAS,WAAW,KAAK,OAAO;CAC1F,IAAI,cAAc,SAAS,OAAO,oBAAoB,SAAS,KAAK,SAAS,SAAS;CACtF,IAAI,cAAc,UAAU,OAAO,qBAAqB,SAAS,KAAK,YAAY,KAAK,OAAO;CAC9F,IAAI,cAAc,UAAU,OAAO,qBAAqB,SAAS,KAAK,QAAQ,KAAK,OAAO;CAC1F,IAAI,cAAc,YAAY,OAAO,uBAAuB,SAAS,KAAK,UAAU,KAAK,SAAS,SAAS;CAC3G,IAAI,cAAc,WAAW,OAAO,sBAAsB,SAAS,KAAK,SAAS,KAAK,OAAO;CAC7F,IAAI,cAAc,UAAU,OAAO,qBAAqB,SAAS,KAAK,eAAe,KAAK,UAAU,KAAK,OAAO;CAChH,OAAO;AACT;AAEA,SAAgB,0BAA0B,MAA2B,WAAgD;CACnH,MAAM,EAAE,SAAS,cAAc;CAC/B,OACE,qBAAC,gBAAD;EAAgB,OAAO,GAAG,QAAQ,KAAK;EAAc,KAAK;GAAE,QAAQ;GAAY,aAAa,QAAQ;GAAM,aAAa,QAAQ;EAAK;EAArI,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,oBAAC,QAAD;OAAM,gBAAa;OAAO,UAAA;MAAc,CAAA,EACtC,CAAA;KACF,EAAA,CAAA;IACD,CAAA,GACL,oBAAC,OAAD;KAAK,OAAM;KACT,UAAA,qBAAC,OAAD,EAAA,UAAA,CACE,oBAAC,MAAD;MAAI,OAAM;MAAqB,UAAA;KAAoB,CAAA,GACnD,qBAAC,KAAD;MAAG,OAAM;MAAT,UAAA;OAA6B;OAAmE,QAAQ;OAAK;MAAI;KAC9G,CAAA,CAAA,EAAA,CAAA;IACF,CAAA,CACF;;GAEL,qBAAC,OAAD;IAAK,OAAM;IAAO,cAAW;IAA7B,UAAA;KACG,cAAc,SAAS,WAAW,WAAW,SAAS;KACtD,cAAc,SAAS,WAAW,SAAS,OAAO;KAClD,cAAc,SAAS,WAAW,UAAU,QAAQ;KACpD,cAAc,SAAS,WAAW,UAAU,QAAQ;KACpD,cAAc,SAAS,WAAW,YAAY,UAAU;KACxD,cAAc,SAAS,WAAW,WAAW,SAAS;KACtD,cAAc,SAAS,WAAW,UAAU,YAAY;IACtD;;GAEJ,gBAAgB,MAAM,SAAS;EAClB;;AAEpB"}
1
+ {"version":3,"file":"project-settings.mjs","names":[],"sources":["../../src/pages/project-settings.tsx"],"sourcesContent":["import type { GitHostProvider } from \"../adapters/git-host/index.ts\";\nimport type { LabelType, Project, Token } from \"../schema.ts\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { renderSettingsGeneral } from \"./settings-general.tsx\";\nimport { renderSettingsLabels } from \"./settings-labels.tsx\";\nimport { renderSettingsMembers, type SettingsMember } from \"./settings-members.tsx\";\nimport { renderSettingsStatus, type SettingsStatusConfig } from \"./settings-status.tsx\";\nimport { renderSettingsTests } from \"./settings-tests.tsx\";\nimport { renderSettingsTokens } from \"./settings-tokens.tsx\";\nimport { renderSettingsWebhooks, type SettingsWebhook } from \"./settings-webhooks.tsx\";\n\n/** Tabs available in the project settings section. */\nexport type SettingsTab = \"general\" | \"tests\" | \"labels\" | \"tokens\" | \"webhooks\" | \"members\" | \"status\";\n\n/** Data required to render the project settings page with its active tab. */\nexport interface ProjectSettingsData {\n project: Project;\n activeTab: SettingsTab;\n labelTypes: LabelType[];\n tokens: Omit<Token, \"hash\">[];\n members: SettingsMember[];\n webhooks: SettingsWebhook[];\n statusConfigs: SettingsStatusConfig[];\n gitHosts: GitHostProvider[];\n isAdmin: boolean;\n}\n\n/** Form state shared by the settings tabs (field errors, global error, one-time secret). */\nexport interface SettingsFormState {\n errors?: Record<string, string>;\n globalError?: string;\n secret?: string;\n}\n\nfunction tabHref(project: Project, tab: SettingsTab): string {\n return tab === \"general\" ? `/projects/${project.slug}/settings` : `/projects/${project.slug}/settings/${tab}`;\n}\n\nfunction renderTabLink(project: Project, activeTab: SettingsTab, tab: SettingsTab, label: string): unknown {\n const active = activeTab === tab;\n return (\n <a class={`tabs__link ${active ? \"tabs__link--active\" : \"\"}`} href={tabHref(project, tab)} aria-current={active ? \"page\" : undefined}>\n {label}\n </a>\n );\n}\n\nfunction renderActiveTab(data: ProjectSettingsData, formState?: SettingsFormState): unknown {\n const { project, activeTab } = data;\n if (activeTab === \"general\") return renderSettingsGeneral(project, formState, data.isAdmin);\n if (activeTab === \"tests\") return renderSettingsTests(project, data.isAdmin, formState);\n if (activeTab === \"labels\") return renderSettingsLabels(project, data.labelTypes, data.isAdmin);\n if (activeTab === \"tokens\") return renderSettingsTokens(project, data.tokens, data.isAdmin);\n if (activeTab === \"webhooks\") return renderSettingsWebhooks(project, data.webhooks, data.isAdmin, formState);\n if (activeTab === \"members\") return renderSettingsMembers(project, data.members, data.isAdmin);\n if (activeTab === \"status\") return renderSettingsStatus(project, data.statusConfigs, data.gitHosts, data.isAdmin);\n return null;\n}\n\n/** Project settings shell: tab navigation plus the active settings tab. */\nexport function renderProjectSettingsPage(data: ProjectSettingsData, formState?: SettingsFormState): RenderedContent {\n const { project, activeTab } = data;\n return (\n <DocumentLayout title={`${project.name} · Settings`} nav={{ active: \"settings\", 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\">Settings</span>\n </li>\n </ol>\n </nav>\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">Project settings</h1>\n <p class=\"page-header__desc\">Manage general settings, labels, tokens, webhooks and members for {project.name}.</p>\n </div>\n </div>\n </div>\n\n <nav class=\"tabs\" aria-label=\"Settings sections\">\n {renderTabLink(project, activeTab, \"general\", \"General\")}\n {renderTabLink(project, activeTab, \"tests\", \"Tests\")}\n {renderTabLink(project, activeTab, \"labels\", \"Labels\")}\n {renderTabLink(project, activeTab, \"tokens\", \"Tokens\")}\n {renderTabLink(project, activeTab, \"webhooks\", \"Webhooks\")}\n {renderTabLink(project, activeTab, \"members\", \"Members\")}\n {renderTabLink(project, activeTab, \"status\", \"Git status\")}\n </nav>\n\n {renderActiveTab(data, formState)}\n </DocumentLayout>\n );\n}"],"mappings":";;;;;;;;;;;;;AAkCA,SAAS,QAAQ,SAAkB,KAA0B;CAC3D,OAAO,QAAQ,YAAY,aAAa,QAAQ,KAAK,aAAa,aAAa,QAAQ,KAAK,YAAY;AAC1G;AAEA,SAAS,cAAc,SAAkB,WAAwB,KAAkB,OAAwB;CACzG,MAAM,SAAS,cAAc;CAC7B,OACE,oBAAC,KAAD;EAAG,OAAO,cAAc,SAAS,uBAAuB;EAAM,MAAM,QAAQ,SAAS,GAAG;EAAG,gBAAc,SAAS,SAAS,KAAA;EACxH,UAAA;CACA,CAAA;AAEP;AAEA,SAAS,gBAAgB,MAA2B,WAAwC;CAC1F,MAAM,EAAE,SAAS,cAAc;CAC/B,IAAI,cAAc,WAAW,OAAO,sBAAsB,SAAS,WAAW,KAAK,OAAO;CAC1F,IAAI,cAAc,SAAS,OAAO,oBAAoB,SAAS,KAAK,SAAS,SAAS;CACtF,IAAI,cAAc,UAAU,OAAO,qBAAqB,SAAS,KAAK,YAAY,KAAK,OAAO;CAC9F,IAAI,cAAc,UAAU,OAAO,qBAAqB,SAAS,KAAK,QAAQ,KAAK,OAAO;CAC1F,IAAI,cAAc,YAAY,OAAO,uBAAuB,SAAS,KAAK,UAAU,KAAK,SAAS,SAAS;CAC3G,IAAI,cAAc,WAAW,OAAO,sBAAsB,SAAS,KAAK,SAAS,KAAK,OAAO;CAC7F,IAAI,cAAc,UAAU,OAAO,qBAAqB,SAAS,KAAK,eAAe,KAAK,UAAU,KAAK,OAAO;CAChH,OAAO;AACT;;AAGA,SAAgB,0BAA0B,MAA2B,WAAgD;CACnH,MAAM,EAAE,SAAS,cAAc;CAC/B,OACE,qBAAC,gBAAD;EAAgB,OAAO,GAAG,QAAQ,KAAK;EAAc,KAAK;GAAE,QAAQ;GAAY,aAAa,QAAQ;GAAM,aAAa,QAAQ;EAAK;EAArI,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,oBAAC,QAAD;OAAM,gBAAa;OAAO,UAAA;MAAc,CAAA,EACtC,CAAA;KACF,EAAA,CAAA;IACD,CAAA,GACL,oBAAC,OAAD;KAAK,OAAM;KACT,UAAA,qBAAC,OAAD,EAAA,UAAA,CACE,oBAAC,MAAD;MAAI,OAAM;MAAqB,UAAA;KAAoB,CAAA,GACnD,qBAAC,KAAD;MAAG,OAAM;MAAT,UAAA;OAA6B;OAAmE,QAAQ;OAAK;MAAI;KAC9G,CAAA,CAAA,EAAA,CAAA;IACF,CAAA,CACF;;GAEL,qBAAC,OAAD;IAAK,OAAM;IAAO,cAAW;IAA7B,UAAA;KACG,cAAc,SAAS,WAAW,WAAW,SAAS;KACtD,cAAc,SAAS,WAAW,SAAS,OAAO;KAClD,cAAc,SAAS,WAAW,UAAU,QAAQ;KACpD,cAAc,SAAS,WAAW,UAAU,QAAQ;KACpD,cAAc,SAAS,WAAW,YAAY,UAAU;KACxD,cAAc,SAAS,WAAW,WAAW,SAAS;KACtD,cAAc,SAAS,WAAW,UAAU,YAAY;IACtD;;GAEJ,gBAAgB,MAAM,SAAS;EAClB;;AAEpB"}
@@ -8,6 +8,7 @@ import { DocumentLayout } from "../ui/document.mjs";
8
8
  import { Badge } from "../ui/components.mjs";
9
9
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
10
10
  //#region src/pages/projects.tsx
11
+ /** Projects overview page: project cards with latest build plus next steps. */
11
12
  async function renderProjectsPage() {
12
13
  const { db, user } = getStore();
13
14
  const projects = await new ProjectModel(db).list();
@@ -1 +1 @@
1
- {"version":3,"file":"projects.mjs","names":[],"sources":["../../src/pages/projects.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { getStore } from \"../store.ts\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { Badge } from \"../ui/components.tsx\";\n\nexport async function renderProjectsPage(): Promise<RenderedContent> {\n const { db, user } = getStore();\n const projects = await new ProjectModel(db).list();\n const canCreate = !user || user.role === \"admin\" || user.role === \"member\";\n\n const recentCounts = await Promise.all(\n projects.map(async (project) => {\n const builds = await new BuildModel(db).list(project.id);\n return { slug: project.slug, count: builds.length, latest: builds[0] ?? null };\n }),\n );\n const countsBySlug = new Map(recentCounts.map((entry) => [entry.slug, entry]));\n\n return (\n <DocumentLayout title=\"Projects\" nav={{ active: \"projects\" }}>\n <div class=\"page-header\">\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">Projects</h1>\n <p class=\"page-header__desc\">Each project is one Storybook. Create a project, then upload builds from CI.</p>\n </div>\n <div class=\"page-header__actions\">\n {canCreate ? (\n <a class=\"btn btn--primary\" href=\"/projects/new\">\n New project\n </a>\n ) : null}\n </div>\n </div>\n </div>\n\n {projects.length === 0 ? (\n <div class=\"empty\">\n <h2 class=\"empty__title\">No projects yet</h2>\n <p class=\"empty__desc\">Create your first project to start visual testing. Projects are free and unlimited.</p>\n {canCreate ? (\n <div class=\"empty__action\">\n <a class=\"btn btn--primary\" href=\"/projects/new\">\n Create project\n </a>\n </div>\n ) : null}\n </div>\n ) : (\n <div class=\"grid\" style=\"gap:.75rem;\">\n {projects.map(\n (project): HtmlEscapedString | Promise<HtmlEscapedString> => {\n const info = countsBySlug.get(project.slug);\n return (\n <div key={project.id} class=\"card card--padded\">\n <div style=\"display:flex; justify-content:space-between; gap:1rem; flex-wrap:wrap; align-items:flex-start;\">\n <div style=\"min-width:0;\">\n <h2 style=\"margin:0; font-size:1.15rem;\">\n <a href={`/projects/${project.slug}/builds`}>{project.name}</a>\n </h2>\n <p class=\"field__hint\" style=\"margin:.2rem 0 0;\">\n <code>{project.slug}</code> {project.gitRepository ? `· ${project.gitRepository}` : \"\"} · default <Badge tone=\"neutral\">{project.gitDefaultBranch}</Badge>\n </p>\n {info?.latest ? (\n <p class=\"field__hint\" style=\"margin:.3rem 0 0;\">\n Latest: {info.latest.gitBranch} · {info.latest.gitSha.slice(0, 7)} · <Badge tone={info.latest.status === \"approved\" ? \"success\" : (info.latest.status === \"reviewing\" ? \"warning\" : \"neutral\")}>{info.latest.status}</Badge>\n </p>\n ) : (\n <p class=\"field__hint\" style=\"margin:.3rem 0 0;\">\n No builds yet.\n </p>\n )}\n </div>\n <div style=\"display:flex; gap:.5rem; flex-wrap:wrap;\">\n <a class=\"btn btn--secondary\" href={`/projects/${project.slug}/builds`}>\n Builds {info?.count ? `(${info.count})` : \"\"}\n </a>\n <a class=\"btn btn--ghost\" href={`/projects/${project.slug}/settings`}>\n Settings\n </a>\n </div>\n </div>\n </div>\n );\n })}\n </div>\n )}\n\n <div class=\"card card--padded\" style=\"margin-top:1rem;\">\n <h3 style=\"margin:0 0 .4rem;\">Next steps</h3>\n <ol style=\"margin:.4rem 0 0; padding-left:1.2rem; color:var(--text-secondary);\">\n <li>\n Create a project (or run <code>npx @storyshelf/cli create --url http://localhost:3000 --name \"My Storybook\" --token $STORYSHELF_ADMIN_TOKEN</code>, or <code>init --url --slug</code> to write <code>.storybook/storyshelf.json</code>)\n </li>\n <li>\n Generate a token in <strong>Settings → Tokens</strong> and set <code>STORYSHELF_TOKEN</code> in CI.\n </li>\n <li>\n Upload: <code>npx @storyshelf/cli upload</code> (or <code>npx @storyshelf/cli</code> defaults to upload when config exists)\n </li>\n </ol>\n </div>\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"projects.mjs","names":[],"sources":["../../src/pages/projects.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { getStore } from \"../store.ts\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { Badge } from \"../ui/components.tsx\";\n\n/** Projects overview page: project cards with latest build plus next steps. */\nexport async function renderProjectsPage(): Promise<RenderedContent> {\n const { db, user } = getStore();\n const projects = await new ProjectModel(db).list();\n const canCreate = !user || user.role === \"admin\" || user.role === \"member\";\n\n const recentCounts = await Promise.all(\n projects.map(async (project) => {\n const builds = await new BuildModel(db).list(project.id);\n return { slug: project.slug, count: builds.length, latest: builds[0] ?? null };\n }),\n );\n const countsBySlug = new Map(recentCounts.map((entry) => [entry.slug, entry]));\n\n return (\n <DocumentLayout title=\"Projects\" nav={{ active: \"projects\" }}>\n <div class=\"page-header\">\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">Projects</h1>\n <p class=\"page-header__desc\">Each project is one Storybook. Create a project, then upload builds from CI.</p>\n </div>\n <div class=\"page-header__actions\">\n {canCreate ? (\n <a class=\"btn btn--primary\" href=\"/projects/new\">\n New project\n </a>\n ) : null}\n </div>\n </div>\n </div>\n\n {projects.length === 0 ? (\n <div class=\"empty\">\n <h2 class=\"empty__title\">No projects yet</h2>\n <p class=\"empty__desc\">Create your first project to start visual testing. Projects are free and unlimited.</p>\n {canCreate ? (\n <div class=\"empty__action\">\n <a class=\"btn btn--primary\" href=\"/projects/new\">\n Create project\n </a>\n </div>\n ) : null}\n </div>\n ) : (\n <div class=\"grid\" style=\"gap:.75rem;\">\n {projects.map(\n (project): HtmlEscapedString | Promise<HtmlEscapedString> => {\n const info = countsBySlug.get(project.slug);\n return (\n <div key={project.id} class=\"card card--padded\">\n <div style=\"display:flex; justify-content:space-between; gap:1rem; flex-wrap:wrap; align-items:flex-start;\">\n <div style=\"min-width:0;\">\n <h2 style=\"margin:0; font-size:1.15rem;\">\n <a href={`/projects/${project.slug}/builds`}>{project.name}</a>\n </h2>\n <p class=\"field__hint\" style=\"margin:.2rem 0 0;\">\n <code>{project.slug}</code> {project.gitRepository ? `· ${project.gitRepository}` : \"\"} · default <Badge tone=\"neutral\">{project.gitDefaultBranch}</Badge>\n </p>\n {info?.latest ? (\n <p class=\"field__hint\" style=\"margin:.3rem 0 0;\">\n Latest: {info.latest.gitBranch} · {info.latest.gitSha.slice(0, 7)} · <Badge tone={info.latest.status === \"approved\" ? \"success\" : (info.latest.status === \"reviewing\" ? \"warning\" : \"neutral\")}>{info.latest.status}</Badge>\n </p>\n ) : (\n <p class=\"field__hint\" style=\"margin:.3rem 0 0;\">\n No builds yet.\n </p>\n )}\n </div>\n <div style=\"display:flex; gap:.5rem; flex-wrap:wrap;\">\n <a class=\"btn btn--secondary\" href={`/projects/${project.slug}/builds`}>\n Builds {info?.count ? `(${info.count})` : \"\"}\n </a>\n <a class=\"btn btn--ghost\" href={`/projects/${project.slug}/settings`}>\n Settings\n </a>\n </div>\n </div>\n </div>\n );\n })}\n </div>\n )}\n\n <div class=\"card card--padded\" style=\"margin-top:1rem;\">\n <h3 style=\"margin:0 0 .4rem;\">Next steps</h3>\n <ol style=\"margin:.4rem 0 0; padding-left:1.2rem; color:var(--text-secondary);\">\n <li>\n Create a project (or run <code>npx @storyshelf/cli create --url http://localhost:3000 --name \"My Storybook\" --token $STORYSHELF_ADMIN_TOKEN</code>, or <code>init --url --slug</code> to write <code>.storybook/storyshelf.json</code>)\n </li>\n <li>\n Generate a token in <strong>Settings → Tokens</strong> and set <code>STORYSHELF_TOKEN</code> in CI.\n </li>\n <li>\n Upload: <code>npx @storyshelf/cli upload</code> (or <code>npx @storyshelf/cli</code> defaults to upload when config exists)\n </li>\n </ol>\n </div>\n </DocumentLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;AAQA,eAAsB,qBAA+C;CACnE,MAAM,EAAE,IAAI,SAAS,SAAS;CAC9B,MAAM,WAAW,MAAM,IAAI,aAAa,EAAE,CAAC,CAAC,KAAK;CACjD,MAAM,YAAY,CAAC,QAAQ,KAAK,SAAS,WAAW,KAAK,SAAS;CAElE,MAAM,eAAe,MAAM,QAAQ,IACjC,SAAS,IAAI,OAAO,YAAY;EAC9B,MAAM,SAAS,MAAM,IAAI,WAAW,EAAE,CAAC,CAAC,KAAK,QAAQ,EAAE;EACvD,OAAO;GAAE,MAAM,QAAQ;GAAM,OAAO,OAAO;GAAQ,QAAQ,OAAO,MAAM;EAAK;CAC/E,CAAC,CACH;CACA,MAAM,eAAe,IAAI,IAAI,aAAa,KAAK,UAAU,CAAC,MAAM,MAAM,KAAK,CAAC,CAAC;CAE7E,OACE,qBAAC,gBAAD;EAAgB,OAAM;EAAW,KAAK,EAAE,QAAQ,WAAW;EAA3D,UAAA;GACE,oBAAC,OAAD;IAAK,OAAM;IACT,UAAA,qBAAC,OAAD;KAAK,OAAM;KAAX,UAAA,CACE,qBAAC,OAAD,EAAA,UAAA,CACE,oBAAC,MAAD;MAAI,OAAM;MAAqB,UAAA;KAAY,CAAA,GAC3C,oBAAC,KAAD;MAAG,OAAM;MAAoB,UAAA;KAA+E,CAAA,CACzG,EAAA,CAAA,GACL,oBAAC,OAAD;MAAK,OAAM;MACR,UAAA,YACC,oBAAC,KAAD;OAAG,OAAM;OAAmB,MAAK;OAAgB,UAAA;MAE9C,CAAA,IACD;KACD,CAAA,CACF;;GACF,CAAA;GAEJ,SAAS,WAAW,IACnB,qBAAC,OAAD;IAAK,OAAM;IAAX,UAAA;KACE,oBAAC,MAAD;MAAI,OAAM;MAAe,UAAA;KAAmB,CAAA;KAC5C,oBAAC,KAAD;MAAG,OAAM;MAAc,UAAA;KAAsF,CAAA;KAC5G,YACC,oBAAC,OAAD;MAAK,OAAM;MACT,UAAA,oBAAC,KAAD;OAAG,OAAM;OAAmB,MAAK;OAAgB,UAAA;MAE9C,CAAA;KACA,CAAA,IACH;IACD;GAEL,CAAA,IAAA,oBAAC,OAAD;IAAK,OAAM;IAAO,OAAM;IACrB,UAAA,SAAS,KACP,YAA4D;KAC7D,MAAM,OAAO,aAAa,IAAI,QAAQ,IAAI;KAC1C,OACE,oBAAC,OAAD;MAAsB,OAAM;MAC1B,UAAA,qBAAC,OAAD;OAAK,OAAM;OAAX,UAAA,CACE,qBAAC,OAAD;QAAK,OAAM;QAAX,UAAA;SACE,oBAAC,MAAD;UAAI,OAAM;UACR,UAAA,oBAAC,KAAD;WAAG,MAAM,aAAa,QAAQ,KAAK;WAAW,UAAA,QAAQ;UAAQ,CAAA;SAC5D,CAAA;SACJ,qBAAC,KAAD;UAAG,OAAM;UAAc,OAAM;UAA7B,UAAA;WACE,oBAAC,QAAD,EAAA,UAAO,QAAQ,KAAW,CAAA;WAAC;WAAE,QAAQ,gBAAgB,KAAK,QAAQ,kBAAkB;WAAG;WAAW,oBAAC,OAAD;YAAO,MAAK;YAAW,UAAA,QAAQ;WAAwB,CAAA;UACxJ;;SACF,MAAM,SACL,qBAAC,KAAD;UAAG,OAAM;UAAc,OAAM;UAA7B,UAAA;WAAiD;WACtC,KAAK,OAAO;WAAU;WAAI,KAAK,OAAO,OAAO,MAAM,GAAG,CAAC;WAAE;WAAG,oBAAC,OAAD;YAAO,MAAM,KAAK,OAAO,WAAW,aAAa,YAAa,KAAK,OAAO,WAAW,cAAc,YAAY;YAAa,UAAA,KAAK,OAAO;WAAc,CAAA;UAC1N;SAEH,CAAA,IAAA,oBAAC,KAAD;UAAG,OAAM;UAAc,OAAM;UAAoB,UAAA;SAE9C,CAAA;QAEF;OACL,CAAA,GAAA,qBAAC,OAAD;QAAK,OAAM;QAAX,UAAA,CACE,qBAAC,KAAD;SAAG,OAAM;SAAqB,MAAM,aAAa,QAAQ,KAAK;SAA9D,UAAA,CAAwE,WAC9D,MAAM,QAAQ,IAAI,KAAK,MAAM,KAAK,EACzC;QACH,CAAA,GAAA,oBAAC,KAAD;SAAG,OAAM;SAAiB,MAAM,aAAa,QAAQ,KAAK;SAAY,UAAA;QAEnE,CAAA,CACA;OACF,CAAA,CAAA;;KACF,GA5BK,QAAQ,EA4Bb;IAET,CAAC;GACE,CAAA;GAGP,qBAAC,OAAD;IAAK,OAAM;IAAoB,OAAM;IAArC,UAAA,CACE,oBAAC,MAAD;KAAI,OAAM;KAAoB,UAAA;IAAc,CAAA,GAC5C,qBAAC,MAAD;KAAI,OAAM;KAAV,UAAA;MACE,qBAAC,MAAD,EAAA,UAAA;OAAI;OACuB,oBAAC,QAAD,EAAA,UAAM,iHAAkH,CAAA;OAAC;OAAK,oBAAC,QAAD,EAAA,UAAM,oBAAuB,CAAA;OAAC;OAAU,oBAAC,QAAD,EAAA,UAAM,6BAAgC,CAAA;OAAC;MACpO,EAAA,CAAA;MACJ,qBAAC,MAAD,EAAA,UAAA;OAAI;OACkB,oBAAC,UAAD,EAAA,UAAQ,oBAAyB,CAAA;OAAC;OAAS,oBAAC,QAAD,EAAA,UAAM,mBAAsB,CAAA;OAAC;MAC1F,EAAA,CAAA;MACJ,qBAAC,MAAD,EAAA,UAAA;OAAI;OACM,oBAAC,QAAD,EAAA,UAAM,6BAAgC,CAAA;OAAC;OAAK,oBAAC,QAAD,EAAA,UAAM,sBAAyB,CAAA;OAAC;MAClF,EAAA,CAAA;KACF;IACD,CAAA,CAAA;;EACS;;AAEpB"}
@@ -4,6 +4,7 @@ import.meta.url;
4
4
  import { DocumentLayout } from "../ui/document.mjs";
5
5
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
6
6
  //#region src/pages/root.tsx
7
+ /** Landing page introducing capture, diff, and review. */
7
8
  function renderRootPage() {
8
9
  return /* @__PURE__ */ jsxs(DocumentLayout, {
9
10
  title: "Welcome",
@@ -1 +1 @@
1
- {"version":3,"file":"root.mjs","names":[],"sources":["../../src/pages/root.tsx"],"sourcesContent":["import { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\n\nexport function renderRootPage(): RenderedContent {\n return (\n <DocumentLayout title=\"Welcome\" nav={{ active: \"projects\" }}>\n <div class=\"card card--padded\" style=\"text-align:center; padding:2rem 1.5rem;\">\n <h1 style=\"margin:0 0 .4rem; font-size:1.9rem; letter-spacing:-0.02em;\">Welcome to StoryShelf</h1>\n <p style=\"color:var(--text-secondary); max-width:60ch; margin:0 auto 1rem;\">\n Self-hosted visual testing for Storybook. Capture every story, diff against baselines, and review changes before they ship.\n </p>\n <div style=\"display:flex; gap:.5rem; justify-content:center; flex-wrap:wrap;\">\n <a class=\"btn btn--primary\" href=\"/projects\">\n View projects\n </a>\n <a class=\"btn btn--secondary\" href=\"/projects/new\">\n Create project\n </a>\n </div>\n <p class=\"field__hint\" style=\"margin-top:1rem;\">\n Tip: use <code>npx @storyshelf/cli create --url &lt;url&gt; --name &lt;name&gt; --token $STORYSHELF_ADMIN_TOKEN</code> to create a project and token for CI, or <code>npx @storyshelf/cli init</code> to write <code>.storybook/storyshelf.json</code>.\n </p>\n </div>\n\n <div class=\"grid grid--3\" style=\"margin-top:1rem;\">\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .3rem;\">Capture</h3>\n <p class=\"field__hint\">Upload your Storybook build. Server renders stories with Playwright — deterministic, no repo cloning.</p>\n </div>\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .3rem;\">Diff</h3>\n <p class=\"field__hint\">Pixel-perfect diff with pixelmatch. Configurable thresholds, overlay images stored on disk.</p>\n </div>\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .3rem;\">Review</h3>\n <p class=\"field__hint\">Per-branch baselines with fallback to default. Approve changes per-story or bulk.</p>\n </div>\n </div>\n </DocumentLayout>\n );\n}\n"],"mappings":";;;;;;AAEA,SAAgB,iBAAkC;CAChD,OACE,qBAAC,gBAAD;EAAgB,OAAM;EAAU,KAAK,EAAE,QAAQ,WAAW;EAA1D,UAAA,CACE,qBAAC,OAAD;GAAK,OAAM;GAAoB,OAAM;GAArC,UAAA;IACE,oBAAC,MAAD;KAAI,OAAM;KAA8D,UAAA;IAAyB,CAAA;IACjG,oBAAC,KAAD;KAAG,OAAM;KAAmE,UAAA;IAEzE,CAAA;IACH,qBAAC,OAAD;KAAK,OAAM;KAAX,UAAA,CACE,oBAAC,KAAD;MAAG,OAAM;MAAmB,MAAK;MAAY,UAAA;KAE1C,CAAA,GACH,oBAAC,KAAD;MAAG,OAAM;MAAqB,MAAK;MAAgB,UAAA;KAEhD,CAAA,CACA;;IACL,qBAAC,KAAD;KAAG,OAAM;KAAc,OAAM;KAA7B,UAAA;MAAgD;MACrC,oBAAC,QAAD,EAAA,UAAM,uFAAsG,CAAA;MAAC;MAA0C,oBAAC,QAAD,EAAA,UAAM,2BAA8B,CAAA;MAAC;MAAU,oBAAC,QAAD,EAAA,UAAM,6BAAgC,CAAA;MAAC;KACrP;;GACA;EAEL,CAAA,GAAA,qBAAC,OAAD;GAAK,OAAM;GAAe,OAAM;GAAhC,UAAA;IACE,qBAAC,OAAD;KAAK,OAAM;KAAX,UAAA,CACE,oBAAC,MAAD;MAAI,OAAM;MAAoB,UAAA;KAAW,CAAA,GACzC,oBAAC,KAAD;MAAG,OAAM;MAAc,UAAA;KAAwG,CAAA,CAC5H;;IACL,qBAAC,OAAD;KAAK,OAAM;KAAX,UAAA,CACE,oBAAC,MAAD;MAAI,OAAM;MAAoB,UAAA;KAAQ,CAAA,GACtC,oBAAC,KAAD;MAAG,OAAM;MAAc,UAAA;KAA8F,CAAA,CAClH;;IACL,qBAAC,OAAD;KAAK,OAAM;KAAX,UAAA,CACE,oBAAC,MAAD;MAAI,OAAM;MAAoB,UAAA;KAAU,CAAA,GACxC,oBAAC,KAAD;MAAG,OAAM;MAAc,UAAA;KAAoF,CAAA,CACxG;;GACF;EACS,CAAA,CAAA;;AAEpB"}
1
+ {"version":3,"file":"root.mjs","names":[],"sources":["../../src/pages/root.tsx"],"sourcesContent":["import { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\n\n/** Landing page introducing capture, diff, and review. */\nexport function renderRootPage(): RenderedContent {\n return (\n <DocumentLayout title=\"Welcome\" nav={{ active: \"projects\" }}>\n <div class=\"card card--padded\" style=\"text-align:center; padding:2rem 1.5rem;\">\n <h1 style=\"margin:0 0 .4rem; font-size:1.9rem; letter-spacing:-0.02em;\">Welcome to StoryShelf</h1>\n <p style=\"color:var(--text-secondary); max-width:60ch; margin:0 auto 1rem;\">\n Self-hosted visual testing for Storybook. Capture every story, diff against baselines, and review changes before they ship.\n </p>\n <div style=\"display:flex; gap:.5rem; justify-content:center; flex-wrap:wrap;\">\n <a class=\"btn btn--primary\" href=\"/projects\">\n View projects\n </a>\n <a class=\"btn btn--secondary\" href=\"/projects/new\">\n Create project\n </a>\n </div>\n <p class=\"field__hint\" style=\"margin-top:1rem;\">\n Tip: use <code>npx @storyshelf/cli create --url &lt;url&gt; --name &lt;name&gt; --token $STORYSHELF_ADMIN_TOKEN</code> to create a project and token for CI, or <code>npx @storyshelf/cli init</code> to write <code>.storybook/storyshelf.json</code>.\n </p>\n </div>\n\n <div class=\"grid grid--3\" style=\"margin-top:1rem;\">\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .3rem;\">Capture</h3>\n <p class=\"field__hint\">Upload your Storybook build. Server renders stories with Playwright — deterministic, no repo cloning.</p>\n </div>\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .3rem;\">Diff</h3>\n <p class=\"field__hint\">Pixel-perfect diff with pixelmatch. Configurable thresholds, overlay images stored on disk.</p>\n </div>\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .3rem;\">Review</h3>\n <p class=\"field__hint\">Per-branch baselines with fallback to default. Approve changes per-story or bulk.</p>\n </div>\n </div>\n </DocumentLayout>\n );\n}\n"],"mappings":";;;;;;;AAGA,SAAgB,iBAAkC;CAChD,OACE,qBAAC,gBAAD;EAAgB,OAAM;EAAU,KAAK,EAAE,QAAQ,WAAW;EAA1D,UAAA,CACE,qBAAC,OAAD;GAAK,OAAM;GAAoB,OAAM;GAArC,UAAA;IACE,oBAAC,MAAD;KAAI,OAAM;KAA8D,UAAA;IAAyB,CAAA;IACjG,oBAAC,KAAD;KAAG,OAAM;KAAmE,UAAA;IAEzE,CAAA;IACH,qBAAC,OAAD;KAAK,OAAM;KAAX,UAAA,CACE,oBAAC,KAAD;MAAG,OAAM;MAAmB,MAAK;MAAY,UAAA;KAE1C,CAAA,GACH,oBAAC,KAAD;MAAG,OAAM;MAAqB,MAAK;MAAgB,UAAA;KAEhD,CAAA,CACA;;IACL,qBAAC,KAAD;KAAG,OAAM;KAAc,OAAM;KAA7B,UAAA;MAAgD;MACrC,oBAAC,QAAD,EAAA,UAAM,uFAAsG,CAAA;MAAC;MAA0C,oBAAC,QAAD,EAAA,UAAM,2BAA8B,CAAA;MAAC;MAAU,oBAAC,QAAD,EAAA,UAAM,6BAAgC,CAAA;MAAC;KACrP;;GACA;EAEL,CAAA,GAAA,qBAAC,OAAD;GAAK,OAAM;GAAe,OAAM;GAAhC,UAAA;IACE,qBAAC,OAAD;KAAK,OAAM;KAAX,UAAA,CACE,oBAAC,MAAD;MAAI,OAAM;MAAoB,UAAA;KAAW,CAAA,GACzC,oBAAC,KAAD;MAAG,OAAM;MAAc,UAAA;KAAwG,CAAA,CAC5H;;IACL,qBAAC,OAAD;KAAK,OAAM;KAAX,UAAA,CACE,oBAAC,MAAD;MAAI,OAAM;MAAoB,UAAA;KAAQ,CAAA,GACtC,oBAAC,KAAD;MAAG,OAAM;MAAc,UAAA;KAA8F,CAAA,CAClH;;IACL,qBAAC,OAAD;KAAK,OAAM;KAAX,UAAA,CACE,oBAAC,MAAD;MAAI,OAAM;MAAoB,UAAA;KAAU,CAAA,GACxC,oBAAC,KAAD;MAAG,OAAM;MAAc,UAAA;KAAoF,CAAA,CACxG;;GACF;EACS,CAAA,CAAA;;AAEpB"}
@@ -3,6 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
5
5
  //#region src/pages/settings-general.tsx
6
+ /** General settings tab: project metadata, diff thresholds, and danger zone. */
6
7
  function renderSettingsGeneral(project, formState, isAdmin) {
7
8
  const errors = formState?.errors ?? {};
8
9
  return /* @__PURE__ */ jsxs("div", {
@@ -1 +1 @@
1
- {"version":3,"file":"settings-general.mjs","names":[],"sources":["../../src/pages/settings-general.tsx"],"sourcesContent":["import type { Project } from \"../schema.ts\";\n\nexport interface SettingsFormState {\n errors?: Record<string, string>;\n globalError?: string;\n}\n\nexport function renderSettingsGeneral(project: Project, formState: SettingsFormState | undefined, isAdmin: boolean): unknown {\n const errors = formState?.errors ?? {};\n return (\n <div class=\"grid\" style=\"max-width: 720px;\">\n {formState?.globalError ? (\n <div class=\"alert alert--danger\" role=\"alert\">\n {formState.globalError}\n </div>\n ) : null}\n <div class=\"card card--padded\">\n <h2 style=\"margin:0 0 .5rem;\">General</h2>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings`}\n hx-post={`/projects/${project.slug}/settings`}\n hx-target=\"body\"\n hx-swap=\"outerHTML\"\n >\n <div class=\"field\">\n <label class=\"field__label\" for=\"name\">\n Name\n </label>\n <input class={`field__input ${errors[\"name\"] ? \"field__input--error\" : \"\"}`} id=\"name\" name=\"name\" value={project.name} />\n {errors[\"name\"] ? (\n <p class=\"field__error\" role=\"alert\">\n {errors[\"name\"]}\n </p>\n ) : null}\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"gitRepository\">\n Git repository\n </label>\n <input class=\"field__input\" id=\"gitRepository\" name=\"gitRepository\" value={project.gitRepository ?? \"\"} placeholder=\"owner/repo\" />\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"gitDefaultBranch\">\n Default branch\n </label>\n <input class=\"field__input\" id=\"gitDefaultBranch\" name=\"gitDefaultBranch\" value={project.gitDefaultBranch} />\n </div>\n <div class=\"grid grid--2\">\n <div class=\"field\">\n <label class=\"field__label\" for=\"pixelThreshold\">\n Pixel threshold\n </label>\n <input class=\"field__input\" id=\"pixelThreshold\" name=\"pixelThreshold\" type=\"number\" step=\"0.01\" min=\"0\" max=\"1\" value={String(project.pixelThreshold)} />\n <p class=\"field__hint\">Per-pixel color distance 0–1</p>\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"maxDiffRatio\">\n Max diff ratio\n </label>\n <input class=\"field__input\" id=\"maxDiffRatio\" name=\"maxDiffRatio\" type=\"number\" step=\"0.001\" min=\"0\" max=\"1\" value={String(project.maxDiffRatio)} />\n <p class=\"field__hint\">Allowed diff ratio 0–1</p>\n </div>\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"publicBranchRegex\">\n Public branch regex\n </label>\n <input class=\"field__input\" id=\"publicBranchRegex\" name=\"publicBranchRegex\" value={project.publicBranchRegex ?? \"\"} placeholder=\"^main$\" />\n <p class=\"field__hint\">Branches matching this regex are publicly viewable.</p>\n </div>\n {isAdmin ? (\n <button class=\"btn btn--primary\" type=\"submit\">\n Save changes\n </button>\n ) : (\n <p class=\"field__hint\">You need admin access to edit settings.</p>\n )}\n </form>\n </div>\n\n {isAdmin ? (\n <div class=\"card card--padded\" style=\"border-color: var(--status-rejected);\">\n <h3 style=\"margin:0 0 .4rem; color: var(--status-rejected);\">Danger zone</h3>\n <p class=\"field__hint\">Deleting a project removes all builds, snapshots, baselines and tokens. This cannot be undone.</p>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/delete`}\n hx-post={`/projects/${project.slug}/delete`}\n hx-target=\"body\"\n hx-confirm=\"Delete this project? This cannot be undone.\"\n >\n <button class=\"btn btn--danger\" type=\"submit\">\n Delete project\n </button>\n </form>\n </div>\n ) : null}\n </div>\n );\n}"],"mappings":"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"}
1
+ {"version":3,"file":"settings-general.mjs","names":[],"sources":["../../src/pages/settings-general.tsx"],"sourcesContent":["import type { Project } from \"../schema.ts\";\n\n/** Form state for the settings tabs (field errors and global error). */\nexport interface SettingsFormState {\n errors?: Record<string, string>;\n globalError?: string;\n}\n\n/** General settings tab: project metadata, diff thresholds, and danger zone. */\nexport function renderSettingsGeneral(project: Project, formState: SettingsFormState | undefined, isAdmin: boolean): unknown {\n const errors = formState?.errors ?? {};\n return (\n <div class=\"grid\" style=\"max-width: 720px;\">\n {formState?.globalError ? (\n <div class=\"alert alert--danger\" role=\"alert\">\n {formState.globalError}\n </div>\n ) : null}\n <div class=\"card card--padded\">\n <h2 style=\"margin:0 0 .5rem;\">General</h2>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings`}\n hx-post={`/projects/${project.slug}/settings`}\n hx-target=\"body\"\n hx-swap=\"outerHTML\"\n >\n <div class=\"field\">\n <label class=\"field__label\" for=\"name\">\n Name\n </label>\n <input class={`field__input ${errors[\"name\"] ? \"field__input--error\" : \"\"}`} id=\"name\" name=\"name\" value={project.name} />\n {errors[\"name\"] ? (\n <p class=\"field__error\" role=\"alert\">\n {errors[\"name\"]}\n </p>\n ) : null}\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"gitRepository\">\n Git repository\n </label>\n <input class=\"field__input\" id=\"gitRepository\" name=\"gitRepository\" value={project.gitRepository ?? \"\"} placeholder=\"owner/repo\" />\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"gitDefaultBranch\">\n Default branch\n </label>\n <input class=\"field__input\" id=\"gitDefaultBranch\" name=\"gitDefaultBranch\" value={project.gitDefaultBranch} />\n </div>\n <div class=\"grid grid--2\">\n <div class=\"field\">\n <label class=\"field__label\" for=\"pixelThreshold\">\n Pixel threshold\n </label>\n <input class=\"field__input\" id=\"pixelThreshold\" name=\"pixelThreshold\" type=\"number\" step=\"0.01\" min=\"0\" max=\"1\" value={String(project.pixelThreshold)} />\n <p class=\"field__hint\">Per-pixel color distance 0–1</p>\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"maxDiffRatio\">\n Max diff ratio\n </label>\n <input class=\"field__input\" id=\"maxDiffRatio\" name=\"maxDiffRatio\" type=\"number\" step=\"0.001\" min=\"0\" max=\"1\" value={String(project.maxDiffRatio)} />\n <p class=\"field__hint\">Allowed diff ratio 0–1</p>\n </div>\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"publicBranchRegex\">\n Public branch regex\n </label>\n <input class=\"field__input\" id=\"publicBranchRegex\" name=\"publicBranchRegex\" value={project.publicBranchRegex ?? \"\"} placeholder=\"^main$\" />\n <p class=\"field__hint\">Branches matching this regex are publicly viewable.</p>\n </div>\n {isAdmin ? (\n <button class=\"btn btn--primary\" type=\"submit\">\n Save changes\n </button>\n ) : (\n <p class=\"field__hint\">You need admin access to edit settings.</p>\n )}\n </form>\n </div>\n\n {isAdmin ? (\n <div class=\"card card--padded\" style=\"border-color: var(--status-rejected);\">\n <h3 style=\"margin:0 0 .4rem; color: var(--status-rejected);\">Danger zone</h3>\n <p class=\"field__hint\">Deleting a project removes all builds, snapshots, baselines and tokens. This cannot be undone.</p>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/delete`}\n hx-post={`/projects/${project.slug}/delete`}\n hx-target=\"body\"\n hx-confirm=\"Delete this project? This cannot be undone.\"\n >\n <button class=\"btn btn--danger\" type=\"submit\">\n Delete project\n </button>\n </form>\n </div>\n ) : null}\n </div>\n );\n}"],"mappings":"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"}
@@ -4,6 +4,7 @@ import.meta.url;
4
4
  import { Badge } from "../ui/components.mjs";
5
5
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
6
6
  //#region src/pages/settings-labels.tsx
7
+ /** Labels settings tab: label-type table plus the create-type form. */
7
8
  function renderSettingsLabels(project, labelTypes, isAdmin) {
8
9
  return /* @__PURE__ */ jsxs("div", {
9
10
  class: "grid",
@@ -1 +1 @@
1
- {"version":3,"file":"settings-labels.mjs","names":[],"sources":["../../src/pages/settings-labels.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\nimport type { LabelType, Project } from \"../schema.ts\";\nimport { Badge } from \"../ui/components.tsx\";\n\nexport function renderSettingsLabels(project: Project, labelTypes: LabelType[], isAdmin: boolean): unknown {\n return (\n <div class=\"grid\" style=\"max-width: 880px;\">\n <div class=\"card card--padded\">\n <h2 style=\"margin:0 0 .3rem;\">Label types</h2>\n <p class=\"field__hint\">Labels attach typed values to builds (e.g. pr=123, jira=ABC-123). Values link out via the template.</p>\n <div class=\"table-wrap\" style=\"margin-top:.75rem;\">\n <table>\n <thead>\n <tr>\n <th>Key</th>\n <th>Name</th>\n <th>Template</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n {labelTypes.map(\n (labelType): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={labelType.id}>\n <td>\n <Badge tone=\"neutral\">{labelType.key}</Badge>\n </td>\n <td>{labelType.name}</td>\n <td style=\"max-width:32ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;\">{labelType.linkTemplate ?? \"—\"}</td>\n <td>\n {isAdmin && labelType.key !== \"persistent\" && labelType.key !== \"branch\" ? (\n <form method=\"post\" action={`/projects/${project.slug}/settings/labels/${labelType.key}/delete`} hx-post={`/projects/${project.slug}/settings/labels/${labelType.key}/delete`} hx-target=\"body\">\n <button class=\"btn btn--ghost\" type=\"submit\" aria-label={`Delete ${labelType.key}`}>\n Delete\n </button>\n </form>\n ) : (\n <span class=\"field__hint\">built-in</span>\n )}\n </td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </div>\n {labelTypes.length === 0 ? <p class=\"field__hint\" style=\"margin-top:.5rem;\">No label types configured.</p> : null}\n </div>\n\n {isAdmin ? (\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .5rem;\">Create label type</h3>\n <form method=\"post\" action={`/projects/${project.slug}/settings/labels`} hx-post={`/projects/${project.slug}/settings/labels`} hx-target=\"body\">\n <div class=\"grid grid--2\">\n <div class=\"field\">\n <label class=\"field__label\" for=\"key\">\n Key\n </label>\n <input class=\"field__input\" id=\"key\" name=\"key\" required placeholder=\"jira\" pattern=\"^[a-z0-9_-]+$\" />\n <p class=\"field__hint\">Lowercase, no spaces.</p>\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"labelName\">\n Name\n </label>\n <input class=\"field__input\" id=\"labelName\" name=\"labelName\" required placeholder=\"Jira issue\" />\n </div>\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"linkTemplate\">\n Link template\n </label>\n <input class=\"field__input\" id=\"linkTemplate\" name=\"linkTemplate\" placeholder=\"https://jira.example.com/browse/{value}\" />\n <p class=\"field__hint\">\n Use {\"{value}\"} placeholder. Optional.\n </p>\n </div>\n <button class=\"btn btn--primary\" type=\"submit\">\n Add label type\n </button>\n </form>\n </div>\n ) : null}\n </div>\n );\n}"],"mappings":"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"}
1
+ {"version":3,"file":"settings-labels.mjs","names":[],"sources":["../../src/pages/settings-labels.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\nimport type { LabelType, Project } from \"../schema.ts\";\nimport { Badge } from \"../ui/components.tsx\";\n\n/** Labels settings tab: label-type table plus the create-type form. */\nexport function renderSettingsLabels(project: Project, labelTypes: LabelType[], isAdmin: boolean): unknown {\n return (\n <div class=\"grid\" style=\"max-width: 880px;\">\n <div class=\"card card--padded\">\n <h2 style=\"margin:0 0 .3rem;\">Label types</h2>\n <p class=\"field__hint\">Labels attach typed values to builds (e.g. pr=123, jira=ABC-123). Values link out via the template.</p>\n <div class=\"table-wrap\" style=\"margin-top:.75rem;\">\n <table>\n <thead>\n <tr>\n <th>Key</th>\n <th>Name</th>\n <th>Template</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n {labelTypes.map(\n (labelType): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={labelType.id}>\n <td>\n <Badge tone=\"neutral\">{labelType.key}</Badge>\n </td>\n <td>{labelType.name}</td>\n <td style=\"max-width:32ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;\">{labelType.linkTemplate ?? \"—\"}</td>\n <td>\n {isAdmin && labelType.key !== \"persistent\" && labelType.key !== \"branch\" ? (\n <form method=\"post\" action={`/projects/${project.slug}/settings/labels/${labelType.key}/delete`} hx-post={`/projects/${project.slug}/settings/labels/${labelType.key}/delete`} hx-target=\"body\">\n <button class=\"btn btn--ghost\" type=\"submit\" aria-label={`Delete ${labelType.key}`}>\n Delete\n </button>\n </form>\n ) : (\n <span class=\"field__hint\">built-in</span>\n )}\n </td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </div>\n {labelTypes.length === 0 ? <p class=\"field__hint\" style=\"margin-top:.5rem;\">No label types configured.</p> : null}\n </div>\n\n {isAdmin ? (\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .5rem;\">Create label type</h3>\n <form method=\"post\" action={`/projects/${project.slug}/settings/labels`} hx-post={`/projects/${project.slug}/settings/labels`} hx-target=\"body\">\n <div class=\"grid grid--2\">\n <div class=\"field\">\n <label class=\"field__label\" for=\"key\">\n Key\n </label>\n <input class=\"field__input\" id=\"key\" name=\"key\" required placeholder=\"jira\" pattern=\"^[a-z0-9_-]+$\" />\n <p class=\"field__hint\">Lowercase, no spaces.</p>\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"labelName\">\n Name\n </label>\n <input class=\"field__input\" id=\"labelName\" name=\"labelName\" required placeholder=\"Jira issue\" />\n </div>\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"linkTemplate\">\n Link template\n </label>\n <input class=\"field__input\" id=\"linkTemplate\" name=\"linkTemplate\" placeholder=\"https://jira.example.com/browse/{value}\" />\n <p class=\"field__hint\">\n Use {\"{value}\"} placeholder. Optional.\n </p>\n </div>\n <button class=\"btn btn--primary\" type=\"submit\">\n Add label type\n </button>\n </form>\n </div>\n ) : null}\n </div>\n );\n}"],"mappings":"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"}
@@ -4,6 +4,7 @@ import.meta.url;
4
4
  import { Badge } from "../ui/components.mjs";
5
5
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
6
6
  //#region src/pages/settings-members.tsx
7
+ /** Members settings tab: member roster plus the add-member form. */
7
8
  function renderSettingsMembers(project, members, isAdmin) {
8
9
  return /* @__PURE__ */ jsxs("div", {
9
10
  class: "grid",
@@ -1 +1 @@
1
- {"version":3,"file":"settings-members.mjs","names":[],"sources":["../../src/pages/settings-members.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\nimport type { Project } from \"../schema.ts\";\nimport { Badge } from \"../ui/components.tsx\";\n\nexport interface SettingsMember {\n id: string;\n projectId: string;\n userId: string;\n role: string;\n createdAt: string;\n}\n\nexport function renderSettingsMembers(project: Project, members: SettingsMember[], isAdmin: boolean): unknown {\n return (\n <div class=\"grid\" style=\"max-width: 880px;\">\n <div class=\"card card--padded\">\n <h2 style=\"margin:0 0 .3rem;\">Members</h2>\n <p class=\"field__hint\">Project members and their roles. Site admins have implicit admin access.</p>\n <div class=\"table-wrap\" style=\"margin-top:.75rem;\">\n <table>\n <thead>\n <tr>\n <th>User</th>\n <th>Role</th>\n <th>Since</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n {members.map(\n (member): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={member.id}>\n <td>\n <code>{member.userId}</code>\n </td>\n <td>\n <Badge tone={member.role === \"admin\" ? \"danger\" : (member.role === \"viewer\" ? \"neutral\" : \"info\")}>{member.role}</Badge>\n </td>\n <td>{new Date(member.createdAt).toLocaleDateString()}</td>\n <td>\n {isAdmin ? (\n <form method=\"post\" action={`/projects/${project.slug}/settings/members/${member.userId}/remove`} hx-post={`/projects/${project.slug}/settings/members/${member.userId}/remove`} hx-target=\"body\">\n <button class=\"btn btn--ghost\" type=\"submit\">\n Remove\n </button>\n </form>\n ) : null}\n </td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </div>\n {members.length === 0 ? <p class=\"field__hint\" style=\"margin-top:.5rem;\">No members yet.</p> : null}\n </div>\n\n {isAdmin ? (\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .5rem;\">Add member</h3>\n <form method=\"post\" action={`/projects/${project.slug}/settings/members`} hx-post={`/projects/${project.slug}/settings/members`} hx-target=\"body\">\n <div class=\"field\">\n <label class=\"field__label\" for=\"userId\">\n User ID\n </label>\n <input class=\"field__input\" id=\"userId\" name=\"userId\" required placeholder=\"user_...\" />\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"role\">\n Role\n </label>\n <select class=\"field__input\" id=\"role\" name=\"role\">\n <option value=\"viewer\">viewer</option>\n <option value=\"developer\">developer</option>\n <option value=\"approver\">approver</option>\n <option value=\"admin\">admin</option>\n </select>\n </div>\n <button class=\"btn btn--primary\" type=\"submit\">\n Add member\n </button>\n </form>\n </div>\n ) : null}\n </div>\n );\n}"],"mappings":"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"}
1
+ {"version":3,"file":"settings-members.mjs","names":[],"sources":["../../src/pages/settings-members.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\nimport type { Project } from \"../schema.ts\";\nimport { Badge } from \"../ui/components.tsx\";\n\n/** Project member row as rendered in the members settings tab. */\nexport interface SettingsMember {\n id: string;\n projectId: string;\n userId: string;\n role: string;\n createdAt: string;\n}\n\n/** Members settings tab: member roster plus the add-member form. */\nexport function renderSettingsMembers(project: Project, members: SettingsMember[], isAdmin: boolean): unknown {\n return (\n <div class=\"grid\" style=\"max-width: 880px;\">\n <div class=\"card card--padded\">\n <h2 style=\"margin:0 0 .3rem;\">Members</h2>\n <p class=\"field__hint\">Project members and their roles. Site admins have implicit admin access.</p>\n <div class=\"table-wrap\" style=\"margin-top:.75rem;\">\n <table>\n <thead>\n <tr>\n <th>User</th>\n <th>Role</th>\n <th>Since</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n {members.map(\n (member): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={member.id}>\n <td>\n <code>{member.userId}</code>\n </td>\n <td>\n <Badge tone={member.role === \"admin\" ? \"danger\" : (member.role === \"viewer\" ? \"neutral\" : \"info\")}>{member.role}</Badge>\n </td>\n <td>{new Date(member.createdAt).toLocaleDateString()}</td>\n <td>\n {isAdmin ? (\n <form method=\"post\" action={`/projects/${project.slug}/settings/members/${member.userId}/remove`} hx-post={`/projects/${project.slug}/settings/members/${member.userId}/remove`} hx-target=\"body\">\n <button class=\"btn btn--ghost\" type=\"submit\">\n Remove\n </button>\n </form>\n ) : null}\n </td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </div>\n {members.length === 0 ? <p class=\"field__hint\" style=\"margin-top:.5rem;\">No members yet.</p> : null}\n </div>\n\n {isAdmin ? (\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .5rem;\">Add member</h3>\n <form method=\"post\" action={`/projects/${project.slug}/settings/members`} hx-post={`/projects/${project.slug}/settings/members`} hx-target=\"body\">\n <div class=\"field\">\n <label class=\"field__label\" for=\"userId\">\n User ID\n </label>\n <input class=\"field__input\" id=\"userId\" name=\"userId\" required placeholder=\"user_...\" />\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"role\">\n Role\n </label>\n <select class=\"field__input\" id=\"role\" name=\"role\">\n <option value=\"viewer\">viewer</option>\n <option value=\"developer\">developer</option>\n <option value=\"approver\">approver</option>\n <option value=\"admin\">admin</option>\n </select>\n </div>\n <button class=\"btn btn--primary\" type=\"submit\">\n Add member\n </button>\n </form>\n </div>\n ) : null}\n </div>\n );\n}"],"mappings":"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"}
@@ -95,6 +95,7 @@ function renderCreateSection(project, providers, formState) {
95
95
  formState
96
96
  });
97
97
  }
98
+ /** Git status settings tab: configured providers plus the add-provider form. */
98
99
  function renderSettingsStatus(project, statusConfigs, providers, isAdmin, formState) {
99
100
  const byProvider = new Map(providers.map((provider) => [provider.metadata.kind, provider]));
100
101
  return /* @__PURE__ */ jsxs("div", {
@@ -1 +1 @@
1
- {"version":3,"file":"settings-status.mjs","names":[],"sources":["../../src/pages/settings-status.tsx"],"sourcesContent":["import type { FC } from \"hono/jsx\";\nimport type { GitHostProvider } from \"../adapters/git-host/index.ts\";\nimport type { Project } from \"../schema.ts\";\nimport { Badge, Field, SelectField, TextareaField } from \"../ui/components.tsx\";\n\nexport interface SettingsStatusConfig {\n id: string;\n provider: string;\n config: Record<string, unknown>;\n hasToken: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface StatusFormState {\n errors?: Record<string, string>;\n globalError?: string;\n}\n\n/* eslint-disable promise-function-async -- JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nfunction providerOptions(providers: GitHostProvider[]): { value: string; label: string }[] {\n return providers.map((provider) => ({ value: provider.metadata.kind, label: provider.metadata.name }));\n}\n\nconst StatusConfigRow: FC<{\n config: SettingsStatusConfig;\n provider: GitHostProvider | undefined;\n project: Project;\n isAdmin: boolean;\n}> = ({ config, provider, project, isAdmin }) => {\n const rendered = JSON.stringify(config.config);\n return (\n <tr key={config.id}>\n <td>{provider?.metadata.name ?? config.provider}</td>\n <td style=\"max-width:32ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;\" title={rendered}>\n <code>{rendered}</code>\n </td>\n <td>{config.hasToken ? <Badge tone=\"success\">configured</Badge> : <Badge tone=\"danger\">missing</Badge>}</td>\n <td>{new Date(config.createdAt).toLocaleDateString()}</td>\n <td>\n {isAdmin ? (\n <form method=\"post\" action={`/projects/${project.slug}/settings/status/${config.id}/delete`} hx-post={`/projects/${project.slug}/settings/status/${config.id}/delete`} hx-target=\"body\">\n <button class=\"btn btn--ghost\" type=\"submit\">\n Delete\n </button>\n </form>\n ) : null}\n </td>\n </tr>\n );\n};\n\nconst StatusCreateCard: FC<{\n project: Project;\n providers: GitHostProvider[];\n formState?: StatusFormState;\n}> = ({ project, providers, formState }) => {\n return (\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .5rem;\">Add git provider</h3>\n <form method=\"post\" action={`/projects/${project.slug}/settings/status`} hx-post={`/projects/${project.slug}/settings/status`} hx-target=\"body\">\n <SelectField label=\"Provider\" name=\"provider\" options={providerOptions(providers)} hint=\"Integration that posts commit statuses for this project.\" />\n <Field label=\"Token\" name=\"token\" type=\"password\" required placeholder=\"ghp_…\" error={formState?.errors?.[\"token\"]} hint=\"Scoped token for the provider (e.g. a GitHub PAT with repo:status).\" />\n <TextareaField\n label=\"Config\"\n name=\"config\"\n rows={6}\n placeholder='{\"owner\":\"my-org\",\"repo\":\"my-repo\"}'\n error={formState?.errors?.[\"config\"]}\n hint=\"JSON configuration for the provider. See the provider documentation for its fields.\"\n />\n <button class=\"btn btn--primary\" type=\"submit\">\n Add git provider\n </button>\n </form>\n </div>\n );\n};\n\nfunction renderCreateSection(project: Project, providers: GitHostProvider[], formState?: StatusFormState): unknown {\n if (providers.length === 0) {\n return <p class=\"field__hint\">No git providers registered on this server.</p>;\n }\n return <StatusCreateCard project={project} providers={providers} formState={formState} />;\n}\n\n// eslint-disable-next-line max-params -- matches sibling settings render functions\nexport function renderSettingsStatus(\n project: Project,\n statusConfigs: SettingsStatusConfig[],\n providers: GitHostProvider[],\n isAdmin: boolean,\n formState?: StatusFormState,\n): unknown {\n const byProvider = new Map(providers.map((provider) => [provider.metadata.kind, provider]));\n return (\n <div class=\"grid\" style=\"max-width: 880px;\">\n {formState?.globalError ? (\n <div class=\"alert alert--danger\" role=\"alert\">\n {formState.globalError}\n </div>\n ) : null}\n\n <div class=\"card card--padded\">\n <h2 style=\"margin:0 0 .3rem;\">Git status</h2>\n <p class=\"field__hint\">\n Post commit statuses to a git provider so visual tests show up in your PR checks. Status is reported for each configured provider: pending while\n capturing, success on approval, failure on rejection or capture errors.\n </p>\n <div class=\"table-wrap\" style=\"margin-top:.75rem;\">\n <table>\n <thead>\n <tr>\n <th>Provider</th>\n <th>Config</th>\n <th>Token</th>\n <th>Created</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n {statusConfigs.map((config) => (\n <StatusConfigRow key={config.id} config={config} provider={byProvider.get(config.provider)} project={project} isAdmin={isAdmin} />\n ))}\n </tbody>\n </table>\n </div>\n {statusConfigs.length === 0 ? <p class=\"field__hint\" style=\"margin-top:.5rem;\">No git providers configured for this project.</p> : null}\n </div>\n\n {isAdmin ? renderCreateSection(project, providers, formState) : null}\n </div>\n );\n}"],"mappings":"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"}
1
+ {"version":3,"file":"settings-status.mjs","names":[],"sources":["../../src/pages/settings-status.tsx"],"sourcesContent":["import type { FC } from \"hono/jsx\";\nimport type { GitHostProvider } from \"../adapters/git-host/index.ts\";\nimport type { Project } from \"../schema.ts\";\nimport { Badge, Field, SelectField, TextareaField } from \"../ui/components.tsx\";\n\n/** Git status configuration row as rendered in the status settings tab. */\nexport interface SettingsStatusConfig {\n id: string;\n provider: string;\n config: Record<string, unknown>;\n hasToken: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\n/** Form state for the git status settings tab. */\nexport interface StatusFormState {\n errors?: Record<string, string>;\n globalError?: string;\n}\n\n/* eslint-disable promise-function-async -- JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nfunction providerOptions(providers: GitHostProvider[]): { value: string; label: string }[] {\n return providers.map((provider) => ({ value: provider.metadata.kind, label: provider.metadata.name }));\n}\n\nconst StatusConfigRow: FC<{\n config: SettingsStatusConfig;\n provider: GitHostProvider | undefined;\n project: Project;\n isAdmin: boolean;\n}> = ({ config, provider, project, isAdmin }) => {\n const rendered = JSON.stringify(config.config);\n return (\n <tr key={config.id}>\n <td>{provider?.metadata.name ?? config.provider}</td>\n <td style=\"max-width:32ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;\" title={rendered}>\n <code>{rendered}</code>\n </td>\n <td>{config.hasToken ? <Badge tone=\"success\">configured</Badge> : <Badge tone=\"danger\">missing</Badge>}</td>\n <td>{new Date(config.createdAt).toLocaleDateString()}</td>\n <td>\n {isAdmin ? (\n <form method=\"post\" action={`/projects/${project.slug}/settings/status/${config.id}/delete`} hx-post={`/projects/${project.slug}/settings/status/${config.id}/delete`} hx-target=\"body\">\n <button class=\"btn btn--ghost\" type=\"submit\">\n Delete\n </button>\n </form>\n ) : null}\n </td>\n </tr>\n );\n};\n\nconst StatusCreateCard: FC<{\n project: Project;\n providers: GitHostProvider[];\n formState?: StatusFormState;\n}> = ({ project, providers, formState }) => {\n return (\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .5rem;\">Add git provider</h3>\n <form method=\"post\" action={`/projects/${project.slug}/settings/status`} hx-post={`/projects/${project.slug}/settings/status`} hx-target=\"body\">\n <SelectField label=\"Provider\" name=\"provider\" options={providerOptions(providers)} hint=\"Integration that posts commit statuses for this project.\" />\n <Field label=\"Token\" name=\"token\" type=\"password\" required placeholder=\"ghp_…\" error={formState?.errors?.[\"token\"]} hint=\"Scoped token for the provider (e.g. a GitHub PAT with repo:status).\" />\n <TextareaField\n label=\"Config\"\n name=\"config\"\n rows={6}\n placeholder='{\"owner\":\"my-org\",\"repo\":\"my-repo\"}'\n error={formState?.errors?.[\"config\"]}\n hint=\"JSON configuration for the provider. See the provider documentation for its fields.\"\n />\n <button class=\"btn btn--primary\" type=\"submit\">\n Add git provider\n </button>\n </form>\n </div>\n );\n};\n\nfunction renderCreateSection(project: Project, providers: GitHostProvider[], formState?: StatusFormState): unknown {\n if (providers.length === 0) {\n return <p class=\"field__hint\">No git providers registered on this server.</p>;\n }\n return <StatusCreateCard project={project} providers={providers} formState={formState} />;\n}\n\n/** Git status settings tab: configured providers plus the add-provider form. */\n// eslint-disable-next-line max-params -- matches sibling settings render functions\nexport function renderSettingsStatus(\n project: Project,\n statusConfigs: SettingsStatusConfig[],\n providers: GitHostProvider[],\n isAdmin: boolean,\n formState?: StatusFormState,\n): unknown {\n const byProvider = new Map(providers.map((provider) => [provider.metadata.kind, provider]));\n return (\n <div class=\"grid\" style=\"max-width: 880px;\">\n {formState?.globalError ? (\n <div class=\"alert alert--danger\" role=\"alert\">\n {formState.globalError}\n </div>\n ) : null}\n\n <div class=\"card card--padded\">\n <h2 style=\"margin:0 0 .3rem;\">Git status</h2>\n <p class=\"field__hint\">\n Post commit statuses to a git provider so visual tests show up in your PR checks. Status is reported for each configured provider: pending while\n capturing, success on approval, failure on rejection or capture errors.\n </p>\n <div class=\"table-wrap\" style=\"margin-top:.75rem;\">\n <table>\n <thead>\n <tr>\n <th>Provider</th>\n <th>Config</th>\n <th>Token</th>\n <th>Created</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n {statusConfigs.map((config) => (\n <StatusConfigRow key={config.id} config={config} provider={byProvider.get(config.provider)} project={project} isAdmin={isAdmin} />\n ))}\n </tbody>\n </table>\n </div>\n {statusConfigs.length === 0 ? <p class=\"field__hint\" style=\"margin-top:.5rem;\">No git providers configured for this project.</p> : null}\n </div>\n\n {isAdmin ? renderCreateSection(project, providers, formState) : null}\n </div>\n );\n}"],"mappings":"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"}
@@ -3,6 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
5
5
  //#region src/pages/settings-tests.tsx
6
+ /** Interaction-tests settings tab: play execution toggle and timeout. */
6
7
  function renderSettingsTests(project, isAdmin, formState) {
7
8
  return /* @__PURE__ */ jsxs("div", {
8
9
  class: "grid",
@@ -1 +1 @@
1
- {"version":3,"file":"settings-tests.mjs","names":[],"sources":["../../src/pages/settings-tests.tsx"],"sourcesContent":["import type { Project } from \"../schema.ts\";\n\nexport function renderSettingsTests(project: Project, isAdmin: boolean, formState?: { globalError?: string }): unknown {\n return (\n <div class=\"grid\" style=\"max-width: 720px;\">\n {formState?.globalError ? (\n <div class=\"alert alert--danger\" role=\"alert\">\n {formState.globalError}\n </div>\n ) : null}\n <div class=\"card card--padded\">\n <h2 style=\"margin:0 0 .5rem;\">Interaction tests</h2>\n <p class=\"field__hint\" style=\"margin-bottom:1rem;\">\n When enabled, Storybook <code>play</code> functions run before each screenshot. Failures block the build unless the story is marked\n <code>flaky-test</code> via <code>tags: ['flaky-test']</code> or\n <code>parameters: &#123; flakyTest: true &#125;</code> (supports both <code>chromatic</code> and <code>storyshelf</code> keys, story-level, case-insensitive). Use{\" \"}\n <code>disableSnapshot</code> to skip a story entirely.\n </p>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/tests`}\n hx-post={`/projects/${project.slug}/settings/tests`}\n hx-target=\"body\"\n hx-swap=\"outerHTML\"\n >\n <div class=\"field\">\n <label class=\"field__label\" style=\"display:flex; gap:.5rem; align-items:center;\">\n <input type=\"checkbox\" name=\"executePlay\" value=\"true\" checked={project.executePlay ? true : undefined} disabled={!isAdmin} />\n Enable interaction tests (play)\n </label>\n <p class=\"field__hint\">When enabled, failing play blocks the build (failed). Flaky stories show warnings but status stays successful.</p>\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"playTimeoutMs\">\n Play timeout (ms)\n </label>\n <input\n class=\"field__input\"\n id=\"playTimeoutMs\"\n name=\"playTimeoutMs\"\n type=\"number\"\n min=\"1000\"\n max=\"30000\"\n step=\"1000\"\n value={String(project.playTimeoutMs)}\n disabled={!isAdmin}\n />\n <p class=\"field__hint\">Timeout for each play function (1000–30000 ms, default 10000).</p>\n </div>\n {isAdmin ? (\n <button class=\"btn btn--primary\" type=\"submit\">\n Save changes\n </button>\n ) : (\n <p class=\"field__hint\">You need admin access to edit settings.</p>\n )}\n </form>\n </div>\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .4rem;\">How to mark stories</h3>\n <pre class=\"field__hint\" style=\"white-space:pre-wrap; background: var(--surface); padding:.75rem; border-radius:.5rem;\">\n {`// Disable snapshot entirely (skip capture + play)\nexport const Hidden: Story = {\n parameters: { storyshelf: { disableSnapshot: true } }\n};\n// Mark as flaky (non-blocking, shows warning)\nexport const Flaky: Story = {\n tags: ['flaky-test'],\n play: async ({canvasElement}) => { /* ... */ }\n};\n// or via parameters (both keys work, storyshelf wins)\nexport const Flaky2: Story = {\n parameters: { chromatic: { flakyTest: true } }\n};`}\n </pre>\n <p class=\"field__hint\">\n Tags are story-level and case-insensitive (<code>flaky-test</code>). Whole story is non-blocking when flaky. GitHub status stays\n <code>success</code> with a warning comment.\n </p>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;AAEA,SAAgB,oBAAoB,SAAkB,SAAkB,WAA+C;CACrH,OACE,qBAAC,OAAD;EAAK,OAAM;EAAO,OAAM;EAAxB,UAAA;GACG,WAAW,cACV,oBAAC,OAAD;IAAK,OAAM;IAAsB,MAAK;IACnC,UAAA,UAAU;GACR,CAAA,IACH;GACJ,qBAAC,OAAD;IAAK,OAAM;IAAX,UAAA;KACE,oBAAC,MAAD;MAAI,OAAM;MAAoB,UAAA;KAAqB,CAAA;KACnD,qBAAC,KAAD;MAAG,OAAM;MAAc,OAAM;MAA7B,UAAA;OAAmD;OACzB,oBAAC,QAAD,EAAA,UAAM,OAAU,CAAA;OAAC;OACzC,oBAAC,QAAD,EAAA,UAAM,aAAgB,CAAA;OAAC;OAAK,oBAAC,QAAD,EAAA,UAAM,uBAA0B,CAAA;OAAC;OAC7D,oBAAC,QAAD,EAAA,UAAM,kCAA+C,CAAA;OAAC;OAAgB,oBAAC,QAAD,EAAA,UAAM,YAAe,CAAA;OAAC;OAAK,oBAAC,QAAD,EAAA,UAAM,aAAgB,CAAA;OAAC;OAA2C;OACnK,oBAAC,QAAD,EAAA,UAAM,kBAAqB,CAAA;OAAC;MAC3B;;KACH,qBAAC,QAAD;MACE,QAAO;MACP,QAAQ,aAAa,QAAQ,KAAK;MAClC,WAAS,aAAa,QAAQ,KAAK;MACnC,aAAU;MACV,WAAQ;MALV,UAAA;OAOE,qBAAC,OAAD;QAAK,OAAM;QAAX,UAAA,CACE,qBAAC,SAAD;SAAO,OAAM;SAAe,OAAM;SAAlC,UAAA,CACE,oBAAC,SAAD;UAAO,MAAK;UAAW,MAAK;UAAc,OAAM;UAAO,SAAS,QAAQ,cAAc,OAAO,KAAA;UAAW,UAAU,CAAC;SAAU,CAAA,GAAC,iCAEzH;QACP,CAAA,GAAA,oBAAC,KAAD;SAAG,OAAM;SAAc,UAAA;QAAiH,CAAA,CACrI;;OACL,qBAAC,OAAD;QAAK,OAAM;QAAX,UAAA;SACE,oBAAC,SAAD;UAAO,OAAM;UAAe,KAAI;UAAgB,UAAA;SAEzC,CAAA;SACP,oBAAC,SAAD;UACE,OAAM;UACN,IAAG;UACH,MAAK;UACL,MAAK;UACL,KAAI;UACJ,KAAI;UACJ,MAAK;UACL,OAAO,OAAO,QAAQ,aAAa;UACnC,UAAU,CAAC;SACZ,CAAA;SACD,oBAAC,KAAD;UAAG,OAAM;UAAc,UAAA;SAAiE,CAAA;QACrF;;OACJ,UACC,oBAAC,UAAD;QAAQ,OAAM;QAAmB,MAAK;QAAS,UAAA;OAEvC,CAAA,IAER,oBAAC,KAAD;QAAG,OAAM;QAAc,UAAA;OAA0C,CAAA;MAE/D;;IACH;;GACL,qBAAC,OAAD;IAAK,OAAM;IAAX,UAAA;KACE,oBAAC,MAAD;MAAI,OAAM;MAAoB,UAAA;KAAuB,CAAA;KACrD,oBAAC,OAAD;MAAK,OAAM;MAAc,OAAM;MAC5B,UAAA;;;;;;;;;;;;;KAaE,CAAA;KACL,qBAAC,KAAD;MAAG,OAAM;MAAT,UAAA;OAAuB;OACsB,oBAAC,QAAD,EAAA,UAAM,aAAgB,CAAA;OAAC;OAClE,oBAAC,QAAD,EAAA,UAAM,UAAa,CAAA;OAAC;MACnB;;IACA;;EACF;;AAET"}
1
+ {"version":3,"file":"settings-tests.mjs","names":[],"sources":["../../src/pages/settings-tests.tsx"],"sourcesContent":["import type { Project } from \"../schema.ts\";\n\n/** Interaction-tests settings tab: play execution toggle and timeout. */\nexport function renderSettingsTests(project: Project, isAdmin: boolean, formState?: { globalError?: string }): unknown {\n return (\n <div class=\"grid\" style=\"max-width: 720px;\">\n {formState?.globalError ? (\n <div class=\"alert alert--danger\" role=\"alert\">\n {formState.globalError}\n </div>\n ) : null}\n <div class=\"card card--padded\">\n <h2 style=\"margin:0 0 .5rem;\">Interaction tests</h2>\n <p class=\"field__hint\" style=\"margin-bottom:1rem;\">\n When enabled, Storybook <code>play</code> functions run before each screenshot. Failures block the build unless the story is marked\n <code>flaky-test</code> via <code>tags: ['flaky-test']</code> or\n <code>parameters: &#123; flakyTest: true &#125;</code> (supports both <code>chromatic</code> and <code>storyshelf</code> keys, story-level, case-insensitive). Use{\" \"}\n <code>disableSnapshot</code> to skip a story entirely.\n </p>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/tests`}\n hx-post={`/projects/${project.slug}/settings/tests`}\n hx-target=\"body\"\n hx-swap=\"outerHTML\"\n >\n <div class=\"field\">\n <label class=\"field__label\" style=\"display:flex; gap:.5rem; align-items:center;\">\n <input type=\"checkbox\" name=\"executePlay\" value=\"true\" checked={project.executePlay ? true : undefined} disabled={!isAdmin} />\n Enable interaction tests (play)\n </label>\n <p class=\"field__hint\">When enabled, failing play blocks the build (failed). Flaky stories show warnings but status stays successful.</p>\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"playTimeoutMs\">\n Play timeout (ms)\n </label>\n <input\n class=\"field__input\"\n id=\"playTimeoutMs\"\n name=\"playTimeoutMs\"\n type=\"number\"\n min=\"1000\"\n max=\"30000\"\n step=\"1000\"\n value={String(project.playTimeoutMs)}\n disabled={!isAdmin}\n />\n <p class=\"field__hint\">Timeout for each play function (1000–30000 ms, default 10000).</p>\n </div>\n {isAdmin ? (\n <button class=\"btn btn--primary\" type=\"submit\">\n Save changes\n </button>\n ) : (\n <p class=\"field__hint\">You need admin access to edit settings.</p>\n )}\n </form>\n </div>\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .4rem;\">How to mark stories</h3>\n <pre class=\"field__hint\" style=\"white-space:pre-wrap; background: var(--surface); padding:.75rem; border-radius:.5rem;\">\n {`// Disable snapshot entirely (skip capture + play)\nexport const Hidden: Story = {\n parameters: { storyshelf: { disableSnapshot: true } }\n};\n// Mark as flaky (non-blocking, shows warning)\nexport const Flaky: Story = {\n tags: ['flaky-test'],\n play: async ({canvasElement}) => { /* ... */ }\n};\n// or via parameters (both keys work, storyshelf wins)\nexport const Flaky2: Story = {\n parameters: { chromatic: { flakyTest: true } }\n};`}\n </pre>\n <p class=\"field__hint\">\n Tags are story-level and case-insensitive (<code>flaky-test</code>). Whole story is non-blocking when flaky. GitHub status stays\n <code>success</code> with a warning comment.\n </p>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;AAGA,SAAgB,oBAAoB,SAAkB,SAAkB,WAA+C;CACrH,OACE,qBAAC,OAAD;EAAK,OAAM;EAAO,OAAM;EAAxB,UAAA;GACG,WAAW,cACV,oBAAC,OAAD;IAAK,OAAM;IAAsB,MAAK;IACnC,UAAA,UAAU;GACR,CAAA,IACH;GACJ,qBAAC,OAAD;IAAK,OAAM;IAAX,UAAA;KACE,oBAAC,MAAD;MAAI,OAAM;MAAoB,UAAA;KAAqB,CAAA;KACnD,qBAAC,KAAD;MAAG,OAAM;MAAc,OAAM;MAA7B,UAAA;OAAmD;OACzB,oBAAC,QAAD,EAAA,UAAM,OAAU,CAAA;OAAC;OACzC,oBAAC,QAAD,EAAA,UAAM,aAAgB,CAAA;OAAC;OAAK,oBAAC,QAAD,EAAA,UAAM,uBAA0B,CAAA;OAAC;OAC7D,oBAAC,QAAD,EAAA,UAAM,kCAA+C,CAAA;OAAC;OAAgB,oBAAC,QAAD,EAAA,UAAM,YAAe,CAAA;OAAC;OAAK,oBAAC,QAAD,EAAA,UAAM,aAAgB,CAAA;OAAC;OAA2C;OACnK,oBAAC,QAAD,EAAA,UAAM,kBAAqB,CAAA;OAAC;MAC3B;;KACH,qBAAC,QAAD;MACE,QAAO;MACP,QAAQ,aAAa,QAAQ,KAAK;MAClC,WAAS,aAAa,QAAQ,KAAK;MACnC,aAAU;MACV,WAAQ;MALV,UAAA;OAOE,qBAAC,OAAD;QAAK,OAAM;QAAX,UAAA,CACE,qBAAC,SAAD;SAAO,OAAM;SAAe,OAAM;SAAlC,UAAA,CACE,oBAAC,SAAD;UAAO,MAAK;UAAW,MAAK;UAAc,OAAM;UAAO,SAAS,QAAQ,cAAc,OAAO,KAAA;UAAW,UAAU,CAAC;SAAU,CAAA,GAAC,iCAEzH;QACP,CAAA,GAAA,oBAAC,KAAD;SAAG,OAAM;SAAc,UAAA;QAAiH,CAAA,CACrI;;OACL,qBAAC,OAAD;QAAK,OAAM;QAAX,UAAA;SACE,oBAAC,SAAD;UAAO,OAAM;UAAe,KAAI;UAAgB,UAAA;SAEzC,CAAA;SACP,oBAAC,SAAD;UACE,OAAM;UACN,IAAG;UACH,MAAK;UACL,MAAK;UACL,KAAI;UACJ,KAAI;UACJ,MAAK;UACL,OAAO,OAAO,QAAQ,aAAa;UACnC,UAAU,CAAC;SACZ,CAAA;SACD,oBAAC,KAAD;UAAG,OAAM;UAAc,UAAA;SAAiE,CAAA;QACrF;;OACJ,UACC,oBAAC,UAAD;QAAQ,OAAM;QAAmB,MAAK;QAAS,UAAA;OAEvC,CAAA,IAER,oBAAC,KAAD;QAAG,OAAM;QAAc,UAAA;OAA0C,CAAA;MAE/D;;IACH;;GACL,qBAAC,OAAD;IAAK,OAAM;IAAX,UAAA;KACE,oBAAC,MAAD;MAAI,OAAM;MAAoB,UAAA;KAAuB,CAAA;KACrD,oBAAC,OAAD;MAAK,OAAM;MAAc,OAAM;MAC5B,UAAA;;;;;;;;;;;;;KAaE,CAAA;KACL,qBAAC,KAAD;MAAG,OAAM;MAAT,UAAA;OAAuB;OACsB,oBAAC,QAAD,EAAA,UAAM,aAAgB,CAAA;OAAC;OAClE,oBAAC,QAAD,EAAA,UAAM,UAAa,CAAA;OAAC;MACnB;;IACA;;EACF;;AAET"}
@@ -3,6 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
5
5
  //#region src/pages/settings-tokens.tsx
6
+ /** Tokens settings tab: project CLI tokens plus the create-token form. */
6
7
  function renderSettingsTokens(project, tokens, isAdmin) {
7
8
  return /* @__PURE__ */ jsxs("div", {
8
9
  class: "grid",
@@ -1 +1 @@
1
- {"version":3,"file":"settings-tokens.mjs","names":[],"sources":["../../src/pages/settings-tokens.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\nimport type { Project, Token } from \"../schema.ts\";\n\nexport function renderSettingsTokens(project: Project, tokens: Omit<Token, \"hash\">[], isAdmin: boolean): unknown {\n return (\n <div class=\"grid\" style=\"max-width: 880px;\">\n <div class=\"card card--padded\">\n <h2 style=\"margin:0 0 .3rem;\">API tokens</h2>\n <p class=\"field__hint\">Tokens are used by the CLI to upload builds. They are scoped to this project.</p>\n <div class=\"table-wrap\" style=\"margin-top:.75rem;\">\n <table>\n <thead>\n <tr>\n <th>Name</th>\n <th>Created</th>\n <th>Last used</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n {tokens.map(\n (token): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={token.id}>\n <td>{token.name}</td>\n <td>{new Date(token.createdAt).toLocaleDateString()}</td>\n <td>{token.lastUsedAt ? new Date(token.lastUsedAt).toLocaleDateString() : \"—\"}</td>\n <td>\n {isAdmin ? (\n <form method=\"post\" action={`/projects/${project.slug}/settings/tokens/${token.id}/delete`} hx-post={`/projects/${project.slug}/settings/tokens/${token.id}/delete`} hx-target=\"body\">\n <button class=\"btn btn--ghost\" type=\"submit\">\n Revoke\n </button>\n </form>\n ) : null}\n </td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </div>\n {tokens.length === 0 ? <p class=\"field__hint\" style=\"margin-top:.5rem;\">No tokens yet. Create one for CI.</p> : null}\n </div>\n\n {isAdmin ? (\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .5rem;\">Create token</h3>\n <form method=\"post\" action={`/projects/${project.slug}/settings/tokens`} hx-post={`/projects/${project.slug}/settings/tokens`} hx-target=\"body\">\n <div class=\"field\">\n <label class=\"field__label\" for=\"tokenName\">\n Name\n </label>\n <input class=\"field__input\" id=\"tokenName\" name=\"tokenName\" required placeholder=\"ci\" />\n </div>\n <button class=\"btn btn--primary\" type=\"submit\">\n Create token\n </button>\n </form>\n <p class=\"field__hint\" style=\"margin-top:.5rem;\">Token value is shown once after creation. Store it securely.</p>\n </div>\n ) : null}\n </div>\n );\n}"],"mappings":";;;;;AAGA,SAAgB,qBAAqB,SAAkB,QAA+B,SAA2B;CAC/G,OACE,qBAAC,OAAD;EAAK,OAAM;EAAO,OAAM;EAAxB,UAAA,CACE,qBAAC,OAAD;GAAK,OAAM;GAAX,UAAA;IACE,oBAAC,MAAD;KAAI,OAAM;KAAoB,UAAA;IAAc,CAAA;IAC5C,oBAAC,KAAD;KAAG,OAAM;KAAc,UAAA;IAAgF,CAAA;IACvG,oBAAC,OAAD;KAAK,OAAM;KAAa,OAAM;KAC5B,UAAA,qBAAC,SAAD,EAAA,UAAA,CACE,oBAAC,SAAD,EAAA,UACE,qBAAC,MAAD,EAAA,UAAA;MACE,oBAAC,MAAD,EAAA,UAAI,OAAQ,CAAA;MACZ,oBAAC,MAAD,EAAA,UAAI,UAAW,CAAA;MACf,oBAAC,MAAD,EAAA,UAAI,YAAa,CAAA;MACjB,oBAAC,MAAD,CAAQ,CAAA;KACN,EAAA,CAAA,EACC,CAAA,GACP,oBAAC,SAAD,EAAA,UACG,OAAO,KACL,UACC,qBAAC,MAAD,EAAA,UAAA;MACE,oBAAC,MAAD,EAAA,UAAK,MAAM,KAAS,CAAA;MACpB,oBAAC,MAAD,EAAA,UAAK,IAAI,KAAK,MAAM,SAAS,CAAC,CAAC,mBAAmB,EAAM,CAAA;MACxD,oBAAC,MAAD,EAAA,UAAK,MAAM,aAAa,IAAI,KAAK,MAAM,UAAU,CAAC,CAAC,mBAAmB,IAAI,IAAQ,CAAA;MAClF,oBAAC,MAAD,EAAA,UACG,UACC,oBAAC,QAAD;OAAM,QAAO;OAAO,QAAQ,aAAa,QAAQ,KAAK,mBAAmB,MAAM,GAAG;OAAU,WAAS,aAAa,QAAQ,KAAK,mBAAmB,MAAM,GAAG;OAAU,aAAU;OAC7K,UAAA,oBAAC,UAAD;QAAQ,OAAM;QAAiB,MAAK;QAAS,UAAA;OAErC,CAAA;MACJ,CAAA,IACJ,KACF,CAAA;KACF,EAAA,GAbK,MAAM,EAaX,CAER,EACK,CAAA,CACF,EAAA,CAAA;IACJ,CAAA;IACJ,OAAO,WAAW,IAAI,oBAAC,KAAD;KAAG,OAAM;KAAc,OAAM;KAAoB,UAAA;IAAoC,CAAA,IAAI;GAC7G;EAEJ,CAAA,GAAA,UACC,qBAAC,OAAD;GAAK,OAAM;GAAX,UAAA;IACE,oBAAC,MAAD;KAAI,OAAM;KAAoB,UAAA;IAAgB,CAAA;IAC9C,qBAAC,QAAD;KAAM,QAAO;KAAO,QAAQ,aAAa,QAAQ,KAAK;KAAmB,WAAS,aAAa,QAAQ,KAAK;KAAmB,aAAU;KAAzI,UAAA,CACE,qBAAC,OAAD;MAAK,OAAM;MAAX,UAAA,CACE,oBAAC,SAAD;OAAO,OAAM;OAAe,KAAI;OAAY,UAAA;MAErC,CAAA,GACP,oBAAC,SAAD;OAAO,OAAM;OAAe,IAAG;OAAY,MAAK;OAAY,UAAA;OAAS,aAAY;MAAM,CAAA,CACpF;KACL,CAAA,GAAA,oBAAC,UAAD;MAAQ,OAAM;MAAmB,MAAK;MAAS,UAAA;KAEvC,CAAA,CACJ;;IACN,oBAAC,KAAD;KAAG,OAAM;KAAc,OAAM;KAAoB,UAAA;IAA+D,CAAA;GAC7G;EACH,CAAA,IAAA,IACD;;AAET"}
1
+ {"version":3,"file":"settings-tokens.mjs","names":[],"sources":["../../src/pages/settings-tokens.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\nimport type { Project, Token } from \"../schema.ts\";\n\n/** Tokens settings tab: project CLI tokens plus the create-token form. */\nexport function renderSettingsTokens(project: Project, tokens: Omit<Token, \"hash\">[], isAdmin: boolean): unknown {\n return (\n <div class=\"grid\" style=\"max-width: 880px;\">\n <div class=\"card card--padded\">\n <h2 style=\"margin:0 0 .3rem;\">API tokens</h2>\n <p class=\"field__hint\">Tokens are used by the CLI to upload builds. They are scoped to this project.</p>\n <div class=\"table-wrap\" style=\"margin-top:.75rem;\">\n <table>\n <thead>\n <tr>\n <th>Name</th>\n <th>Created</th>\n <th>Last used</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n {tokens.map(\n (token): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={token.id}>\n <td>{token.name}</td>\n <td>{new Date(token.createdAt).toLocaleDateString()}</td>\n <td>{token.lastUsedAt ? new Date(token.lastUsedAt).toLocaleDateString() : \"—\"}</td>\n <td>\n {isAdmin ? (\n <form method=\"post\" action={`/projects/${project.slug}/settings/tokens/${token.id}/delete`} hx-post={`/projects/${project.slug}/settings/tokens/${token.id}/delete`} hx-target=\"body\">\n <button class=\"btn btn--ghost\" type=\"submit\">\n Revoke\n </button>\n </form>\n ) : null}\n </td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </div>\n {tokens.length === 0 ? <p class=\"field__hint\" style=\"margin-top:.5rem;\">No tokens yet. Create one for CI.</p> : null}\n </div>\n\n {isAdmin ? (\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .5rem;\">Create token</h3>\n <form method=\"post\" action={`/projects/${project.slug}/settings/tokens`} hx-post={`/projects/${project.slug}/settings/tokens`} hx-target=\"body\">\n <div class=\"field\">\n <label class=\"field__label\" for=\"tokenName\">\n Name\n </label>\n <input class=\"field__input\" id=\"tokenName\" name=\"tokenName\" required placeholder=\"ci\" />\n </div>\n <button class=\"btn btn--primary\" type=\"submit\">\n Create token\n </button>\n </form>\n <p class=\"field__hint\" style=\"margin-top:.5rem;\">Token value is shown once after creation. Store it securely.</p>\n </div>\n ) : null}\n </div>\n );\n}"],"mappings":";;;;;;AAIA,SAAgB,qBAAqB,SAAkB,QAA+B,SAA2B;CAC/G,OACE,qBAAC,OAAD;EAAK,OAAM;EAAO,OAAM;EAAxB,UAAA,CACE,qBAAC,OAAD;GAAK,OAAM;GAAX,UAAA;IACE,oBAAC,MAAD;KAAI,OAAM;KAAoB,UAAA;IAAc,CAAA;IAC5C,oBAAC,KAAD;KAAG,OAAM;KAAc,UAAA;IAAgF,CAAA;IACvG,oBAAC,OAAD;KAAK,OAAM;KAAa,OAAM;KAC5B,UAAA,qBAAC,SAAD,EAAA,UAAA,CACE,oBAAC,SAAD,EAAA,UACE,qBAAC,MAAD,EAAA,UAAA;MACE,oBAAC,MAAD,EAAA,UAAI,OAAQ,CAAA;MACZ,oBAAC,MAAD,EAAA,UAAI,UAAW,CAAA;MACf,oBAAC,MAAD,EAAA,UAAI,YAAa,CAAA;MACjB,oBAAC,MAAD,CAAQ,CAAA;KACN,EAAA,CAAA,EACC,CAAA,GACP,oBAAC,SAAD,EAAA,UACG,OAAO,KACL,UACC,qBAAC,MAAD,EAAA,UAAA;MACE,oBAAC,MAAD,EAAA,UAAK,MAAM,KAAS,CAAA;MACpB,oBAAC,MAAD,EAAA,UAAK,IAAI,KAAK,MAAM,SAAS,CAAC,CAAC,mBAAmB,EAAM,CAAA;MACxD,oBAAC,MAAD,EAAA,UAAK,MAAM,aAAa,IAAI,KAAK,MAAM,UAAU,CAAC,CAAC,mBAAmB,IAAI,IAAQ,CAAA;MAClF,oBAAC,MAAD,EAAA,UACG,UACC,oBAAC,QAAD;OAAM,QAAO;OAAO,QAAQ,aAAa,QAAQ,KAAK,mBAAmB,MAAM,GAAG;OAAU,WAAS,aAAa,QAAQ,KAAK,mBAAmB,MAAM,GAAG;OAAU,aAAU;OAC7K,UAAA,oBAAC,UAAD;QAAQ,OAAM;QAAiB,MAAK;QAAS,UAAA;OAErC,CAAA;MACJ,CAAA,IACJ,KACF,CAAA;KACF,EAAA,GAbK,MAAM,EAaX,CAER,EACK,CAAA,CACF,EAAA,CAAA;IACJ,CAAA;IACJ,OAAO,WAAW,IAAI,oBAAC,KAAD;KAAG,OAAM;KAAc,OAAM;KAAoB,UAAA;IAAoC,CAAA,IAAI;GAC7G;EAEJ,CAAA,GAAA,UACC,qBAAC,OAAD;GAAK,OAAM;GAAX,UAAA;IACE,oBAAC,MAAD;KAAI,OAAM;KAAoB,UAAA;IAAgB,CAAA;IAC9C,qBAAC,QAAD;KAAM,QAAO;KAAO,QAAQ,aAAa,QAAQ,KAAK;KAAmB,WAAS,aAAa,QAAQ,KAAK;KAAmB,aAAU;KAAzI,UAAA,CACE,qBAAC,OAAD;MAAK,OAAM;MAAX,UAAA,CACE,oBAAC,SAAD;OAAO,OAAM;OAAe,KAAI;OAAY,UAAA;MAErC,CAAA,GACP,oBAAC,SAAD;OAAO,OAAM;OAAe,IAAG;OAAY,MAAK;OAAY,UAAA;OAAS,aAAY;MAAM,CAAA,CACpF;KACL,CAAA,GAAA,oBAAC,UAAD;MAAQ,OAAM;MAAmB,MAAK;MAAS,UAAA;KAEvC,CAAA,CACJ;;IACN,oBAAC,KAAD;KAAG,OAAM;KAAc,OAAM;KAAoB,UAAA;IAA+D,CAAA;GAC7G;EACH,CAAA,IAAA,IACD;;AAET"}
@@ -3,6 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
5
5
  //#region src/pages/settings-webhooks.tsx
6
+ /** Webhooks settings tab: webhook list plus the create-webhook form. */
6
7
  function renderSettingsWebhooks(project, webhooks, isAdmin, formState) {
7
8
  return /* @__PURE__ */ jsxs("div", {
8
9
  class: "grid",
@@ -1 +1 @@
1
- {"version":3,"file":"settings-webhooks.mjs","names":[],"sources":["../../src/pages/settings-webhooks.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\nimport type { Project } from \"../schema.ts\";\n\nexport interface SettingsWebhook {\n id: string;\n url: string;\n events: string[];\n}\n\nexport function renderSettingsWebhooks(project: Project, webhooks: SettingsWebhook[], isAdmin: boolean, formState?: { errors?: Record<string, string>; globalError?: string; secret?: string }): unknown {\n return (\n <div class=\"grid\" style=\"max-width: 880px;\">\n {formState?.secret ? (\n <div class=\"alert alert--success\" role=\"alert\">\n <strong class=\"alert__title\">Webhook created</strong>\n <div class=\"alert__body\">\n Copy the signing secret now — shown once: <code>{formState.secret}</code>\n </div>\n </div>\n ) : null}\n {formState?.globalError ? (\n <div class=\"alert alert--danger\" role=\"alert\">\n {formState.globalError}\n </div>\n ) : null}\n\n <div class=\"card card--padded\">\n <h2 style=\"margin:0 0 .3rem;\">Webhooks</h2>\n <p class=\"field__hint\">\n Notify external services when builds are created, updated, approved or rejected. Payloads are POSTed as JSON and signed with the webhook secret.\n </p>\n <div class=\"table-wrap\" style=\"margin-top:.75rem;\">\n <table>\n <thead>\n <tr>\n <th>URL</th>\n <th>Events</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n {webhooks.map(\n (webhook): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={webhook.id}>\n <td style=\"max-width:36ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;\" title={webhook.url}>\n {webhook.url}\n </td>\n <td>\n {webhook.events.length === 0 ? (\n <span class=\"field__hint\">all events</span>\n ) : (\n <div style=\"display:flex; gap:.25rem; flex-wrap:wrap;\">\n {webhook.events.map(\n (event): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <span key={event} class=\"badge badge--neutral\">\n {event}\n </span>\n ),\n )}\n </div>\n )}\n </td>\n <td>\n {isAdmin ? (\n <form method=\"post\" action={`/projects/${project.slug}/settings/webhooks/${webhook.id}/delete`} hx-post={`/projects/${project.slug}/settings/webhooks/${webhook.id}/delete`} hx-target=\"body\">\n <button class=\"btn btn--ghost\" type=\"submit\">\n Delete\n </button>\n </form>\n ) : null}\n </td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </div>\n {webhooks.length === 0 ? <p class=\"field__hint\" style=\"margin-top:.5rem;\">No webhooks configured.</p> : null}\n </div>\n\n {isAdmin ? (\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .5rem;\">Create webhook</h3>\n <form method=\"post\" action={`/projects/${project.slug}/settings/webhooks`} hx-post={`/projects/${project.slug}/settings/webhooks`} hx-target=\"body\">\n <div class=\"field\">\n <label class=\"field__label\" for=\"url\">\n URL\n </label>\n <input class={`field__input ${formState?.errors?.[\"url\"] ? \"field__input--error\" : \"\"}`} id=\"url\" name=\"url\" type=\"url\" required placeholder=\"https://example.com/hooks/storyshelf\" />\n {formState?.errors?.[\"url\"] ? (\n <p class=\"field__error\" role=\"alert\">\n {formState.errors[\"url\"]}\n </p>\n ) : null}\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"events\">\n Events\n </label>\n <input class=\"field__input\" id=\"events\" name=\"events\" placeholder=\"build.created, build.approved, snapshot.reviewed\" />\n <p class=\"field__hint\">Comma-separated. Leave blank to receive all events.</p>\n </div>\n <button class=\"btn btn--primary\" type=\"submit\">\n Add webhook\n </button>\n </form>\n </div>\n ) : null}\n </div>\n );\n}"],"mappings":"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"}
1
+ {"version":3,"file":"settings-webhooks.mjs","names":[],"sources":["../../src/pages/settings-webhooks.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\nimport type { Project } from \"../schema.ts\";\n\n/** Webhook row as rendered in the webhooks settings tab. */\nexport interface SettingsWebhook {\n id: string;\n url: string;\n events: string[];\n}\n\n/** Webhooks settings tab: webhook list plus the create-webhook form. */\nexport function renderSettingsWebhooks(project: Project, webhooks: SettingsWebhook[], isAdmin: boolean, formState?: { errors?: Record<string, string>; globalError?: string; secret?: string }): unknown {\n return (\n <div class=\"grid\" style=\"max-width: 880px;\">\n {formState?.secret ? (\n <div class=\"alert alert--success\" role=\"alert\">\n <strong class=\"alert__title\">Webhook created</strong>\n <div class=\"alert__body\">\n Copy the signing secret now — shown once: <code>{formState.secret}</code>\n </div>\n </div>\n ) : null}\n {formState?.globalError ? (\n <div class=\"alert alert--danger\" role=\"alert\">\n {formState.globalError}\n </div>\n ) : null}\n\n <div class=\"card card--padded\">\n <h2 style=\"margin:0 0 .3rem;\">Webhooks</h2>\n <p class=\"field__hint\">\n Notify external services when builds are created, updated, approved or rejected. Payloads are POSTed as JSON and signed with the webhook secret.\n </p>\n <div class=\"table-wrap\" style=\"margin-top:.75rem;\">\n <table>\n <thead>\n <tr>\n <th>URL</th>\n <th>Events</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n {webhooks.map(\n (webhook): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={webhook.id}>\n <td style=\"max-width:36ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;\" title={webhook.url}>\n {webhook.url}\n </td>\n <td>\n {webhook.events.length === 0 ? (\n <span class=\"field__hint\">all events</span>\n ) : (\n <div style=\"display:flex; gap:.25rem; flex-wrap:wrap;\">\n {webhook.events.map(\n (event): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <span key={event} class=\"badge badge--neutral\">\n {event}\n </span>\n ),\n )}\n </div>\n )}\n </td>\n <td>\n {isAdmin ? (\n <form method=\"post\" action={`/projects/${project.slug}/settings/webhooks/${webhook.id}/delete`} hx-post={`/projects/${project.slug}/settings/webhooks/${webhook.id}/delete`} hx-target=\"body\">\n <button class=\"btn btn--ghost\" type=\"submit\">\n Delete\n </button>\n </form>\n ) : null}\n </td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </div>\n {webhooks.length === 0 ? <p class=\"field__hint\" style=\"margin-top:.5rem;\">No webhooks configured.</p> : null}\n </div>\n\n {isAdmin ? (\n <div class=\"card card--padded\">\n <h3 style=\"margin:0 0 .5rem;\">Create webhook</h3>\n <form method=\"post\" action={`/projects/${project.slug}/settings/webhooks`} hx-post={`/projects/${project.slug}/settings/webhooks`} hx-target=\"body\">\n <div class=\"field\">\n <label class=\"field__label\" for=\"url\">\n URL\n </label>\n <input class={`field__input ${formState?.errors?.[\"url\"] ? \"field__input--error\" : \"\"}`} id=\"url\" name=\"url\" type=\"url\" required placeholder=\"https://example.com/hooks/storyshelf\" />\n {formState?.errors?.[\"url\"] ? (\n <p class=\"field__error\" role=\"alert\">\n {formState.errors[\"url\"]}\n </p>\n ) : null}\n </div>\n <div class=\"field\">\n <label class=\"field__label\" for=\"events\">\n Events\n </label>\n <input class=\"field__input\" id=\"events\" name=\"events\" placeholder=\"build.created, build.approved, snapshot.reviewed\" />\n <p class=\"field__hint\">Comma-separated. Leave blank to receive all events.</p>\n </div>\n <button class=\"btn btn--primary\" type=\"submit\">\n Add webhook\n </button>\n </form>\n </div>\n ) : null}\n </div>\n );\n}"],"mappings":"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"}
@@ -5,14 +5,17 @@ import { StorageAdapter } from "../adapter/storage.mjs";
5
5
  import { Project } from "../schema.mjs";
6
6
  import { Logger } from "pino";
7
7
  //#region src/retention/purge.d.ts
8
+ /** Options controlling which builds a retention purge removes. */
8
9
  interface PurgeOptions {
9
10
  ttlDays: number;
10
11
  keepLatestPerBranch: boolean;
11
12
  }
13
+ /** Counts of builds and files removed by a retention purge. */
12
14
  interface PurgeResult {
13
15
  removedBuilds: number;
14
16
  removedFiles: number;
15
17
  }
18
+ /** Removes expired transient builds while keeping baselines and persistent builds. */
16
19
  declare class Retention {
17
20
  private readonly db;
18
21
  private readonly storage;
@@ -1,12 +1,13 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- import { builds } from "../schema.mjs";
4
+ import { builds } from "../schema-tables.mjs";
5
5
  import { BuildModel } from "../models/build.mjs";
6
6
  import { TERMINAL_BUILD_STATUSES } from "../types.mjs";
7
7
  import { LabelModel } from "../models/label.mjs";
8
8
  import { and, eq, inArray, lt, sql } from "drizzle-orm";
9
9
  //#region src/retention/purge.ts
10
+ /** Removes expired transient builds while keeping baselines and persistent builds. */
10
11
  var Retention = class {
11
12
  db;
12
13
  storage;
@@ -1 +1 @@
1
- {"version":3,"file":"purge.mjs","names":[],"sources":["../../src/retention/purge.ts"],"sourcesContent":["import { and, eq, inArray, lt, sql } from \"drizzle-orm\";\nimport type { Logger } from \"pino\";\n\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { LabelModel } from \"../models/label.ts\";\nimport { builds, type Project } from \"../schema.ts\";\nimport { TERMINAL_BUILD_STATUSES } from \"../types.ts\";\n\nexport interface PurgeOptions {\n ttlDays: number;\n keepLatestPerBranch: boolean;\n}\n\nexport interface PurgeResult {\n removedBuilds: number;\n removedFiles: number;\n}\nexport class Retention {\n constructor(\n private readonly db: DatabaseAdapter,\n private readonly storage: StorageAdapter,\n private readonly logger?: Logger,\n ) {}\n\n\n async purge(project: Project, options: PurgeOptions): Promise<PurgeResult> {\n const cutoff = new Date(Date.now() - options.ttlDays * 86_400_000).toISOString();\n const candidates = await this.db.list(builds, {\n where: and(eq(builds.projectId, project.id), inArray(builds.status, [...TERMINAL_BUILD_STATUSES]), lt(builds.updatedAt, cutoff)),\n });\n\n const keep = options.keepLatestPerBranch ? await this.latestPerBranch(project.id) : new Set<string>();\n const labelModel = new LabelModel(this.db);\n const target = await Promise.all(\n candidates.map(async (build) => {\n if (keep.has(build.id) || (await labelModel.hasPersistent(project.id, build.id))) {\n return null;\n }\n return build.id;\n }),\n );\n\n const buildIds = target.filter((id): id is string => id !== null);\n const results = await Promise.all(\n buildIds.map(async (buildId) => ({\n files: await this.deleteBuildFiles(project.id, buildId),\n removed: await this.removeBuild(buildId),\n })),\n );\n const removedBuilds = results.filter((r) => r.removed).length;\n const removedFiles = results.reduce((sum, r) => sum + r.files, 0);\n this.logger?.info({ projectId: project.id, removedBuilds, removedFiles }, \"build retention purge complete\");\n return { removedBuilds, removedFiles };\n }\n\n private async removeBuild(buildId: string): Promise<boolean> {\n await new BuildModel(this.db).remove(buildId);\n return true;\n }\n\n private async latestPerBranch(projectId: string): Promise<Set<string>> {\n const rows = await this.db.all<{ id: string; gitBranch: string }>(\n sql`SELECT id, gitBranch, createdAt FROM builds WHERE projectId = ${projectId} ORDER BY createdAt DESC`,\n );\n const latest = new Map<string, string>();\n for (const row of rows) {\n const current = latest.get(row.gitBranch);\n if (!current) {\n latest.set(row.gitBranch, row.id);\n }\n }\n return new Set(latest.values());\n }\n\n private async deleteBuildFiles(projectId: string, buildId: string): Promise<number> {\n const prefix = `${projectId}/builds/${buildId}/`;\n const files = await this.storage.list(prefix);\n await Promise.all(\n files.map(async (file) => {\n await this.storage.delete(file);\n }),\n );\n return files.length;\n }\n}\n"],"mappings":";;;;;;;;;AAmBA,IAAa,YAAb,MAAuB;CAEF;CACA;CACA;CAHnB,YACE,IACA,SACA,QACA;EAHiB,KAAA,KAAA;EACA,KAAA,UAAA;EACA,KAAA,SAAA;CAChB;CAGH,MAAM,MAAM,SAAkB,SAA6C;EACzE,MAAM,0BAAS,IAAI,KAAK,KAAK,IAAI,IAAI,QAAQ,UAAU,KAAU,EAAA,CAAE,YAAY;EAC/E,MAAM,aAAa,MAAM,KAAK,GAAG,KAAK,QAAQ,EAC5C,OAAO,IAAI,GAAG,OAAO,WAAW,QAAQ,EAAE,GAAG,QAAQ,OAAO,QAAQ,CAAC,GAAG,uBAAuB,CAAC,GAAG,GAAG,OAAO,WAAW,MAAM,CAAC,EACjI,CAAC;EAED,MAAM,OAAO,QAAQ,sBAAsB,MAAM,KAAK,gBAAgB,QAAQ,EAAE,oBAAI,IAAI,IAAY;EACpG,MAAM,aAAa,IAAI,WAAW,KAAK,EAAE;EAUzC,MAAM,YAAW,MATI,QAAQ,IAC3B,WAAW,IAAI,OAAO,UAAU;GAC9B,IAAI,KAAK,IAAI,MAAM,EAAE,KAAM,MAAM,WAAW,cAAc,QAAQ,IAAI,MAAM,EAAE,GAC5E,OAAO;GAET,OAAO,MAAM;EACf,CAAC,CACH,EAAA,CAEwB,QAAQ,OAAqB,OAAO,IAAI;EAChE,MAAM,UAAU,MAAM,QAAQ,IAC5B,SAAS,IAAI,OAAO,aAAa;GAC/B,OAAO,MAAM,KAAK,iBAAiB,QAAQ,IAAI,OAAO;GACtD,SAAS,MAAM,KAAK,YAAY,OAAO;EACzC,EAAE,CACJ;EACA,MAAM,gBAAgB,QAAQ,QAAQ,MAAM,EAAE,OAAO,CAAC,CAAC;EACvD,MAAM,eAAe,QAAQ,QAAQ,KAAK,MAAM,MAAM,EAAE,OAAO,CAAC;EAChE,KAAK,QAAQ,KAAK;GAAE,WAAW,QAAQ;GAAI;GAAe;EAAa,GAAG,gCAAgC;EAC1G,OAAO;GAAE;GAAe;EAAa;CACvC;CAEA,MAAc,YAAY,SAAmC;EAC3D,MAAM,IAAI,WAAW,KAAK,EAAE,CAAC,CAAC,OAAO,OAAO;EAC5C,OAAO;CACT;CAEA,MAAc,gBAAgB,WAAyC;EACrE,MAAM,OAAO,MAAM,KAAK,GAAG,IACzB,GAAG,iEAAiE,UAAU,yBAChF;EACA,MAAM,yBAAS,IAAI,IAAoB;EACvC,KAAK,MAAM,OAAO,MAEhB,IAAI,CADY,OAAO,IAAI,IAAI,SACpB,GACT,OAAO,IAAI,IAAI,WAAW,IAAI,EAAE;EAGpC,OAAO,IAAI,IAAI,OAAO,OAAO,CAAC;CAChC;CAEA,MAAc,iBAAiB,WAAmB,SAAkC;EAClF,MAAM,SAAS,GAAG,UAAU,UAAU,QAAQ;EAC9C,MAAM,QAAQ,MAAM,KAAK,QAAQ,KAAK,MAAM;EAC5C,MAAM,QAAQ,IACZ,MAAM,IAAI,OAAO,SAAS;GACxB,MAAM,KAAK,QAAQ,OAAO,IAAI;EAChC,CAAC,CACH;EACA,OAAO,MAAM;CACf;AACF"}
1
+ {"version":3,"file":"purge.mjs","names":[],"sources":["../../src/retention/purge.ts"],"sourcesContent":["import { and, eq, inArray, lt, sql } from \"drizzle-orm\";\nimport type { Logger } from \"pino\";\n\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { LabelModel } from \"../models/label.ts\";\nimport { builds } from \"../schema-tables.ts\";\nimport type { Project } from \"../schema.ts\";\nimport { TERMINAL_BUILD_STATUSES } from \"../types.ts\";\n\n/** Options controlling which builds a retention purge removes. */\nexport interface PurgeOptions {\n ttlDays: number;\n keepLatestPerBranch: boolean;\n}\n\n/** Counts of builds and files removed by a retention purge. */\nexport interface PurgeResult {\n removedBuilds: number;\n removedFiles: number;\n}\n/** Removes expired transient builds while keeping baselines and persistent builds. */\nexport class Retention {\n constructor(\n private readonly db: DatabaseAdapter,\n private readonly storage: StorageAdapter,\n private readonly logger?: Logger,\n ) {}\n\n\n async purge(project: Project, options: PurgeOptions): Promise<PurgeResult> {\n const cutoff = new Date(Date.now() - options.ttlDays * 86_400_000).toISOString();\n const candidates = await this.db.list(builds, {\n where: and(eq(builds.projectId, project.id), inArray(builds.status, [...TERMINAL_BUILD_STATUSES]), lt(builds.updatedAt, cutoff)),\n });\n\n const keep = options.keepLatestPerBranch ? await this.latestPerBranch(project.id) : new Set<string>();\n const labelModel = new LabelModel(this.db);\n const target = await Promise.all(\n candidates.map(async (build) => {\n if (keep.has(build.id) || (await labelModel.hasPersistent(project.id, build.id))) {\n return null;\n }\n return build.id;\n }),\n );\n\n const buildIds = target.filter((id): id is string => id !== null);\n const results = await Promise.all(\n buildIds.map(async (buildId) => ({\n files: await this.deleteBuildFiles(project.id, buildId),\n removed: await this.removeBuild(buildId),\n })),\n );\n const removedBuilds = results.filter((r) => r.removed).length;\n const removedFiles = results.reduce((sum, r) => sum + r.files, 0);\n this.logger?.info({ projectId: project.id, removedBuilds, removedFiles }, \"build retention purge complete\");\n return { removedBuilds, removedFiles };\n }\n\n private async removeBuild(buildId: string): Promise<boolean> {\n await new BuildModel(this.db).remove(buildId);\n return true;\n }\n\n private async latestPerBranch(projectId: string): Promise<Set<string>> {\n const rows = await this.db.all<{ id: string; gitBranch: string }>(\n sql`SELECT id, gitBranch, createdAt FROM builds WHERE projectId = ${projectId} ORDER BY createdAt DESC`,\n );\n const latest = new Map<string, string>();\n for (const row of rows) {\n const current = latest.get(row.gitBranch);\n if (!current) {\n latest.set(row.gitBranch, row.id);\n }\n }\n return new Set(latest.values());\n }\n\n private async deleteBuildFiles(projectId: string, buildId: string): Promise<number> {\n const prefix = `${projectId}/builds/${buildId}/`;\n const files = await this.storage.list(prefix);\n await Promise.all(\n files.map(async (file) => {\n await this.storage.delete(file);\n }),\n );\n return files.length;\n }\n}\n"],"mappings":";;;;;;;;;;AAuBA,IAAa,YAAb,MAAuB;CAEF;CACA;CACA;CAHnB,YACE,IACA,SACA,QACA;EAHiB,KAAA,KAAA;EACA,KAAA,UAAA;EACA,KAAA,SAAA;CAChB;CAGH,MAAM,MAAM,SAAkB,SAA6C;EACzE,MAAM,0BAAS,IAAI,KAAK,KAAK,IAAI,IAAI,QAAQ,UAAU,KAAU,EAAA,CAAE,YAAY;EAC/E,MAAM,aAAa,MAAM,KAAK,GAAG,KAAK,QAAQ,EAC5C,OAAO,IAAI,GAAG,OAAO,WAAW,QAAQ,EAAE,GAAG,QAAQ,OAAO,QAAQ,CAAC,GAAG,uBAAuB,CAAC,GAAG,GAAG,OAAO,WAAW,MAAM,CAAC,EACjI,CAAC;EAED,MAAM,OAAO,QAAQ,sBAAsB,MAAM,KAAK,gBAAgB,QAAQ,EAAE,oBAAI,IAAI,IAAY;EACpG,MAAM,aAAa,IAAI,WAAW,KAAK,EAAE;EAUzC,MAAM,YAAW,MATI,QAAQ,IAC3B,WAAW,IAAI,OAAO,UAAU;GAC9B,IAAI,KAAK,IAAI,MAAM,EAAE,KAAM,MAAM,WAAW,cAAc,QAAQ,IAAI,MAAM,EAAE,GAC5E,OAAO;GAET,OAAO,MAAM;EACf,CAAC,CACH,EAAA,CAEwB,QAAQ,OAAqB,OAAO,IAAI;EAChE,MAAM,UAAU,MAAM,QAAQ,IAC5B,SAAS,IAAI,OAAO,aAAa;GAC/B,OAAO,MAAM,KAAK,iBAAiB,QAAQ,IAAI,OAAO;GACtD,SAAS,MAAM,KAAK,YAAY,OAAO;EACzC,EAAE,CACJ;EACA,MAAM,gBAAgB,QAAQ,QAAQ,MAAM,EAAE,OAAO,CAAC,CAAC;EACvD,MAAM,eAAe,QAAQ,QAAQ,KAAK,MAAM,MAAM,EAAE,OAAO,CAAC;EAChE,KAAK,QAAQ,KAAK;GAAE,WAAW,QAAQ;GAAI;GAAe;EAAa,GAAG,gCAAgC;EAC1G,OAAO;GAAE;GAAe;EAAa;CACvC;CAEA,MAAc,YAAY,SAAmC;EAC3D,MAAM,IAAI,WAAW,KAAK,EAAE,CAAC,CAAC,OAAO,OAAO;EAC5C,OAAO;CACT;CAEA,MAAc,gBAAgB,WAAyC;EACrE,MAAM,OAAO,MAAM,KAAK,GAAG,IACzB,GAAG,iEAAiE,UAAU,yBAChF;EACA,MAAM,yBAAS,IAAI,IAAoB;EACvC,KAAK,MAAM,OAAO,MAEhB,IAAI,CADY,OAAO,IAAI,IAAI,SACpB,GACT,OAAO,IAAI,IAAI,WAAW,IAAI,EAAE;EAGpC,OAAO,IAAI,IAAI,OAAO,OAAO,CAAC;CAChC;CAEA,MAAc,iBAAiB,WAAmB,SAAkC;EAClF,MAAM,SAAS,GAAG,UAAU,UAAU,QAAQ;EAC9C,MAAM,QAAQ,MAAM,KAAK,QAAQ,KAAK,MAAM;EAC5C,MAAM,QAAQ,IACZ,MAAM,IAAI,OAAO,SAAS;GACxB,MAAM,KAAK,QAAQ,OAAO,IAAI;EAChC,CAAC,CACH;EACA,OAAO,MAAM;CACf;AACF"}
@@ -20,6 +20,7 @@ const purgeRoute = createRoute({
20
20
  ...forbidden
21
21
  }
22
22
  });
23
+ /** Register the site-admin retention purge endpoint. */
23
24
  function registerAdmin(app) {
24
25
  app.openapi(purgeRoute, async (c) => {
25
26
  requireSiteAdmin();