@jant/core 0.3.45 → 0.3.47

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 (114) hide show
  1. package/bin/commands/db/execute-file.js +12 -4
  2. package/bin/commands/db/rehearse.js +2 -2
  3. package/bin/commands/export.js +12 -4
  4. package/bin/commands/import-site.js +99 -305
  5. package/bin/commands/migrate.js +36 -69
  6. package/bin/commands/reset-password.js +10 -4
  7. package/bin/commands/site/export.js +59 -248
  8. package/bin/commands/site/snapshot/export.js +58 -45
  9. package/bin/commands/site/snapshot/import.js +104 -52
  10. package/bin/lib/node-env.js +100 -0
  11. package/bin/lib/runtime-target.js +64 -0
  12. package/bin/lib/site-snapshot.js +185 -54
  13. package/bin/lib/sql-export.js +19 -2
  14. package/dist/{app-C-L7wL6o.js → app-3REcR-3U.js} +332 -190
  15. package/dist/app-B67XOEyo.js +6 -0
  16. package/dist/client/.vite/manifest.json +2 -2
  17. package/dist/client/_assets/{client-auth-Dcon89Av.js → client-auth-Ce5WEAVS.js} +236 -183
  18. package/dist/client/_assets/client-s71Js1Cu.css +2 -0
  19. package/dist/{github-sync-CQ1x271f.js → export-ZBlfKSKm.js} +12 -439
  20. package/dist/github-sync-C593r22F.js +4 -0
  21. package/dist/github-sync-bL1hnx3Q.js +428 -0
  22. package/dist/index.js +3 -2
  23. package/dist/node.js +5 -4
  24. package/package.json +3 -2
  25. package/src/__tests__/helpers/export-fixtures.ts +0 -1
  26. package/src/__tests__/import-site-command.test.ts +18 -0
  27. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -0
  28. package/src/client/components/__tests__/jant-settings-general.test.ts +70 -0
  29. package/src/client/components/jant-compose-dialog.ts +7 -6
  30. package/src/client/components/jant-compose-editor.ts +6 -5
  31. package/src/client/components/jant-settings-general.ts +164 -22
  32. package/src/client/components/settings-types.ts +4 -6
  33. package/src/client/random-uuid.ts +23 -0
  34. package/src/client-auth.ts +1 -1
  35. package/src/db/__tests__/demo-canonical-snapshot.test.ts +1 -1
  36. package/src/db/__tests__/migration-rehearsal.test.ts +2 -5
  37. package/src/db/backfills/0004_register_apple_touch_media_rows.sql +65 -0
  38. package/src/db/migrations/0021_thankful_phalanx.sql +16 -0
  39. package/src/db/migrations/meta/0021_snapshot.json +2121 -0
  40. package/src/db/migrations/meta/_journal.json +7 -0
  41. package/src/db/migrations/pg/0019_gray_natasha_romanoff.sql +20 -0
  42. package/src/db/migrations/pg/meta/0019_snapshot.json +2718 -0
  43. package/src/db/migrations/pg/meta/_journal.json +7 -0
  44. package/src/db/pg/schema.ts +21 -26
  45. package/src/db/rehearsal-fixtures/demo-current.json +1 -1
  46. package/src/db/schema.ts +16 -20
  47. package/src/i18n/__tests__/middleware.test.ts +43 -1
  48. package/src/i18n/coverage.generated.ts +17 -0
  49. package/src/i18n/i18n.ts +18 -2
  50. package/src/i18n/index.ts +3 -0
  51. package/src/i18n/locales/settings/en.po +16 -11
  52. package/src/i18n/locales/settings/en.ts +1 -1
  53. package/src/i18n/locales/settings/zh-Hans.po +17 -12
  54. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  55. package/src/i18n/locales/settings/zh-Hant.po +16 -11
  56. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  57. package/src/i18n/locales.ts +84 -2
  58. package/src/i18n/middleware.ts +25 -16
  59. package/src/i18n/supported-locales.ts +153 -0
  60. package/src/lib/__tests__/csp-builder.test.ts +19 -2
  61. package/src/lib/__tests__/feed.test.ts +242 -1
  62. package/src/lib/__tests__/post-meta.test.ts +0 -1
  63. package/src/lib/__tests__/view.test.ts +0 -1
  64. package/src/lib/csp-builder.ts +28 -10
  65. package/src/lib/feed.ts +153 -3
  66. package/src/middleware/__tests__/secure-headers.test.ts +89 -0
  67. package/src/middleware/auth.ts +1 -1
  68. package/src/middleware/secure-headers.ts +47 -1
  69. package/src/node/__tests__/cli-runtime-target.test.ts +110 -2
  70. package/src/node/__tests__/cli-site-snapshot.test.ts +308 -13
  71. package/src/node/__tests__/cli-site-token-env.test.ts +2 -7
  72. package/src/node/__tests__/cli-snapshot-meta.test.ts +85 -0
  73. package/src/node/__tests__/cli-sql-export.test.ts +49 -0
  74. package/src/node/index.ts +1 -0
  75. package/src/preset.css +8 -2
  76. package/src/routes/api/__tests__/settings.test.ts +3 -2
  77. package/src/routes/api/github-sync.tsx +1 -1
  78. package/src/routes/api/settings.ts +4 -1
  79. package/src/routes/auth/signin.tsx +6 -0
  80. package/src/routes/pages/archive.tsx +4 -2
  81. package/src/services/__tests__/post.test.ts +19 -19
  82. package/src/services/__tests__/search.test.ts +0 -1
  83. package/src/services/__tests__/settings.test.ts +22 -3
  84. package/src/services/bootstrap.ts +7 -3
  85. package/src/services/collection.ts +3 -3
  86. package/src/services/export.ts +0 -3
  87. package/src/services/navigation.ts +0 -2
  88. package/src/services/path.ts +1 -38
  89. package/src/services/post.ts +32 -66
  90. package/src/services/search.ts +0 -6
  91. package/src/services/settings.ts +47 -6
  92. package/src/services/site-admin.ts +6 -1
  93. package/src/styles/ui.css +12 -23
  94. package/src/types/entities.ts +0 -1
  95. package/src/ui/color-themes.ts +1 -1
  96. package/src/ui/dash/settings/GeneralContent.tsx +17 -19
  97. package/src/ui/dash/settings/SettingsRootContent.tsx +17 -28
  98. package/src/ui/feed/NoteCard.tsx +1 -11
  99. package/src/ui/feed/__tests__/timeline-cards.test.ts +1 -1
  100. package/src/ui/pages/HomePage.tsx +1 -4
  101. package/src/ui/pages/PostPage.tsx +2 -0
  102. package/bin/commands/collections.js +0 -268
  103. package/bin/commands/media.js +0 -302
  104. package/bin/commands/posts.js +0 -262
  105. package/bin/commands/search.js +0 -53
  106. package/bin/commands/settings.js +0 -93
  107. package/bin/lib/http-api.js +0 -223
  108. package/bin/lib/media-upload.js +0 -206
  109. package/dist/app-Hvqe7Ks_.js +0 -5
  110. package/dist/client/_assets/client-DDs6NzB3.css +0 -2
  111. package/src/__tests__/bin/content-cli.test.ts +0 -179
  112. package/src/__tests__/bin/media-cli.test.ts +0 -192
  113. /package/dist/{github-api-BkRWnqMx.js → github-api-Bh0PH3zr.js} +0 -0
  114. /package/dist/{github-app-WeadXMb8.js → github-app-D0GvNnqp.js} +0 -0
@@ -47,6 +47,7 @@ import {
47
47
  adoptPendingInlineImageUploads,
48
48
  } from "../tiptap/inline-image-upload.js";
49
49
  import { isSafeAbsoluteUrl } from "../../lib/url.js";
50
+ import { randomUUID } from "../random-uuid.js";
50
51
 
51
52
  interface ComposeFilePickerCloseDetail {
52
53
  cancelled: boolean;
@@ -220,7 +221,7 @@ export class JantComposeEditor extends LitElement {
220
221
  private _lastEditorSelection: ComposeEditorSelection | null = null;
221
222
  private _emojiPickerEl: HTMLElement | null = null;
222
223
  private _emojiContainer: HTMLElement | null = null;
223
- private readonly _urlStatusId = `compose-url-status-${crypto.randomUUID()}`;
224
+ private readonly _urlStatusId = `compose-url-status-${randomUUID()}`;
224
225
  private _onDocClickBound = this._onDocumentClick.bind(this);
225
226
  private _scrollBufferApplied = false;
226
227
  private _filePickerCleanup: (() => void) | null = null;
@@ -880,7 +881,7 @@ export class JantComposeEditor extends LitElement {
880
881
  // Convert media attachments to ComposeAttachment[] with status "done"
881
882
  if (data.media?.length) {
882
883
  const attachments = data.media.map((m) => ({
883
- clientId: crypto.randomUUID(),
884
+ clientId: randomUUID(),
884
885
  file: new File([], m.originalName ?? "existing", { type: m.mimeType }),
885
886
  previewUrl: m.previewUrl,
886
887
  posterUrl: null,
@@ -906,7 +907,7 @@ export class JantComposeEditor extends LitElement {
906
907
  // Invalid JSON — leave as null
907
908
  }
908
909
  return {
909
- clientId: t.clientId ?? crypto.randomUUID(),
910
+ clientId: t.clientId ?? randomUUID(),
910
911
  bodyJson: parsed,
911
912
  bodyHtml: t.bodyHtml ?? "",
912
913
  summary: t.summary,
@@ -979,7 +980,7 @@ export class JantComposeEditor extends LitElement {
979
980
 
980
981
  private _openAttachedText() {
981
982
  const item: AttachedTextItem = {
982
- clientId: crypto.randomUUID(),
983
+ clientId: randomUUID(),
983
984
  bodyJson: null,
984
985
  bodyHtml: "",
985
986
  summary: "",
@@ -1242,7 +1243,7 @@ export class JantComposeEditor extends LitElement {
1242
1243
  continue;
1243
1244
  }
1244
1245
 
1245
- const clientId = crypto.randomUUID();
1246
+ const clientId = randomUUID();
1246
1247
  const previewUrl = URL.createObjectURL(file);
1247
1248
  newAttachments.push({
1248
1249
  clientId,
@@ -10,12 +10,16 @@
10
10
  import { LitElement, html, nothing } from "lit";
11
11
  import type { Editor } from "@tiptap/core";
12
12
  import { MAX_SITE_NAME_LENGTH } from "../../types.js";
13
+ import {
14
+ getSupportedLocaleEntries,
15
+ getOrBuildEntry,
16
+ type LocaleEntry,
17
+ } from "../../i18n/supported-locales.js";
13
18
  import type {
14
19
  SettingsInitialData,
15
20
  SettingsLabels,
16
21
  SettingsTimezone,
17
22
  SettingsCjkFont,
18
- SettingsLanguage,
19
23
  } from "./settings-types.js";
20
24
  import { showToast } from "../toast.js";
21
25
  import {
@@ -28,7 +32,6 @@ export class JantSettingsGeneral extends LitElement {
28
32
  labels: { type: Object },
29
33
  timezones: { type: Array },
30
34
  cjkFonts: { type: Array, attribute: "cjk-fonts" },
31
- languages: { type: Array },
32
35
  siteNameFallback: { type: String, attribute: "sitename-fallback" },
33
36
  siteDescriptionFallback: {
34
37
  type: String,
@@ -49,6 +52,8 @@ export class JantSettingsGeneral extends LitElement {
49
52
 
50
53
  // Language, CJK & time group
51
54
  _siteLanguage: { state: true },
55
+ _localeOpen: { state: true },
56
+ _localeQuery: { state: true },
52
57
  _cjkSerifFont: { state: true },
53
58
  _timeZone: { state: true },
54
59
  _origLocale: { state: true },
@@ -75,7 +80,6 @@ export class JantSettingsGeneral extends LitElement {
75
80
  declare labels: SettingsLabels;
76
81
  declare timezones: SettingsTimezone[];
77
82
  declare cjkFonts: SettingsCjkFont[];
78
- declare languages: SettingsLanguage[];
79
83
  declare siteNameFallback: string;
80
84
  declare siteDescriptionFallback: string;
81
85
  declare demoMode: boolean;
@@ -97,6 +101,10 @@ export class JantSettingsGeneral extends LitElement {
97
101
 
98
102
  // Language, CJK & time
99
103
  declare _siteLanguage: string;
104
+ /** Whether the locale combobox dropdown is currently open. */
105
+ declare _localeOpen: boolean;
106
+ /** Search query inside the locale combobox. */
107
+ declare _localeQuery: string;
100
108
  declare _cjkSerifFont: string;
101
109
  declare _timeZone: string;
102
110
  declare _origLocale: {
@@ -137,7 +145,6 @@ export class JantSettingsGeneral extends LitElement {
137
145
  this.labels = {} as SettingsLabels;
138
146
  this.timezones = [];
139
147
  this.cjkFonts = [];
140
- this.languages = [];
141
148
  this.siteNameFallback = "";
142
149
  this.siteDescriptionFallback = "";
143
150
  this.demoMode = false;
@@ -157,6 +164,8 @@ export class JantSettingsGeneral extends LitElement {
157
164
  this._siteLoading = false;
158
165
 
159
166
  this._siteLanguage = "en";
167
+ this._localeOpen = false;
168
+ this._localeQuery = "";
160
169
  this._cjkSerifFont = "off";
161
170
  this._timeZone = "UTC";
162
171
  this._origLocale = {
@@ -181,8 +190,16 @@ export class JantSettingsGeneral extends LitElement {
181
190
  this._searchLoading = false;
182
191
  }
183
192
 
193
+ connectedCallback() {
194
+ super.connectedCallback();
195
+ document.addEventListener("click", this._onLocalePickerDocumentClick);
196
+ document.addEventListener("keydown", this._onLocalePickerKeydown);
197
+ }
198
+
184
199
  disconnectedCallback() {
185
200
  super.disconnectedCallback();
201
+ document.removeEventListener("click", this._onLocalePickerDocumentClick);
202
+ document.removeEventListener("keydown", this._onLocalePickerKeydown);
186
203
  this._descEditor?.destroy();
187
204
  this._descEditor = null;
188
205
  this._footerEditor?.destroy();
@@ -387,6 +404,146 @@ export class JantSettingsGeneral extends LitElement {
387
404
  );
388
405
  }
389
406
 
407
+ // ── Locale combobox ────────────────────────────────────────────────
408
+
409
+ private _filteredLocaleEntries(): LocaleEntry[] {
410
+ const all = getSupportedLocaleEntries();
411
+ const query = this._localeQuery.trim().toLowerCase();
412
+ if (!query) return all;
413
+ return all.filter(
414
+ (e) =>
415
+ e.tag.toLowerCase().includes(query) ||
416
+ e.native.toLowerCase().includes(query) ||
417
+ e.english.toLowerCase().includes(query),
418
+ );
419
+ }
420
+
421
+ private _toggleLocalePicker = () => {
422
+ this._localeOpen = !this._localeOpen;
423
+ if (!this._localeOpen) {
424
+ this._localeQuery = "";
425
+ } else {
426
+ // Focus the search input on next paint.
427
+ this.updateComplete.then(() => {
428
+ const input = this.querySelector<HTMLInputElement>(
429
+ "[data-locale-search]",
430
+ );
431
+ input?.focus();
432
+ });
433
+ }
434
+ };
435
+
436
+ private _selectLocale(tag: string) {
437
+ this._siteLanguage = tag;
438
+ this._localeOpen = false;
439
+ this._localeQuery = "";
440
+ this._syncLocaleDirty();
441
+ }
442
+
443
+ private _onLocalePickerDocumentClick = (e: Event) => {
444
+ if (!this._localeOpen) return;
445
+ const target = e.target as Node | null;
446
+ const picker = this.querySelector("[data-locale-picker]");
447
+ if (picker && target && !picker.contains(target)) {
448
+ this._localeOpen = false;
449
+ }
450
+ };
451
+
452
+ private _onLocalePickerKeydown = (e: KeyboardEvent) => {
453
+ if (e.key === "Escape" && this._localeOpen) {
454
+ this._localeOpen = false;
455
+ this._localeQuery = "";
456
+ }
457
+ };
458
+
459
+ private _renderLanguagePicker() {
460
+ const current = getOrBuildEntry(this._siteLanguage || "en");
461
+ const filtered = this._filteredLocaleEntries();
462
+ const searchPlaceholder =
463
+ this.labels.siteLanguageSearchPlaceholder || "Search…";
464
+ const noMatches = this.labels.siteLanguageNoMatches || "No matches.";
465
+
466
+ return html`
467
+ <div class="relative" data-locale-picker>
468
+ <button
469
+ type="button"
470
+ class="input flex w-full items-center justify-between text-left"
471
+ aria-expanded=${this._localeOpen ? "true" : "false"}
472
+ aria-haspopup="listbox"
473
+ aria-labelledby="site-language-label"
474
+ @click=${this._toggleLocalePicker}
475
+ >
476
+ <span class="truncate">
477
+ ${current.native}
478
+ <span class="ml-2 text-xs text-muted-foreground">
479
+ ${current.tag} · ${Math.round(current.coverage * 100)}% translated
480
+ </span>
481
+ </span>
482
+ <span class="ml-2 text-muted-foreground" aria-hidden="true">▾</span>
483
+ </button>
484
+
485
+ ${this._localeOpen
486
+ ? html`
487
+ <div
488
+ class="absolute left-0 right-0 top-full z-10 mt-1 max-h-72 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md"
489
+ >
490
+ <div class="border-b p-2">
491
+ <input
492
+ type="text"
493
+ class="input w-full"
494
+ data-locale-search
495
+ placeholder=${searchPlaceholder}
496
+ autocomplete="off"
497
+ spellcheck="false"
498
+ .value=${this._localeQuery}
499
+ @input=${(e: Event) => {
500
+ this._localeQuery = (e.target as HTMLInputElement).value;
501
+ }}
502
+ />
503
+ </div>
504
+ <div role="listbox" class="max-h-56 overflow-auto py-1">
505
+ ${filtered.length === 0
506
+ ? html`
507
+ <div class="px-3 py-2 text-sm text-muted-foreground">
508
+ ${noMatches}
509
+ </div>
510
+ `
511
+ : filtered.map(
512
+ (entry) => html`
513
+ <button
514
+ type="button"
515
+ role="option"
516
+ aria-selected=${entry.tag === this._siteLanguage
517
+ ? "true"
518
+ : "false"}
519
+ class=${[
520
+ "flex w-full items-center justify-between gap-3 px-3 py-2 text-left text-sm hover:bg-accent",
521
+ entry.tag === this._siteLanguage
522
+ ? "bg-accent/60"
523
+ : "",
524
+ ].join(" ")}
525
+ @click=${() => this._selectLocale(entry.tag)}
526
+ >
527
+ <span class="flex flex-col">
528
+ <span>${entry.native}</span>
529
+ <span class="text-xs text-muted-foreground">
530
+ ${entry.tag} · ${entry.english}
531
+ </span>
532
+ </span>
533
+ <span class="text-xs text-muted-foreground">
534
+ ${Math.round(entry.coverage * 100)}% translated
535
+ </span>
536
+ </button>
537
+ `,
538
+ )}
539
+ </div>
540
+ </div>
541
+ `
542
+ : nothing}
543
+ </div>
544
+ `;
545
+ }
546
+
390
547
  // ── Feed group helpers ────────────────────────────────────────────
391
548
 
392
549
  private _syncFeedDirty() {
@@ -652,25 +809,10 @@ export class JantSettingsGeneral extends LitElement {
652
809
  >
653
810
  ${this._renderSectionTitle(this.labels.languageAndTime)}
654
811
  <div class="field">
655
- <label class="label">${this.labels.siteLanguage}</label>
656
- <select
657
- class="select"
658
- @change=${(e: Event) => {
659
- this._siteLanguage = (e.target as HTMLSelectElement).value;
660
- this._syncLocaleDirty();
661
- }}
812
+ <label id="site-language-label" class="label"
813
+ >${this.labels.siteLanguage}</label
662
814
  >
663
- ${this.languages.map(
664
- (lang) => html`
665
- <option
666
- value=${lang.value}
667
- ?selected=${this._siteLanguage === lang.value}
668
- >
669
- ${lang.label}
670
- </option>
671
- `,
672
- )}
673
- </select>
815
+ ${this._renderLanguagePicker()}
674
816
  <p class="text-sm text-muted-foreground mt-1">
675
817
  ${this.labels.siteLanguageHelp}
676
818
  </p>
@@ -43,6 +43,10 @@ export interface SettingsLabels {
43
43
  markdownSupported: string;
44
44
  siteLanguage: string;
45
45
  siteLanguageHelp: string;
46
+ /** Placeholder shown inside the locale combobox search field. */
47
+ siteLanguageSearchPlaceholder: string;
48
+ /** Empty-state message when the search filters out every option. */
49
+ siteLanguageNoMatches: string;
46
50
  cjkFont: string;
47
51
  cjkFontHelp: string;
48
52
  timeZone: string;
@@ -71,12 +75,6 @@ export interface SettingsCjkFont {
71
75
  label: string;
72
76
  }
73
77
 
74
- /** Site language option for the select dropdown */
75
- export interface SettingsLanguage {
76
- value: string;
77
- label: string;
78
- }
79
-
80
78
  export interface SettingsInitialData {
81
79
  siteName: string;
82
80
  siteDescription: string;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * RFC4122 v4 UUID generator with fallback for insecure contexts.
3
+ *
4
+ * `crypto.randomUUID()` is only available in secure contexts (HTTPS or
5
+ * localhost). Self-hosted Jant deployments are often accessed over plain
6
+ * HTTP on a LAN address, where the native API is `undefined`. This helper
7
+ * uses the native API when available and falls back to a `Math.random`-based
8
+ * v4 string otherwise. The fallback is not cryptographically strong, but
9
+ * client-side IDs here only need to be unique within a single page session.
10
+ */
11
+ export const randomUUID = (): string => {
12
+ if (
13
+ typeof crypto !== "undefined" &&
14
+ typeof crypto.randomUUID === "function"
15
+ ) {
16
+ return crypto.randomUUID();
17
+ }
18
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
19
+ const r = (Math.random() * 16) | 0;
20
+ const v = c === "x" ? r : (r & 0x3) | 0x8;
21
+ return v.toString(16);
22
+ });
23
+ };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Authenticated client entry point.
3
3
  *
4
- * Extends the public bundle with editor, upload, and dashboard interactions
4
+ * Extends the public bundle with editor, upload, and settings interactions
5
5
  * that should not be shipped to anonymous page views.
6
6
  */
7
7
 
@@ -74,5 +74,5 @@ describe("demo canonical snapshot", () => {
74
74
  } finally {
75
75
  rmSync(persistDir, { recursive: true, force: true });
76
76
  }
77
- }, 240_000);
77
+ }, 360_000);
78
78
  });
@@ -62,10 +62,7 @@ describe("migration rehearsal", () => {
62
62
  );
63
63
 
64
64
  expect(
65
- queryLocalCount(
66
- persistDir,
67
- "SELECT COUNT(*) AS count FROM post WHERE deleted_at IS NULL",
68
- ),
65
+ queryLocalCount(persistDir, "SELECT COUNT(*) AS count FROM post"),
69
66
  ).toBeGreaterThan(0);
70
67
  expect(
71
68
  queryLocalCount(persistDir, "SELECT COUNT(*) AS count FROM post_fts"),
@@ -73,5 +70,5 @@ describe("migration rehearsal", () => {
73
70
  } finally {
74
71
  rmSync(persistDir, { recursive: true, force: true });
75
72
  }
76
- }, 90_000);
73
+ }, 600_000);
77
74
  });
@@ -0,0 +1,65 @@
1
+ -- Register apple-touch icons in the media table.
2
+ --
3
+ -- Pre-JAN-6, apple-touch icons were uploaded to storage but only referenced
4
+ -- via the SITE_FAVICON_APPLE_TOUCH setting; they had no corresponding media
5
+ -- row. The snapshot logic compensated with a hardcoded list of "storage
6
+ -- setting keys", and the dashboard's media manager couldn't see the file.
7
+ --
8
+ -- JAN-6 makes apple-touch a first-class media entry so snapshot/admin
9
+ -- behavior is uniform with avatar. This backfill creates the missing media
10
+ -- rows for sites that already had apple-touch icons uploaded.
11
+ --
12
+ -- The size column is a placeholder (1). We have no way to read the real byte
13
+ -- length from pure SQL, and the field is only used for storage budgeting
14
+ -- that doesn't apply to favicon assets. Re-uploading the apple-touch icon
15
+ -- through the dashboard refreshes size to the accurate value.
16
+ --
17
+ -- The provider is borrowed from an existing media row in the same site so
18
+ -- the (provider, storage_key) unique index stays consistent with how that
19
+ -- site's other media is tracked.
20
+ INSERT INTO "media" (
21
+ "id",
22
+ "site_id",
23
+ "filename",
24
+ "original_name",
25
+ "mime_type",
26
+ "size",
27
+ "storage_key",
28
+ "provider",
29
+ "position",
30
+ "media_kind",
31
+ "created_at",
32
+ "updated_at"
33
+ )
34
+ SELECT
35
+ 'med_apt_' || substr(s."site_id", 5),
36
+ s."site_id",
37
+ 'apple-touch-icon.png',
38
+ 'apple-touch-icon.png',
39
+ 'image/png',
40
+ 1,
41
+ s."value",
42
+ COALESCE(
43
+ (
44
+ SELECT m."provider"
45
+ FROM "media" m
46
+ WHERE m."site_id" = s."site_id"
47
+ ORDER BY m."created_at"
48
+ LIMIT 1
49
+ ),
50
+ 'r2'
51
+ ),
52
+ 'a0',
53
+ 'image',
54
+ s."updated_at",
55
+ s."updated_at"
56
+ FROM "site_setting" s
57
+ WHERE s."key" = 'SITE_FAVICON_APPLE_TOUCH'
58
+ AND s."value" IS NOT NULL
59
+ AND trim(s."value") <> ''
60
+ AND NOT EXISTS (
61
+ SELECT 1
62
+ FROM "media" m
63
+ WHERE m."site_id" = s."site_id"
64
+ AND m."storage_key" = s."value"
65
+ );
@@ -0,0 +1,16 @@
1
+ DELETE FROM `post` WHERE `deleted_at` IS NOT NULL;--> statement-breakpoint
2
+ DROP INDEX `idx_post_site_thread_live_created`;--> statement-breakpoint
3
+ DROP INDEX `idx_post_site_status_deleted_published`;--> statement-breakpoint
4
+ DROP INDEX `idx_post_site_status_deleted_activity`;--> statement-breakpoint
5
+ DROP INDEX `idx_post_site_root_live_published_activity`;--> statement-breakpoint
6
+ DROP INDEX `idx_post_site_root_live_draft_updated`;--> statement-breakpoint
7
+ DROP INDEX `idx_post_site_reply_live_thread_created`;--> statement-breakpoint
8
+ DROP INDEX `idx_post_site_featured_live_featured_at`;--> statement-breakpoint
9
+ CREATE INDEX `idx_post_site_thread_created` ON `post` (`site_id`,`thread_id`,`created_at`,`id`);--> statement-breakpoint
10
+ CREATE INDEX `idx_post_site_status_published` ON `post` (`site_id`,`status`,`published_at`);--> statement-breakpoint
11
+ CREATE INDEX `idx_post_site_status_activity` ON `post` (`site_id`,`status`,`last_activity_at`);--> statement-breakpoint
12
+ CREATE INDEX `idx_post_site_root_published_activity` ON `post` (`site_id`,`last_activity_at`,`id`) WHERE "post"."reply_to_id" IS NULL AND "post"."status" = 'published';--> statement-breakpoint
13
+ CREATE INDEX `idx_post_site_root_draft_updated` ON `post` (`site_id`,`updated_at`,`id`) WHERE "post"."reply_to_id" IS NULL AND "post"."status" = 'draft';--> statement-breakpoint
14
+ CREATE INDEX `idx_post_site_reply_thread_created` ON `post` (`site_id`,`thread_id`,`created_at`,`id`) WHERE "post"."reply_to_id" IS NOT NULL AND "post"."status" = 'published';--> statement-breakpoint
15
+ CREATE INDEX `idx_post_site_featured_featured_at` ON `post` (`site_id`,`featured_at`,`thread_id`,`id`) WHERE "post"."status" = 'published' AND "post"."featured_at" IS NOT NULL;--> statement-breakpoint
16
+ ALTER TABLE `post` DROP COLUMN `deleted_at`;