includio-cms 0.15.4 → 0.16.0

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 (80) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/DOCS.md +1 -1
  3. package/ROADMAP.md +22 -8
  4. package/dist/admin/components/fields/media-field.svelte +54 -2
  5. package/dist/admin/components/media/file/file-details.svelte +65 -0
  6. package/dist/admin/remote/media.remote.d.ts +1 -0
  7. package/dist/admin/remote/media.remote.js +5 -0
  8. package/dist/cmp/types.d.ts +25 -0
  9. package/dist/cmp/types.js +1 -0
  10. package/dist/core/cms.d.ts +2 -0
  11. package/dist/core/cms.js +2 -0
  12. package/dist/core/server/cmp/getCountryFromHeaders.d.ts +10 -0
  13. package/dist/core/server/cmp/getCountryFromHeaders.js +30 -0
  14. package/dist/core/server/cmp/operations/create.d.ts +17 -0
  15. package/dist/core/server/cmp/operations/create.js +38 -0
  16. package/dist/core/server/cmp/operations/get.d.ts +2 -0
  17. package/dist/core/server/cmp/operations/get.js +8 -0
  18. package/dist/core/server/cmp/operations/list.d.ts +3 -0
  19. package/dist/core/server/cmp/operations/list.js +15 -0
  20. package/dist/core/server/cmp/truncateIpAddress.d.ts +7 -0
  21. package/dist/core/server/cmp/truncateIpAddress.js +57 -0
  22. package/dist/core/server/fields/resolveImageFields.d.ts +5 -0
  23. package/dist/core/server/fields/resolveImageFields.js +9 -1
  24. package/dist/core/server/generator/generator.js +22 -6
  25. package/dist/core/server/media/operations/backgroundMaintenance.js +51 -20
  26. package/dist/core/server/media/operations/findMediaReferences.d.ts +16 -0
  27. package/dist/core/server/media/operations/findMediaReferences.js +60 -0
  28. package/dist/db-postgres/index.js +46 -1
  29. package/dist/db-postgres/schema/consentLog.d.ts +17 -0
  30. package/dist/db-postgres/schema/consentLog.js +4 -1
  31. package/dist/paraglide/messages/_index.d.ts +36 -3
  32. package/dist/paraglide/messages/_index.js +71 -3
  33. package/dist/paraglide/messages/en.d.ts +5 -0
  34. package/dist/paraglide/messages/en.js +14 -0
  35. package/dist/paraglide/messages/pl.d.ts +5 -0
  36. package/dist/paraglide/messages/pl.js +14 -0
  37. package/dist/sveltekit/server/handle.js +1 -1
  38. package/dist/types/adapters/db.d.ts +7 -1
  39. package/dist/types/cms.d.ts +3 -0
  40. package/dist/types/consent.d.ts +11 -0
  41. package/dist/updates/0.15.5/index.d.ts +2 -0
  42. package/dist/updates/0.15.5/index.js +15 -0
  43. package/dist/updates/0.16.0/index.d.ts +2 -0
  44. package/dist/updates/0.16.0/index.js +14 -0
  45. package/dist/updates/index.js +3 -1
  46. package/package.json +1 -1
  47. package/dist/demo/seed.d.ts +0 -1
  48. package/dist/demo/seed.js +0 -117
  49. package/dist/inline-edit-proto/ModeToggle.svelte +0 -36
  50. package/dist/inline-edit-proto/ModeToggle.svelte.d.ts +0 -18
  51. package/dist/inline-edit-proto/blocks/AddBlockButton.svelte +0 -47
  52. package/dist/inline-edit-proto/blocks/AddBlockButton.svelte.d.ts +0 -8
  53. package/dist/inline-edit-proto/blocks/BlockToolbar.svelte +0 -80
  54. package/dist/inline-edit-proto/blocks/BlockToolbar.svelte.d.ts +0 -13
  55. package/dist/inline-edit-proto/blocks/BlockWrapper.svelte +0 -83
  56. package/dist/inline-edit-proto/blocks/BlockWrapper.svelte.d.ts +0 -11
  57. package/dist/inline-edit-proto/context.svelte.d.ts +0 -65
  58. package/dist/inline-edit-proto/context.svelte.js +0 -194
  59. package/dist/inline-edit-proto/hybrid/EditableHybrid.svelte +0 -70
  60. package/dist/inline-edit-proto/hybrid/EditableHybrid.svelte.d.ts +0 -11
  61. package/dist/inline-edit-proto/hybrid/FieldRenderer.svelte +0 -94
  62. package/dist/inline-edit-proto/hybrid/FieldRenderer.svelte.d.ts +0 -11
  63. package/dist/inline-edit-proto/hybrid/HybridEditor.svelte +0 -107
  64. package/dist/inline-edit-proto/hybrid/HybridEditor.svelte.d.ts +0 -7
  65. package/dist/inline-edit-proto/hybrid/SyncPanel.svelte +0 -181
  66. package/dist/inline-edit-proto/hybrid/SyncPanel.svelte.d.ts +0 -3
  67. package/dist/inline-edit-proto/inline/EditableInline.svelte +0 -120
  68. package/dist/inline-edit-proto/inline/EditableInline.svelte.d.ts +0 -11
  69. package/dist/inline-edit-proto/inline/InlineToolbar.svelte +0 -71
  70. package/dist/inline-edit-proto/inline/InlineToolbar.svelte.d.ts +0 -6
  71. package/dist/inline-edit-proto/panel/EditSheet.svelte +0 -130
  72. package/dist/inline-edit-proto/panel/EditSheet.svelte.d.ts +0 -3
  73. package/dist/inline-edit-proto/panel/EditablePanel.svelte +0 -44
  74. package/dist/inline-edit-proto/panel/EditablePanel.svelte.d.ts +0 -9
  75. package/dist/paraglide/messages/hello_world.d.ts +0 -5
  76. package/dist/paraglide/messages/hello_world.js +0 -33
  77. package/dist/paraglide/messages/login_hello.d.ts +0 -16
  78. package/dist/paraglide/messages/login_hello.js +0 -34
  79. package/dist/paraglide/messages/login_please_login.d.ts +0 -16
  80. package/dist/paraglide/messages/login_please_login.js +0 -34
@@ -1,4 +1,4 @@
1
- import { mkdirSync, writeFileSync } from 'node:fs';
1
+ import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { generateTsTypeFromFields, generateFlatTsTypeFromFields, generateInlineBlockTypeString, setGeneratorCustomFields } from './fields.js';
4
4
  import { generateTsTypeFromFormFields } from './formFields.js';
@@ -9,6 +9,22 @@ function createCmsRuntimeDir() {
9
9
  const cmsDir = join(process.cwd(), 'src/lib/cms/runtime');
10
10
  mkdirSync(cmsDir, { recursive: true });
11
11
  }
12
+ // Avoids touching mtime when content is unchanged — otherwise Vite's file
13
+ // watcher triggers an SSR reload, which re-runs the generator, which rewrites
14
+ // the same content, looping forever in dev.
15
+ function writeIfChanged(filePath, content) {
16
+ if (existsSync(filePath)) {
17
+ try {
18
+ const current = readFileSync(filePath, 'utf8');
19
+ if (current === content)
20
+ return;
21
+ }
22
+ catch {
23
+ // fall through and write
24
+ }
25
+ }
26
+ writeFileSync(filePath, content);
27
+ }
12
28
  function generateTypesStringForRecords(type, records) {
13
29
  const recordTypeString = type.charAt(0).toUpperCase() + type.slice(1);
14
30
  const singleSlugs = records.map((s) => s.slug) || [];
@@ -156,7 +172,7 @@ function generateTypes(config) {
156
172
  }
157
173
  }
158
174
  code += `\n export type SiteLanguage = ${config.languages.map((lang) => JSON.stringify(lang)).join(' | ')};\n\n`;
159
- writeFileSync(filePath, code);
175
+ writeIfChanged(filePath, code);
160
176
  }
161
177
  function generateAPI(config) {
162
178
  const cmsDir = join(process.cwd(), 'src/lib/cms/runtime');
@@ -232,7 +248,7 @@ function generateAPI(config) {
232
248
  : ''}
233
249
 
234
250
  `;
235
- writeFileSync(filePath, code);
251
+ writeIfChanged(filePath, code);
236
252
  }
237
253
  function generateSchemas(config) {
238
254
  const cmsDir = join(process.cwd(), 'src/lib/cms/runtime');
@@ -247,7 +263,7 @@ function generateSchemas(config) {
247
263
  export const ${varName}FormSchema = ${generateZodSchemaStringFromFormFieldsAsString(form.fields)} \n
248
264
  `;
249
265
  });
250
- writeFileSync(filePath, code);
266
+ writeIfChanged(filePath, code);
251
267
  }
252
268
  function generateDrizzleSchema(config) {
253
269
  const cmsDir = join(process.cwd(), 'src/lib/cms/runtime');
@@ -259,7 +275,7 @@ function generateDrizzleSchema(config) {
259
275
  if (config.shop) {
260
276
  code += `export * from 'includio-cms/db-postgres/schema-shop';\n`;
261
277
  }
262
- writeFileSync(filePath, code);
278
+ writeIfChanged(filePath, code);
263
279
  }
264
280
  function generateRemote(config) {
265
281
  if (!config.forms || config.forms.length === 0)
@@ -282,7 +298,7 @@ function generateRemote(config) {
282
298
  code += `\t}\n`;
283
299
  code += `);\n\n`;
284
300
  });
285
- writeFileSync(filePath, code);
301
+ writeIfChanged(filePath, code);
286
302
  }
287
303
  export function generateRuntime(config) {
288
304
  // Build custom fields map from plugins for type generation
@@ -1,22 +1,36 @@
1
1
  import { getCMS } from '../../../cms.js';
2
- let running = false;
3
- let timer = null;
4
- let lastResult = null;
5
- let nextRunAt = null;
2
+ // State held on globalThis so it survives Vite HMR module re-evaluation in dev;
3
+ // without this, each re-eval would leak a new timer while the old one kept ticking.
4
+ const STATE_KEY = Symbol.for('includio.maintenance.state');
5
+ function getState() {
6
+ const g = globalThis;
7
+ if (!g[STATE_KEY]) {
8
+ g[STATE_KEY] = {
9
+ running: false,
10
+ pendingTimeout: null,
11
+ timer: null,
12
+ lastResult: null,
13
+ nextRunAt: null
14
+ };
15
+ }
16
+ return g[STATE_KEY];
17
+ }
6
18
  export function getMaintenanceStatus() {
19
+ const state = getState();
7
20
  return {
8
- running,
9
- lastRun: lastResult?.ranAt ?? null,
10
- nextRun: nextRunAt,
11
- lastResult
21
+ running: state.running,
22
+ lastRun: state.lastResult?.ranAt ?? null,
23
+ nextRun: state.nextRunAt,
24
+ lastResult: state.lastResult
12
25
  };
13
26
  }
14
27
  async function runMaintenance() {
15
- if (running) {
28
+ const state = getState();
29
+ if (state.running) {
16
30
  console.info('[maintenance] Skipping — previous run still active');
17
31
  return;
18
32
  }
19
- running = true;
33
+ state.running = true;
20
34
  console.info('[maintenance] Starting background maintenance...');
21
35
  const result = {
22
36
  stylesCreated: 0,
@@ -59,8 +73,8 @@ async function runMaintenance() {
59
73
  console.warn('[maintenance] Error during background maintenance:', err);
60
74
  }
61
75
  finally {
62
- running = false;
63
- lastResult = result;
76
+ state.running = false;
77
+ state.lastResult = result;
64
78
  const parts = [];
65
79
  if (result.stylesCreated > 0)
66
80
  parts.push(`${result.stylesCreated} styles`);
@@ -79,27 +93,44 @@ async function runMaintenance() {
79
93
  }
80
94
  }
81
95
  export function startBackgroundMaintenance() {
96
+ const state = getState();
97
+ if (state.pendingTimeout || state.timer) {
98
+ console.info('[maintenance] Already scheduled, skipping');
99
+ return;
100
+ }
82
101
  const config = getCMS().mediaConfig?.maintenance;
83
102
  if (config?.autoRun === false)
84
103
  return;
85
104
  const intervalHours = config?.intervalHours ?? 6;
86
105
  const intervalMs = intervalHours * 60 * 60 * 1000;
87
106
  // First run after 30s delay
88
- setTimeout(() => {
107
+ state.pendingTimeout = setTimeout(() => {
108
+ state.pendingTimeout = null;
89
109
  runMaintenance();
90
110
  // Then repeat on interval
91
- nextRunAt = new Date(Date.now() + intervalMs);
92
- timer = setInterval(() => {
93
- nextRunAt = new Date(Date.now() + intervalMs);
111
+ state.nextRunAt = new Date(Date.now() + intervalMs);
112
+ state.timer = setInterval(() => {
113
+ state.nextRunAt = new Date(Date.now() + intervalMs);
94
114
  runMaintenance();
95
115
  }, intervalMs);
96
116
  }, 30_000);
97
117
  console.info(`[maintenance] Scheduled: first run in 30s, then every ${intervalHours}h`);
98
118
  }
99
119
  export function stopBackgroundMaintenance() {
100
- if (timer) {
101
- clearInterval(timer);
102
- timer = null;
103
- nextRunAt = null;
120
+ const state = getState();
121
+ if (state.pendingTimeout) {
122
+ clearTimeout(state.pendingTimeout);
123
+ state.pendingTimeout = null;
124
+ }
125
+ if (state.timer) {
126
+ clearInterval(state.timer);
127
+ state.timer = null;
104
128
  }
129
+ state.nextRunAt = null;
130
+ }
131
+ // Clean up on Vite HMR so timers don't accumulate across module re-evaluations.
132
+ if (import.meta.hot) {
133
+ import.meta.hot.dispose(() => {
134
+ stopBackgroundMaintenance();
135
+ });
105
136
  }
@@ -0,0 +1,16 @@
1
+ export interface MediaReferenceBreakdown {
2
+ collection: string;
3
+ label: string;
4
+ count: number;
5
+ kind: 'collection' | 'single';
6
+ }
7
+ export interface MediaReferenceResult {
8
+ total: number;
9
+ byCollection: MediaReferenceBreakdown[];
10
+ }
11
+ /**
12
+ * Find how many entries (across all collections + singles) reference a given media file.
13
+ * Scans only the latest version per entry per language (MVP — history is not inspected).
14
+ * Grouping is by collection/single slug with a human-readable label.
15
+ */
16
+ export declare function findMediaReferences(fileId: string): Promise<MediaReferenceResult>;
@@ -0,0 +1,60 @@
1
+ import { getCMS } from '../../../cms.js';
2
+ import { getFieldsFromConfig } from '../../../fields/layoutUtils.js';
3
+ import { extractMediaIdsFromData } from '../../fields/resolveImageFields.js';
4
+ function pickLabel(lang, localized, fallback) {
5
+ if (!localized)
6
+ return fallback ?? '';
7
+ return localized[lang] ?? Object.values(localized)[0] ?? fallback ?? '';
8
+ }
9
+ /**
10
+ * Find how many entries (across all collections + singles) reference a given media file.
11
+ * Scans only the latest version per entry per language (MVP — history is not inspected).
12
+ * Grouping is by collection/single slug with a human-readable label.
13
+ */
14
+ export async function findMediaReferences(fileId) {
15
+ const cms = getCMS();
16
+ const lang = cms.languages[0];
17
+ const db = cms.databaseAdapter;
18
+ const breakdown = [];
19
+ let total = 0;
20
+ const scanSlug = async (slug, label, fields, kind) => {
21
+ const entries = await db.getEntries({ slug });
22
+ if (entries.length === 0)
23
+ return;
24
+ const alive = entries.filter((e) => e.archivedAt == null);
25
+ if (alive.length === 0)
26
+ return;
27
+ const versions = await db.getEntryVersions({
28
+ entryIds: alive.map((e) => e.id),
29
+ lang
30
+ });
31
+ // Pick the latest version per entryId.
32
+ const latestByEntry = new Map();
33
+ for (const v of versions) {
34
+ const prev = latestByEntry.get(v.entryId);
35
+ if (!prev || v.versionNumber > prev.versionNumber) {
36
+ latestByEntry.set(v.entryId, v);
37
+ }
38
+ }
39
+ let count = 0;
40
+ for (const v of latestByEntry.values()) {
41
+ const ids = extractMediaIdsFromData(v.data, fields);
42
+ if (ids.includes(fileId))
43
+ count += 1;
44
+ }
45
+ if (count > 0) {
46
+ breakdown.push({ collection: slug, label, count, kind });
47
+ total += count;
48
+ }
49
+ };
50
+ for (const col of Object.values(cms.collections)) {
51
+ const label = pickLabel(lang, col.labels?.plural, col.slug);
52
+ await scanSlug(col.slug, label, getFieldsFromConfig(col), 'collection');
53
+ }
54
+ for (const single of Object.values(cms.singles)) {
55
+ const label = pickLabel(lang, single.label, single.slug);
56
+ await scanSlug(single.slug, label, getFieldsFromConfig(single), 'single');
57
+ }
58
+ breakdown.sort((a, b) => b.count - a.count);
59
+ return { total, byCollection: breakdown };
60
+ }
@@ -1,7 +1,7 @@
1
1
  import { drizzle } from 'drizzle-orm/postgres-js';
2
2
  import postgres from 'postgres';
3
3
  import * as schema from './schema/index.js';
4
- import { eq, and, inArray, sql, isNull, desc, asc, SQL, ilike, or, count } from 'drizzle-orm';
4
+ import { eq, and, inArray, sql, isNull, desc, asc, SQL, ilike, or, count, gte, lte } from 'drizzle-orm';
5
5
  const SAFE_JSON_KEY = /^[a-zA-Z0-9_]+$/;
6
6
  function validateJsonPathKeys(path) {
7
7
  for (const key of path) {
@@ -527,6 +527,51 @@ export function pg(config) {
527
527
  createConsentLog: async (data) => {
528
528
  await db.insert(schema.consentLogsTable).values(data);
529
529
  },
530
+ getConsentLogs: async (filters) => {
531
+ const conditions = [];
532
+ if (filters.startDate) {
533
+ conditions.push(gte(schema.consentLogsTable.timestamp, filters.startDate));
534
+ }
535
+ if (filters.endDate) {
536
+ conditions.push(lte(schema.consentLogsTable.timestamp, filters.endDate));
537
+ }
538
+ if (filters.country) {
539
+ conditions.push(eq(schema.consentLogsTable.countryCode, filters.country.toUpperCase()));
540
+ }
541
+ const rows = await db
542
+ .select()
543
+ .from(schema.consentLogsTable)
544
+ .where(conditions.length ? and(...conditions) : undefined)
545
+ .orderBy(desc(schema.consentLogsTable.timestamp))
546
+ .limit(filters.limit ?? 50)
547
+ .offset(filters.offset ?? 0);
548
+ return rows;
549
+ },
550
+ countConsentLogs: async (filters) => {
551
+ const conditions = [];
552
+ if (filters.startDate) {
553
+ conditions.push(gte(schema.consentLogsTable.timestamp, filters.startDate));
554
+ }
555
+ if (filters.endDate) {
556
+ conditions.push(lte(schema.consentLogsTable.timestamp, filters.endDate));
557
+ }
558
+ if (filters.country) {
559
+ conditions.push(eq(schema.consentLogsTable.countryCode, filters.country.toUpperCase()));
560
+ }
561
+ const [row] = await db
562
+ .select({ value: count() })
563
+ .from(schema.consentLogsTable)
564
+ .where(conditions.length ? and(...conditions) : undefined);
565
+ return row?.value ?? 0;
566
+ },
567
+ getConsentLog: async (id) => {
568
+ const [row] = await db
569
+ .select()
570
+ .from(schema.consentLogsTable)
571
+ .where(eq(schema.consentLogsTable.id, id))
572
+ .limit(1);
573
+ return row ?? null;
574
+ },
530
575
  // --- Media Tags ---
531
576
  getMediaTags: async () => {
532
577
  return db.select().from(schema.mediaTagsTable).orderBy(schema.mediaTagsTable.name);
@@ -189,6 +189,23 @@ export declare const consentLogsTable: import("drizzle-orm/pg-core/table", { wit
189
189
  identity: undefined;
190
190
  generated: undefined;
191
191
  }, {}, {}>;
192
+ parentLogId: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
193
+ name: "parent_log_id";
194
+ tableName: "consent_logs";
195
+ dataType: "string";
196
+ columnType: "PgUUID";
197
+ data: string;
198
+ driverParam: string;
199
+ notNull: false;
200
+ hasDefault: false;
201
+ isPrimaryKey: false;
202
+ isAutoincrement: false;
203
+ hasRuntimeDefault: false;
204
+ enumValues: undefined;
205
+ baseColumn: never;
206
+ identity: undefined;
207
+ generated: undefined;
208
+ }, {}, {}>;
192
209
  };
193
210
  dialect: "pg";
194
211
  }>;
@@ -10,5 +10,8 @@ export const consentLogsTable = pgTable('consent_logs', {
10
10
  consents: jsonb('consents').notNull(),
11
11
  consentModeStatus: jsonb('consent_mode_status').notNull(),
12
12
  cmpVersion: text('cmp_version').notNull(),
13
- policyVersion: text('policy_version').notNull()
13
+ policyVersion: text('policy_version').notNull(),
14
+ parentLogId: uuid('parent_log_id').references(() => consentLogsTable.id, {
15
+ onDelete: 'set null'
16
+ })
14
17
  });
@@ -1,3 +1,36 @@
1
- export * from "./hello_world.js";
2
- export * from "./login_hello.js";
3
- export * from "./login_please_login.js";
1
+ export function hello_world(inputs: {
2
+ name: NonNullable<unknown>;
3
+ }, options?: {
4
+ locale?: "en" | "pl";
5
+ }): string;
6
+ /**
7
+ * This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
8
+ *
9
+ * - Changing this function will be over-written by the next build.
10
+ *
11
+ * - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
12
+ * use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
13
+ *
14
+ * @param {{}} inputs
15
+ * @param {{ locale?: "en" | "pl" }} options
16
+ * @returns {string}
17
+ */
18
+ declare function login_hello(inputs?: {}, options?: {
19
+ locale?: "en" | "pl";
20
+ }): string;
21
+ /**
22
+ * This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
23
+ *
24
+ * - Changing this function will be over-written by the next build.
25
+ *
26
+ * - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
27
+ * use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
28
+ *
29
+ * @param {{}} inputs
30
+ * @param {{ locale?: "en" | "pl" }} options
31
+ * @returns {string}
32
+ */
33
+ declare function login_please_login(inputs?: {}, options?: {
34
+ locale?: "en" | "pl";
35
+ }): string;
36
+ export { login_hello as login.hello, login_please_login as login.please_login };
@@ -1,4 +1,72 @@
1
1
  /* eslint-disable */
2
- export * from './hello_world.js'
3
- export * from './login_hello.js'
4
- export * from './login_please_login.js'
2
+ import { getLocale, trackMessageCall, experimentalMiddlewareLocaleSplitting, isServer } from "../runtime.js"
3
+ import * as en from "./en.js"
4
+ import * as pl from "./pl.js"
5
+ /**
6
+ * This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
7
+ *
8
+ * - Changing this function will be over-written by the next build.
9
+ *
10
+ * - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
11
+ * use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
12
+ *
13
+ * @param {{ name: NonNullable<unknown> }} inputs
14
+ * @param {{ locale?: "en" | "pl" }} options
15
+ * @returns {string}
16
+ */
17
+ /* @__NO_SIDE_EFFECTS__ */
18
+ export const hello_world = (inputs, options = {}) => {
19
+ if (experimentalMiddlewareLocaleSplitting && isServer === false) {
20
+ return /** @type {any} */ (globalThis).__paraglide_ssr.hello_world(inputs)
21
+ }
22
+ const locale = options.locale ?? getLocale()
23
+ trackMessageCall("hello_world", locale)
24
+ if (locale === "en") return en.hello_world(inputs)
25
+ return pl.hello_world(inputs)
26
+ };
27
+ /**
28
+ * This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
29
+ *
30
+ * - Changing this function will be over-written by the next build.
31
+ *
32
+ * - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
33
+ * use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
34
+ *
35
+ * @param {{}} inputs
36
+ * @param {{ locale?: "en" | "pl" }} options
37
+ * @returns {string}
38
+ */
39
+ /* @__NO_SIDE_EFFECTS__ */
40
+ const login_hello = (inputs = {}, options = {}) => {
41
+ if (experimentalMiddlewareLocaleSplitting && isServer === false) {
42
+ return /** @type {any} */ (globalThis).__paraglide_ssr.login_hello(inputs)
43
+ }
44
+ const locale = options.locale ?? getLocale()
45
+ trackMessageCall("login_hello", locale)
46
+ if (locale === "en") return en.login_hello(inputs)
47
+ return pl.login_hello(inputs)
48
+ };
49
+ export { login_hello as "login.hello" }
50
+ /**
51
+ * This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
52
+ *
53
+ * - Changing this function will be over-written by the next build.
54
+ *
55
+ * - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
56
+ * use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
57
+ *
58
+ * @param {{}} inputs
59
+ * @param {{ locale?: "en" | "pl" }} options
60
+ * @returns {string}
61
+ */
62
+ /* @__NO_SIDE_EFFECTS__ */
63
+ const login_please_login = (inputs = {}, options = {}) => {
64
+ if (experimentalMiddlewareLocaleSplitting && isServer === false) {
65
+ return /** @type {any} */ (globalThis).__paraglide_ssr.login_please_login(inputs)
66
+ }
67
+ const locale = options.locale ?? getLocale()
68
+ trackMessageCall("login_please_login", locale)
69
+ if (locale === "en") return en.login_please_login(inputs)
70
+ return pl.login_please_login(inputs)
71
+ };
72
+ export { login_please_login as "login.please_login" }
@@ -0,0 +1,5 @@
1
+ export const hello_world: (inputs: {
2
+ name: NonNullable<unknown>;
3
+ }) => string;
4
+ export const login_hello: (inputs: {}) => string;
5
+ export const login_please_login: (inputs: {}) => string;
@@ -0,0 +1,14 @@
1
+ /* eslint-disable */
2
+
3
+
4
+ export const hello_world = /** @type {(inputs: { name: NonNullable<unknown> }) => string} */ (i) => {
5
+ return `Hello, ${i.name} from en!`
6
+ };
7
+
8
+ export const login_hello = /** @type {(inputs: {}) => string} */ () => {
9
+ return `Welcome back`
10
+ };
11
+
12
+ export const login_please_login = /** @type {(inputs: {}) => string} */ () => {
13
+ return `Login to your account`
14
+ };
@@ -0,0 +1,5 @@
1
+ export const hello_world: (inputs: {
2
+ name: NonNullable<unknown>;
3
+ }) => string;
4
+ export const login_hello: (inputs: {}) => string;
5
+ export const login_please_login: (inputs: {}) => string;
@@ -0,0 +1,14 @@
1
+ /* eslint-disable */
2
+
3
+
4
+ export const hello_world = /** @type {(inputs: { name: NonNullable<unknown> }) => string} */ (i) => {
5
+ return `Hello, ${i.name} from pl!`
6
+ };
7
+
8
+ export const login_hello = /** @type {(inputs: {}) => string} */ () => {
9
+ return `Witaj ponownie`
10
+ };
11
+
12
+ export const login_please_login = /** @type {(inputs: {}) => string} */ () => {
13
+ return `Zaloguj się na swoje konto`
14
+ };
@@ -80,7 +80,7 @@ export function includioCMS(cmsConfig) {
80
80
  const securityHeaders = async ({ event, resolve }) => {
81
81
  const response = await resolve(event);
82
82
  response.headers.set('X-Content-Type-Options', 'nosniff');
83
- response.headers.set('X-Frame-Options', 'DENY');
83
+ response.headers.set('X-Frame-Options', 'SAMEORIGIN');
84
84
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
85
85
  return response;
86
86
  };
@@ -1,4 +1,4 @@
1
- import type { ConsentLogData } from '../consent.js';
1
+ import type { ConsentLogData, ConsentLogRecord, GetConsentLogsFilters } from '../consent.js';
2
2
  import type { DbEntry, DbEntryInsert, DbEntryVersion, DbEntryVersionInsert, GetDbEntriesOptions, GetDbEntryVersionsOptions, GetPaginatedEntriesOptions, PaginatedEntryRow, PaginationOptions } from '../entries.js';
3
3
  import type { ImageFieldStyle } from '../fields.js';
4
4
  import type { FormSubmission } from '../forms.js';
@@ -23,6 +23,9 @@ export interface DatabaseAdapter {
23
23
  getPaginatedEntries?: GetPaginatedEntries;
24
24
  countPaginatedEntries?: CountPaginatedEntries;
25
25
  createConsentLog: CreateConsentLog;
26
+ getConsentLogs?: GetConsentLogs;
27
+ countConsentLogs?: CountConsentLogs;
28
+ getConsentLog?: GetConsentLog;
26
29
  getMediaTags: GetMediaTags;
27
30
  createMediaTag: CreateMediaTag;
28
31
  updateMediaTag: UpdateMediaTag;
@@ -86,6 +89,9 @@ export type GetFormSubmission = (id: string) => Promise<FormSubmission | null>;
86
89
  export type UpdateFormSubmission = (id: string, data: Partial<FormSubmission>) => Promise<FormSubmission>;
87
90
  export type DeleteFormSubmission = (id: string) => Promise<void>;
88
91
  export type CreateConsentLog = (data: ConsentLogData) => Promise<void>;
92
+ export type GetConsentLogs = (filters: GetConsentLogsFilters) => Promise<ConsentLogRecord[]>;
93
+ export type CountConsentLogs = (filters: Omit<GetConsentLogsFilters, 'limit' | 'offset'>) => Promise<number>;
94
+ export type GetConsentLog = (id: string) => Promise<ConsentLogRecord | null>;
89
95
  export interface GetMediaFilesOptions {
90
96
  data: {
91
97
  tagIds?: string[];
@@ -8,6 +8,7 @@ import type { FormConfig } from './forms.js';
8
8
  import type { AIAdapter } from './adapters/ai.js';
9
9
  import type { EmailAdapter } from './adapters/email.js';
10
10
  import type { ResolvedShopConfig } from '../shop/types.js';
11
+ import type { ResolvedCmpConfig } from '../cmp/types.js';
11
12
  export interface VideoTranscodeConfig {
12
13
  /** Enable/disable auto-transcoding on upload (default: true) */
13
14
  transcode?: boolean;
@@ -79,6 +80,7 @@ export interface CMSConfig {
79
80
  typography?: TypographyConfig;
80
81
  sidebarHelp?: boolean;
81
82
  shop?: ResolvedShopConfig;
83
+ cmp?: ResolvedCmpConfig;
82
84
  }
83
85
  export interface ICMS {
84
86
  collections: Record<string, CollectionConfigWithType>;
@@ -96,4 +98,5 @@ export interface ICMS {
96
98
  apiKeys: ApiKeyConfig[];
97
99
  typographyConfig: TypographyConfig;
98
100
  shopConfig: ResolvedShopConfig | null;
101
+ cmpConfig: ResolvedCmpConfig | null;
99
102
  }
@@ -19,4 +19,15 @@ export interface ConsentLogData {
19
19
  };
20
20
  cmpVersion: string;
21
21
  policyVersion: string;
22
+ parentLogId?: string | null;
23
+ }
24
+ export interface ConsentLogRecord extends ConsentLogData {
25
+ timestamp: Date;
26
+ }
27
+ export interface GetConsentLogsFilters {
28
+ startDate?: Date;
29
+ endDate?: Date;
30
+ country?: string;
31
+ limit?: number;
32
+ offset?: number;
22
33
  }
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;
@@ -0,0 +1,15 @@
1
+ export const update = {
2
+ version: '0.15.5',
3
+ date: '2026-04-23',
4
+ description: 'Media field recovers from orphan references; delete dialog shows usage breakdown + replace hint. X-Frame-Options relaxed to SAMEORIGIN. Background maintenance no longer duplicates on Vite HMR. Runtime generator no longer triggers an infinite SSR reload loop in dev.',
5
+ features: [
6
+ 'Admin media library: delete dialog (`FileDetails`) teraz pokazuje breakdown użyć pliku per kolekcja/single ("Plik jest używany w: Strony: 1") oraz hint wskazujący na funkcję **Zamień plik** (która zachowuje ID → wszystkie referencje). Dane ładowane asynchronicznie przez nowy remote query `findMediaReferences(id)` uruchamiany w momencie otwarcia dialogu. Scope (MVP): tylko najnowsza wersja per entry; historia poza zakresem. Walker po schematach (`media`, `file`, `seo.ogImage`, `object`, `blocks`, `content` z inline blocks) wyekstrahowany z `resolveImageFields.ts` jako reużywalne `extractMediaIdsFromData(data, fields)`.'
7
+ ],
8
+ fixes: [
9
+ '`media-field.svelte` nie blokuje już edycji entry gdy pole media wskazuje na usunięty plik (orphan reference). Wcześniej render wchodził w `{:else if singleFile}` z `singleFile=null` po nieudanym `getFileById`, co wyrzucało UI do pustego diva bez przycisków Zmień/Usuń — user zablokowany, nie mógł wstawić nowego obrazu. Nowa gałąź `{:else}` renderuje dashed warning placeholder ("Brakujący plik" / "Missing file") z zachowanymi kontrolkami. Analogicznie dla multi-media: per-item fallback pozwala usunąć pojedynczy orphan z tablicy bez czyszczenia pozostałych.',
10
+ '`X-Frame-Options` zmieniony z `DENY` na `SAMEORIGIN` w middleware `securityHeaders`. Wcześniej `DENY` blokował nawet same-origin framing, przez co admin CMS nie mógł załadować preview entry w iframe (`previewUrl` w konfiguracji kolekcji/single). Safari egzekwuje to rygorystycznie ("Refused to display ... in a frame because it set X-Frame-Options to DENY"). `SAMEORIGIN` dalej chroni przed clickjackingiem z obcych domen.',
11
+ '`startBackgroundMaintenance()` jest teraz idempotentne. Wcześniej każde wywołanie tworzyło nowy `setTimeout`/`setInterval` bez czyszczenia poprzedniego — w `pnpm dev` Vite HMR re-executuje `hooks.server.ts` (i tym samym `initCMS()`) przy każdej zmianie, więc po kilku edycjach działało N równoległych przebiegów maintenance. Stan timerów (`pendingTimeout`, `timer`, `running`, `lastResult`, `nextRunAt`) przeniesiony na `globalThis[Symbol.for("includio.maintenance.state")]`, żeby przeżył re-eval modułu w dev. Dodany guard: gdy timer już zaplanowany, kolejne `start()` loguje `already scheduled, skipping` i wychodzi. `stopBackgroundMaintenance()` teraz czyści również initial 30s `setTimeout`, nie tylko interval. Dodany `import.meta.hot.dispose()` czyszczący timery gdy Vite unlinkuje moduł.',
12
+ '`generateRuntime()` (`generator.ts`) nie zapisuje już plików gdy treść się nie zmieniła. Wcześniej każdy wywołanie `includioCMS()` (a więc każdy SSR reload Vite) bezwarunkowo `writeFileSync` na 5 plikach w `src/lib/cms/runtime/` (`api.ts`, `types.ts`, `schemas.ts`, `schema.ts`, `remote.ts`) — co aktualizowało mtime, Vite wykrywał zmianę, robił `(ssr) page reload`, znów wołał `includioCMS()` → znów zapis → nieskończona pętla reload w `pnpm dev`, blokująca pracę. Nowy helper `writeIfChanged(filePath, content)` najpierw czyta plik i zapisuje wyłącznie gdy treść się różni.'
13
+ ],
14
+ breakingChanges: []
15
+ };
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;
@@ -0,0 +1,14 @@
1
+ export const update = {
2
+ version: '0.16.0',
3
+ date: '2026-04-29',
4
+ description: 'Hard reset martwego kodu — start drogi do v1.0.0. Wycięte: prototyp inline-edit, demo seed, demo routes. Zostają: cmp/ (backend działa), mockups/ (designy), isomorphic-dompurify (Faza 5), tippy.js (slash-command).',
5
+ features: [],
6
+ fixes: [],
7
+ breakingChanges: [
8
+ '`src/lib/inline-edit-proto/` — usunięty. Prototyp hybrid editor (`HybridEditor`, `EditableInline`, `EditablePanel`, `BlockWrapper`, `ModeToggle` itd.) skasowany w całości. Inline edit zostanie zaprojektowany od zera w v1.x z innym podejściem. Pełen kod zarchiwizowany lokalnie na branchu `archive/inline-edit-proto` (niepublikowany na origin). Projekty importujące cokolwiek z `$lib/inline-edit-proto/*` lub `includio-cms/inline-edit-proto` muszą wyciąć te wywołania — moduł nigdy nie był publikowany w `package.json` exports, więc realnie żadna external integracja się nie zerwie.',
9
+ '`src/lib/demo/seed.ts` — usunięte. Funkcja `seedDemoData()` (wpisywanie demo użytkownika `demo@includio.dev`) skasowana. Demo content zostanie zaprojektowany od zera w v1.x.',
10
+ '`src/routes/(site)/demo/` i `src/routes/admin/(afterLogin)/demo/` — usunięte. Demo pages (`/demo`, `/demo/inline-edit-test`, `/admin/demo/hybrid-editor`) skasowane razem z modułami. Live preview demo trzeba odbudować od zera.',
11
+ '`ROADMAP-EDITOR.md` — usunięty. Plan unified editora (Faza 0.2.0–0.5.0) zarchiwizowany razem z prototypem na branchu `archive/inline-edit-proto`.'
12
+ ],
13
+ notes: 'Brak SQL migration. Brak zmian publicznego API w `package.json` exports — `inline-edit-proto`, `demo`, ani `cmp` nigdy nie były tam wymienione, więc `pnpm i` w projekcie konsumującym przejdzie clean. `cmp/` ZOSTAJE: backend CMP (operations, `consent_logs` schema, public `createConsentLog`) i typy publiczne (`ResolvedCmpConfig`, `CmpStrings` w `CMSConfig.cmp`) działają bez zmian. Frontend banner dla CMP wróci jako feature w v1.x. Backlog `ideas/*.md` przeniesiony do `ideas/post-v1/` (lokalnie, katalog w `.gitignore`); `select-field-defaultvalue-bug.md` promowany do ROADMAP jako fix do v1.0. Bundle delta: `dist/` 8.8M → 8.5M (−0.3 MB) — mniej niż meta-plan szacował (−1.5 MB+), bo `inline-edit-proto/` był w `.gitignore` i nigdy nie trafiał do `dist/`. Realny zysk to czystsza struktura repo + zero martwego kodu w gałęzi roboczej.'
14
+ };
@@ -49,7 +49,9 @@ import { update as update0151 } from './0.15.1/index.js';
49
49
  import { update as update0152 } from './0.15.2/index.js';
50
50
  import { update as update0153 } from './0.15.3/index.js';
51
51
  import { update as update0154 } from './0.15.4/index.js';
52
- export const updates = [update0065, update0066, update0067, update0068, update0069, update010, update011, update012, update013, update014, update015, update020, update022, update050, update051, update052, update053, update054, update055, update056, update057, update058, update060, update061, update062, update070, update071, update072, update073, update080, update090, update0100, update0110, update0120, update0130, update0131, update0132, update0133, update0134, update0140, update0141, update0142, update0143, update0144, update0145, update0146, update0150, update0151, update0152, update0153, update0154];
52
+ import { update as update0155 } from './0.15.5/index.js';
53
+ import { update as update0160 } from './0.16.0/index.js';
54
+ export const updates = [update0065, update0066, update0067, update0068, update0069, update010, update011, update012, update013, update014, update015, update020, update022, update050, update051, update052, update053, update054, update055, update056, update057, update058, update060, update061, update062, update070, update071, update072, update073, update080, update090, update0100, update0110, update0120, update0130, update0131, update0132, update0133, update0134, update0140, update0141, update0142, update0143, update0144, update0145, update0146, update0150, update0151, update0152, update0153, update0154, update0155, update0160];
53
55
  export const getUpdatesFrom = (fromVersion) => {
54
56
  const fromParts = fromVersion.split('.').map(Number);
55
57
  return updates.filter((update) => {