@jant/core 0.6.14 → 0.6.15

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 (311) hide show
  1. package/bin/commands/import-site.js +81 -13
  2. package/bin/commands/migrate.js +55 -0
  3. package/bin/commands/posts/rebuild-html.js +184 -0
  4. package/bin/commands/site/snapshot/import.js +14 -10
  5. package/bin/lib/d1-query.js +23 -5
  6. package/bin/lib/migration-runner.js +29 -7
  7. package/bin/lib/node-database.js +32 -0
  8. package/bin/lib/site-snapshot.js +347 -46
  9. package/bin/lib/sql-export.js +1 -1
  10. package/bin/lib/thread-collection-migration.js +326 -0
  11. package/dist/app-BxCOR3Uc.js +6 -0
  12. package/dist/{app-2i7-WQHg.js → app-C-oi_t8W.js} +7408 -6343
  13. package/dist/client/.vite/manifest.json +3 -3
  14. package/dist/client/_assets/client-B_eGKN5p.css +2 -0
  15. package/dist/client/_assets/{client-BvID5ucz.js → client-DxY5BFe8.js} +58 -37
  16. package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
  17. package/dist/{export-DWn149b7.js → export-CsFx6F_M.js} +1259 -125
  18. package/dist/{github-sync-Ckk0sJxK.js → github-sync-Cq1pzzOI.js} +1 -1
  19. package/dist/{github-sync-C0zyF6XS.js → github-sync-ppWXN3jb.js} +2 -2
  20. package/dist/index.js +3 -3
  21. package/dist/node.js +5 -5
  22. package/package.json +11 -10
  23. package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
  24. package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
  25. package/src/__tests__/export-hugo-build.test.ts +63 -4
  26. package/src/__tests__/export-import-roundtrip.test.ts +6 -4
  27. package/src/__tests__/export-service.test.ts +142 -9
  28. package/src/__tests__/helpers/app.ts +3 -0
  29. package/src/__tests__/helpers/export-fixtures.ts +2 -0
  30. package/src/__tests__/import-site-command.test.ts +65 -0
  31. package/src/__tests__/vite-config.test.ts +45 -0
  32. package/src/app.tsx +16 -3
  33. package/src/client/__tests__/collection-created-notice.test.ts +36 -0
  34. package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
  35. package/src/client/__tests__/collection-page-actions.test.ts +71 -2
  36. package/src/client/__tests__/compose-bridge.test.ts +64 -0
  37. package/src/client/__tests__/feed-video-player.test.ts +16 -1
  38. package/src/client/__tests__/footnote-rail.test.ts +472 -0
  39. package/src/client/__tests__/hydrate-partial.test.ts +3 -0
  40. package/src/client/__tests__/toast.test.ts +18 -1
  41. package/src/client/collection-created-notice.ts +66 -0
  42. package/src/client/collection-form-bridge.ts +17 -0
  43. package/src/client/collection-navigation.ts +44 -0
  44. package/src/client/collection-page-actions.ts +57 -1
  45. package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
  46. package/src/client/components/__tests__/jant-compose-dialog.test.ts +370 -5
  47. package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
  48. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
  49. package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
  50. package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
  51. package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
  52. package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
  53. package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
  54. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  55. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  56. package/src/client/components/collection-manager-types.ts +9 -3
  57. package/src/client/components/compose-types.ts +8 -0
  58. package/src/client/components/jant-collection-directory.ts +165 -30
  59. package/src/client/components/jant-command-palette.ts +2 -1
  60. package/src/client/components/jant-compose-dialog.ts +85 -21
  61. package/src/client/components/jant-compose-editor.ts +13 -7
  62. package/src/client/components/jant-compose-fullscreen.ts +24 -4
  63. package/src/client/components/jant-config-editor.ts +722 -0
  64. package/src/client/components/jant-media-lightbox.ts +100 -18
  65. package/src/client/components/jant-nav-manager.ts +1283 -54
  66. package/src/client/components/jant-post-form.ts +3 -3
  67. package/src/client/components/jant-post-menu.ts +86 -74
  68. package/src/client/components/nav-manager-types.ts +51 -2
  69. package/src/client/components/post-form-types.ts +1 -1
  70. package/src/client/compose-bridge.ts +4 -1
  71. package/src/client/feed-video-player.ts +62 -2
  72. package/src/client/footnote-rail.ts +478 -0
  73. package/src/client/hydrate-partial.ts +2 -0
  74. package/src/client/media-lightbox-events.ts +63 -0
  75. package/src/client/site-header-fragment.ts +38 -0
  76. package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
  77. package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
  78. package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
  79. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
  80. package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
  81. package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
  82. package/src/client/tiptap/bubble-menu.ts +88 -10
  83. package/src/client/tiptap/continuous-lists.ts +102 -0
  84. package/src/client/tiptap/create-editor.ts +3 -0
  85. package/src/client/tiptap/extensions.ts +14 -5
  86. package/src/client/tiptap/markdown-clipboard.ts +608 -11
  87. package/src/client/tiptap/slash-commands.ts +8 -1
  88. package/src/client/tiptap/structural-keymap.ts +211 -0
  89. package/src/client/tiptap/tab-indent.ts +8 -22
  90. package/src/client/tiptap/table-control-labels.ts +30 -0
  91. package/src/client/tiptap/table-controls.ts +537 -0
  92. package/src/client/tiptap/table-size-picker.ts +254 -0
  93. package/src/client/toast.ts +8 -6
  94. package/src/client-auth.ts +1 -0
  95. package/src/client.ts +1 -0
  96. package/src/db/__tests__/d1-query.test.ts +43 -1
  97. package/src/db/__tests__/migration-runner.test.ts +63 -0
  98. package/src/db/__tests__/migrations.test.ts +469 -0
  99. package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
  100. package/src/db/migrations/0027_old_blue_blade.sql +39 -0
  101. package/src/db/migrations/0028_superb_post.sql +2 -0
  102. package/src/db/migrations/0029_boring_magma.sql +1 -0
  103. package/src/db/migrations/0030_typical_vivisector.sql +50 -0
  104. package/src/db/migrations/meta/0027_snapshot.json +2511 -0
  105. package/src/db/migrations/meta/0028_snapshot.json +2511 -0
  106. package/src/db/migrations/meta/0029_snapshot.json +2519 -0
  107. package/src/db/migrations/meta/0030_snapshot.json +2541 -0
  108. package/src/db/migrations/meta/_journal.json +28 -0
  109. package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
  110. package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
  111. package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
  112. package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
  113. package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
  114. package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
  115. package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
  116. package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
  117. package/src/db/migrations/pg/meta/_journal.json +28 -0
  118. package/src/db/pg/schema.ts +28 -20
  119. package/src/db/rehearsal-fixtures/demo-current.json +39 -4
  120. package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
  121. package/src/db/schema.ts +28 -20
  122. package/src/i18n/__tests__/detect.test.ts +8 -2
  123. package/src/i18n/detect.ts +14 -5
  124. package/src/i18n/locales/public/en.po +133 -30
  125. package/src/i18n/locales/public/en.ts +1 -1
  126. package/src/i18n/locales/public/zh-Hans.po +128 -25
  127. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  128. package/src/i18n/locales/public/zh-Hant.po +128 -25
  129. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  130. package/src/i18n/locales/settings/en.po +513 -10
  131. package/src/i18n/locales/settings/en.ts +1 -1
  132. package/src/i18n/locales/settings/zh-Hans.po +515 -12
  133. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  134. package/src/i18n/locales/settings/zh-Hant.po +515 -12
  135. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  136. package/src/lib/__tests__/api-settings.test.ts +299 -0
  137. package/src/lib/__tests__/constants.test.ts +5 -0
  138. package/src/lib/__tests__/feed-policy.test.ts +25 -0
  139. package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
  140. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
  141. package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
  142. package/src/lib/__tests__/markdown.test.ts +8 -6
  143. package/src/lib/__tests__/navigation.test.ts +59 -2
  144. package/src/lib/__tests__/post-body-html.test.ts +176 -0
  145. package/src/lib/__tests__/post-display.test.ts +64 -0
  146. package/src/lib/__tests__/resolve-config.test.ts +113 -1
  147. package/src/lib/__tests__/summary.test.ts +82 -0
  148. package/src/lib/__tests__/timeline.test.ts +109 -70
  149. package/src/lib/__tests__/timezones.test.ts +31 -3
  150. package/src/lib/__tests__/tiptap-render.test.ts +268 -9
  151. package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
  152. package/src/lib/__tests__/view.test.ts +53 -0
  153. package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
  154. package/src/lib/api-settings.ts +239 -12
  155. package/src/lib/collection-sort.ts +7 -5
  156. package/src/lib/constants.ts +2 -0
  157. package/src/lib/feed-path.ts +25 -0
  158. package/src/lib/feed-policy.ts +64 -0
  159. package/src/lib/footnotes.ts +264 -0
  160. package/src/lib/github-sync-site-config.ts +2 -0
  161. package/src/lib/hugo-markdown.ts +10 -3
  162. package/src/lib/jant-branding.ts +1 -0
  163. package/src/lib/markdown-manager.ts +188 -5
  164. package/src/lib/markdown.ts +10 -3
  165. package/src/lib/navigation.ts +16 -3
  166. package/src/lib/post-body-html.ts +131 -0
  167. package/src/lib/post-display.ts +7 -1
  168. package/src/lib/render.tsx +9 -2
  169. package/src/lib/resolve-config.ts +100 -16
  170. package/src/lib/schemas.ts +133 -1
  171. package/src/lib/settings-paths.ts +5 -0
  172. package/src/lib/site-skill.ts +78 -0
  173. package/src/lib/summary.ts +16 -5
  174. package/src/lib/timeline.ts +98 -122
  175. package/src/lib/timezones.ts +54 -14
  176. package/src/lib/tiptap-render.ts +355 -81
  177. package/src/lib/view.ts +30 -20
  178. package/src/lib/worker-response-cache.ts +11 -18
  179. package/src/middleware/__tests__/public-content-access.test.ts +91 -0
  180. package/src/middleware/config.ts +2 -2
  181. package/src/middleware/public-content-access.ts +87 -0
  182. package/src/node/__tests__/cli-export.test.ts +14 -0
  183. package/src/node/__tests__/cli-migrate.test.ts +6 -0
  184. package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
  185. package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
  186. package/src/node/index.ts +2 -1
  187. package/src/preset.css +16 -2
  188. package/src/routes/__tests__/compose.test.ts +51 -0
  189. package/src/routes/__tests__/site-skill.test.ts +112 -0
  190. package/src/routes/api/__tests__/collections.test.ts +91 -19
  191. package/src/routes/api/__tests__/mcp.test.ts +65 -1
  192. package/src/routes/api/__tests__/nav-items.test.ts +91 -0
  193. package/src/routes/api/__tests__/public-access.test.ts +81 -0
  194. package/src/routes/api/__tests__/settings.test.ts +217 -0
  195. package/src/routes/api/collections.ts +22 -26
  196. package/src/routes/api/export.ts +2 -0
  197. package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
  198. package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
  199. package/src/routes/api/internal/post-body-html.ts +43 -0
  200. package/src/routes/api/internal/sites.ts +24 -0
  201. package/src/routes/api/nav-items.ts +24 -1
  202. package/src/routes/api/posts.ts +2 -2
  203. package/src/routes/api/public/__tests__/archive.test.ts +35 -0
  204. package/src/routes/api/public/__tests__/posts.test.ts +208 -0
  205. package/src/routes/api/public/archive.ts +4 -0
  206. package/src/routes/api/public/posts.ts +41 -24
  207. package/src/routes/api/search.ts +3 -0
  208. package/src/routes/api/settings.ts +33 -0
  209. package/src/routes/auth/__tests__/setup.test.ts +7 -4
  210. package/src/routes/compose.tsx +2 -1
  211. package/src/routes/dash/settings.tsx +45 -2
  212. package/src/routes/feed/__tests__/feed.test.ts +210 -6
  213. package/src/routes/feed/feed.ts +23 -14
  214. package/src/routes/pages/__tests__/archive-params.test.ts +92 -3
  215. package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
  216. package/src/routes/pages/__tests__/collections.test.ts +9 -9
  217. package/src/routes/pages/__tests__/preview.test.ts +174 -0
  218. package/src/routes/pages/__tests__/search.test.ts +69 -0
  219. package/src/routes/pages/archive.tsx +27 -7
  220. package/src/routes/pages/collection.tsx +32 -19
  221. package/src/routes/pages/collections.tsx +6 -1
  222. package/src/routes/pages/page.tsx +83 -11
  223. package/src/routes/site-skill.ts +18 -0
  224. package/src/services/__tests__/auth.test.ts +7 -5
  225. package/src/services/__tests__/collection.test.ts +159 -44
  226. package/src/services/__tests__/navigation.test.ts +229 -21
  227. package/src/services/__tests__/post-timeline.test.ts +109 -30
  228. package/src/services/__tests__/post.test.ts +567 -5
  229. package/src/services/__tests__/search.test.ts +48 -1
  230. package/src/services/__tests__/settings.test.ts +8 -0
  231. package/src/services/__tests__/site-admin.test.ts +121 -1
  232. package/src/services/auth.ts +2 -2
  233. package/src/services/bootstrap.ts +3 -2
  234. package/src/services/collection.ts +226 -167
  235. package/src/services/export-theme/assets/client-site.css +1 -1
  236. package/src/services/export-theme/assets/client-site.js +46 -25
  237. package/src/services/export-theme/layouts/_default/list.html +18 -4
  238. package/src/services/export-theme/layouts/_default/rss.xml +8 -12
  239. package/src/services/export-theme/layouts/featured/list.html +5 -4
  240. package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
  241. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  242. package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
  243. package/src/services/export-theme/styles/main.css +30 -2
  244. package/src/services/export.ts +91 -80
  245. package/src/services/mcp.ts +37 -27
  246. package/src/services/media.ts +1 -1
  247. package/src/services/navigation.ts +185 -46
  248. package/src/services/post.ts +874 -271
  249. package/src/services/search.ts +8 -2
  250. package/src/services/settings.ts +2 -10
  251. package/src/services/site-admin.ts +63 -43
  252. package/src/style-cjk-jp.css +1 -1
  253. package/src/style-cjk-kr.css +1 -1
  254. package/src/style-cjk-tc.css +1 -1
  255. package/src/style-cjk.css +1 -1
  256. package/src/styles/components.css +540 -0
  257. package/src/styles/site-media.css +111 -19
  258. package/src/styles/tokens.css +14 -9
  259. package/src/styles/ui.css +829 -29
  260. package/src/types/bindings.ts +3 -0
  261. package/src/types/config.ts +243 -12
  262. package/src/types/constants.ts +22 -1
  263. package/src/types/entities.ts +9 -8
  264. package/src/types/operations.ts +18 -10
  265. package/src/types/props.ts +8 -2
  266. package/src/types/raw-assets.d.ts +5 -0
  267. package/src/types/views.ts +16 -4
  268. package/src/ui/__tests__/font-themes.test.ts +42 -30
  269. package/src/ui/compose/ComposeDialog.tsx +92 -2
  270. package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
  271. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
  272. package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
  273. package/src/ui/dash/settings/GeneralContent.tsx +15 -5
  274. package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
  275. package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
  276. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
  277. package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
  278. package/src/ui/feed/LinkCard.tsx +2 -12
  279. package/src/ui/feed/NoteCard.tsx +10 -20
  280. package/src/ui/feed/QuoteCard.tsx +2 -12
  281. package/src/ui/feed/ThreadPreview.tsx +26 -34
  282. package/src/ui/feed/TimelineFeed.tsx +2 -2
  283. package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
  284. package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
  285. package/src/ui/feed/thread-preview-state.ts +5 -7
  286. package/src/ui/font-themes.ts +74 -26
  287. package/src/ui/layouts/BaseLayout.tsx +22 -8
  288. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
  289. package/src/ui/pages/CollectionPage.tsx +101 -22
  290. package/src/ui/pages/CollectionsPage.tsx +3 -1
  291. package/src/ui/pages/PostPage.tsx +39 -12
  292. package/src/ui/pages/SearchPage.tsx +6 -14
  293. package/src/ui/pages/__tests__/ArchivePage.test.tsx +7 -0
  294. package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
  295. package/src/ui/shared/CollectionDirectory.tsx +13 -9
  296. package/src/ui/shared/CollectionsManager.tsx +17 -7
  297. package/src/ui/shared/DraftPreviewBar.tsx +33 -0
  298. package/src/ui/shared/HomePageBranding.tsx +2 -2
  299. package/src/ui/shared/MediaGallery.tsx +1 -0
  300. package/src/ui/shared/PostFooter.tsx +3 -1
  301. package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
  302. package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
  303. package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
  304. package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
  305. package/src/ui/shared/collection-management-labels.ts +33 -2
  306. package/src/ui/shared/navigation-labels.ts +2 -4
  307. package/src/ui/shared/post-article-attributes.ts +46 -0
  308. package/dist/app-DZDlmh7C.js +0 -6
  309. package/dist/client/_assets/client-Cevfpm8H.css +0 -2
  310. package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
  311. package/src/client/tiptap/wrapping-input-rules.ts +0 -102
@@ -8,6 +8,9 @@ import { createPostService } from "../post.js";
8
8
  import type { Database } from "../../db/index.js";
9
9
  import type { RawQueryClient } from "../../db/raw-query.js";
10
10
  import type BetterSqlite3 from "better-sqlite3";
11
+ import { eq } from "drizzle-orm";
12
+ import { posts } from "../../db/schema.js";
13
+ import { POST_BODY_HTML_VERSION } from "../../lib/post-body-html.js";
11
14
 
12
15
  /** Wraps plain text in a minimal valid TipTap JSON document. */
13
16
  function tiptapDoc(text: string): string {
@@ -37,11 +40,11 @@ function createSearchRow(overrides?: Partial<Record<string, unknown>>) {
37
40
  url: null,
38
41
  body: tiptapDoc("Hello world"),
39
42
  body_html: "<p>Hello world</p>",
43
+ body_html_version: POST_BODY_HTML_VERSION,
40
44
  body_text: "Hello world",
41
45
  quote_text: null,
42
46
  summary: null,
43
47
  rating: null,
44
- collection_id: null,
45
48
  reply_to_id: null,
46
49
  thread_id: "pst_01jpyz2pvf4m7s2k8r5c9t0qce",
47
50
  published_at: 1774009100,
@@ -138,6 +141,50 @@ describe("SearchService", () => {
138
141
  expect(results[0]?.post.title).toContain("TypeScript");
139
142
  });
140
143
 
144
+ it("resolves stale body HTML on the raw search read path", async () => {
145
+ const body = JSON.stringify({
146
+ type: "doc",
147
+ content: [
148
+ {
149
+ type: "paragraph",
150
+ content: [
151
+ { type: "text", text: "semanticneedle" },
152
+ { type: "footnoteReference", attrs: { label: "1" } },
153
+ ],
154
+ },
155
+ {
156
+ type: "footnoteDefinition",
157
+ attrs: { label: "1" },
158
+ content: [
159
+ {
160
+ type: "paragraph",
161
+ content: [{ type: "text", text: "Definition" }],
162
+ },
163
+ ],
164
+ },
165
+ ],
166
+ });
167
+ const post = await postService.create({ format: "note", body });
168
+ await db
169
+ .update(posts)
170
+ .set({
171
+ bodyHtml: '<span class="sidenote">legacy</span>',
172
+ bodyHtmlVersion: 1,
173
+ })
174
+ .where(eq(posts.id, post.id));
175
+
176
+ const searchService = createSearchService(
177
+ createMockD1(sqlite),
178
+ DEFAULT_TEST_SITE_ID,
179
+ );
180
+ const results = await searchService.search("semanticneedle");
181
+
182
+ expect(results[0]?.post.bodyHtml).toContain('role="doc-noteref"');
183
+ expect(results[0]?.post.bodyHtml).toMatch(/id="fn-[a-z0-9]{13}-1"/);
184
+ expect(results[0]?.post.bodyHtml).not.toContain(post.id);
185
+ expect(results[0]?.post.bodyHtml).not.toContain("legacy");
186
+ });
187
+
141
188
  it("respects status filter", async () => {
142
189
  await postService.create({
143
190
  format: "note",
@@ -50,6 +50,14 @@ describe("SettingsService", () => {
50
50
  expect(result).toBe("Asia/Shanghai");
51
51
  });
52
52
 
53
+ it("preserves runtime-supported IANA zones and fixed offsets", async () => {
54
+ await settingsService.set("TIME_ZONE", "America/Phoenix");
55
+ expect(await settingsService.get("TIME_ZONE")).toBe("America/Phoenix");
56
+
57
+ await settingsService.set("TIME_ZONE", "+08");
58
+ expect(await settingsService.get("TIME_ZONE")).toBe("+08:00");
59
+ });
60
+
53
61
  it("rejects unsupported timezone values", async () => {
54
62
  await expect(settingsService.set("TIME_ZONE", "+8")).rejects.toThrowError(
55
63
  "Choose a valid time zone.",
@@ -44,10 +44,68 @@ describe("SiteAdminService", () => {
44
44
  .all(created.site.id) as { key: string }[];
45
45
 
46
46
  expect(siteRows).toEqual([{ key: "demo-cloud" }]);
47
+
48
+ const navigationRows = sqlite
49
+ .prepare(
50
+ 'SELECT "system_key" AS systemKey, "placement" FROM "nav_item" WHERE "site_id" = ? ORDER BY "position"',
51
+ )
52
+ .all(created.site.id) as { systemKey: string; placement: string }[];
53
+
54
+ expect(navigationRows).toEqual([
55
+ { systemKey: "featured", placement: "header" },
56
+ { systemKey: "collections", placement: "header" },
57
+ { systemKey: "archive", placement: "header" },
58
+ { systemKey: "rss", placement: "more" },
59
+ { systemKey: "settings", placement: "more" },
60
+ ]);
61
+ });
62
+
63
+ it("preserves valid browser timezones during managed site creation", async () => {
64
+ const { db, sqlite } = createTestDatabase();
65
+ const service = createSiteAdminService(db, sqliteSchemaBundle, "sqlite", {
66
+ siteResolutionMode: "host-based",
67
+ });
68
+
69
+ const created = await service.createManagedSite({
70
+ key: "timezone-site",
71
+ primaryHost: "timezone-site.example.com",
72
+ siteName: "Timezone Site",
73
+ timeZone: "Etc/GMT-8",
74
+ });
75
+
76
+ const timeZone = sqlite
77
+ .prepare(
78
+ 'SELECT "value" FROM "site_setting" WHERE "site_id" = ? AND "key" = \'TIME_ZONE\'',
79
+ )
80
+ .get(created.site.id) as { value: string } | undefined;
81
+
82
+ expect(timeZone?.value).toBe("Etc/GMT-8");
83
+ });
84
+
85
+ it("falls back to UTC when optional provisioning timezone metadata is invalid", async () => {
86
+ const { db, sqlite } = createTestDatabase();
87
+ const service = createSiteAdminService(db, sqliteSchemaBundle, "sqlite", {
88
+ siteResolutionMode: "host-based",
89
+ });
90
+
91
+ const created = await service.createManagedSite({
92
+ key: "fallback-site",
93
+ primaryHost: "fallback-site.example.com",
94
+ siteName: "Fallback Site",
95
+ timeZone: "Unknown/Zone",
96
+ });
97
+
98
+ const timeZone = sqlite
99
+ .prepare(
100
+ 'SELECT "value" FROM "site_setting" WHERE "site_id" = ? AND "key" = \'TIME_ZONE\'',
101
+ )
102
+ .get(created.site.id) as { value: string } | undefined;
103
+
104
+ expect(timeZone).toBeUndefined();
47
105
  });
48
106
 
49
107
  it("returns the existing site when replayed with the same idempotency key", async () => {
50
- const { db } = createTestDatabase();
108
+ const { db, sqlite } = createTestDatabase();
51
109
  const service = createSiteAdminService(db, sqliteSchemaBundle, "sqlite", {
52
110
  siteResolutionMode: "host-based",
53
111
  });
@@ -59,6 +117,12 @@ describe("SiteAdminService", () => {
59
117
  idempotencyKey: "job_abc",
60
118
  });
61
119
 
120
+ sqlite
121
+ .prepare(
122
+ 'DELETE FROM "nav_item" WHERE "site_id" = ? AND "system_key" = \'featured\'',
123
+ )
124
+ .run(first.site.id);
125
+
62
126
  const second = await service.createManagedSite({
63
127
  key: "idem-site",
64
128
  primaryHost: "idem-site.example.com",
@@ -68,6 +132,62 @@ describe("SiteAdminService", () => {
68
132
 
69
133
  expect(second.site.id).toBe(first.site.id);
70
134
  expect(second.domain.id).toBe(first.domain.id);
135
+ const featuredCount = sqlite
136
+ .prepare(
137
+ 'SELECT COUNT(*) AS count FROM "nav_item" WHERE "site_id" = ? AND "system_key" = \'featured\'',
138
+ )
139
+ .get(first.site.id) as { count: number };
140
+ expect(featuredCount.count).toBe(0);
141
+ });
142
+
143
+ it("recovers an incomplete idempotent provisioning attempt", async () => {
144
+ const { db, sqlite } = createTestDatabase();
145
+ const service = createSiteAdminService(db, sqliteSchemaBundle, "sqlite", {
146
+ siteResolutionMode: "host-based",
147
+ });
148
+
149
+ const first = await service.createManagedSite({
150
+ key: "recovery-site",
151
+ primaryHost: "recovery-site.example.com",
152
+ siteName: "Recovery Site",
153
+ idempotencyKey: "job_recovery",
154
+ });
155
+ sqlite
156
+ .prepare('DELETE FROM "nav_item" WHERE "site_id" = ?')
157
+ .run(first.site.id);
158
+ sqlite
159
+ .prepare(
160
+ 'DELETE FROM "site_setting" WHERE "site_id" = ? AND "key" = \'ONBOARDING_STATUS\'',
161
+ )
162
+ .run(first.site.id);
163
+
164
+ const recovered = await service.createManagedSite({
165
+ key: "recovery-site",
166
+ primaryHost: "recovery-site.example.com",
167
+ siteName: "Recovery Site",
168
+ idempotencyKey: "job_recovery",
169
+ });
170
+
171
+ const navigationRows = sqlite
172
+ .prepare(
173
+ 'SELECT "system_key" AS systemKey FROM "nav_item" WHERE "site_id" = ? ORDER BY "position"',
174
+ )
175
+ .all(first.site.id) as { systemKey: string }[];
176
+ const onboarding = sqlite
177
+ .prepare(
178
+ 'SELECT "value" FROM "site_setting" WHERE "site_id" = ? AND "key" = \'ONBOARDING_STATUS\'',
179
+ )
180
+ .get(first.site.id) as { value: string };
181
+
182
+ expect(recovered.site.id).toBe(first.site.id);
183
+ expect(navigationRows.map((row) => row.systemKey)).toEqual([
184
+ "featured",
185
+ "collections",
186
+ "archive",
187
+ "rss",
188
+ "settings",
189
+ ]);
190
+ expect(onboarding.value).toBe("completed");
71
191
  });
72
192
 
73
193
  it("rejects reuse of an idempotency key with different key or primary host", async () => {
@@ -97,7 +97,7 @@ export function createAuthService(
97
97
  verification,
98
98
  media,
99
99
  collections,
100
- postCollections,
100
+ threadCollections,
101
101
  pathRegistry,
102
102
  collectionDirectoryItems: directoryItemsTable,
103
103
  navItems,
@@ -111,7 +111,7 @@ export function createAuthService(
111
111
 
112
112
  async function deleteDataRows(targetDb: Database): Promise<void> {
113
113
  // Junction/dependent tables first
114
- await targetDb.delete(postCollections);
114
+ await targetDb.delete(threadCollections);
115
115
  await targetDb.delete(pathRegistry);
116
116
  await targetDb.delete(directoryItemsTable);
117
117
  await targetDb.delete(media);
@@ -31,7 +31,8 @@ export interface CompleteInitialSetupData {
31
31
  export interface BootstrapService {
32
32
  /**
33
33
  * Complete first-run setup for a newly created account.
34
- * Ensures default system navigation exists and marks onboarding complete last.
34
+ * Materializes the versioned navigation profile and marks onboarding complete
35
+ * last so an interrupted setup can recover safely.
35
36
  *
36
37
  * @param data - Initial site shell values captured during setup
37
38
  */
@@ -58,7 +59,7 @@ export function createBootstrapService(
58
59
  const siteMembers = createSiteMemberService(db, databaseSchema);
59
60
 
60
61
  await siteMembers.ensure(site.id, data.ownerUserId, "owner");
61
- await navItems.ensureSystemDefaults();
62
+ await navItems.materializeDefaultNavigation();
62
63
  await settings.set("SITE_NAME", data.siteName.trim());
63
64
  await settings.set("TIME_ZONE", data.timeZone ?? "UTC");
64
65
  const siteLanguage =