includio-cms 0.36.7 → 0.36.9

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 (66) hide show
  1. package/API.md +9 -2
  2. package/CHANGELOG.md +29 -0
  3. package/DOCS.md +1 -1
  4. package/dist/admin/api/handler.js +2 -0
  5. package/dist/admin/api/media-jobs.d.ts +5 -0
  6. package/dist/admin/api/media-jobs.js +33 -0
  7. package/dist/admin/client/collection/collection-entries.svelte +20 -1
  8. package/dist/admin/client/collection/row-actions.svelte +12 -4
  9. package/dist/admin/client/collection/row-actions.svelte.d.ts +1 -0
  10. package/dist/admin/client/maintenance/maintenance-page.svelte +9 -0
  11. package/dist/admin/client/maintenance/media-jobs-panel.svelte +310 -0
  12. package/dist/admin/client/maintenance/media-jobs-panel.svelte.d.ts +3 -0
  13. package/dist/admin/remote/entry.remote.d.ts +3 -0
  14. package/dist/admin/remote/entry.remote.js +14 -3
  15. package/dist/core/cms.js +7 -0
  16. package/dist/core/fields/dotPath.d.ts +4 -0
  17. package/dist/core/fields/dotPath.js +18 -0
  18. package/dist/core/fields/slugify.d.ts +3 -0
  19. package/dist/core/fields/slugify.js +4 -0
  20. package/dist/core/server/entries/operations/duplicate.d.ts +10 -0
  21. package/dist/core/server/entries/operations/duplicate.js +73 -0
  22. package/dist/core/server/entries/operations/slugNormalize.d.ts +8 -0
  23. package/dist/core/server/entries/operations/slugNormalize.js +20 -0
  24. package/dist/core/server/entries/operations/slugUniqueness.d.ts +17 -0
  25. package/dist/core/server/entries/operations/slugUniqueness.js +39 -0
  26. package/dist/core/server/fields/utils/imageStyles.js +41 -4
  27. package/dist/core/server/index.d.ts +3 -0
  28. package/dist/core/server/index.js +3 -0
  29. package/dist/core/server/media/jobs/enqueue.d.ts +6 -0
  30. package/dist/core/server/media/jobs/enqueue.js +20 -0
  31. package/dist/core/server/media/jobs/processImageStyleJob.d.ts +19 -0
  32. package/dist/core/server/media/jobs/processImageStyleJob.js +23 -0
  33. package/dist/core/server/media/jobs/startWorker.d.ts +2 -0
  34. package/dist/core/server/media/jobs/startWorker.js +29 -0
  35. package/dist/core/server/media/jobs/worker.d.ts +29 -0
  36. package/dist/core/server/media/jobs/worker.js +54 -0
  37. package/dist/core/server/media/operations/replaceFile.js +2 -2
  38. package/dist/core/server/media/operations/uploadFile.js +10 -3
  39. package/dist/core/server/media/styles/buildStyleSpecs.d.ts +12 -0
  40. package/dist/core/server/media/styles/buildStyleSpecs.js +36 -0
  41. package/dist/core/server/media/styles/imagesConfig.d.ts +3 -0
  42. package/dist/core/server/media/styles/imagesConfig.js +16 -0
  43. package/dist/core/server/media/styles/operations/batchGenerateStyles.js +19 -41
  44. package/dist/core/server/media/styles/operations/generateFallbackVariant.d.ts +10 -0
  45. package/dist/core/server/media/styles/operations/generateFallbackVariant.js +21 -0
  46. package/dist/core/server/media/styles/operations/getImageStyle.js +12 -8
  47. package/dist/core/server/media/styles/sharp/sharpConfig.d.ts +7 -0
  48. package/dist/core/server/media/styles/sharp/sharpConfig.js +12 -0
  49. package/dist/db-postgres/index.js +113 -1
  50. package/dist/db-postgres/schema/imageStyle.js +1 -1
  51. package/dist/db-postgres/schema/index.d.ts +1 -0
  52. package/dist/db-postgres/schema/index.js +1 -0
  53. package/dist/db-postgres/schema/mediaFile.d.ts +17 -0
  54. package/dist/db-postgres/schema/mediaFile.js +1 -0
  55. package/dist/db-postgres/schema/mediaJob.d.ts +279 -0
  56. package/dist/db-postgres/schema/mediaJob.js +23 -0
  57. package/dist/sveltekit/components/image.svelte +1 -1
  58. package/dist/types/adapters/db.d.ts +51 -0
  59. package/dist/types/cms.d.ts +46 -0
  60. package/dist/types/media.d.ts +1 -0
  61. package/dist/updates/0.36.8/index.d.ts +2 -0
  62. package/dist/updates/0.36.8/index.js +20 -0
  63. package/dist/updates/0.36.9/index.d.ts +2 -0
  64. package/dist/updates/0.36.9/index.js +12 -0
  65. package/dist/updates/index.js +5 -1
  66. package/package.json +1 -1
@@ -59,7 +59,7 @@
59
59
  {/each}
60
60
 
61
61
  <img
62
- src={data.data.url}
62
+ src={data.data.fallbackUrl ?? data.data.url}
63
63
  alt={data.data.alt}
64
64
  width={data.data.width}
65
65
  height={data.data.height}
@@ -46,6 +46,14 @@ export interface DatabaseAdapter {
46
46
  getImageStyle: GetImageStyle;
47
47
  getImageStylesBatch: GetImageStylesBatch;
48
48
  createImageStyle: CreateImageStyle;
49
+ enqueueMediaJob: EnqueueMediaJob;
50
+ claimNextMediaJob: ClaimNextMediaJob;
51
+ completeMediaJob: CompleteMediaJob;
52
+ failMediaJob: FailMediaJob;
53
+ resetStaleMediaJobs: ResetStaleMediaJobs;
54
+ listMediaJobs: ListMediaJobs;
55
+ countMediaJobs: CountMediaJobs;
56
+ retryMediaJob: RetryMediaJob;
49
57
  deleteImageStylesByMediaFileId: DeleteImageStylesByMediaFileId;
50
58
  getAllImageStyles: GetAllImageStyles;
51
59
  deleteAllImageStyles: DeleteAllImageStyles;
@@ -139,6 +147,49 @@ export type DeleteMediaTag = (id: string) => Promise<void>;
139
147
  export type SetMediaFileTags = (fileId: string, tagIds: string[]) => Promise<void>;
140
148
  export type BulkSetMediaFileTags = (fileIds: string[], tagIds: string[]) => Promise<void>;
141
149
  export type BulkDeleteMediaFiles = (ids: string[]) => Promise<void>;
150
+ export type MediaJobStatus = 'pending' | 'processing' | 'done' | 'failed' | 'skipped';
151
+ export interface MediaJob {
152
+ id: string;
153
+ type: string;
154
+ mediaFileId: string;
155
+ payload: string;
156
+ dedupeKey: string;
157
+ status: MediaJobStatus;
158
+ priority: number;
159
+ attempts: number;
160
+ maxAttempts: number;
161
+ lastError: string | null;
162
+ runAfter: Date;
163
+ lockedAt: Date | null;
164
+ lockedBy: string | null;
165
+ createdAt: Date;
166
+ updatedAt: Date;
167
+ completedAt: Date | null;
168
+ }
169
+ export interface EnqueueMediaJobInput {
170
+ type: string;
171
+ mediaFileId: string;
172
+ payload: string;
173
+ dedupeKey: string;
174
+ priority?: number;
175
+ maxAttempts?: number;
176
+ }
177
+ export type EnqueueMediaJob = (input: EnqueueMediaJobInput) => Promise<void>;
178
+ export type ClaimNextMediaJob = (lockedBy: string) => Promise<MediaJob | null>;
179
+ export type CompleteMediaJob = (id: string) => Promise<void>;
180
+ export type FailMediaJob = (id: string, error: string, opts: {
181
+ retry: boolean;
182
+ runAfter?: Date;
183
+ }) => Promise<void>;
184
+ export type ResetStaleMediaJobs = (olderThan: Date) => Promise<number>;
185
+ export type ListMediaJobs = (opts: {
186
+ status?: MediaJobStatus;
187
+ type?: string;
188
+ limit: number;
189
+ offset?: number;
190
+ }) => Promise<MediaJob[]>;
191
+ export type CountMediaJobs = () => Promise<Record<MediaJobStatus, number>>;
192
+ export type RetryMediaJob = (id: string) => Promise<void>;
142
193
  export type UpdateMediaFile = (data: {
143
194
  id: string;
144
195
  data: Partial<MediaFile>;
@@ -30,11 +30,57 @@ export interface MaintenanceConfig {
30
30
  /** Interval in hours between maintenance runs (default: 6) */
31
31
  intervalHours?: number;
32
32
  }
33
+ export interface ImagesConfig {
34
+ /** Formaty wariantów (default: ['avif','webp']). Oryginał zostaje jako plik/fallback. */
35
+ formats: ('avif' | 'webp' | 'jpeg' | 'png')[];
36
+ /** Szerokości srcset (default: [640,1024,1536,2560]). Bez wariantu bazowego pełno-res. */
37
+ widths: number[];
38
+ /** Jakość per format (default: avif 52 / webp 78 / jpeg 80) */
39
+ quality: {
40
+ avif: number;
41
+ webp: number;
42
+ jpeg: number;
43
+ };
44
+ /** Twardy cap wymiaru wariantu (default: 2560) */
45
+ maxDimension: number;
46
+ /** Lekki fallback generowany synchronicznie przy uploadzie (default: 1280 jpeg) */
47
+ fallback: {
48
+ width: number;
49
+ format: 'jpeg' | 'webp';
50
+ };
51
+ }
52
+ export interface ImagesConfigInput {
53
+ formats?: ('avif' | 'webp' | 'jpeg' | 'png')[];
54
+ widths?: number[];
55
+ quality?: Partial<ImagesConfig['quality']>;
56
+ maxDimension?: number;
57
+ fallback?: Partial<ImagesConfig['fallback']>;
58
+ }
59
+ export interface OptimizationConfig {
60
+ /** In-process worker generujący warianty w tle (default: true) */
61
+ worker?: boolean;
62
+ /** Ile jobów sharp naraz (default: 1) */
63
+ concurrency?: number;
64
+ /** Prób przed oznaczeniem failed (default: 3) */
65
+ maxAttempts?: number;
66
+ /** Baza backoffu wykładniczego w ms (default: 30000) */
67
+ backoffBaseMs?: number;
68
+ /** Po jakim czasie processing uznać za martwe i wrócić do pending (default: 300000) */
69
+ staleLockMs?: number;
70
+ /** Interwał pollingu kolejki gdy pusta (default: 2000) */
71
+ pollIntervalMs?: number;
72
+ /** Limit pikseli źródła — poison-guard „image bomb" (default: 100_000_000) */
73
+ limitPixels?: number;
74
+ /** sharp.cache memory MB (default: 50) */
75
+ cacheMemoryMB?: number;
76
+ }
33
77
  export interface MediaConfig {
34
78
  maxOriginalWidth?: number;
35
79
  maxOriginalHeight?: number;
36
80
  video?: VideoTranscodeConfig;
37
81
  maintenance?: MaintenanceConfig;
82
+ images?: ImagesConfigInput;
83
+ optimization?: OptimizationConfig;
38
84
  }
39
85
  export interface AuthAdapter {
40
86
  api: {
@@ -37,6 +37,7 @@ export interface MediaFile {
37
37
  posterUrl: string | null;
38
38
  mimeType: string | null;
39
39
  blurDataUrl: string | null;
40
+ fallbackUrl?: string | null;
40
41
  focalX: number | null;
41
42
  focalY: number | null;
42
43
  transcriptFileId: string | null;
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;
@@ -0,0 +1,20 @@
1
+ export const update = {
2
+ version: '0.36.8',
3
+ date: '2026-07-02',
4
+ description: 'Optymalizacja obrazów przeniesiona do tła: render nigdy nie odpala sharpa. Warianty generuje in-process worker zasilany trwałą kolejką media_job (concurrency=1, twarde limity sharp, retry/backoff, circuit-breaker). Lżejsza, konfigurowalna matryca (avif+webp, bez pełno-res bazy, cap 2560) tnie ~połowę plików/jobów i eliminuje bombę CPU pełno-res AVIF, która OOM-owała VPS.',
5
+ features: [
6
+ 'Kolejka `media_job` + in-process worker: claim `FOR UPDATE SKIP LOCKED`, backoff wykładniczy, circuit-breaker po N próbach, poison-guard `limitInputPixels`, reset stale-locków. Start w `initCMS`, wyłączalny `media.optimization.worker=false`.',
7
+ 'Render kolejkuje brakujące style zamiast generować synchronicznie — odpowiedź HTTP nigdy nie czeka na sharpa; frontend serwuje lekki fallback + blur do czasu gotowości wariantów.',
8
+ 'Konfigurowalna matryca `mediaConfig.images`: `formats` (domyślnie avif+webp), `widths` [640,1024,1536,2560], `quality`, `maxDimension` (2560), `fallback` (jpeg ~1280). Bez wariantu bazowego pełno-res i bez martwej drabiny w formacie oryginalnym (~15 → ~8 wariantów/obraz).',
9
+ 'Upload generuje synchronicznie lekki fallback (`media_file.fallback_url`) i kolejkuje warianty; sweep oraz `replaceFile` też kolejkują (nie generują inline).',
10
+ 'Admin `/admin/maintenance`: sekcja „Kolejka mediów / Logi" — liczniki pending/processing/failed, lista failed z akcjami Ponów/Pomiń.',
11
+ 'Globalne limity sharp (`sharp.concurrency`/`cache`) ustawiane raz przy init. Deduplikacja zadań po efektywnej specyfikacji (format+wymiary+crop+quality).',
12
+ 'Public API: `enqueueStyleSpecs`, `startMediaWorker`, `stopMediaWorker`, typy `MediaJob`/`MediaJobStatus`.'
13
+ ],
14
+ fixes: [
15
+ '`image_styles`: dodany `crop` do klucza unikalnego (koniec cichego nadpisywania wariantów różniących się tylko cropem) + spójny `ON CONFLICT`.'
16
+ ],
17
+ breakingChanges: [
18
+ 'Nowa tabela `media_job` i kolumna `media_file.fallback_url` — wymagana migracja (`drizzle-kit push`). Render nie generuje styli synchronicznie (dogeneruje worker w tle). Konsumenci, którzy wyłączyli auto-maintenance z powodu OOM, mogą usunąć `media.maintenance.autoRun=false` — nowy worker jest bezpieczny by-design.'
19
+ ]
20
+ };
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;
@@ -0,0 +1,12 @@
1
+ export const update = {
2
+ version: '0.36.9',
3
+ date: '2026-07-02',
4
+ description: 'Duplikacja wpisu z tabeli kolekcji + serwerowa normalizacja pól slug przy zapisie. Menu wiersza zyskuje „Duplikuj" — tworzy nieopublikowany szkic z tą samą treścią (wszystkie języki), z prefiksem tytułu „Kopia — " i unikalnym slugiem („-kopia"), i przenosi do edytora duplikatu. Media i relacje są współdzielone przez referencje (bez głębokiego klonu). Niezależnie od duplikacji: każde pole typu slug (SlugField oraz slug w polu SEO) jest przy zapisie normalizowane przez slugify — „Singapur"/„Hello World" zapisują się jako „singapur"/„hello-world", transparentnie dla użytkownika. Spec: `docs/superpowers/specs/2026-07-02-duplicate-entry-and-slug-normalization-design.md`, plan: `docs/superpowers/plans/2026-07-02-duplicate-entry-and-slug-normalization.md`.',
5
+ features: [
6
+ 'Duplikacja wpisu (`duplicateEntry` + remote `duplicateEntryCommand`, akcja „Duplikuj" w menu wiersza tabeli kolekcji `/admin/collections/[collection]`): kopiuje najświeższą treść per język (draft ?? published ?? scheduled) na nowy wpis jako wersje nieopublikowane (`publishedAt: null`), prefiksuje `entryAdminTitle` przez „Kopia — " i nadaje każdemu polu slug unikalny sufiks („-kopia", „-kopia-2", …) sprawdzany wobec published-or-scheduled slugów kolekcji, po czym przenosi do edytora duplikatu. Media/relacje = współdzielone referencje. Akcja ukryta dla wpisów archiwalnych.'
7
+ ],
8
+ fixes: [
9
+ 'Normalizacja slug przy zapisie (`normalizeSlugFields` wpięte w `updateEntryVersionCommand`, ścieżka draft i publish): wartości pól typu slug (SlugField oraz slug w polu SEO) są normalizowane przez `slugify({ lower, strict, trim })` przed zapisem i przed kontrolą unikalności. Nietechniczny użytkownik wpisujący np. „Singapur" czy „Hello World" dostaje poprawny „singapur"/„hello-world" bez ręcznej korekty. Puste/nie-string i pola nie-slug pozostają nietknięte.'
10
+ ],
11
+ breakingChanges: []
12
+ };
@@ -73,6 +73,8 @@ import { update as update0363 } from './0.36.3/index.js';
73
73
  import { update as update0364 } from './0.36.4/index.js';
74
74
  import { update as update0365 } from './0.36.5/index.js';
75
75
  import { update as update0367 } from './0.36.7/index.js';
76
+ import { update as update0368 } from './0.36.8/index.js';
77
+ import { update as update0369 } from './0.36.9/index.js';
76
78
  export const updates = [
77
79
  update0065,
78
80
  update0066,
@@ -148,7 +150,9 @@ export const updates = [
148
150
  update0363,
149
151
  update0364,
150
152
  update0365,
151
- update0367
153
+ update0367,
154
+ update0368,
155
+ update0369
152
156
  ];
153
157
  export const getUpdatesFrom = (fromVersion) => {
154
158
  const fromParts = fromVersion.split('.').map(Number);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "includio-cms",
3
- "version": "0.36.7",
3
+ "version": "0.36.9",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",