From c358bc65f7fe6dd9a4b80fd4c8f3649bf96d7631 Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Sun, 6 Sep 2026 19:23:14 +0200 Subject: [PATCH] feat(usage): add consented beta capabilities and gallery photo totals --- .../__tests__/routes/usageXmpEvidence.test.js | 42 + .../services/usageCoverageInventory.test.js | 16 +- .../services/usageSnapshotSignals.test.js | 6 +- backend/__tests__/services/usageV3.test.js | 138 ++ backend/src/routes/adminContracts.js | 3 + backend/src/routes/adminCustomers.js | 3 + backend/src/routes/adminInvoices.js | 2 + backend/src/routes/adminPhotoExport.js | 3 + backend/src/routes/adminPhotos.js | 4 +- backend/src/routes/adminQuotes.js | 4 + backend/src/routes/adminUsage.js | 8 +- backend/src/usage/UsageService.js | 29 +- backend/src/usage/expandedSnapshot.js | 26 +- backend/src/usage/features.v3.json | 1531 ++++++++++++++ backend/src/usage/inventorySnapshot.js | 21 + backend/src/usage/schema.cjs | 33 +- docs/FEATURE_COVERAGE.md | 371 ++-- docs/PRODUCT_USAGE.md | 17 + docs/usage-coverage.v3.json | 1813 +++++++++++++++++ .../src/features/settings/UsageCatalog.tsx | 9 +- .../__tests__/ProductUsageTab.test.tsx | 2 +- .../features/settings/usageFeatures.v3.json | 1531 ++++++++++++++ frontend/src/i18n/locales/de.json | 95 +- frontend/src/i18n/locales/en.json | 95 +- frontend/src/services/productUsage.service.ts | 4 +- 25 files changed, 5497 insertions(+), 309 deletions(-) create mode 100644 backend/__tests__/routes/usageXmpEvidence.test.js create mode 100644 backend/__tests__/services/usageV3.test.js create mode 100644 backend/src/usage/features.v3.json create mode 100644 backend/src/usage/inventorySnapshot.js create mode 100644 docs/usage-coverage.v3.json create mode 100644 frontend/src/features/settings/usageFeatures.v3.json diff --git a/backend/__tests__/routes/usageXmpEvidence.test.js b/backend/__tests__/routes/usageXmpEvidence.test.js new file mode 100644 index 00000000..89002109 --- /dev/null +++ b/backend/__tests__/routes/usageXmpEvidence.test.js @@ -0,0 +1,42 @@ +const express = require('express'); +const request = require('supertest'); +const mockExport = jest.fn(); +const mockMarkUsed = jest.fn().mockResolvedValue(); +jest.mock('../../src/database/db', () => ({ + db: jest.fn(() => ({ where: jest.fn().mockReturnThis(), first: jest.fn().mockResolvedValue({ id: 1 }) })), + withRetry: fn => fn() +})); +jest.mock('../../src/middleware/auth', () => ({ adminAuth: (req, res, next) => { + if (!req.headers.authorization) return res.sendStatus(401); + req.admin = { id: 1 }; next(); +} })); +jest.mock('../../src/middleware/permissions', () => ({ requirePermission: () => (_req, _res, next) => next() })); +jest.mock('../../src/middleware/ownership', () => ({ requireEventOwnership: (_req, _res, next) => next() })); +jest.mock('../../src/services/photoExportService', () => ({ PhotoExportService: jest.fn().mockImplementation(() => ({ exportPhotos: mockExport })) })); +jest.mock('../../src/services/photoAdminMarksService', () => ({})); +jest.mock('../../src/services/feedbackService', () => ({})); +jest.mock('../../src/services/productUsageService', () => ({ markUsed: (...args) => mockMarkUsed(...args) })); +const { productUsage } = require('../../src/middleware/productUsage'); +const router = require('../../src/routes/adminPhotoExport'); +const app = express(); +app.use(express.json()); +app.use('/admin', productUsage); +app.use('/admin/photo-export', router); + +beforeEach(() => { mockMarkUsed.mockClear(); mockExport.mockReset(); }); +test('only a successful authenticated XMP export produces the new bit, without request or exported content', async () => { + mockExport.mockResolvedValue({ type: 'content', contentType: 'text/plain', filename: 'PRIVATE-export.txt', content: 'PRIVATE-content' }); + await request(app).post('/admin/photo-export/1/export').set('Authorization', 'test').send({ photo_ids: [9], format: 'xmp' }).expect(200); + expect(mockMarkUsed).toHaveBeenCalledWith(['photo_exports', 'photo_xmp_export'], { legacyFeatures: [], destinationBackup: false }); + expect(JSON.stringify(mockMarkUsed.mock.calls)).not.toContain('PRIVATE'); + mockMarkUsed.mockClear(); + await request(app).post('/admin/photo-export/1/export').set('Authorization', 'test').send({ photo_ids: [9], format: 'csv' }).expect(200); + expect(mockMarkUsed.mock.calls[0][0]).toEqual(['photo_exports']); +}); +test('failed, invalid and unauthenticated exports never produce an XMP-use marker', async () => { + mockExport.mockRejectedValue(new Error('Synthetic export failure')); + await request(app).post('/admin/photo-export/1/export').set('Authorization', 'test').send({ photo_ids: [9], format: 'xmp' }).expect(500); + await request(app).post('/admin/photo-export/1/export').set('Authorization', 'test').send({ photo_ids: [9], format: 'unknown' }).expect(400); + await request(app).post('/admin/photo-export/1/export').send({ photo_ids: [9], format: 'xmp' }).expect(401); + expect(mockMarkUsed).not.toHaveBeenCalled(); +}); diff --git a/backend/__tests__/services/usageCoverageInventory.test.js b/backend/__tests__/services/usageCoverageInventory.test.js index c365eed2..f421fd32 100644 --- a/backend/__tests__/services/usageCoverageInventory.test.js +++ b/backend/__tests__/services/usageCoverageInventory.test.js @@ -1,8 +1,8 @@ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); -const catalog = require('../../src/usage/features.v2.json'); -const inventory = require('../../../docs/usage-coverage.v2.json'); +const catalog = require('../../src/usage/features.v3.json'); +const inventory = require('../../../docs/usage-coverage.v3.json'); const protocol = require('../../src/usage/schema.cjs'); const { RULES_V2, capabilityKeys } = require('../../src/usage/capabilityRules'); const { acceptedUpload, capabilityEvidence } = require('../../src/usage/capabilityEvidence'); @@ -55,11 +55,13 @@ test('all current settings tabs have an explicit scope decision', () => { test('v1 wire validation is immutable; catalog, UI and translated descriptions agree', () => { expect(crypto.createHash('sha256').update(JSON.stringify(protocol.envelopeSchemas['usage.v1'].properties)).digest('hex')) .toBe('cc8d0a865d21e36d2b24d23ca6aa8dd8d48000cb17aef83996786f70755bc922'); - expect(protocol.FEATURE_KEYS).toHaveLength(73); + expect(crypto.createHash('sha256').update(JSON.stringify(protocol.envelopeSchemas['usage.v2'].properties)).digest('hex')) + .toBe('159821cf45c1951016d33a4ed9ca55a0a7ee1b60dd715b803fcfed33e5c8a846'); + expect(protocol.FEATURE_KEYS).toHaveLength(86); expect(protocol.LEGACY_FEATURE_KEYS).toHaveLength(19); - expect(inventory.configuration_only).toHaveLength(17); + expect(inventory.configuration_only).toHaveLength(23); const frontend = path.resolve(__dirname, '../../../frontend'); - expect(JSON.parse(fs.readFileSync(path.join(frontend, 'src/features/settings/usageFeatures.v2.json')))).toEqual(catalog); + expect(JSON.parse(fs.readFileSync(path.join(frontend, 'src/features/settings/usageFeatures.v3.json')))).toEqual(catalog); for (const lang of ['en', 'de']) { const translated = JSON.parse(fs.readFileSync(path.join(frontend, `src/i18n/locales/${lang}.json`))).productUsage.catalog; for (const [key, value] of Object.entries(catalog.features)) { @@ -70,9 +72,9 @@ test('v1 wire validation is immutable; catalog, UI and translated descriptions a test('every used field has either a fixed route rule or explicit trusted success evidence', () => { const explicit = ['custom_css', 'oauth', 'smtp', 'email_webhook', 'whatsapp', 'incoming_mail', - 'video_uploads', 'camera_raw_uploads', 's3_storage', 's3_photo_storage', 's3_backups', 'api_integration']; + 'video_uploads', 'camera_raw_uploads', 's3_storage', 's3_photo_storage', 's3_backups', 'api_integration', 'photo_xmp_export', 'photo_replacement', 'photo_admin_marks', 'crm_invoice_import', 'crm_combined_billing', 'crm_monthly_billing_manual', 'crm_document_conversion']; const covered = new Set([...explicit, ...RULES_V2.flatMap(([, , keys]) => keys)]); - expect(protocol.FEATURE_KEYS.filter(protocol.observesUse).filter((key) => !covered.has(key))).toEqual([]); + expect(protocol.FEATURE_KEYS.filter((key) => protocol.observesUse(key)).filter((key) => !covered.has(key))).toEqual([]); for (const key of covered) expect(protocol.observesUse(key)).toBe(true); }); diff --git a/backend/__tests__/services/usageSnapshotSignals.test.js b/backend/__tests__/services/usageSnapshotSignals.test.js index c12b5407..ad4d3403 100644 --- a/backend/__tests__/services/usageSnapshotSignals.test.js +++ b/backend/__tests__/services/usageSnapshotSignals.test.js @@ -10,7 +10,9 @@ */ const knex = require('knex'); const { UsageService } = require('../../src/usage/UsageService'); -const { FEATURE_KEYS, CATALOG, generateIdentity, makePacket, signPacket, verifyEnvelope } = require('../../src/usage/protocol.cjs'); +const { featureKeysFor, CATALOGS, generateIdentity, makePacket, signPacket, verifyEnvelope } = require('../../src/usage/protocol.cjs'); +const FEATURE_KEYS = featureKeysFor('usage.v2'); +const CATALOG = CATALOGS['usage.v2']; async function bootDb() { const db = knex({ @@ -369,7 +371,7 @@ describe('v2 technical configuration and privacy boundaries', () => { expect(await db('product_usage_markers').pluck('feature')).toHaveLength(56); expect(JSON.stringify(report)).not.toContain('PRIVATE'); const identity = generateIdentity(); - const envelope = signPacket(makePacket(identity, 'report', 1, report), identity, new Date(report.generated_at)); + const envelope = signPacket(makePacket(identity, 'report', 1, report, 'usage.v2'), identity, new Date(report.generated_at)); expect(verifyEnvelope(envelope, Date.parse(report.generated_at))).toEqual(envelope.packet); }); diff --git a/backend/__tests__/services/usageV3.test.js b/backend/__tests__/services/usageV3.test.js new file mode 100644 index 00000000..0f53da6e --- /dev/null +++ b/backend/__tests__/services/usageV3.test.js @@ -0,0 +1,138 @@ +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); +const knex = require('knex'); +const { UsageService } = require('../../src/usage/UsageService'); +const p = require('../../src/usage/protocol.cjs'); +const { expandSnapshot } = require('../../src/usage/expandedSnapshot'); +const { capabilityEvidence } = require('../../src/usage/capabilityEvidence'); + +for (const engine of ['sqlite3', ...(process.env.PICPEAK_PG_TEST_URL ? ['pg'] : [])]) { + describe(`usage.v3 on ${engine}`, () => { + let db, admin, schema, client; + const now = Date.parse('2026-09-06T12:00:00.000Z'); + const savedEnv = { ...process.env }; + beforeEach(async () => { + if (engine === 'pg') { + admin = knex({ client: 'pg', connection: process.env.PICPEAK_PG_TEST_URL }); + schema = `usage_v3_${crypto.randomUUID().replaceAll('-', '')}`; + await admin.schema.createSchema(schema); + db = knex({ client: 'pg', connection: process.env.PICPEAK_PG_TEST_URL, searchPath: [schema] }); + } else db = knex({ client: 'sqlite3', connection: { filename: ':memory:' }, useNullAsDefault: true }); + const migrations = path.resolve(__dirname, '../../migrations/core'); + for (const file of fs.readdirSync(migrations).filter(name => /^20[1-6]_product_usage/.test(name)).sort()) + await require(path.join(migrations, file)).up(db); + await db('product_usage_state').where({ id: 1 }).update({ status: 'active', consent_version: 'usage-consent.v3' }); + await db.schema.createTable('app_settings', t => { t.string('setting_key').primary(); t.text('setting_value'); }); + await db.schema.createTable('feature_flags', t => { t.string('key').primary(); t.boolean('value'); }); + await db.schema.createTable('events', t => { + t.increments('id'); t.text('color_theme'); t.string('external_path'); t.integer('css_template_id'); + t.string('default_photo_sort'); t.boolean('is_archived'); t.boolean('is_draft'); + }); + await db.schema.createTable('photos', t => { t.increments('id'); t.integer('event_id'); t.string('media_type'); t.string('filename'); }); + await db.schema.createTable('css_templates', t => { t.increments('id'); t.boolean('is_enabled'); t.text('css_content'); }); + await db.schema.createTable('photo_categories', t => { t.increments('id'); t.integer('event_id'); t.boolean('is_folder'); }); + await db.schema.createTable('workflows', t => { t.increments('id'); t.boolean('enabled'); }); + await db.schema.createTable('transfers', t => { + t.increments('id'); t.string('upload_token'); t.boolean('allow_uploads'); t.timestamp('deleted_at'); t.timestamp('upload_expires_at'); t.timestamp('expires_at'); + }); + for (const table of ['email_configs', 'mail_accounts']) + await db.schema.createTable(table, t => { t.increments('id'); t.string('smtp_host'); }); + await db.schema.createTable('whatsapp_configs', t => { t.increments('id'); t.boolean('enabled'); t.string('phone_number_id'); t.string('access_token'); }); + client = new UsageService(db, { now: () => now, secret: 'v3-test-only-secret'.repeat(3) }); + }); + afterEach(async () => { + process.env = { ...savedEnv }; + await db?.destroy(); + if (admin) { await admin.schema.dropSchema(schema, true); await admin.destroy(); admin = null; } + }); + + test('counts retained gallery/photo records, excluding videos, without loading entities', async () => { + await db('events').insert([{ is_draft: true }, { is_archived: true }, { is_archived: false }]); + await db('photos').insert([ + { event_id: 1, media_type: 'image', filename: 'PRIVATE-original.dng' }, + { event_id: 2, media_type: null, filename: 'PRIVATE-archive.jpg' }, + { event_id: 3, media_type: 'video', filename: 'PRIVATE-video.mov' }, + ]); + const queries = []; + db.on('query', q => queries.push(q.sql)); + const report = await client.snapshot(); + expect(report.inventory).toEqual({ galleries: 3, photos: 2 }); + expect(Object.keys(report.features)).toHaveLength(86); + expect(queries.filter(sql => /from ["`]photos["`]/.test(sql))).toEqual([expect.stringMatching(/select count\(\*\)/)]); + expect(JSON.stringify(report)).not.toContain('PRIVATE'); + const identity = p.generateIdentity(); + const envelope = p.signPacket(p.makePacket(identity, 'report', 1, report), identity, new Date(now)); + expect(p.verifyEnvelope(envelope, now).payload).toEqual(report); + await db('photos').where({ id: 1 }).delete(); + await db('events').where({ id: 1 }).delete(); + expect((await client.snapshot()).inventory).toEqual({ galleries: 2, photos: 1 }); + }); + + test.each(['usage.v1', 'usage.v2'])('%s consent never collects v3 markers or counts', async (version) => { + await db('product_usage_state').where({ id: 1 }).update({ consent_version: p.CONSENT_VERSIONS[version] }); + const queries = []; + db.on('query', q => queries.push(q.sql)); + await client.markUsed(['crm_invoice_import', 'photo_admin_marks', 'face_recognition']); + const report = await client.preview(); + expect(report).not.toHaveProperty('inventory'); + expect(report.features).not.toHaveProperty('crm_invoice_import'); + expect(await db('product_usage_markers').pluck('feature')).toEqual(['face_recognition']); + expect(queries.filter(sql => /from ["`]photos["`]/.test(sql))).toEqual([]); + expect(queries.some(sql => /count\(\*\)/.test(sql))).toBe(false); + expect((await client.status()).consent_update_available).toBe(true); + }); + + test('only allowed successful-capability bits survive and preview is read-only', async () => { + const res = { locals: {} }; + capabilityEvidence(res, 'photo_xmp_export', 'photo_replacement', 'photo_admin_marks', 'crm_invoice_import', + 'crm_combined_billing', 'crm_monthly_billing_manual', 'crm_document_conversion', 'PRIVATE@example.test', 'gallery_folders'); + await client.markUsed(res.locals.productUsageFeatures); + expect(await db('product_usage_markers').pluck('feature')).toHaveLength(7); + const before = await db('product_usage_markers').orderBy('feature'); + const report = await client.preview(); + expect(report.inventory).toEqual({ galleries: 0, photos: 0 }); + expect(report.features.crm_invoice_import.used).toBe(true); + expect(report.features.gallery_folders).not.toHaveProperty('used'); + expect(await db('product_usage_markers').orderBy('feature')).toEqual(before); + await db('product_usage_state').where({ id: 1 }).update({ status: 'deletion_pending' }); + await client.markUsed(['face_recognition']); + expect(await db('product_usage_markers').where({ feature: 'face_recognition' })).toHaveLength(0); + }); + + test('configuration reflects effective modules, applicable folders and unexpired upload permission', async () => { + await db('events').insert({ default_photo_sort: 'capture_date_asc' }); + await db('photo_categories').insert({ event_id: 1, is_folder: true }); + await db('workflows').insert({ enabled: true }); + await db('transfers').insert({ upload_token: 'PRIVATE', allow_uploads: true, upload_expires_at: '2026-09-07T00:00:00.000Z' }); + await db('app_settings').insert({ setting_key: 'general_use_original_filenames_for_downloads', setting_value: 'true' }); + process.env.STORAGE_BACKEND = 's3'; process.env.STORAGE_AUTO_IMPORT = 'true'; + process.env.STORAGE_S3_BUCKET = 'PRIVATE'; process.env.STORAGE_S3_ACCESS_KEY = 'PRIVATE'; process.env.STORAGE_S3_SECRET_KEY = 'PRIVATE'; + const snap = flags => expandSnapshot(db, { features: p.emptyFeatures('usage.v1'), flags, used: new Set(), now, version: 'usage.v3' }); + const enabled = await snap({ transfers: true, workflows: true, quotes: true, bills: true, incomingInvoices: true }); + for (const key of ['gallery_folders', 'transfer_upload_links', 'workflow_automation_enabled', 's3_auto_import', 'gallery_capture_date_sort', 'download_original_filenames', 'crm_invoice_import', 'crm_combined_billing']) + expect(enabled[key].configured).toBe(true); + expect(JSON.stringify(enabled)).not.toContain('PRIVATE'); + const disabled = await snap({ transfers: false, workflows: false, quotes: false, bills: true }); + for (const key of ['transfer_upload_links', 'workflow_automation_enabled', 'crm_invoice_import', 'crm_combined_billing']) + expect(disabled[key].configured).toBe(false); + await db('transfers').update({ upload_expires_at: '2026-09-06T12:00:00.000Z' }); + expect((await snap({ transfers: true })).transfer_upload_links.configured).toBe(false); + await db('transfers').update({ upload_expires_at: null, expires_at: '2026-09-07T00:00:00.000Z' }); + expect((await snap({ transfers: true })).transfer_upload_links.configured).toBe(true); + await db('transfers').update({ deleted_at: '2026-09-06T11:00:00.000Z' }); + expect((await snap({ transfers: true })).transfer_upload_links.configured).toBe(false); + await db('photo_categories').update({ event_id: 999 }); + expect((await snap({})).gallery_folders.configured).toBe(false); + }); + + test('ML recognition is already represented without querying faces or results', async () => { + await db('feature_flags').insert({ key: 'faces', value: true }); + await client.markUsed(['face_recognition']); + expect((await client.snapshot()).features.face_recognition).toEqual({ configured: true, used: true }); + process.env.PICPEAK_SINGLE_CONTAINER = 'true'; + expect((await client.snapshot()).features.face_recognition).toEqual({ configured: false, used: true }); + // No faces, people, embeddings or recognition-result tables exist in this fixture. + }); + }); +} diff --git a/backend/src/routes/adminContracts.js b/backend/src/routes/adminContracts.js index 94579212..0ca399a4 100644 --- a/backend/src/routes/adminContracts.js +++ b/backend/src/routes/adminContracts.js @@ -23,6 +23,7 @@ */ const express = require('express'); +const { capabilityEvidence } = require('../usage/capabilityEvidence'); const fs = require('fs'); const path = require('path'); const multer = require('multer'); @@ -414,6 +415,7 @@ router.post( handleAsync(async (req, res) => { validateRequest(req); const result = await contractService.convertToEvent(parseInt(req.params.id, 10), req.admin?.id); + if (!result.alreadyConverted) capabilityEvidence(res, 'crm_document_conversion'); return successResponse(res, result, 200, result.alreadyConverted ? 'Already converted to event' : 'Contract converted to event'); }), @@ -427,6 +429,7 @@ router.post( handleAsync(async (req, res) => { validateRequest(req); const result = await contractService.convertToInvoiceOnly(parseInt(req.params.id, 10), req.admin?.id); + if (!result.alreadyConverted) capabilityEvidence(res, 'crm_document_conversion'); return successResponse(res, result, 200, 'Invoices created from contract'); }), ); diff --git a/backend/src/routes/adminCustomers.js b/backend/src/routes/adminCustomers.js index 891875fb..ba553a2a 100644 --- a/backend/src/routes/adminCustomers.js +++ b/backend/src/routes/adminCustomers.js @@ -7,6 +7,7 @@ */ const express = require('express'); +const { capabilityEvidence } = require('../usage/capabilityEvidence'); const { body, param, query } = require('express-validator'); const { adminAuth } = require('../middleware/auth'); const { requirePermission } = require('../middleware/permissions'); @@ -719,6 +720,7 @@ router.post('/:id/bill-combined', [ { includeHours: req.body.includeHours !== false, includeRebills: req.body.includeRebills !== false }, req.admin.id, ); + if (result.invoiceId) capabilityEvidence(res, 'crm_combined_billing'); successResponse(res, result, 201); })); @@ -769,6 +771,7 @@ router.post('/:id/trigger-monthly-bill', [ parseInt(req.params.id, 10), req.admin.id, ); + if (result.invoiceId) capabilityEvidence(res, 'crm_monthly_billing_manual'); successResponse(res, result, 201); })); diff --git a/backend/src/routes/adminInvoices.js b/backend/src/routes/adminInvoices.js index d1ba008a..e1a3d084 100644 --- a/backend/src/routes/adminInvoices.js +++ b/backend/src/routes/adminInvoices.js @@ -19,6 +19,7 @@ */ const express = require('express'); +const { capabilityEvidence } = require('../usage/capabilityEvidence'); const { body, param, query } = require('express-validator'); const multer = require('multer'); const path = require('path'); @@ -563,6 +564,7 @@ router.post( const inserted = await db('invoices').insert(row).returning('id'); const invoiceId = typeof inserted[0] === 'object' ? inserted[0].id : inserted[0]; + capabilityEvidence(res, 'crm_invoice_import'); return successResponse(res, { invoice: transformInvoice(await db('invoices').where({ id: invoiceId }).first()), diff --git a/backend/src/routes/adminPhotoExport.js b/backend/src/routes/adminPhotoExport.js index ff5a0554..349b26d4 100644 --- a/backend/src/routes/adminPhotoExport.js +++ b/backend/src/routes/adminPhotoExport.js @@ -4,6 +4,7 @@ */ const express = require('express'); +const { capabilityEvidence } = require('../usage/capabilityEvidence'); const router = express.Router(); const { body, query, validationResult } = require('express-validator'); const { db, withRetry } = require('../database/db'); @@ -228,6 +229,8 @@ router.post('/:eventId/export', adminAuth, requirePermission('photos.download'), admin_id: req.admin.id, }); + if (format === 'xmp') capabilityEvidence(res, 'photo_xmp_export'); + if (result.type === 'stream') { res.setHeader('Content-Type', result.contentType); res.setHeader('Content-Disposition', `attachment; filename="${result.filename}"`); diff --git a/backend/src/routes/adminPhotos.js b/backend/src/routes/adminPhotos.js index ac0cc220..bd281f55 100644 --- a/backend/src/routes/adminPhotos.js +++ b/backend/src/routes/adminPhotos.js @@ -7,7 +7,7 @@ const { adminAuth } = require('../middleware/auth'); const { requirePermission } = require('../middleware/permissions'); const { ensureThumbnail } = require('../services/imageProcessor'); const { isVideoMimeType } = require('../services/videoProcessor'); -const { acceptedUpload } = require('../usage/capabilityEvidence'); +const { acceptedUpload, capabilityEvidence } = require('../usage/capabilityEvidence'); const { generatePhotoFilename, buildContentDisposition } = require('../utils/filenameSanitizer'); const { getUseOriginalFilenames, @@ -358,6 +358,7 @@ router.post('/:eventId/upload', adminAuth, requirePermission('photos.upload'), r event, }); if (result.success) { + capabilityEvidence(res, 'photo_replacement'); acceptedUpload(res, { video: isVideoMimeType(file.mimetype), raw: path.extname(file.originalname).toLowerCase() === '.dng', @@ -861,6 +862,7 @@ router.put('/:eventId/photos/:photoId/mark', adminAuth, requirePermission('photo parseInt(eventId, 10), photoId, req.admin.id, mark, ); + capabilityEvidence(res, 'photo_admin_marks'); res.json({ success: true, mark: result }); } catch (error) { // Validation errors from the service are the caller's fault, not a 500. diff --git a/backend/src/routes/adminQuotes.js b/backend/src/routes/adminQuotes.js index 89384926..b16ac813 100644 --- a/backend/src/routes/adminQuotes.js +++ b/backend/src/routes/adminQuotes.js @@ -27,6 +27,7 @@ */ const express = require('express'); +const { capabilityEvidence } = require('../usage/capabilityEvidence'); const { body, param, query } = require('express-validator'); const { adminAuth } = require('../middleware/auth'); const { requirePermission } = require('../middleware/permissions'); @@ -484,6 +485,7 @@ router.post( validateRequest(req); const id = parseInt(req.params.id, 10); const result = await quoteService.convertToEvent(id, req.admin.id); + if (!result.alreadyConverted) capabilityEvidence(res, 'crm_document_conversion'); return successResponse(res, result, 200, result.alreadyConverted ? 'Already converted' : 'Quote converted'); }) ); @@ -499,6 +501,7 @@ router.post( validateRequest(req); const id = parseInt(req.params.id, 10); const result = await quoteService.convertToInvoiceOnly(id, req.admin.id); + if (!result.alreadyConverted) capabilityEvidence(res, 'crm_document_conversion'); return successResponse(res, result, 200, 'Invoices created from quote'); }) ); @@ -519,6 +522,7 @@ router.post( const contractService = require('../services/contractService'); const id = parseInt(req.params.id, 10); const result = await contractService.createFromQuote(id, req.admin.id); + if (!result.alreadyConverted) capabilityEvidence(res, 'crm_document_conversion'); return successResponse(res, result, 200, result.alreadyConverted ? 'Already linked to a contract' : 'Contract drafted from quote'); }) diff --git a/backend/src/routes/adminUsage.js b/backend/src/routes/adminUsage.js index e14f15bf..1be02f29 100644 --- a/backend/src/routes/adminUsage.js +++ b/backend/src/routes/adminUsage.js @@ -5,7 +5,7 @@ const { adminAuth } = require('../middleware/auth'); const { requirePermission } = require('../middleware/permissions'); const { ValidationError } = require('../utils/errors'); const service = require('../services/productUsageService'); -const { ProtocolError } = require('../usage/protocol.cjs'); +const { ProtocolError, schemaForConsent } = require('../usage/protocol.cjs'); const router = express.Router(); const wrap = (fn) => (req, res, next) => Promise.resolve(fn(req, res)).catch((error) => { @@ -82,9 +82,9 @@ router.post( router.post( '/consent', wrap(async (req, res) => { - if (!req.body || Object.keys(req.body).length !== 1 || req.body.consent_version !== 'usage-consent.v2') - throw new ValidationError('Explicit usage v2 consent is required'); - res.json(await service.command('consent', { consent_version: 'usage-consent.v2' })); + if (!req.body || Object.keys(req.body).length !== 1 || !['usage.v2', 'usage.v3'].includes(schemaForConsent(req.body.consent_version))) + throw new ValidationError('Explicit usage consent is required'); + res.json(await service.command('consent', { consent_version: req.body.consent_version })); }) ); router.post( diff --git a/backend/src/usage/UsageService.js b/backend/src/usage/UsageService.js index 20037391..bd38341c 100644 --- a/backend/src/usage/UsageService.js +++ b/backend/src/usage/UsageService.js @@ -20,7 +20,9 @@ const { LEGACY_FEATURE_KEYS, CATALOG, CURRENT_SCHEMA_VERSION, - CURRENT_CONSENT_VERSION, + CONSENT_VERSIONS, + schemaForConsent, + schemaRank, featureKeysFor, observesUse, LAYOUTS @@ -130,8 +132,7 @@ class UsageService { } schemaVersion(state) { - return state?.consent_version === CURRENT_CONSENT_VERSION - ? CURRENT_SCHEMA_VERSION : 'usage.v1'; + return schemaForConsent(state?.consent_version) || 'usage.v1'; } constructor(db, options = {}) { this.db = db; @@ -343,7 +344,7 @@ class UsageService { return this.status(); } async enable(consent) { - if (!['usage-consent.v1', CURRENT_CONSENT_VERSION].includes(consent)) + if (!Object.values(CONSENT_VERSIONS).includes(consent)) throw new ValidationError('Explicit usage consent is required'); // Read BEFORE the lease, deliberately. locked() claims the lease and then // reads the row in a second statement; a /disable completing between @@ -670,7 +671,7 @@ class UsageService { await this.db.transaction(async (tx) => { const upgraded = await tx('product_usage_state') .where({ id: 1, status: 'active', installation_id: packet.installation_id }) - .update({ ...update, consent_version: CURRENT_CONSENT_VERSION }); + .update({ ...update, consent_version: packet.payload.consent_version }); if (upgraded) await tx('product_usage_markers').delete(); }); } else { @@ -816,7 +817,7 @@ class UsageService { .first(); if (destination && parse(destination.setting_value) === 's3') { allowed.push('s3_storage'); - if (version === CURRENT_SCHEMA_VERSION) allowed.push('s3_backups'); + if (version !== 'usage.v1') allowed.push('s3_backups'); } } await tx('product_usage_markers') @@ -841,7 +842,7 @@ class UsageService { rows.map((r) => [r.setting_key, parse(r.setting_value)]) ); const flagRows = await this.db('feature_flags') - .whereIn('key', version === CURRENT_SCHEMA_VERSION + .whereIn('key', version !== 'usage.v1' ? [...new Set([...Object.values(FLAG_MAP), 'incomingMail', ...Object.values(CATALOG.features).map((f) => f.flag).filter(Boolean)])] : Object.values(FLAG_MAP)) .select('key', 'value'); @@ -947,15 +948,16 @@ class UsageService { features.custom_css.used = true; } const now = new Date(this.now()).toISOString(); - const expanded = version === CURRENT_SCHEMA_VERSION - ? await require('./expandedSnapshot').expandSnapshot(this.db, { features, flags, used, now: this.now() }) + const expanded = version !== 'usage.v1' + ? await require('./expandedSnapshot').expandSnapshot(this.db, { features, flags, used, now: this.now(), version }) : features; return { picpeak_version: this.version, report_date: now.slice(0, 10), generated_at: now, features: expanded, - gallery_layouts: [...layouts].sort() + gallery_layouts: [...layouts].sort(), + ...(version === 'usage.v3' ? { inventory: await require('./inventorySnapshot').inventorySnapshot(this.db) } : {}) }; } @@ -974,14 +976,17 @@ class UsageService { throw new ConflictError('Retry the pending usage operation first'); if (!['feedback', 'vote', 'session', 'consent'].includes(action)) throw new ValidationError('Invalid usage action'); - if (action === 'consent' && state.consent_version === CURRENT_CONSENT_VERSION) + const targetVersion = action === 'consent' ? schemaForConsent(payload?.consent_version) : null; + if (action === 'consent' && (!targetVersion || targetVersion === 'usage.v1')) + throw new ValidationError('Explicit usage consent is required'); + if (action === 'consent' && schemaRank(targetVersion) <= schemaRank(this.schemaVersion(state))) throw new ConflictError('Usage consent is already current'); const packet = makePacket( state, action, Number(state.sequence) + 1, payload, - action === 'consent' ? CURRENT_SCHEMA_VERSION : this.schemaVersion(state) + action === 'consent' ? targetVersion : this.schemaVersion(state) ); // Validate the complete packet before storing an un-sendable operation. verifyEnvelope( diff --git a/backend/src/usage/expandedSnapshot.js b/backend/src/usage/expandedSnapshot.js index 53ffe305..2b966367 100644 --- a/backend/src/usage/expandedSnapshot.js +++ b/backend/src/usage/expandedSnapshot.js @@ -1,5 +1,5 @@ 'use strict'; -const { CATALOG, emptyFeatures } = require('./schema.cjs'); +const { CATALOGS, emptyFeatures } = require('./schema.cjs'); const { formatBoolean } = require('../utils/dbCompat'); const truth = (value) => value === true || value === 1 || value === '1'; @@ -14,8 +14,8 @@ const parse = (value) => { // Technical configuration only. Never read photos, feedback contents, guest / // customer / admin profiles, messages, audit logs, delivery logs or counts. // Presence queries return a literal 1, not even a row's identifying primary key. -async function expandSnapshot(db, { features, flags, used, now }) { - const result = { ...emptyFeatures('usage.v2'), ...features }; +async function expandSnapshot(db, { features, flags, used, now, version = 'usage.v2' }) { + const result = { ...emptyFeatures(version), ...features }; const effective = { analytics: true, userManagement: true, ...flags }; if (!effective.quotes) effective.bills = false; if (effective.bills) effective.accounting = true; @@ -27,7 +27,7 @@ async function expandSnapshot(db, { features, flags, used, now }) { effective.clients = ['customerPortal', 'quotes', 'bills', 'contracts', 'projects', 'calendar', 'hoursLogging', 'newsletters'] .some((flag) => effective[flag]); if (['1', 'true', 'yes'].includes(String(process.env.PICPEAK_SINGLE_CONTAINER || '').toLowerCase())) effective.faces = false; - for (const [key, definition] of Object.entries(CATALOG.features)) { + for (const [key, definition] of Object.entries(CATALOGS[version].features)) { if (definition.configuration === 'builtin') result[key].configured = true; if (definition.flag) result[key].configured = Boolean(effective[definition.flag]); if (definition.used && key !== 'custom_css') result[key].used = used.has(key); @@ -116,6 +116,24 @@ async function expandSnapshot(db, { features, flags, used, now }) { query.where({ feedback_enabled: formatBoolean(true), [column]: formatBoolean(true) })); result.gallery_guest_accounts.configured = await exists('event_feedback_settings', ['feedback_enabled', 'identity_mode'], (query) => query.where('feedback_enabled', formatBoolean(true)).whereIn('identity_mode', ['guest', 'shared'])); + if (version === 'usage.v3') { + result.gallery_folders.configured = await exists('photo_categories', ['is_folder', 'event_id'], (query) => + query.where('is_folder', formatBoolean(true)).where((q) => q.whereNull('event_id').orWhereIn('event_id', db('events').select('id')))); + result.transfer_upload_links.configured = Boolean(effective.transfers) && await exists('transfers', + ['allow_uploads', 'deleted_at', 'upload_expires_at', 'expires_at', 'upload_token'], (query) => + query.where('allow_uploads', formatBoolean(true)).whereNull('deleted_at').whereNotNull('upload_token').whereNot('upload_token', '') + .where((q) => q.where('upload_expires_at', '>', new Date(now).toISOString()) + .orWhere((fallback) => fallback.whereNull('upload_expires_at').where((expiry) => + expiry.whereNull('expires_at').orWhere('expires_at', '>', new Date(now).toISOString()))))); + result.workflow_automation_enabled.configured = Boolean(effective.workflows) && await enabled('workflows', 'enabled'); + result.s3_auto_import.configured = result.s3_photo_storage.configured && process.env.STORAGE_AUTO_IMPORT === 'true'; + result.crm_combined_billing.configured = Boolean(effective.bills && effective.incomingInvoices); + result.crm_document_conversion.configured = Boolean(effective.quotes || effective.contracts); + result.gallery_capture_date_sort.configured = await exists('events', ['default_photo_sort'], (query) => + query.whereIn('default_photo_sort', ['capture_date_asc', 'capture_date_desc'])); + const originalNames = await db('app_settings').where({ setting_key: 'general_use_original_filenames_for_downloads' }).first('setting_value'); + result.download_original_filenames.configured = truth(parse(originalNames?.setting_value)); + } return result; } module.exports = { expandSnapshot }; diff --git a/backend/src/usage/features.v3.json b/backend/src/usage/features.v3.json new file mode 100644 index 00000000..acb1de0b --- /dev/null +++ b/backend/src/usage/features.v3.json @@ -0,0 +1,1531 @@ +{ + "schema_version": "usage.v3", + "consent_version": "usage-consent.v3", + "features": { + "crm": { + "category": "crm", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "clients", + "name": { + "en": "Client management", + "de": "Kundenverwaltung" + }, + "configured": { + "en": "The clients capability switch is effectively enabled; only a boolean.", + "de": "Der Funktionsschalter clients ist effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "crm_quotes": { + "category": "crm", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "quotes", + "name": { + "en": "Quotes", + "de": "Angebote" + }, + "configured": { + "en": "The quotes capability switch is effectively enabled; only a boolean.", + "de": "Der Funktionsschalter quotes ist effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "crm_invoices": { + "category": "crm", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "bills", + "name": { + "en": "Invoices", + "de": "Rechnungen" + }, + "configured": { + "en": "The bills capability switch is effectively enabled; only a boolean.", + "de": "Der Funktionsschalter bills ist effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "crm_contracts": { + "category": "crm", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "contracts", + "name": { + "en": "Contracts", + "de": "Verträge" + }, + "configured": { + "en": "The contracts capability switch is effectively enabled; only a boolean.", + "de": "Der Funktionsschalter contracts ist effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "crm_projects": { + "category": "crm", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "projects", + "name": { + "en": "Projects", + "de": "Projekte" + }, + "configured": { + "en": "The projects capability switch is effectively enabled; only a boolean.", + "de": "Der Funktionsschalter projects ist effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "crm_calendar": { + "category": "crm", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "calendar", + "name": { + "en": "Admin calendar", + "de": "Admin-Kalender" + }, + "configured": { + "en": "The calendar capability switch is effectively enabled; only a boolean.", + "de": "Der Funktionsschalter calendar ist effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "crm_hours": { + "category": "crm", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "hoursLogging", + "name": { + "en": "Hours logging", + "de": "Zeiterfassung" + }, + "configured": { + "en": "The hoursLogging capability switch is effectively enabled; only a boolean.", + "de": "Der Funktionsschalter hoursLogging ist effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "customer_portal": { + "category": "crm", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "customerPortal", + "name": { + "en": "Customer portal", + "de": "Kundenportal" + }, + "configured": { + "en": "The customerPortal capability switch is effectively enabled; only a boolean.", + "de": "Der Funktionsschalter customerPortal ist effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "accounting": { + "category": "accounting", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "accounting", + "name": { + "en": "Accounting", + "de": "Buchhaltung" + }, + "configured": { + "en": "The accounting capability switch is effectively enabled; only a boolean.", + "de": "Der Funktionsschalter accounting ist effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "workflows": { + "category": "automation", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "workflows", + "name": { + "en": "Workflows", + "de": "Workflows" + }, + "configured": { + "en": "The workflows capability switch is effectively enabled; only a boolean.", + "de": "Der Funktionsschalter workflows ist effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "newsletters": { + "category": "communication", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "newsletters", + "name": { + "en": "Newsletters", + "de": "Newsletter" + }, + "configured": { + "en": "The newsletters capability switch is effectively enabled; only a boolean.", + "de": "Der Funktionsschalter newsletters ist effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "face_recognition": { + "category": "gallery", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "faces", + "name": { + "en": "ML face recognition", + "de": "ML-Gesichtserkennung" + }, + "configured": { + "en": "The faces capability switch is effectively enabled; only a boolean.", + "de": "Der Funktionsschalter faces ist effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "custom_css": { + "category": "appearance", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Custom CSS", + "de": "Eigenes CSS" + }, + "configured": { + "en": "Custom CSS is configured globally or applied through a gallery/theme/template; CSS text is not sent.", + "de": "Eigenes CSS ist global oder über Galerie/Theme/Vorlage eingerichtet; CSS-Inhalte werden nicht gesendet." + }, + "used": { + "en": "Applied CSS observed after consent, without observing visitors.", + "de": "Angewendetes CSS nach Zustimmung festgestellt, ohne Besucher zu beobachten." + } + }, + "oauth": { + "category": "integration", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Admin SSO", + "de": "Admin-SSO" + }, + "configured": { + "en": "Admin OIDC is enabled and issuer/client configuration is present; no provider or credential values.", + "de": "Admin-OIDC ist aktiviert und die Anbieter-/Client-Konfiguration vorhanden; keine Anbieter- oder Zugangsdaten." + }, + "used": { + "en": "Successful admin SSO login; no account, identity-provider or session details.", + "de": "Erfolgreiche Admin-SSO-Anmeldung; keine Konto-, Anbieter- oder Sitzungsdetails." + } + }, + "smtp": { + "category": "communication", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "SMTP delivery", + "de": "SMTP-Versand" + }, + "configured": { + "en": "An outgoing SMTP host is configured; no host, account, address or credentials.", + "de": "Ein ausgehender SMTP-Host ist konfiguriert; keine Hosts, Konten, Adressen oder Zugangsdaten." + }, + "used": { + "en": "A successful explicitly initiated admin SMTP test/send; no recipients or messages.", + "de": "Erfolgreicher ausdrücklich ausgelöster Admin-SMTP-Test/-Versand; keine Empfänger oder Nachrichten." + } + }, + "whatsapp": { + "category": "communication", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "WhatsApp integration", + "de": "WhatsApp-Integration" + }, + "configured": { + "en": "The WhatsApp capability is enabled and a usable configuration is present; no phone number, token or template.", + "de": "Die WhatsApp-Funktion ist aktiviert und eine nutzbare Konfiguration vorhanden; keine Telefonnummer, Tokens oder Vorlagen." + }, + "used": { + "en": "Successful admin integration test; no recipient, message or delivery history.", + "de": "Erfolgreicher Admin-Integrationstest; keine Empfänger, Nachrichten oder Zustellverläufe." + } + }, + "backup": { + "category": "operations", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Backups", + "de": "Sicherungen" + }, + "configured": { + "en": "A full or database backup schedule is enabled; no schedule, path, storage sizes or backup names.", + "de": "Ein Voll- oder Datenbanksicherungsplan ist aktiviert; keine Zeitpläne, Pfade, Speichergrößen oder Sicherungsnamen." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "s3_storage": { + "category": "integration", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "S3 storage", + "de": "S3-Speicher" + }, + "configured": { + "en": "S3 is configured for media or backups; no bucket, endpoint, credentials or object keys.", + "de": "S3 ist für Medien oder Sicherungen konfiguriert; keine Buckets, Endpunkte, Zugangsdaten oder Objektschlüssel." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "share_mounts": { + "category": "integration", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "External folders", + "de": "Externe Ordner" + }, + "configured": { + "en": "At least one gallery uses an external folder; only existence, no folder paths or gallery identifiers.", + "de": "Mindestens eine Galerie verwendet einen externen Ordner; nur Existenz, keine Ordnerpfade oder Galeriekennungen." + }, + "used": { + "en": "An admin initiated an accepted external-folder import; no scanned paths, files or counts.", + "de": "Ein Admin hat einen angenommenen Import aus einem externen Ordner ausgelöst; keine Pfade, Dateien oder Anzahlen." + } + }, + "galleries": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Gallery management", + "de": "Galerieverwaltung" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use.", + "de": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "photo_management": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Media management", + "de": "Medienverwaltung" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use.", + "de": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "photo_exports": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Admin media export", + "de": "Admin-Medienexport" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use.", + "de": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "photo_processing": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Media maintenance tools", + "de": "Medien-Wartungswerkzeuge" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use.", + "de": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "archive_management": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Gallery archives", + "de": "Galeriearchive" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use.", + "de": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "gallery_sharing": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Gallery sharing and QR", + "de": "Galeriefreigabe und QR" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use.", + "de": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "short_links": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Short links", + "de": "Kurzlinks" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use.", + "de": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "gallery_categories": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Photo categories", + "de": "Fotokategorien" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use.", + "de": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "event_types": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Event types and presets", + "de": "Ereignistypen und Vorlagen" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use.", + "de": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "slideshow": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "slideshow", + "name": { + "en": "Live slideshow", + "de": "Live-Diashow" + }, + "configured": { + "en": "The slideshow capability switch is effectively enabled; only a boolean.", + "de": "Der Funktionsschalter slideshow ist effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "transfers": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "transfers", + "name": { + "en": "PicTransfer", + "de": "PicTransfer" + }, + "configured": { + "en": "The transfers capability switch is effectively enabled; only a boolean.", + "de": "Der Funktionsschalter transfers ist effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "video_uploads": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Admin video uploads", + "de": "Admin-Video-Uploads" + }, + "configured": { + "en": "Video extensions are allowed in global upload settings; no uploaded-file metadata.", + "de": "Videoformate sind in den globalen Upload-Einstellungen erlaubt; keine Metadaten hochgeladener Dateien." + }, + "used": { + "en": "At least one admin video file was successfully stored/accepted; no names, formats, lengths, sizes or processing/visitor history.", + "de": "Mindestens eine Admin-Videodatei wurde erfolgreich gespeichert/angenommen; keine Namen, Formate, Längen, Größen oder Verarbeitungs-/Besucherverläufe." + } + }, + "camera_raw_uploads": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Admin camera RAW uploads", + "de": "Admin-Kamera-RAW-Uploads" + }, + "configured": { + "en": "Camera RAW (DNG) is allowed in global upload settings; no camera models or EXIF.", + "de": "Kamera-RAW (DNG) ist in den globalen Upload-Einstellungen erlaubt; keine Kameramodelle oder EXIF-Daten." + }, + "used": { + "en": "At least one admin camera RAW upload was stored/accepted; only the capability bit, no filename or metadata.", + "de": "Mindestens ein Admin-Kamera-RAW-Upload wurde gespeichert/angenommen; nur das Capability-Bit, keine Dateinamen oder Metadaten." + } + }, + "messaging": { + "category": "communication", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "messaging", + "name": { + "en": "Messaging tools", + "de": "Nachrichtenwerkzeuge" + }, + "configured": { + "en": "The messaging capability switch is effectively enabled; only a boolean.", + "de": "Der Funktionsschalter messaging ist effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "incoming_mail": { + "category": "communication", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "IMAP intake", + "de": "IMAP-Empfang" + }, + "configured": { + "en": "Incoming mail is enabled and an IMAP configuration is present; no mailbox, server, folders or credentials.", + "de": "Eingehende E-Mails sind aktiviert und eine IMAP-Konfiguration vorhanden; keine Postfächer, Server, Ordner oder Zugangsdaten." + }, + "used": { + "en": "A successful explicit admin connection test or non-skipped manual poll; no background intake, messages, attachments or counts.", + "de": "Erfolgreicher expliziter Admin-Verbindungstest oder nicht übersprungener manueller Abruf; kein Hintergrundempfang, keine Nachrichten, Anhänge oder Anzahlen." + } + }, + "reminder_emails": { + "category": "communication", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "flag": "reminderEmails", + "name": { + "en": "Automatic event reminders", + "de": "Automatische Ereigniserinnerungen" + }, + "configured": { + "en": "The reminderEmails capability switch is effectively enabled; only a boolean.", + "de": "Der Funktionsschalter reminderEmails ist effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": null + }, + "email_templates": { + "category": "communication", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Email templates", + "de": "E-Mail-Vorlagen" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use.", + "de": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "email_webhook": { + "category": "communication", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Email webhook transport", + "de": "E-Mail-Webhook-Transport" + }, + "configured": { + "en": "Both email webhook settings are present; no URL or secret.", + "de": "Beide E-Mail-Webhook-Einstellungen sind vorhanden; keine URL oder Geheimnisse." + }, + "used": { + "en": "Successful explicitly initiated admin send/test through the webhook transport; no recipients, messages or automatic deliveries.", + "de": "Erfolgreicher ausdrücklich ausgelöster Admin-Versand/-Test über den Webhook-Transport; keine Empfänger, Nachrichten oder automatischen Zustellungen." + } + }, + "accounting_incoming_invoices": { + "category": "accounting", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "incomingInvoices", + "name": { + "en": "Incoming invoices", + "de": "Eingangsrechnungen" + }, + "configured": { + "en": "The incomingInvoices capability switch is effectively enabled; only a boolean.", + "de": "Der Funktionsschalter incomingInvoices ist effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "accounting_expenses": { + "category": "accounting", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "expenses", + "name": { + "en": "Expenses", + "de": "Ausgaben" + }, + "configured": { + "en": "The expenses capability switch is effectively enabled; only a boolean.", + "de": "Der Funktionsschalter expenses ist effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "accounting_tax_report": { + "category": "accounting", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "taxReport", + "name": { + "en": "Tax reports", + "de": "Steuerberichte" + }, + "configured": { + "en": "The taxReport capability switch is effectively enabled; only a boolean.", + "de": "Der Funktionsschalter taxReport ist effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "accounting_ledger": { + "category": "accounting", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "accounting", + "name": { + "en": "Ledger and accounting export", + "de": "Kontenplan und Buchhaltungsexport" + }, + "configured": { + "en": "The accounting capability switch is effectively enabled; only a boolean.", + "de": "Der Funktionsschalter accounting ist effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "crm_installments": { + "category": "crm", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Installment-plan tools", + "de": "Ratenplan-Werkzeuge" + }, + "configured": { + "en": "Quotes or invoices are enabled; no actual payment plans, amounts or statuses are inspected.", + "de": "Angebote oder Rechnungen sind aktiviert; tatsächliche Ratenpläne, Beträge oder Zahlungsstatus werden nicht geprüft." + }, + "used": { + "en": "An admin saved an installment plan; no dates, amounts, currencies, payment status or document IDs.", + "de": "Ein Admin hat einen Ratenplan gespeichert; keine Termine, Beträge, Währungen, Zahlungsstatus oder Dokumentkennungen." + } + }, + "document_templates": { + "category": "crm", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Document presets and blocks", + "de": "Dokumentvorlagen und Bausteine" + }, + "configured": { + "en": "Quotes or contracts are enabled, making document presets/blocks available; no template content.", + "de": "Angebote oder Verträge sind aktiviert und stellen Dokumentvorlagen/-bausteine bereit; keine Vorlageninhalte." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "cms": { + "category": "appearance", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "CMS pages", + "de": "CMS-Seiten" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use.", + "de": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "public_site": { + "category": "appearance", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Public landing page", + "de": "Öffentliche Startseite" + }, + "configured": { + "en": "The public landing-page setting is enabled; no page HTML, texts, domains or visitors.", + "de": "Die Einstellung für die öffentliche Startseite ist aktiviert; keine HTML-Inhalte, Texte, Domains oder Besucher." + }, + "used": null + }, + "branding": { + "category": "appearance", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Branding settings", + "de": "Branding-Einstellungen" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use.", + "de": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "seo_customization": { + "category": "appearance", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "SEO settings", + "de": "SEO-Einstellungen" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use.", + "de": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "admin_management": { + "category": "operations", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "userManagement", + "name": { + "en": "Admin and role management", + "de": "Admin- und Rollenverwaltung" + }, + "configured": { + "en": "The userManagement capability switch is effectively enabled; only a boolean.", + "de": "Der Funktionsschalter userManagement ist effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "api_integration": { + "category": "integration", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "HTTP API integration", + "de": "HTTP-API-Integration" + }, + "configured": { + "en": "An unrevoked, unexpired API credential exists; no tokens, names, scopes or owner data.", + "de": "Ein nicht widerrufener und nicht abgelaufener API-Zugang existiert; keine Tokens, Namen, Berechtigungswerte oder Inhaberdaten." + }, + "used": { + "en": "Successful authenticated HTTP API capability call; only this bit, never URLs, request values, token/owner IDs or call counts. Does not trigger a report.", + "de": "Erfolgreicher authentifizierter HTTP-API-Funktionsaufruf; nur dieses Bit, niemals URLs, Requestwerte, Token-/Inhaberkennungen oder Aufrufzahlen. Löst keinen Report aus." + } + }, + "webhooks": { + "category": "integration", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Outbound webhooks", + "de": "Ausgehende Webhooks" + }, + "configured": { + "en": "At least one active webhook is configured; no destinations, subscriptions, secrets or delivery logs.", + "de": "Mindestens ein aktiver Webhook ist konfiguriert; keine Ziele, Abonnements, Geheimnisse oder Zustellprotokolle." + }, + "used": { + "en": "Successful explicit admin webhook test/replay; no automatic or visitor-triggered deliveries.", + "de": "Erfolgreicher expliziter Admin-Webhook-Test/-Replay; keine automatischen oder durch Besucher ausgelösten Zustellungen." + } + }, + "restore": { + "category": "operations", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Restore", + "de": "Wiederherstellung" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use.", + "de": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "portable_backup": { + "category": "operations", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Portable PicPeak export/import", + "de": "Portabler PicPeak-Export/Import" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use.", + "de": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "database_backup": { + "category": "operations", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Database backups", + "de": "Datenbanksicherungen" + }, + "configured": { + "en": "Scheduled database backups are enabled; no schedules, file names or database contents.", + "de": "Geplante Datenbanksicherungen sind aktiviert; keine Zeitpläne, Dateinamen oder Datenbankinhalte." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "s3_photo_storage": { + "category": "integration", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "S3 media storage", + "de": "S3-Medienspeicher" + }, + "configured": { + "en": "S3 is the configured media backend and required credentials are present; no values are sent.", + "de": "S3 ist als Medienspeicher konfiguriert und erforderliche Zugangsdaten sind vorhanden; keine Werte werden gesendet." + }, + "used": { + "en": "Successful admin media storage/accepted upload to S3; no buckets, objects or sizes.", + "de": "Erfolgreiche Admin-Medienspeicherung/angenommener Upload nach S3; keine Buckets, Objekte oder Größen." + } + }, + "s3_backups": { + "category": "integration", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "S3 backup destination", + "de": "S3-Sicherungsziel" + }, + "configured": { + "en": "The configured backup destination is S3 with a bucket present; no bucket or credentials.", + "de": "Das konfigurierte Sicherungsziel ist S3 und ein Bucket ist angegeben; kein Bucketname oder Zugangsdaten." + }, + "used": { + "en": "An admin started a backup to the configured S3 destination or a successful S3 test upload; local exports never imply S3 use.", + "de": "Ein Admin hat eine Sicherung zum konfigurierten S3-Ziel oder einen erfolgreichen S3-Testupload gestartet; lokale Exporte implizieren keine S3-Nutzung." + } + }, + "analytics_dashboard": { + "category": "operations", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "analytics", + "name": { + "en": "Existing analytics module", + "de": "Bestehendes Analytics-Modul" + }, + "configured": { + "en": "The analytics capability switch is effectively enabled; only a boolean.", + "de": "Der Funktionsschalter analytics ist effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "feedback_moderation": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Feedback moderation", + "de": "Feedback-Moderation" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use.", + "de": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "guest_management": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Guest administration tools", + "de": "Gastverwaltungswerkzeuge" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use.", + "de": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts.", + "de": "Eine dokumentierte erfolgreiche authentifizierte Admin-Funktionsoperation wurde seit Zustimmung zu diesem Schema beobachtet. Keine Person, Operationshistorie, Parameter oder Anzahlen." + } + }, + "gallery_feedback_likes": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery likes enabled", + "de": "Galerie-Likes aktiviert" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts.", + "de": "In der betreffenden Galerie-/Globalkonfiguration aktiviert; nur installationsweite Existenz, niemals Galeriekennungen oder Anzahlen." + }, + "used": null + }, + "gallery_feedback_ratings": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery star ratings enabled", + "de": "Galerie-Sternebewertungen aktiviert" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts.", + "de": "In der betreffenden Galerie-/Globalkonfiguration aktiviert; nur installationsweite Existenz, niemals Galeriekennungen oder Anzahlen." + }, + "used": null + }, + "gallery_feedback_comments": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery comments enabled", + "de": "Galerie-Kommentare aktiviert" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts.", + "de": "In der betreffenden Galerie-/Globalkonfiguration aktiviert; nur installationsweite Existenz, niemals Galeriekennungen oder Anzahlen." + }, + "used": null + }, + "gallery_feedback_favorites": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery favorites enabled", + "de": "Galerie-Favoriten aktiviert" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts.", + "de": "In der betreffenden Galerie-/Globalkonfiguration aktiviert; nur installationsweite Existenz, niemals Galeriekennungen oder Anzahlen." + }, + "used": null + }, + "gallery_feedback_reactions": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery reactions enabled", + "de": "Galerie-Reaktionen aktiviert" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts.", + "de": "In der betreffenden Galerie-/Globalkonfiguration aktiviert; nur installationsweite Existenz, niemals Galeriekennungen oder Anzahlen." + }, + "used": null + }, + "gallery_feedback_color_labels": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery color labels enabled", + "de": "Galerie-Farblabels aktiviert" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts.", + "de": "In der betreffenden Galerie-/Globalkonfiguration aktiviert; nur installationsweite Existenz, niemals Galeriekennungen oder Anzahlen." + }, + "used": null + }, + "gallery_guest_accounts": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Guest identities enabled", + "de": "Gastidentitäten aktiviert" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts.", + "de": "In der betreffenden Galerie-/Globalkonfiguration aktiviert; nur installationsweite Existenz, niemals Galeriekennungen oder Anzahlen." + }, + "used": null + }, + "gallery_guest_uploads": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Guest uploads enabled", + "de": "Gast-Uploads aktiviert" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts.", + "de": "In der betreffenden Galerie-/Globalkonfiguration aktiviert; nur installationsweite Existenz, niemals Galeriekennungen oder Anzahlen." + }, + "used": null + }, + "gallery_downloads": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery downloads allowed", + "de": "Galerie-Downloads erlaubt" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts.", + "de": "In der betreffenden Galerie-/Globalkonfiguration aktiviert; nur installationsweite Existenz, niemals Galeriekennungen oder Anzahlen." + }, + "used": null + }, + "download_resolution_picker": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Download resolution picker enabled", + "de": "Download-Auflösungswahl aktiviert" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts.", + "de": "In der betreffenden Galerie-/Globalkonfiguration aktiviert; nur installationsweite Existenz, niemals Galeriekennungen oder Anzahlen." + }, + "used": null + }, + "gallery_client_access": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Client access enabled", + "de": "Client-Zugang aktiviert" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts.", + "de": "In der betreffenden Galerie-/Globalkonfiguration aktiviert; nur installationsweite Existenz, niemals Galeriekennungen oder Anzahlen." + }, + "used": null + }, + "gallery_watermarks": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Watermarks enabled", + "de": "Wasserzeichen aktiviert" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts.", + "de": "In der betreffenden Galerie-/Globalkonfiguration aktiviert; nur installationsweite Existenz, niemals Galeriekennungen oder Anzahlen." + }, + "used": null + }, + "gallery_image_protection": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Image protection enabled", + "de": "Bildschutz aktiviert" + }, + "configured": { + "en": "Enabled beyond the shipped defaults — a stronger protection level, canvas rendering, or right-click disabled — globally or on at least one gallery; only existence across the installation, never gallery IDs or counts.", + "de": "Über die Auslieferungsvorgaben hinaus aktiviert — höhere Schutzstufe, Canvas-Rendering oder deaktivierter Rechtsklick — global oder in mindestens einer Galerie; nur installationsweite Existenz, niemals Galeriekennungen oder Anzahlen." + }, + "used": null + }, + "gallery_reveal": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery reveal enabled", + "de": "Galerie-Enthüllung aktiviert" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts.", + "de": "In der betreffenden Galerie-/Globalkonfiguration aktiviert; nur installationsweite Existenz, niemals Galeriekennungen oder Anzahlen." + }, + "used": null + }, + "gallery_expiration": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery expiration configured", + "de": "Galerieablauf konfiguriert" + }, + "configured": { + "en": "At least one gallery has an expiry configured; no dates, gallery IDs or counts.", + "de": "Mindestens eine Galerie hat einen Ablauf konfiguriert; keine Daten, Galeriekennungen oder Anzahlen." + }, + "used": null + }, + "photo_xmp_export": { + "category": "gallery", + "since": "usage.v3", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "XMP export", + "de": "XMP-Export" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use.", + "de": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis." + }, + "used": { + "en": "An admin successfully generated an XMP export; no sidecars, filenames, ratings, selections or counts.", + "de": "Ein Admin hat erfolgreich einen XMP-Export erstellt; keine Sidecars, Dateinamen, Bewertungen, Auswahlen oder Anzahlen." + } + }, + "photo_replacement": { + "category": "gallery", + "since": "usage.v3", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Photo replacement", + "de": "Fotoersetzung" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use.", + "de": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis." + }, + "used": { + "en": "An admin upload actually replaced a photo successfully; no filenames, matching values, IDs or counts.", + "de": "Ein Admin-Upload hat tatsächlich erfolgreich ein Foto ersetzt; keine Dateinamen, Abgleichwerte, Kennungen oder Anzahlen." + } + }, + "photo_admin_marks": { + "category": "gallery", + "since": "usage.v3", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Photographer marks", + "de": "Fotografenmarkierungen" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use.", + "de": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis." + }, + "used": { + "en": "An admin successfully saved their own photo mark; no rating, color, photo or admin identity.", + "de": "Ein Admin hat eine eigene Fotomarkierung erfolgreich gespeichert; keine Bewertung, Farbe, Foto- oder Admin-Identität." + } + }, + "gallery_folders": { + "category": "gallery_configuration", + "since": "usage.v3", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery folders configured", + "de": "Galerieordner eingerichtet" + }, + "configured": { + "en": "An applicable global or gallery category is configured as a folder; no names, contents, counts or visitor activity.", + "de": "Eine anwendbare globale oder Galerie-Kategorie ist als Ordner eingerichtet; keine Namen, Inhalte, Anzahlen oder Besucheraktivität." + }, + "used": null + }, + "transfer_upload_links": { + "category": "gallery_configuration", + "since": "usage.v3", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "PicTransfer upload links enabled", + "de": "PicTransfer-Uploadlinks aktiviert" + }, + "configured": { + "en": "PicTransfer is enabled and a non-deleted transfer allows unexpired uploads; no links, tokens, dates, recipients or uploads.", + "de": "PicTransfer ist aktiviert und ein nicht gelöschter Transfer erlaubt noch gültige Uploads; keine Links, Tokens, Daten, Empfänger oder Uploads." + }, + "used": null + }, + "workflow_automation_enabled": { + "category": "automation", + "since": "usage.v3", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Workflow automation enabled", + "de": "Workflow-Automation aktiviert" + }, + "configured": { + "en": "The workflows module and at least one workflow are enabled; no names, graphs, triggers, decisions or runs.", + "de": "Das Workflow-Modul und mindestens ein Workflow sind aktiviert; keine Namen, Graphen, Auslöser, Entscheidungen oder Durchläufe." + }, + "used": null + }, + "s3_auto_import": { + "category": "integration", + "since": "usage.v3", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "S3 automatic import enabled", + "de": "Automatischer S3-Import aktiviert" + }, + "configured": { + "en": "S3 media storage is configured and STORAGE_AUTO_IMPORT is enabled; no bucket, prefix, credentials, polling or imported objects.", + "de": "S3-Medienspeicher ist eingerichtet und STORAGE_AUTO_IMPORT aktiviert; keine Buckets, Präfixe, Zugangsdaten, Abfragen oder importierten Objekte." + }, + "used": null + }, + "crm_invoice_import": { + "category": "crm", + "since": "usage.v3", + "measurement": "configuration_and_use", + "configuration": "flag", + "name": { + "en": "Invoice import", + "de": "Rechnungsimport" + }, + "configured": { + "en": "The required product capabilities are effectively enabled; only a boolean.", + "de": "Die erforderlichen Produktfunktionen sind effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": { + "en": "An admin successfully imported an existing invoice; no PDF, invoice number, amount, currency, customer or payment status.", + "de": "Ein Admin hat eine bestehende Rechnung erfolgreich importiert; keine PDF, Rechnungsnummer, Beträge, Währung, Kunden oder Zahlungsstände." + }, + "flag": "bills" + }, + "crm_combined_billing": { + "category": "crm", + "since": "usage.v3", + "measurement": "configuration_and_use", + "configuration": "capability", + "name": { + "en": "Combined billing", + "de": "Kombinierte Abrechnung" + }, + "configured": { + "en": "The required product capabilities are effectively enabled; only a boolean.", + "de": "Die erforderlichen Produktfunktionen sind effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": { + "en": "An admin successfully created a combined bill; no hours, expenses, customer, documents or financial values.", + "de": "Ein Admin hat erfolgreich eine kombinierte Abrechnung erstellt; keine Stunden, Ausgaben, Kunden, Dokumente oder Finanzwerte." + } + }, + "crm_monthly_billing_manual": { + "category": "crm", + "since": "usage.v3", + "measurement": "configuration_and_use", + "configuration": "flag", + "name": { + "en": "Manual monthly billing", + "de": "Manuelle Monatsabrechnung" + }, + "configured": { + "en": "The required product capabilities are effectively enabled; only a boolean.", + "de": "Die erforderlichen Produktfunktionen sind effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": { + "en": "An admin successfully released a monthly draft for delivery; actual email delivery is not measured. No scheduler activity, customer, cadence or invoice values.", + "de": "Ein Admin hat einen Monatsentwurf erfolgreich zum Versand freigegeben; die tatsächliche E-Mail-Zustellung wird nicht gemessen. Keine Scheduler-Aktivität, Kunden, Intervalle oder Rechnungswerte." + }, + "flag": "bills" + }, + "crm_document_conversion": { + "category": "crm", + "since": "usage.v3", + "measurement": "configuration_and_use", + "configuration": "capability", + "name": { + "en": "Document conversion", + "de": "Dokumentumwandlung" + }, + "configured": { + "en": "The required product capabilities are effectively enabled; only a boolean.", + "de": "Die erforderlichen Produktfunktionen sind effektiv aktiviert; nur ein Wahrheitswert." + }, + "used": { + "en": "An admin successfully converted a quote or contract into a document or gallery; no content, links, acceptance states or automatic workflows.", + "de": "Ein Admin hat ein Angebot oder einen Vertrag erfolgreich in ein Dokument oder eine Galerie umgewandelt; keine Inhalte, Verknüpfungen, Annahmestände oder automatischen Workflows." + } + }, + "gallery_capture_date_sort": { + "category": "gallery_configuration", + "since": "usage.v3", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Capture-date sorting configured", + "de": "Sortierung nach Aufnahmezeit eingerichtet" + }, + "configured": { + "en": "A gallery defaults to sorting by capture date; no capture dates, EXIF or visitor sorting actions.", + "de": "Eine Galerie sortiert standardmäßig nach Aufnahmezeit; keine Aufnahmedaten, EXIF oder Sortieraktionen von Besuchern." + }, + "used": null + }, + "download_original_filenames": { + "category": "gallery_configuration", + "since": "usage.v3", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Original download filenames enabled", + "de": "Originaldateinamen für Downloads aktiviert" + }, + "configured": { + "en": "The original-download-filenames switch is enabled; no filenames or downloads are read or sent.", + "de": "Der Schalter für Originaldateinamen beim Download ist aktiviert; keine Dateinamen oder Downloads werden gelesen oder gesendet." + }, + "used": null + } + }, + "inventory": { + "galleries": { + "name": { + "en": "Stored galleries", + "de": "Gespeicherte Galerien" + }, + "description": { + "en": "Current number of gallery records, including drafts, inactive and archived galleries. Deleted galleries are excluded. One total for the installation, no breakdown or identifiers.", + "de": "Aktuelle Anzahl gespeicherter Galerien einschließlich Entwürfen, inaktiven und archivierten Galerien. Gelöschte Galerien zählen nicht. Eine Gesamtzahl der Installation, ohne Aufschlüsselung oder Kennungen." + } + }, + "photos": { + "name": { + "en": "Stored photo records", + "de": "Gespeicherte Fotoeinträge" + }, + "description": { + "en": "Current number of non-video photo records, including RAW, guest uploads and records of archived galleries. One total for the installation; not unique files, thumbnails, processing success or photo contents. Deleted records are excluded.", + "de": "Aktuelle Anzahl der Fotoeinträge ohne Videos, einschließlich RAW, Gast-Uploads und Einträgen archivierter Galerien. Eine Gesamtzahl der Installation; keine eindeutigen Dateien, Vorschaubilder, Verarbeitungserfolge oder Fotoinhalte. Gelöschte Einträge zählen nicht." + } + } + } +} diff --git a/backend/src/usage/inventorySnapshot.js b/backend/src/usage/inventorySnapshot.js new file mode 100644 index 00000000..15d6b039 --- /dev/null +++ b/backend/src/usage/inventorySnapshot.js @@ -0,0 +1,21 @@ +'use strict'; +const { MAX_INVENTORY_COUNT } = require('./schema.cjs'); + +// Two installation totals only. No entity rows, IDs, names, file metadata, +// groupings, logs, processing results or visitor actions reach this layer. +async function inventorySnapshot(db) { + return db.transaction(async (tx) => { + const [{ count: galleries }] = await tx('events').count('* as count'); + const photosQuery = tx('photos'); + if (await tx.schema.hasColumn('photos', 'media_type')) + photosQuery.where((q) => q.whereNull('media_type').orWhereNot('media_type', 'video')); + const [{ count: photos }] = await photosQuery.count('* as count'); + const inventory = { galleries: Number(galleries), photos: Number(photos) }; + for (const count of Object.values(inventory)) { + if (!Number.isSafeInteger(count) || count < 0 || count > MAX_INVENTORY_COUNT) + throw new Error('Usage inventory total is outside the supported range'); + } + return inventory; + }, db.client.config.client === 'pg' ? { isolationLevel: 'repeatable read', readOnly: true } : {}); +} +module.exports = { inventorySnapshot }; diff --git a/backend/src/usage/schema.cjs b/backend/src/usage/schema.cjs index cd57ba70..0c254f10 100644 --- a/backend/src/usage/schema.cjs +++ b/backend/src/usage/schema.cjs @@ -1,10 +1,17 @@ "use strict"; -// Vendored byte-identical in PicPeak. v1 stays immutable; a larger allowlist -// has a new wire version and requires explicit, signed v2 consent. -const CATALOG = require("./features.v2.json"); -const CURRENT_SCHEMA_VERSION = "usage.v2"; -const CURRENT_CONSENT_VERSION = "usage-consent.v2"; +// Vendored byte-identical in PicPeak. Existing wire versions stay immutable; +// every expansion requires explicit consent to its own version. +const CATALOG = require("./features.v3.json"); +const CATALOGS = { "usage.v2": require("./features.v2.json"), "usage.v3": CATALOG }; +const CONSENT_VERSIONS = { "usage.v1": "usage-consent.v1", "usage.v2": "usage-consent.v2", "usage.v3": "usage-consent.v3" }; +const CURRENT_SCHEMA_VERSION = "usage.v3"; +const CURRENT_CONSENT_VERSION = CONSENT_VERSIONS[CURRENT_SCHEMA_VERSION]; +const schemaForConsent = (consent) => Object.keys(CONSENT_VERSIONS).find((version) => CONSENT_VERSIONS[version] === consent); +const schemaRank = (version) => Object.keys(CONSENT_VERSIONS).indexOf(version); +const INVENTORY_KEYS = ["galleries", "photos"]; +// At the collector's 100,000-reporter limit, sums remain safe JS integers. +const MAX_INVENTORY_COUNT = 1000000000; const LEGACY_FEATURE_KEYS = [ "crm", "crm_quotes", "crm_invoices", "crm_contracts", "crm_projects", "crm_calendar", "crm_hours", "customer_portal", "accounting", "workflows", @@ -22,9 +29,9 @@ const timestamp = { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2 const text = (maxLength, minLength = 1) => ({ type: "string", minLength, maxLength }); const boolean = { type: "boolean" }; const featureKeysFor = (version = CURRENT_SCHEMA_VERSION) => - version === "usage.v1" ? LEGACY_FEATURE_KEYS : version === "usage.v2" ? FEATURE_KEYS : []; + version === "usage.v1" ? LEGACY_FEATURE_KEYS : Object.keys(CATALOGS[version]?.features || {}); const observesUse = (key, version = CURRENT_SCHEMA_VERSION) => - version === "usage.v1" || CATALOG.features[key]?.measurement === "configuration_and_use"; + version === "usage.v1" || CATALOGS[version]?.features[key]?.measurement === "configuration_and_use"; const emptyFeatures = (version = CURRENT_SCHEMA_VERSION) => Object.fromEntries( featureKeysFor(version).map(key => [key, { configured: false, ...(observesUse(key, version) ? { used: false } : {}) @@ -38,6 +45,9 @@ const report = (version) => object({ key, object({ configured: boolean, ...(observesUse(key, version) ? { used: boolean } : {}) }) ]))), gallery_layouts: { type: "array", uniqueItems: true, maxItems: LAYOUTS.length, items: { enum: LAYOUTS } }, + ...(version === "usage.v3" ? { inventory: object(Object.fromEntries(INVENTORY_KEYS.map(key => [key, + { type: "integer", minimum: 0, maximum: MAX_INVENTORY_COUNT } + ]))) } : {}), }); const feedback = object({ feedback_id: uuid, kind: { enum: ["feedback", "feature_request", "testimonial"] }, @@ -45,14 +55,14 @@ const feedback = object({ allow_public: boolean, allow_marketing: boolean, }); const makePayloads = (version) => ({ - register: object({ consent_version: { const: version === "usage.v1" ? "usage-consent.v1" : CURRENT_CONSENT_VERSION } }), + register: object({ consent_version: { const: CONSENT_VERSIONS[version] } }), report: report(version), delete: object({}), feedback, vote: object({ feedback_id: uuid, voted: boolean }), session: object({}), - ...(version === "usage.v2" ? { consent: object({ consent_version: { const: CURRENT_CONSENT_VERSION } }) } : {}), + ...(version !== "usage.v1" ? { consent: object({ consent_version: { const: CONSENT_VERSIONS[version] } }) } : {}), }); -const payloadsByVersion = Object.fromEntries(["usage.v1", "usage.v2"].map(version => [version, makePayloads(version)])); +const payloadsByVersion = Object.fromEntries(Object.keys(CONSENT_VERSIONS).map(version => [version, makePayloads(version)])); const envelopeSchemas = Object.fromEntries(Object.entries(payloadsByVersion).map(([version, actions]) => [version, { $schema: "http://json-schema.org/draft-07/schema#", $id: `https://usage.picpeak.app/schema/${version}.json`, @@ -73,7 +83,8 @@ const envelopeSchemas = Object.fromEntries(Object.entries(payloadsByVersion).map const envelopeSchema = envelopeSchemas[CURRENT_SCHEMA_VERSION]; const payloads = payloadsByVersion[CURRENT_SCHEMA_VERSION]; module.exports = { - FEATURE_KEYS, LEGACY_FEATURE_KEYS, LAYOUTS, CATALOG, CURRENT_SCHEMA_VERSION, + FEATURE_KEYS, LEGACY_FEATURE_KEYS, LAYOUTS, CATALOG, CATALOGS, CONSENT_VERSIONS, + schemaForConsent, schemaRank, INVENTORY_KEYS, MAX_INVENTORY_COUNT, CURRENT_SCHEMA_VERSION, CURRENT_CONSENT_VERSION, featureKeysFor, observesUse, emptyFeatures, envelopeSchema, envelopeSchemas, payloads, payloadsByVersion, }; diff --git a/docs/FEATURE_COVERAGE.md b/docs/FEATURE_COVERAGE.md index 12a6fc27..c5482816 100644 --- a/docs/FEATURE_COVERAGE.md +++ b/docs/FEATURE_COVERAGE.md @@ -1,65 +1,65 @@ -# Product-usage coverage: usage.v2 +# Product-usage coverage: usage.v3 -Reviewed PicPeak baseline: a5ff9264 (3.124.1-beta.0). Review scope: -all 81 current backend route families, -all 26 feature flags, admin routes/settings -and runtime/public boundaries. This is capability coverage, not instrumentation -of every UI field. Source of truth: `usage-coverage.v2.json`; the PicPeak inventory -test fails on an added/removed route family, literal route declaration or feature flag. +Reviewed PicPeak baseline: a5ff9264 (3.124.1-beta.0), plus the usage integration. +The inventory covers 81 backend route families (80 product families plus usage), +all 26 feature flags and all current settings tabs. This is capability coverage, +not instrumentation of every UI field. Source of truth: `usage-coverage.v3.json`. +The prior `usage-coverage.v2.json` and v2 wire catalog remain available unchanged. -## Privacy decision +## Data scope -The purpose remains feature prioritization, fixes and maintenance from #1110. -Only **installation-wide booleans** and the existing fixed gallery-layout enums. -No user/customer/guest identity, business values, documents, photos, messages, -IP/domain/URL, per-action time, event IDs, frequencies or user-level history. -A stable installation fingerprint remains pseudonymous (not anonymous); rare -combinations can be distinctive. Participant-only dataset access and opt-out -deletion therefore remain mandatory. +There are 86 capabilities: the original 19 in v1, 54 added in v2, and 13 added in +v3. 63 have configured/used booleans; 23 are configuration-only and omit `used`. +ML face recognition was already included: only effective availability and a +successful authenticated admin capability operation, never biometric results. -Of 73 capabilities, 19 were already present in v1 and 54 are new in v2: -56 configured/used pairs and 17 **configuration-only** signals. Configuration-only -signals omit `used` entirely; this is deliberately not a false “unused” value. -Guest-facing capabilities are measured from technical configuration only, never -from actual likes, comments, uploads, downloads, newsletter interactions or views. +v3 additionally reports exactly two installation-wide integers under `inventory`: +current gallery records and non-video photo records. Counts include drafts and +retained archive records. They are not uploads, unique files or processing-success +counts. No grouping by gallery, customer, user, content, media format or source. +No extra entity rows are fetched: inventory is computed using database counts. +Each count is a nonnegative integer at most 1,000,000,000; invalid/out-of-range +counts fail rather than being silently rounded or truncated. -`configured` = current technical availability/configuration. Built-in means -available, not evidence of use. `used` = one monotonic yes/no bit since consent -to the current schema (v1: since joining; v2: since joining or explicit upgrade). -It means successful allowlisted **admin capability operation**, not necessarily -completion of a queued job. It is not an event log. Repeated operations do not -store anything more. The marker table contains only constant capability keys. +No identities, business values, documents, image contents, messages, IPs, domains, +URLs, filenames, secrets, biometric results, per-action timestamps or frequencies. +A stable reporter fingerprint and feature/count combinations remain pseudonymous, +not anonymous. Existing access controls and opt-out deletion apply to all fields. + +`configured` means technical availability or configuration, not evidence of use. +`used` is one monotonic bit since confirmed consent to the reporting schema. It +means successful authenticated admin capability operation, not necessarily final +completion of a queued job. New operation-specific bits use trusted success +signals in their handlers; failed operations and no-op conversions do not count. +Configuration-only signals never observe visitor/customer activity. Total photo +records can include guest uploads without observing individual upload actions. ## Consent and version transition -- Existing participation and migration default to `usage-consent.v1`. A client - update alone does not collect any of the 54 new local markers or report fields. -- The settings page presents the full local EN/DE catalog before v2 opt-in or - upgrade; an unchecked checkbox requires an explicit decision. -- A signed `usage.v2 / consent` command updates the same installation, after all - prior queued operations have finished. It preserves its raw history and lookup - identity. No downgrade or automatic expansion occurs. -- Only a matching collector receipt upgrades local consent and atomically resets - local usage markers. Until confirmation, collection remains v1, even if a - receipt is lost. A pending consent is durable/retryable; opt-out always wins. -- No second report on the same UTC day. The first expanded report may be on the - next day of admin activity. API integration use alone does not trigger a report. -- Collector must be deployed first. Old collectors reject the new schema; - the client shows delivery pending instead of assuming consent or sending v2. -- v1 validation remains unchanged and old envelopes remain exportable exactly as - first received. Raw history contains the original schema version on each packet. -- Aggregate projections include their schema version. Absent v2 fields in v1 - projections are **unknown**, never false. `reported` and `used_reported` - supply each metric's real denominator. Configuration-only use has denominator - zero and is displayed as “Not collected”, not 0% adoption. +- v1 and v2 keep their exact wire schemas and feature allowlists. Updating code + does not grant consent or collect v3 markers/inventory for an older participant. +- The local EN/DE dialog lists all 86 capabilities and both inventory definitions. + An unchecked checkbox requires explicit consent to `usage-consent.v3`. +- A signed v3 consent command upgrades v1 or v2 without changing identity/history. + Prior queued operations finish first. Only a matching accepted receipt changes + local consent and atomically clears previous local usage markers. Lost receipts + remain retryable; a withdrawal always wins over a late upgrade receipt. +- Consent cannot downgrade. Older clients may continue sending their already + consented older report schema. Old reports retain their original raw envelopes. +- Collector first, client second. Older collectors reject v3 rather than accepting + undisclosed fields. No second report on the same UTC day; the first v3 report + may be on the next day of admin activity. +- Summary/history count the latest report per reporter (per period for history). + Missing older fields are unknown. Feature denominators use only supplied fields. + Inventory has `{ total, reported }` per key; zero with `reported=0` means unknown, + while zero with a positive denominator is a reported empty inventory. Never sum + every daily report as if it were a different installation. Opt-out removes + current and historical contributions, including these totals. ## Every reported capability -The static bilingual definitions below are also shipped as -`features.v2.json` in both applications, exposed at -`/schema/features.v2.json`, and displayed in both usage interfaces. -“Since” is the schema in which a key was introduced; definitions here describe v2. -Legacy v1 semantics remain documented separately in the protocol reference. +Definitions are shipped byte-identically in both applications as +`features.v3.json`, served at `/schema/features.v3.json`, and shown in EN and DE. | Key (EN / DE) | Since | Configured | Used | | --- | --- | --- | --- | @@ -74,7 +74,7 @@ Legacy v1 semantics remain documented separately in the protocol reference. | `accounting` — Accounting / Buchhaltung | usage.v1 | The accounting capability switch is effectively enabled; only a boolean. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | | `workflows` — Workflows / Workflows | usage.v1 | The workflows capability switch is effectively enabled; only a boolean. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | | `newsletters` — Newsletters / Newsletter | usage.v1 | The newsletters capability switch is effectively enabled; only a boolean. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | -| `face_recognition` — Face recognition / Gesichtserkennung | usage.v1 | The faces capability switch is effectively enabled; only a boolean. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `face_recognition` — ML face recognition / ML-Gesichtserkennung | usage.v1 | The faces capability switch is effectively enabled; only a boolean. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | | `custom_css` — Custom CSS / Eigenes CSS | usage.v1 | Custom CSS is configured globally or applied through a gallery/theme/template; CSS text is not sent. | Applied CSS observed after consent, without observing visitors. | | `oauth` — Admin SSO / Admin-SSO | usage.v1 | Admin OIDC is enabled and issuer/client configuration is present; no provider or credential values. | Successful admin SSO login; no account, identity-provider or session details. | | `smtp` — SMTP delivery / SMTP-Versand | usage.v1 | An outgoing SMTP host is configured; no host, account, address or credentials. | A successful explicitly initiated admin SMTP test/send; no recipients or messages. | @@ -97,7 +97,7 @@ Legacy v1 semantics remain documented separately in the protocol reference. | `camera_raw_uploads` — Admin camera RAW uploads / Admin-Kamera-RAW-Uploads | usage.v2 | Camera RAW (DNG) is allowed in global upload settings; no camera models or EXIF. | At least one admin camera RAW upload was stored/accepted; only the capability bit, no filename or metadata. | | `messaging` — Messaging tools / Nachrichtenwerkzeuge | usage.v2 | The messaging capability switch is effectively enabled; only a boolean. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | | `incoming_mail` — IMAP intake / IMAP-Empfang | usage.v2 | Incoming mail is enabled and an IMAP configuration is present; no mailbox, server, folders or credentials. | A successful explicit admin connection test or non-skipped manual poll; no background intake, messages, attachments or counts. | -| `reminder_emails` — Automatic event reminders / Automatische Ereigniserinnerungen | usage.v2 | The reminderEmails capability switch is effectively enabled; only a boolean. | **Not collected. Configuration only.** | +| `reminder_emails` — Automatic event reminders / Automatische Ereigniserinnerungen | usage.v2 | The reminderEmails capability switch is effectively enabled; only a boolean. | Not collected: configuration only. | | `email_templates` — Email templates / E-Mail-Vorlagen | usage.v2 | Built-in capability is available; this is not evidence of use. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | | `email_webhook` — Email webhook transport / E-Mail-Webhook-Transport | usage.v2 | Both email webhook settings are present; no URL or secret. | Successful explicitly initiated admin send/test through the webhook transport; no recipients, messages or automatic deliveries. | | `accounting_incoming_invoices` — Incoming invoices / Eingangsrechnungen | usage.v2 | The incomingInvoices capability switch is effectively enabled; only a boolean. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | @@ -107,7 +107,7 @@ Legacy v1 semantics remain documented separately in the protocol reference. | `crm_installments` — Installment-plan tools / Ratenplan-Werkzeuge | usage.v2 | Quotes or invoices are enabled; no actual payment plans, amounts or statuses are inspected. | An admin saved an installment plan; no dates, amounts, currencies, payment status or document IDs. | | `document_templates` — Document presets and blocks / Dokumentvorlagen und Bausteine | usage.v2 | Quotes or contracts are enabled, making document presets/blocks available; no template content. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | | `cms` — CMS pages / CMS-Seiten | usage.v2 | Built-in capability is available; this is not evidence of use. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | -| `public_site` — Public landing page / Öffentliche Startseite | usage.v2 | The public landing-page setting is enabled; no page HTML, texts, domains or visitors. | **Not collected. Configuration only.** | +| `public_site` — Public landing page / Öffentliche Startseite | usage.v2 | The public landing-page setting is enabled; no page HTML, texts, domains or visitors. | Not collected: configuration only. | | `branding` — Branding settings / Branding-Einstellungen | usage.v2 | Built-in capability is available; this is not evidence of use. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | | `seo_customization` — SEO settings / SEO-Einstellungen | usage.v2 | Built-in capability is available; this is not evidence of use. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | | `admin_management` — Admin and role management / Admin- und Rollenverwaltung | usage.v2 | The userManagement capability switch is effectively enabled; only a boolean. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | @@ -121,88 +121,69 @@ Legacy v1 semantics remain documented separately in the protocol reference. | `analytics_dashboard` — Existing analytics module / Bestehendes Analytics-Modul | usage.v2 | The analytics capability switch is effectively enabled; only a boolean. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | | `feedback_moderation` — Feedback moderation / Feedback-Moderation | usage.v2 | Built-in capability is available; this is not evidence of use. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | | `guest_management` — Guest administration tools / Gastverwaltungswerkzeuge | usage.v2 | Built-in capability is available; this is not evidence of use. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | -| `gallery_feedback_likes` — Gallery likes enabled / Galerie-Likes aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | **Not collected. Configuration only.** | -| `gallery_feedback_ratings` — Gallery star ratings enabled / Galerie-Sternebewertungen aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | **Not collected. Configuration only.** | -| `gallery_feedback_comments` — Gallery comments enabled / Galerie-Kommentare aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | **Not collected. Configuration only.** | -| `gallery_feedback_favorites` — Gallery favorites enabled / Galerie-Favoriten aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | **Not collected. Configuration only.** | -| `gallery_feedback_reactions` — Gallery reactions enabled / Galerie-Reaktionen aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | **Not collected. Configuration only.** | -| `gallery_feedback_color_labels` — Gallery color labels enabled / Galerie-Farblabels aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | **Not collected. Configuration only.** | -| `gallery_guest_accounts` — Guest identities enabled / Gastidentitäten aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | **Not collected. Configuration only.** | -| `gallery_guest_uploads` — Guest uploads enabled / Gast-Uploads aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | **Not collected. Configuration only.** | -| `gallery_downloads` — Gallery downloads allowed / Galerie-Downloads erlaubt | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | **Not collected. Configuration only.** | -| `download_resolution_picker` — Download resolution picker enabled / Download-Auflösungswahl aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | **Not collected. Configuration only.** | -| `gallery_client_access` — Client access enabled / Client-Zugang aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | **Not collected. Configuration only.** | -| `gallery_watermarks` — Watermarks enabled / Wasserzeichen aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | **Not collected. Configuration only.** | -| `gallery_image_protection` — Image protection enabled / Bildschutz aktiviert | usage.v2 | Enabled beyond the shipped defaults — a stronger protection level, canvas rendering, or right-click disabled — globally or on at least one gallery; only existence across the installation, never gallery IDs or counts. | **Not collected. Configuration only.** | -| `gallery_reveal` — Gallery reveal enabled / Galerie-Enthüllung aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | **Not collected. Configuration only.** | -| `gallery_expiration` — Gallery expiration configured / Galerieablauf konfiguriert | usage.v2 | At least one gallery has an expiry configured; no dates, gallery IDs or counts. | **Not collected. Configuration only.** | +| `gallery_feedback_likes` — Gallery likes enabled / Galerie-Likes aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | Not collected: configuration only. | +| `gallery_feedback_ratings` — Gallery star ratings enabled / Galerie-Sternebewertungen aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | Not collected: configuration only. | +| `gallery_feedback_comments` — Gallery comments enabled / Galerie-Kommentare aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | Not collected: configuration only. | +| `gallery_feedback_favorites` — Gallery favorites enabled / Galerie-Favoriten aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | Not collected: configuration only. | +| `gallery_feedback_reactions` — Gallery reactions enabled / Galerie-Reaktionen aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | Not collected: configuration only. | +| `gallery_feedback_color_labels` — Gallery color labels enabled / Galerie-Farblabels aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | Not collected: configuration only. | +| `gallery_guest_accounts` — Guest identities enabled / Gastidentitäten aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | Not collected: configuration only. | +| `gallery_guest_uploads` — Guest uploads enabled / Gast-Uploads aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | Not collected: configuration only. | +| `gallery_downloads` — Gallery downloads allowed / Galerie-Downloads erlaubt | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | Not collected: configuration only. | +| `download_resolution_picker` — Download resolution picker enabled / Download-Auflösungswahl aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | Not collected: configuration only. | +| `gallery_client_access` — Client access enabled / Client-Zugang aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | Not collected: configuration only. | +| `gallery_watermarks` — Watermarks enabled / Wasserzeichen aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | Not collected: configuration only. | +| `gallery_image_protection` — Image protection enabled / Bildschutz aktiviert | usage.v2 | Enabled beyond the shipped defaults — a stronger protection level, canvas rendering, or right-click disabled — globally or on at least one gallery; only existence across the installation, never gallery IDs or counts. | Not collected: configuration only. | +| `gallery_reveal` — Gallery reveal enabled / Galerie-Enthüllung aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | Not collected: configuration only. | +| `gallery_expiration` — Gallery expiration configured / Galerieablauf konfiguriert | usage.v2 | At least one gallery has an expiry configured; no dates, gallery IDs or counts. | Not collected: configuration only. | +| `photo_xmp_export` — XMP export / XMP-Export | usage.v3 | Built-in capability is available; this is not evidence of use. | An admin successfully generated an XMP export; no sidecars, filenames, ratings, selections or counts. | +| `photo_replacement` — Photo replacement / Fotoersetzung | usage.v3 | Built-in capability is available; this is not evidence of use. | An admin upload actually replaced a photo successfully; no filenames, matching values, IDs or counts. | +| `photo_admin_marks` — Photographer marks / Fotografenmarkierungen | usage.v3 | Built-in capability is available; this is not evidence of use. | An admin successfully saved their own photo mark; no rating, color, photo or admin identity. | +| `gallery_folders` — Gallery folders configured / Galerieordner eingerichtet | usage.v3 | An applicable global or gallery category is configured as a folder; no names, contents, counts or visitor activity. | Not collected: configuration only. | +| `transfer_upload_links` — PicTransfer upload links enabled / PicTransfer-Uploadlinks aktiviert | usage.v3 | PicTransfer is enabled and a non-deleted transfer allows unexpired uploads; no links, tokens, dates, recipients or uploads. | Not collected: configuration only. | +| `workflow_automation_enabled` — Workflow automation enabled / Workflow-Automation aktiviert | usage.v3 | The workflows module and at least one workflow are enabled; no names, graphs, triggers, decisions or runs. | Not collected: configuration only. | +| `s3_auto_import` — S3 automatic import enabled / Automatischer S3-Import aktiviert | usage.v3 | S3 media storage is configured and STORAGE_AUTO_IMPORT is enabled; no bucket, prefix, credentials, polling or imported objects. | Not collected: configuration only. | +| `crm_invoice_import` — Invoice import / Rechnungsimport | usage.v3 | The required product capabilities are effectively enabled; only a boolean. | An admin successfully imported an existing invoice; no PDF, invoice number, amount, currency, customer or payment status. | +| `crm_combined_billing` — Combined billing / Kombinierte Abrechnung | usage.v3 | The required product capabilities are effectively enabled; only a boolean. | An admin successfully created a combined bill; no hours, expenses, customer, documents or financial values. | +| `crm_monthly_billing_manual` — Manual monthly billing / Manuelle Monatsabrechnung | usage.v3 | The required product capabilities are effectively enabled; only a boolean. | An admin successfully released a monthly draft for delivery; actual email delivery is not measured. No scheduler activity, customer, cadence or invoice values. | +| `crm_document_conversion` — Document conversion / Dokumentumwandlung | usage.v3 | The required product capabilities are effectively enabled; only a boolean. | An admin successfully converted a quote or contract into a document or gallery; no content, links, acceptance states or automatic workflows. | +| `gallery_capture_date_sort` — Capture-date sorting configured / Sortierung nach Aufnahmezeit eingerichtet | usage.v3 | A gallery defaults to sorting by capture date; no capture dates, EXIF or visitor sorting actions. | Not collected: configuration only. | +| `download_original_filenames` — Original download filenames enabled / Originaldateinamen für Downloads aktiviert | usage.v3 | The original-download-filenames switch is enabled; no filenames or downloads are read or sent. | Not collected: configuration only. | -Gallery layouts (unchanged): `grid`, `masonry`, `carousel`, `timeline`, -`mosaic`, `gallery-premium`, `gallery-story`, `other`. Only set membership, -not how many galleries use a layout. Unknown names are normalized to other. +## Inventory totals -## Exact observation sources +- `inventory.galleries` — Current number of gallery records, including drafts, inactive and archived galleries. Deleted galleries are excluded. One total for the installation, no breakdown or identifiers. +- `inventory.photos` — Current number of non-video photo records, including RAW, guest uploads and records of archived galleries. One total for the installation; not unique files, thumbnails, processing success or photo contents. Deleted records are excluded. -PicPeak `backend/src/usage/capabilityRules.js` is the fixed method/path -allowlist; request paths, query/body/response values never leave the middleware. -Only the resulting constant keys reach `markUsed`, with active schema consent, -authenticated admin and 2xx response checks. Status/health polls are excluded. +## Route-family decisions -Additional trusted success evidence in `capabilityEvidence.js`: -accepted admin file storage (video / DNG / S3 booleans only, not chunk -initialization), successful manual SMTP or email-webhook send/test, non-skipped -manual IMAP poll/connection test, successful manual WhatsApp test, and successful -S3 backup roundtrip test. SMTP vs webhook uses the actual selected transport -(including per-account SMTP overrides), not just environment presence. -Webhook test/replay means **accepted enqueue**, never remote delivery tracking. - -`UsageService.snapshot` and `expandedSnapshot.js` inspect allowlisted settings, -effective flags and technical configuration existence. They do not query -customer/guest profiles, financial records, photos/EXIF, message/feedback bodies, -audit/security logs or delivery histories. Inherited technical defaults count as -configuration; disabled feature dependencies cannot be inferred as active. -Optional-module tables/columns are guarded. CSS/layout inspection maps locally -to presence/enums; no free-form CSS/theme content is sent. - -OAuth is marked only by the successful **admin** OIDC callback, without claims -or provider metadata. S3 backup use is inferred only for backup operations -writing to the configured destination; a local DB/portable export is not S3 use. -Background jobs and public/customer/visitor handlers never record product use. - -## Complete route-family decision matrix - -Paths below are relative to PicPeak `backend/src/routes/`. “Partial” means only -the disclosed allowlist/evidence, not every endpoint in that file. All literal -route declarations are captured in the companion inventory, with excluded -methods remaining unobserved. - -| Source | Decision / signals | Reason / limits | +| Family | Coverage | Boundary | | --- | --- | --- | -| `acceptInvite.js` | excluded | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | -| `admin.js` | composition | Router composition / helpers; decisions are recorded for each mounted family. | +| `acceptInvite.js` | excluded: no telemetry | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | +| `admin.js` | composition: no telemetry | Router composition / helpers; decisions are recorded for each mounted family. | | `adminApiTokens.js` | configuration: `api_integration` | Only existence of a valid credential; no marker from token listing/creation, no scope, owner, token, expiry date or last-used time. | | `adminArchives.js` | partial: `galleries`, `archive_management`, `photo_exports` | Admin archive/delete/restore/download initiation only; filenames, histories, storage sizes and polling excluded. | -| `adminAuth.js` | excluded | Bootstrap, passwords/MFA/session/profile, per-person notifications, developer helpers and operational health/update/log polling are outside the prioritization purpose. | +| `adminAuth.js` | excluded: no telemetry | Bootstrap, passwords/MFA/session/profile, per-person notifications, developer helpers and operational health/update/log polling are outside the prioritization purpose. | | `adminBackup.js` | partial: `backup`, `portable_backup`, `restore`, `s3_storage`, `s3_backups` | Admin backup initiation, portable export/import and successful S3 roundtrip test. Local export never implies S3; names, schedules, sizes, contents and history excluded. | -| `adminBusinessProfile.js` | excluded | Business identity/bank/tax-address configuration and VAT-code helper surface are not separate usage signals. Billing/accounting capabilities are covered without profiling the business. | +| `adminBusinessProfile.js` | excluded: no telemetry | Business identity/bank/tax-address configuration and VAT-code helper surface are not separate usage signals. Billing/accounting capabilities are covered without profiling the business. | | `adminCalendar.js` | partial: `crm`, `crm_calendar` | Authenticated admin calendar retrieval is capability use; no calendar entries, dates, recurrence, availability or bookings. | -| `adminCategories.js` | partial: `gallery_categories` | Admin category CRUD; no names, descriptions, colors or ordering values. | +| `adminCategories.js` | partial: `gallery_categories`, `gallery_folders` | Admin category CRUD; no names, descriptions, colors or ordering values. v3 adds only: gallery_folders. Exact definitions are in features.v3.json; configuration-only signals never observe the public surface. | | `adminCMS.js` | partial: `cms` | Admin CMS page CRUD only. Public page traffic, slug, HTML, text, links and media excluded. | -| `adminContracts.js` | partial: `crm`, `crm_contracts`, `document_templates` | Admin contract/block operations only; no legal text, signatures, signing parties or customer signing events. | +| `adminContracts.js` | partial: `crm`, `crm_contracts`, `document_templates`, `crm_document_conversion` | Admin contract/block operations only; no legal text, signatures, signing parties or customer signing events. v3 adds only: crm_document_conversion. Exact definitions are in features.v3.json; configuration-only signals never observe the public surface. | | `adminCssTemplates.js` | configuration: `custom_css` | Only existence of enabled applied CSS and locally observed application, not editing/viewing templates or any CSS text. | -| `adminCustomers.js` | partial: `crm`, `crm_hours`, `customer_portal` | Successful admin CRM/hour-entry/invitation operations only. No customer/account names, IDs, rates, billed hours, payment state or portal behavior. | +| `adminCustomers.js` | partial: `crm`, `crm_hours`, `customer_portal`, `crm_combined_billing`, `crm_monthly_billing_manual` | Successful admin CRM/hour-entry/invitation operations only. No customer/account names, IDs, rates, billed hours, payment state or portal behavior. v3 adds only: crm_combined_billing, crm_monthly_billing_manual. Exact definitions are in features.v3.json; configuration-only signals never observe the public surface. | | `adminDashboard.js` | partial: `analytics_dashboard` | Admin analytics capability endpoint only; no stats, activities, health/CRM polls, underlying visitor data or dashboard values. | | `adminDatabaseBackup.js` | partial: `backup`, `database_backup` | Admin database-backup initiation plus schedule-enabled boolean, no file data/history. | | `adminDeals.js` | partial: `crm`, `crm_installments` | Admin installment-plan changes only. No actual plans, invoice links, amounts, paid states or deal reporting. | -| `adminDev.js` | excluded | Bootstrap, passwords/MFA/session/profile, per-person notifications, developer helpers and operational health/update/log polling are outside the prioritization purpose. | +| `adminDev.js` | excluded: no telemetry | Bootstrap, passwords/MFA/session/profile, per-person notifications, developer helpers and operational health/update/log polling are outside the prioritization purpose. | | `adminEmail.js` | partial: `messaging`, `incoming_mail`, `smtp`, `email_templates`, `email_webhook`, `reminder_emails` | Admin message operation/template edit, actual successful manual send/test transport and non-skipped manual IMAP poll/test. Reminder flag configuration only. No automated sends/polls, received-message or recipient data, queue/log reads, mailbox addresses or templates. | | `adminEventRename.js` | partial: `galleries` | Successful rename only, not validate-rename. No former/new names or identifiers. | | `adminEvents/archiveBulk.js` | partial: `galleries`, `archive_management`, `photo_exports` | Admin archive/delete/restore/download initiation only; filenames, histories, storage sizes and polling excluded. | -| `adminEvents/crud.js` | partial: `galleries`, `gallery_guest_uploads`, `gallery_downloads`, `gallery_client_access`, `gallery_watermarks`, `gallery_reveal`, `gallery_expiration`, `gallery_sharing`, `custom_css` | Admin creation/edit/publish etc. set galleries; sharing has its own fixed key. Guest/download/protection/reveal/expiry are configuration only; themes contribute controlled layouts and CSS presence. No gallery metadata or guest action history. | +| `adminEvents/crud.js` | partial: `galleries`, `gallery_guest_uploads`, `gallery_downloads`, `gallery_client_access`, `gallery_watermarks`, `gallery_reveal`, `gallery_expiration`, `gallery_sharing`, `custom_css`, `gallery_capture_date_sort` | Admin creation/edit/publish etc. set galleries; sharing has its own fixed key. Guest/download/protection/reveal/expiry are configuration only; themes contribute controlled layouts and CSS presence. No gallery metadata or guest action history. v3 adds only: gallery_capture_date_sort. Exact definitions are in features.v3.json; configuration-only signals never observe the public surface. | | `adminEvents/downloadResolutions.js` | configuration: `download_resolution_picker` | Only whether a picker is configured globally or in a gallery. No chosen resolution, download event or counts. | | `adminEvents/faces.js` | partial: `face_recognition` | Effective flag plus successful admin faces/people operation. No health polling, embeddings, names, groups, detections or visitor searches. | -| `adminEvents/helpers.js` | composition | Router composition / helpers; decisions are recorded for each mounted family. | -| `adminEvents/index.js` | composition | Router composition / helpers; decisions are recorded for each mounted family. | +| `adminEvents/helpers.js` | composition: no telemetry | Router composition / helpers; decisions are recorded for each mounted family. | +| `adminEvents/index.js` | composition: no telemetry | Router composition / helpers; decisions are recorded for each mounted family. | | `adminEvents/logo.js` | partial: `branding` | Successful admin logo operation only; image/filename/content excluded. | | `adminEvents/qr.js` | partial: `gallery_sharing` | Admin QR generation only; no scans, tokens or URLs. | | `adminEvents/resets.js` | partial: `galleries`, `gallery_sharing` | Admin gallery reset/sharing capability only; no password, recipient, token or reset statistics. | @@ -214,148 +195,58 @@ methods remaining unobserved. | `adminFeedback.js` | partial: `feedback_moderation`, `gallery_feedback_likes`, `gallery_feedback_ratings`, `gallery_feedback_comments`, `gallery_feedback_favorites`, `gallery_feedback_reactions`, `gallery_feedback_color_labels`, `gallery_guest_accounts` | Admin moderation/word-filter operations only. Visitor feedback is not observed. Master-enabled per-gallery feedback-option booleans only; no contents, ratings, likes, colors, identities or word lists. | | `adminGuests.js` | partial: `guest_management` | Admin guest management/export initiation only. No guest names, invitations, tokens, contact data, guest counts or visitor interactions. | | `adminImageSecurity.js` | configuration: `gallery_image_protection` | Only gallery/global technical protection configuration existence. No security events, blocked IPs, request counts, threat scores or admin monitoring access. | -| `adminInvoices.js` | partial: `crm`, `crm_invoices` | Admin invoice operations only; no amounts, VAT/customer/payment values or payment-check responses. | +| `adminInvoices.js` | partial: `crm`, `crm_invoices`, `crm_invoice_import` | Admin invoice operations only; no amounts, VAT/customer/payment values or payment-check responses. v3 adds only: crm_invoice_import. Exact definitions are in features.v3.json; configuration-only signals never observe the public surface. | | `adminLedger.js` | partial: `accounting`, `accounting_ledger` | Admin ledger-account/VAT/mapping edits and ledger export initiation only; no account/currency/VAT identifiers or exported records. | | `adminNewsletters.js` | partial: `newsletters` | Admin campaign changes/test/queue/cancel only. Recipient resolution, previews, subscriptions/unsubscribes, delivery/open/click data and automatic sending excluded. | -| `adminNotifications.js` | excluded | Bootstrap, passwords/MFA/session/profile, per-person notifications, developer helpers and operational health/update/log polling are outside the prioritization purpose. | +| `adminNotifications.js` | excluded: no telemetry | Bootstrap, passwords/MFA/session/profile, per-person notifications, developer helpers and operational health/update/log polling are outside the prioritization purpose. | | `adminPhotoDimensions.js` | partial: `photo_processing` | Admin repair/regenerate/configuration initiation, never status polling or processing totals. | -| `adminPhotoExport.js` | partial: `photo_exports` | Admin export initiation only; export filters, selected files, sizes and contents excluded. | -| `adminPhotos.js` | partial: `photo_management`, `photo_exports`, `photo_processing`, `video_uploads`, `camera_raw_uploads`, `s3_storage`, `s3_photo_storage` | Successful admin edits/exports and accepted upload evidence only. Chunk init/status, failed uploads and public downloads excluded. Only video/RAW/S3 booleans survive, never file metadata/EXIF/content. | +| `adminPhotoExport.js` | partial: `photo_exports`, `photo_xmp_export` | Admin export initiation only; export filters, selected files, sizes and contents excluded. v3 adds only: photo_xmp_export. Exact definitions are in features.v3.json; configuration-only signals never observe the public surface. | +| `adminPhotos.js` | partial: `photo_management`, `photo_exports`, `photo_processing`, `video_uploads`, `camera_raw_uploads`, `s3_storage`, `s3_photo_storage`, `photo_replacement`, `photo_admin_marks` | Successful admin edits/exports and accepted upload evidence only. Chunk init/status, failed uploads and public downloads excluded. Only video/RAW/S3 booleans survive, never file metadata/EXIF/content. v3 adds only: photo_replacement, photo_admin_marks. Exact definitions are in features.v3.json; configuration-only signals never observe the public surface. | | `adminProjects.js` | partial: `crm`, `crm_projects` | Admin project operations only; project/person names, business performance, metadata and totals excluded. | -| `adminQuotes.js` | partial: `crm`, `crm_quotes`, `document_templates` | Admin quote/preset operations only; no quote content, prices, customer acceptance or signatures. | +| `adminQuotes.js` | partial: `crm`, `crm_quotes`, `document_templates`, `crm_document_conversion` | Admin quote/preset operations only; no quote content, prices, customer acceptance or signatures. v3 adds only: crm_document_conversion. Exact definitions are in features.v3.json; configuration-only signals never observe the public surface. | | `adminRestore.js` | partial: `restore` | Admin restore initiation only, never file selection, content, progress, errors or timing. | | `adminRoles.js` | partial: `admin_management` | Admin account/role management capability; no names, permissions, role labels, password reset operations or active-user counts. Auth/self-profile endpoints excluded. | -| `adminSettings.js` | partial: `custom_css`, `oauth`, `smtp`, `backup`, `s3_storage`, `video_uploads`, `camera_raw_uploads`, `public_site`, `branding`, `seo_customization`, `slideshow`, `download_resolution_picker`, `gallery_watermarks`, `database_backup` | Only specified configuration presence/booleans and explicit branding/SEO/slideshow operations. Generic settings reads, security policies, passwords, storage data, SMTP/OIDC credentials, custom HTML/CSS/SEO values excluded. | +| `adminSettings.js` | partial: `custom_css`, `oauth`, `smtp`, `backup`, `s3_storage`, `video_uploads`, `camera_raw_uploads`, `public_site`, `branding`, `seo_customization`, `slideshow`, `download_resolution_picker`, `gallery_watermarks`, `database_backup`, `s3_auto_import`, `download_original_filenames` | Only specified configuration presence/booleans and explicit branding/SEO/slideshow operations. Generic settings reads, security policies, passwords, storage data, SMTP/OIDC credentials, custom HTML/CSS/SEO values excluded. v3 adds only: s3_auto_import, download_original_filenames. Exact definitions are in features.v3.json; configuration-only signals never observe the public surface. | | `adminShortUrls.js` | partial: `gallery_sharing`, `short_links` | Admin short-link creation/deletion only; link/token/click metadata excluded. | -| `adminSystem.js` | excluded | Bootstrap, passwords/MFA/session/profile, per-person notifications, developer helpers and operational health/update/log polling are outside the prioritization purpose. | -| `adminSystemHealth.js` | excluded | Bootstrap, passwords/MFA/session/profile, per-person notifications, developer helpers and operational health/update/log polling are outside the prioritization purpose. | +| `adminSystem.js` | excluded: no telemetry | Bootstrap, passwords/MFA/session/profile, per-person notifications, developer helpers and operational health/update/log polling are outside the prioritization purpose. | +| `adminSystemHealth.js` | excluded: no telemetry | Bootstrap, passwords/MFA/session/profile, per-person notifications, developer helpers and operational health/update/log polling are outside the prioritization purpose. | | `adminTaxReport.js` | partial: `accounting`, `accounting_tax_report` | Admin tax report generation/export only; no totals, dates, tax regimes, geography or currency. | | `adminThumbnails.js` | partial: `photo_processing` | Admin repair/regenerate/configuration initiation, never status polling or processing totals. | -| `adminTransfers.js` | partial: `transfers` | Admin transfer CRUD/files/link management/download only. Public recipients, received-file data, upload and download statistics excluded. | -| `adminUsage.js` | excluded | Consent, inspection, export, feedback, voting and deletion are explicit protocol operations; not product-use signals. Activity only triggers a due fixed report. | +| `adminTransfers.js` | partial: `transfers`, `transfer_upload_links` | Admin transfer CRUD/files/link management/download only. Public recipients, received-file data, upload and download statistics excluded. v3 adds only: transfer_upload_links. Exact definitions are in features.v3.json; configuration-only signals never observe the public surface. | +| `adminUsage.js` | excluded: no telemetry | Consent, inspection, export, feedback, voting, deletion and abandoning an unsignable deletion are explicit protocol operations; not product-use signals. Activity only triggers a due fixed report. | | `adminUsers.js` | partial: `admin_management` | Admin account/role management capability; no names, permissions, role labels, password reset operations or active-user counts. Auth/self-profile endpoints excluded. | -| `adminVatCodes.js` | excluded | Business identity/bank/tax-address configuration and VAT-code helper surface are not separate usage signals. Billing/accounting capabilities are covered without profiling the business. | +| `adminVatCodes.js` | excluded: no telemetry | Business identity/bank/tax-address configuration and VAT-code helper surface are not separate usage signals. Billing/accounting capabilities are covered without profiling the business. | | `adminWebhooks.js` | partial: `webhooks` | Active configuration existence plus successful admin manual test/replay enqueue. Actual network delivery/results/subscriptions/destinations excluded. | | `adminWhatsapp.js` | partial: `whatsapp` | Effective configured sender and successful manual test only. No automated deliveries, phone numbers, templates or delivery statuses. | -| `adminWorkflows.js` | partial: `workflows` | Admin workflow authoring/approval/test initiation only. Runtime triggers, payloads, execution frequency/results and public approvals excluded. | -| `analyticsTrackerProxy.js` | excluded | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | +| `adminWorkflows.js` | partial: `workflows`, `workflow_automation_enabled` | Admin workflow authoring/approval/test initiation only. Runtime triggers, payloads, execution frequency/results and public approvals excluded. v3 adds only: workflow_automation_enabled. Exact definitions are in features.v3.json; configuration-only signals never observe the public surface. | +| `analyticsTrackerProxy.js` | excluded: no telemetry | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | | `auth.js` | partial: `oauth` | Only successful admin OIDC callback sets oauth. Password/gallery authentication, MFA, account claims and provider details excluded. | -| `customer.js` | excluded | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | -| `customerAuth.js` | excluded | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | -| `gallery.js` | excluded | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | -| `galleryFeedback.js` | excluded | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | -| `galleryGuests.js` | excluded | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | -| `protectedImages.js` | excluded | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | -| `publicCMS.js` | excluded | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | -| `publicContracts.js` | excluded | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | -| `publicFonts.js` | excluded | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | -| `publicNewsletter.js` | excluded | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | -| `publicPaymentCheck.js` | excluded | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | -| `publicQuotes.js` | excluded | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | -| `publicSettings.js` | excluded | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | -| `publicTransfer.js` | excluded | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | -| `publicTransferUpload.js` | excluded | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | -| `publicWorkflowApprovals.js` | excluded | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | -| `secureImages.js` | excluded | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | -| `setup.js` | excluded | Bootstrap, passwords/MFA/session/profile, per-person notifications, developer helpers and operational health/update/log polling are outside the prioritization purpose. | +| `customer.js` | excluded: no telemetry | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | +| `customerAuth.js` | excluded: no telemetry | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | +| `gallery.js` | excluded: no telemetry | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | +| `galleryFeedback.js` | excluded: no telemetry | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | +| `galleryGuests.js` | excluded: no telemetry | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | +| `protectedImages.js` | excluded: no telemetry | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | +| `publicCMS.js` | excluded: no telemetry | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | +| `publicContracts.js` | excluded: no telemetry | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | +| `publicFonts.js` | excluded: no telemetry | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | +| `publicNewsletter.js` | excluded: no telemetry | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | +| `publicPaymentCheck.js` | excluded: no telemetry | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | +| `publicQuotes.js` | excluded: no telemetry | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | +| `publicSettings.js` | excluded: no telemetry | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | +| `publicTransfer.js` | excluded: no telemetry | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | +| `publicTransferUpload.js` | excluded: no telemetry | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | +| `publicWorkflowApprovals.js` | excluded: no telemetry | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | +| `secureImages.js` | excluded: no telemetry | Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers. | +| `setup.js` | excluded: no telemetry | Bootstrap, passwords/MFA/session/profile, per-person notifications, developer helpers and operational health/update/log polling are outside the prioritization purpose. | | `v1/events.js` | partial: `api_integration` | Single bit after successful admin-owned scoped API authentication. No request/response values; API requests do not trigger reports. | -## Every admin settings tab - -These 29 current SettingsPage tabs are also inventoried and tested against -the frontend TabType. Page navigation itself is not tracked. - -| Tab | Capability / exclusion | -| --- | --- | -| `usage` | Explicit consent/report inspection/feedback is not itself adoption telemetry. | -| `features` | Only the allowlisted effective feature booleans; no settings visit/save marker. | -| `general` | `video_uploads`, `camera_raw_uploads`, `public_site`, `custom_css`. General technical upload/public-site/CSS configuration only; no title, URLs, limits, times, HTML or identity. | -| `events` | `galleries`, `gallery_guest_uploads`, `gallery_downloads`, `gallery_client_access`, `gallery_watermarks`, `gallery_image_protection`, `gallery_reveal`, `gallery_expiration`. Gallery operations and disclosed configuration only; no event/customer values or visitor use. | -| `eventTypes` | `event_types`. General admin event-type capability; no names or preset contents. | -| `branding` | `branding`, `gallery_watermarks`. Branding operation and watermark configuration only; no branding text, logos or colors. | -| `categories` | `gallery_categories`. Category management capability only; no names/order/category membership. | -| `thumbnails` | `photo_processing`. Admin processing settings/regeneration initiation only; no image data or progress. | -| `downloads` | `download_resolution_picker`. Configuration boolean only; no actual download/selection behavior or resolution values. | -| `styling` | `custom_css`. Presence/application only plus controlled gallery-layout enums, never CSS/theme values. | -| `cms` | `cms`, `public_site`. Admin page editing capability/public-site enabled only; no HTML, slugs or traffic. | -| `email` | `smtp`, `incoming_mail`, `messaging`, `email_templates`, `email_webhook`. Configuration and documented manual admin capability operations only; messages, recipients, automatic activity and mailbox values excluded. | -| `moderation` | `feedback_moderation`. Admin moderation/word-filter capability, never feedback content or visitor behavior. | -| `security` | Excluded password/MFA/session/rate-limit/security profiles and operations. | -| `sso` | `oauth`. Enabled/config-present and successful admin callback only; no claims/provider details. | -| `imageSecurity` | `gallery_image_protection`. Configuration presence only; no blocked-IP/security analytics or monitoring history. | -| `seo` | `seo_customization`. Admin SEO configuration operation only; no meta tags, URLs, robots or verification tokens. | -| `apiTokens` | `api_integration`. Valid credential presence and one successful scoped API capability bit; no tokens/scopes/owner metadata. | -| `webhooks` | `webhooks`. Active configuration and manual test/replay enqueue only; no delivery data. | -| `status` | Excluded operational health, diagnostics, resource data, update and storage polling. | -| `analytics` | `analytics_dashboard`. Analytics capability and admin aggregate-view use only; no embedded analytics results/tracker IDs or visitors. | -| `backup` | `backup`, `database_backup`, `portable_backup`, `restore`, `s3_backups`. Schedule presence/manual capability initiation only, no histories, sizes, paths or files. | -| `businessProfile` | Excluded business identity, bank accounts and addresses. | -| `crm` | `crm`, `crm_quotes`, `crm_invoices`, `crm_projects`, `crm_hours`, `customer_portal`, `crm_installments`. Only coarse module capabilities; no policies/amounts/customer/payment values. | -| `contracts` | `crm_contracts`, `document_templates`. Admin contract/template capability only; no legal text or signatures. | -| `reminderTemplates` | `reminder_emails`, `email_templates`. Reminder flag configuration and admin template editing only; no automatic reminder sends/recipients/content. | -| `accounting` | `accounting`, `accounting_incoming_invoices`, `accounting_expenses`, `accounting_tax_report`, `accounting_ledger`. Only module capabilities, no tax codes, rates, balances or business identity. | -| `whatsapp` | `whatsapp`. Configured integration plus manual test only; no phone numbers, tokens or automatic delivery. | -| `slideshow` | `slideshow`. Admin setup capability only; no kiosk viewers, slide progress or photos. | - -## Every feature flag (configuration decisions) - -| Flag | Signal / exclusion | -| --- | --- | -| `accounting` | `accounting`, `accounting_ledger`. Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean. | -| `analytics` | `analytics_dashboard`. Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean. | -| `bills` | `crm_invoices`. Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean. | -| `calendar` | `crm_calendar`. Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean. | -| `calendarBooking` | Excluded: disabled roadmap placeholder, not an implemented booking capability. | -| `clients` | `crm`. Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean. | -| `contracts` | `crm_contracts`. Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean. | -| `crmDevelopment` | Excluded: internal development/test helpers, not product adoption. | -| `customerPortal` | `customer_portal`. Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean. | -| `expenses` | `accounting_expenses`. Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean. | -| `faces` | `face_recognition`. Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean. | -| `galleries` | `galleries`. Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean. | -| `hoursLogging` | `crm_hours`. Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean. | -| `incomingInvoices` | `accounting_incoming_invoices`. Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean. | -| `incomingMail` | `incoming_mail`. Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean. | -| `messaging` | `messaging`. Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean. | -| `newsletters` | `newsletters`. Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean. | -| `projects` | `crm_projects`. Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean. | -| `quotes` | `crm_quotes`. Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean. | -| `reminderEmails` | `reminder_emails`. Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean. | -| `slideshow` | `slideshow`. Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean. | -| `taxReport` | `accounting_tax_report`. Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean. | -| `transfers` | `transfers`. Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean. | -| `userManagement` | `admin_management`. Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean. | -| `whatsapp` | `whatsapp`. Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean. | -| `workflows` | `workflows`. Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean. | - -## Deliberately excluded runtime and future features +## Excluded runtime - Gallery/customer/public events and optional website analytics - Automated newsletter, reminder, WhatsApp, webhook and IMAP jobs - Security/audit logs, biometric embeddings and recognition results - Operational health, migration, update and polling metrics -- Business/customer/user identities, geography, amounts and document contents +- Business/customer/user identities, geography, financial amounts and document contents; only explicit v3 inventory totals are permitted. - Disabled calendarBooking and internal crmDevelopment; hosted future product #1111 - Image fragmentation: removed from current PicPeak, not a live capability - -The Messages and Reminder Emails implementations were reviewed as real features, -despite stale placeholder comments. Reminder Emails remains configuration-only. -Calendar booking is still a disabled placeholder and is not presented as a -working capability. This review does not approve any public visitor tracking, -even if another optional analytics integration is configured. - -All exclusion decisions still permit the existing product functions themselves. -They restrict this usage program; they do not disable galleries, email or jobs. -Adding capabilities requires a documented scope review, updated inventory, -closed schema, both UI disclosures/docs and tests; a wider collection scope -requires renewed explicit consent, not a silent catalog expansion. - -## Verification obligations - -Required checks include unchanged v1 validation, closed v2 fields, all 73 -configuration signals and privacy canaries, all route/flag decisions, no -configuration-only use, disabled/pending/upgrade/opt-out boundaries, mixed-version -denominators, byte-identical protocol/catalogs, EN/DE UI catalog consistency, -raw export and deletion, SQLite/PostgreSQL and paired local Docker/browser tests. -Test outcomes are recorded separately; this document is not a claim of legal -certification or proof that modified self-hosted clients report truthfully. diff --git a/docs/PRODUCT_USAGE.md b/docs/PRODUCT_USAGE.md index df9bb4fc..9b8f1981 100644 --- a/docs/PRODUCT_USAGE.md +++ b/docs/PRODUCT_USAGE.md @@ -1,5 +1,22 @@ # Optional product usage and feedback (#1110) +Current scope: **usage.v3**. The expanded catalog contains 86 capabilities +(including ML face recognition and invoice import) and exactly two inventory +totals: stored gallery records and non-video photo records, including drafts +and retained archive records. No content, identifiers, per-gallery breakdowns, +biometric results, financial values or visitor actions. + +Existing v1/v2 participants retain their previous scope until explicit signed +v3 consent is confirmed. New count queries and markers do not run before that +confirmation. Collector must be deployed first. v1/v2 wire schemas and raw +history remain unchanged. See [current coverage](FEATURE_COVERAGE.md) for all +definitions and [v3 inventory](usage-coverage.v3.json) for code boundaries. + +The sections below also document the historical v1/v2 implementation. Any +statements excluding all gallery/photo counts describe those earlier versions; +v3 adds only the two installation totals above. + + Tracking is disabled by default. After updating, settings editors see a dismissible invitation in the admin shell. Only explicit consent in Settings → Product usage & feedback registers an installation. Public galleries never diff --git a/docs/usage-coverage.v3.json b/docs/usage-coverage.v3.json new file mode 100644 index 00000000..b57150ba --- /dev/null +++ b/docs/usage-coverage.v3.json @@ -0,0 +1,1813 @@ +{ + "settings_tabs": { + "usage": { + "signals": [], + "reason": "Explicit consent/report inspection/feedback is not itself adoption telemetry." + }, + "features": { + "signals": [], + "reason": "Only the allowlisted effective feature booleans; no settings visit/save marker." + }, + "general": { + "signals": [ + "video_uploads", + "camera_raw_uploads", + "public_site", + "custom_css", + "download_original_filenames" + ], + "reason": "General technical upload/public-site/CSS configuration only; no title, URLs, limits, times, HTML or identity. v3 also reports two installation inventory totals separately, with explicit consent." + }, + "events": { + "signals": [ + "galleries", + "gallery_guest_uploads", + "gallery_downloads", + "gallery_client_access", + "gallery_watermarks", + "gallery_image_protection", + "gallery_reveal", + "gallery_expiration", + "gallery_capture_date_sort" + ], + "reason": "Gallery operations and disclosed configuration only; no event/customer values or visitor use. v3 also reports two installation inventory totals separately, with explicit consent." + }, + "eventTypes": { + "signals": [ + "event_types" + ], + "reason": "General admin event-type capability; no names or preset contents." + }, + "branding": { + "signals": [ + "branding", + "gallery_watermarks" + ], + "reason": "Branding operation and watermark configuration only; no branding text, logos or colors." + }, + "categories": { + "signals": [ + "gallery_categories", + "gallery_folders" + ], + "reason": "Category management capability only; no names/order/category membership." + }, + "thumbnails": { + "signals": [ + "photo_processing" + ], + "reason": "Admin processing settings/regeneration initiation only; no image data or progress." + }, + "downloads": { + "signals": [ + "download_resolution_picker" + ], + "reason": "Configuration boolean only; no actual download/selection behavior or resolution values." + }, + "styling": { + "signals": [ + "custom_css" + ], + "reason": "Presence/application only plus controlled gallery-layout enums, never CSS/theme values." + }, + "cms": { + "signals": [ + "cms", + "public_site" + ], + "reason": "Admin page editing capability/public-site enabled only; no HTML, slugs or traffic." + }, + "email": { + "signals": [ + "smtp", + "incoming_mail", + "messaging", + "email_templates", + "email_webhook" + ], + "reason": "Configuration and documented manual admin capability operations only; messages, recipients, automatic activity and mailbox values excluded." + }, + "moderation": { + "signals": [ + "feedback_moderation" + ], + "reason": "Admin moderation/word-filter capability, never feedback content or visitor behavior." + }, + "security": { + "signals": [], + "reason": "Excluded password/MFA/session/rate-limit/security profiles and operations." + }, + "sso": { + "signals": [ + "oauth" + ], + "reason": "Enabled/config-present and successful admin callback only; no claims/provider details." + }, + "imageSecurity": { + "signals": [ + "gallery_image_protection" + ], + "reason": "Configuration presence only; no blocked-IP/security analytics or monitoring history." + }, + "seo": { + "signals": [ + "seo_customization" + ], + "reason": "Admin SEO configuration operation only; no meta tags, URLs, robots or verification tokens." + }, + "apiTokens": { + "signals": [ + "api_integration" + ], + "reason": "Valid credential presence and one successful scoped API capability bit; no tokens/scopes/owner metadata." + }, + "webhooks": { + "signals": [ + "webhooks" + ], + "reason": "Active configuration and manual test/replay enqueue only; no delivery data." + }, + "status": { + "signals": [], + "reason": "Excluded operational health, diagnostics, resource data, update and storage polling." + }, + "analytics": { + "signals": [ + "analytics_dashboard" + ], + "reason": "Analytics capability and admin aggregate-view use only; no embedded analytics results/tracker IDs or visitors." + }, + "backup": { + "signals": [ + "backup", + "database_backup", + "portable_backup", + "restore", + "s3_backups" + ], + "reason": "Schedule presence/manual capability initiation only, no histories, sizes, paths or files." + }, + "businessProfile": { + "signals": [], + "reason": "Excluded business identity, bank accounts and addresses." + }, + "crm": { + "signals": [ + "crm", + "crm_quotes", + "crm_invoices", + "crm_projects", + "crm_hours", + "customer_portal", + "crm_installments", + "crm_invoice_import", + "crm_combined_billing", + "crm_monthly_billing_manual", + "crm_document_conversion" + ], + "reason": "Only coarse module capabilities; no policies/amounts/customer/payment values." + }, + "contracts": { + "signals": [ + "crm_contracts", + "document_templates" + ], + "reason": "Admin contract/template capability only; no legal text or signatures." + }, + "reminderTemplates": { + "signals": [ + "reminder_emails", + "email_templates" + ], + "reason": "Reminder flag configuration and admin template editing only; no automatic reminder sends/recipients/content." + }, + "accounting": { + "signals": [ + "accounting", + "accounting_incoming_invoices", + "accounting_expenses", + "accounting_tax_report", + "accounting_ledger" + ], + "reason": "Only module capabilities, no tax codes, rates, balances or business identity." + }, + "whatsapp": { + "signals": [ + "whatsapp" + ], + "reason": "Configured integration plus manual test only; no phone numbers, tokens or automatic delivery." + }, + "slideshow": { + "signals": [ + "slideshow" + ], + "reason": "Admin setup capability only; no kiosk viewers, slide progress or photos." + } + }, + "reviewed_picpeak_base": "a5ff9264 (3.124.1-beta.0)", + "schema_version": "usage.v3", + "purpose": "Product capability prioritization and installation gallery/photo totals; no contents, identities, per-entity breakdowns, action frequencies or visitor observations.", + "route_families": { + "acceptInvite.js": { + "decision": "excluded", + "signals": [], + "reason": "Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers.", + "route_signatures": [ + "GET /:token", + "POST /:token" + ] + }, + "admin.js": { + "decision": "composition", + "signals": [], + "reason": "Router composition / helpers; decisions are recorded for each mounted family.", + "route_signatures": [] + }, + "adminApiTokens.js": { + "decision": "configuration", + "signals": [ + "api_integration" + ], + "reason": "Only existence of a valid credential; no marker from token listing/creation, no scope, owner, token, expiry date or last-used time.", + "route_signatures": [ + "GET /", + "POST /", + "DELETE /:id" + ] + }, + "adminArchives.js": { + "decision": "partial", + "signals": [ + "galleries", + "archive_management", + "photo_exports" + ], + "reason": "Admin archive/delete/restore/download initiation only; filenames, histories, storage sizes and polling excluded.", + "route_signatures": [ + "GET /", + "GET /:id", + "POST /:id/restore", + "GET /:id/download", + "DELETE /:id" + ] + }, + "adminAuth.js": { + "decision": "excluded", + "signals": [], + "reason": "Bootstrap, passwords/MFA/session/profile, per-person notifications, developer helpers and operational health/update/log polling are outside the prioritization purpose.", + "route_signatures": [ + "GET /profile", + "PUT /profile", + "POST /change-password", + "POST /logout", + "GET /mfa/status", + "POST /mfa/setup", + "POST /mfa/enable", + "POST /mfa/disable", + "POST /mfa/recovery-codes" + ] + }, + "adminBackup.js": { + "decision": "partial", + "signals": [ + "backup", + "portable_backup", + "restore", + "s3_storage", + "s3_backups" + ], + "reason": "Admin backup initiation, portable export/import and successful S3 roundtrip test. Local export never implies S3; names, schedules, sizes, contents and history excluded.", + "route_signatures": [ + "GET /config", + "PUT /config", + "GET /status", + "POST /run", + "GET /picpeak/export", + "POST /picpeak/import", + "GET /runs/:id", + "GET /files", + "DELETE /cleanup", + "POST /test-connection", + "GET /manifest/:backupRunId", + "POST /manifest/validate", + "GET /manifest/:backupRunId/download", + "GET /manifests/:backupId", + "GET /manifests/:backupId/download", + "POST /manifests/validate", + "GET /s3/buckets", + "GET /s3/files", + "DELETE /s3/cleanup", + "POST /s3/test-upload", + "GET /download/:backupId", + "GET /checksums", + "POST /estimate" + ] + }, + "adminBusinessProfile.js": { + "decision": "excluded", + "signals": [], + "reason": "Business identity/bank/tax-address configuration and VAT-code helper surface are not separate usage signals. Billing/accounting capabilities are covered without profiling the business.", + "route_signatures": [ + "GET /", + "GET /logo-diagnostic", + "POST /logo", + "DELETE /logo", + "PUT /", + "GET /bank-accounts", + "POST /bank-accounts", + "PUT /bank-accounts/:id", + "DELETE /bank-accounts/:id" + ] + }, + "adminCalendar.js": { + "decision": "partial", + "signals": [ + "crm", + "crm_calendar" + ], + "reason": "Authenticated admin calendar retrieval is capability use; no calendar entries, dates, recurrence, availability or bookings.", + "route_signatures": [ + "GET /items" + ] + }, + "adminCategories.js": { + "decision": "partial", + "signals": [ + "gallery_categories", + "gallery_folders" + ], + "reason": "Admin category CRUD; no names, descriptions, colors or ordering values. v3 adds only: gallery_folders. Exact definitions are in features.v3.json; configuration-only signals never observe the public surface.", + "route_signatures": [ + "GET /global", + "GET /event/:eventId", + "POST /", + "PUT /:id", + "PUT /:id/hero", + "DELETE /:id", + "POST /reorder", + "DELETE /reorder/:eventId", + "POST /reorder-global" + ] + }, + "adminCMS.js": { + "decision": "partial", + "signals": [ + "cms" + ], + "reason": "Admin CMS page CRUD only. Public page traffic, slug, HTML, text, links and media excluded.", + "route_signatures": [ + "GET /pages", + "GET /pages/:slug", + "PUT /pages/:slug", + "POST /pages/:slug/logo", + "DELETE /pages/:slug/logo" + ] + }, + "adminContracts.js": { + "decision": "partial", + "signals": [ + "crm", + "crm_contracts", + "document_templates", + "crm_document_conversion" + ], + "reason": "Admin contract/block operations only; no legal text, signatures, signing parties or customer signing events. v3 adds only: crm_document_conversion. Exact definitions are in features.v3.json; configuration-only signals never observe the public surface.", + "route_signatures": [ + "GET /blocks", + "POST /blocks", + "PUT /blocks/:id", + "DELETE /blocks/:id", + "GET /", + "POST /", + "GET /:id", + "PUT /:id", + "POST /:id/send", + "POST /:id/cancel", + "POST /:id/convert-to-event", + "POST /:id/convert-to-invoice", + "POST /:id/resend-signed", + "POST /:id/restamp-signatures", + "POST /:id/countersign", + "POST /:id/upload-signed-pdf", + "GET /:id/pdf", + "GET /:id/signed-pdf", + "GET /:id/audit-trail", + "GET /:id/verify-integrity", + "GET /:id/preview" + ] + }, + "adminCssTemplates.js": { + "decision": "configuration", + "signals": [ + "custom_css" + ], + "reason": "Only existence of enabled applied CSS and locally observed application, not editing/viewing templates or any CSS text.", + "route_signatures": [ + "GET /", + "GET /enabled", + "GET /:slotNumber", + "PUT /:slotNumber", + "POST /:slotNumber/reset" + ] + }, + "adminCustomers.js": { + "decision": "partial", + "signals": [ + "crm", + "crm_hours", + "customer_portal", + "crm_combined_billing", + "crm_monthly_billing_manual" + ], + "reason": "Successful admin CRM/hour-entry/invitation operations only. No customer/account names, IDs, rates, billed hours, payment state or portal behavior. v3 adds only: crm_combined_billing, crm_monthly_billing_manual. Exact definitions are in features.v3.json; configuration-only signals never observe the public surface.", + "route_signatures": [ + "GET /", + "GET /search", + "GET /invitations", + "POST /invite", + "DELETE /invitations/:id", + "POST /", + "POST /:id/send-invite", + "GET /:id", + "PUT /:id", + "POST /:id/deactivate", + "POST /:id/reactivate", + "POST /:id/erase", + "POST /:id/password-reset", + "PUT /:id/events", + "GET /hour-entries/unbilled-summary", + "GET /:id/hour-entries", + "POST /:id/hour-entries", + "PUT /:id/hour-entries/:entryId", + "DELETE /:id/hour-entries/:entryId", + "POST /:id/hour-entries/bill", + "POST /:id/bill-combined", + "POST /:id/trigger-monthly-bill", + "GET /:id/monthly-draft" + ] + }, + "adminDashboard.js": { + "decision": "partial", + "signals": [ + "analytics_dashboard" + ], + "reason": "Admin analytics capability endpoint only; no stats, activities, health/CRM polls, underlying visitor data or dashboard values.", + "route_signatures": [ + "GET /stats", + "GET /activity", + "GET /health", + "GET /analytics", + "GET /crm-stats" + ] + }, + "adminDatabaseBackup.js": { + "decision": "partial", + "signals": [ + "backup", + "database_backup" + ], + "reason": "Admin database-backup initiation plus schedule-enabled boolean, no file data/history.", + "route_signatures": [ + "GET /status", + "PUT /config", + "POST /backup", + "GET /progress", + "GET /history", + "DELETE /cleanup", + "POST /test", + "GET /checksums" + ] + }, + "adminDeals.js": { + "decision": "partial", + "signals": [ + "crm", + "crm_installments" + ], + "reason": "Admin installment-plan changes only. No actual plans, invoice links, amounts, paid states or deal reporting.", + "route_signatures": [ + "GET /:uuid/documents", + "PUT /:uuid/installment-plan" + ] + }, + "adminDev.js": { + "decision": "excluded", + "signals": [], + "reason": "Bootstrap, passwords/MFA/session/profile, per-person notifications, developer helpers and operational health/update/log polling are outside the prioritization purpose.", + "route_signatures": [ + "GET /email-templates", + "POST /send-test-email" + ] + }, + "adminEmail.js": { + "decision": "partial", + "signals": [ + "messaging", + "incoming_mail", + "smtp", + "email_templates", + "email_webhook", + "reminder_emails" + ], + "reason": "Admin message operation/template edit, actual successful manual send/test transport and non-skipped manual IMAP poll/test. Reminder flag configuration only. No automated sends/polls, received-message or recipient data, queue/log reads, mailbox addresses or templates.", + "route_signatures": [ + "GET /config", + "POST /config", + "GET /incoming-config", + "POST /incoming-config", + "POST /incoming-config/folders", + "POST /incoming-config/test", + "POST /incoming-config/roundtrip", + "POST /incoming-config/poll", + "GET /received", + "GET /received/:id", + "POST /item/:kind/:id/state", + "DELETE /item/:kind/:id", + "GET /accounts", + "GET /identities", + "POST /accounts", + "POST /accounts/test", + "POST /test", + "POST /flush-queue", + "GET /queue", + "GET /queue/:id", + "POST /send", + "GET /templates", + "GET /templates/:key", + "PUT /templates/:key", + "POST /templates", + "POST /templates/:key/preview" + ] + }, + "adminEventRename.js": { + "decision": "partial", + "signals": [ + "galleries" + ], + "reason": "Successful rename only, not validate-rename. No former/new names or identifiers.", + "route_signatures": [ + "POST /:eventId/rename", + "POST /:eventId/validate-rename" + ] + }, + "adminEvents/archiveBulk.js": { + "decision": "partial", + "signals": [ + "galleries", + "archive_management", + "photo_exports" + ], + "reason": "Admin archive/delete/restore/download initiation only; filenames, histories, storage sizes and polling excluded.", + "route_signatures": [ + "POST /:id/archive", + "POST /bulk-archive", + "POST /bulk-delete" + ] + }, + "adminEvents/crud.js": { + "decision": "partial", + "signals": [ + "galleries", + "gallery_guest_uploads", + "gallery_downloads", + "gallery_client_access", + "gallery_watermarks", + "gallery_reveal", + "gallery_expiration", + "gallery_sharing", + "custom_css", + "gallery_capture_date_sort" + ], + "reason": "Admin creation/edit/publish etc. set galleries; sharing has its own fixed key. Guest/download/protection/reveal/expiry are configuration only; themes contribute controlled layouts and CSS presence. No gallery metadata or guest action history. v3 adds only: gallery_capture_date_sort. Exact definitions are in features.v3.json; configuration-only signals never observe the public surface.", + "route_signatures": [ + "POST /", + "GET /", + "GET /:id", + "POST /:id/send-gallery-email", + "POST /:id/publish", + "POST /:id/duplicate", + "PUT /:id", + "POST /:id/reveal", + "DELETE /:id", + "POST /:id/toggle-status", + "POST /:id/extend" + ] + }, + "adminEvents/downloadResolutions.js": { + "decision": "configuration", + "signals": [ + "download_resolution_picker" + ], + "reason": "Only whether a picker is configured globally or in a gallery. No chosen resolution, download event or counts.", + "route_signatures": [ + "GET /:id/download-resolutions", + "PATCH /:id/download-resolutions" + ] + }, + "adminEvents/faces.js": { + "decision": "partial", + "signals": [ + "face_recognition" + ], + "reason": "Effective flag plus successful admin faces/people operation. No health polling, embeddings, names, groups, detections or visitor searches.", + "route_signatures": [ + "GET /faces/health", + "GET /:id/faces", + "PATCH /:id/faces", + "GET /:id/people", + "GET /:id/people/suggestions", + "POST /:id/people/suggestions/dismiss", + "PATCH /:id/people/:personId", + "POST /:id/people/merge", + "POST /:id/people/:personId/split", + "GET /:id/people/:personId/faces", + "POST /:id/faces/rescan", + "POST /:id/faces/recluster", + "GET /faces/auto-categories", + "PUT /faces/auto-categories", + "POST /:id/faces/categorize", + "DELETE /:id/faces/categorize", + "DELETE /:id/faces" + ] + }, + "adminEvents/helpers.js": { + "decision": "composition", + "signals": [], + "reason": "Router composition / helpers; decisions are recorded for each mounted family.", + "route_signatures": [] + }, + "adminEvents/index.js": { + "decision": "composition", + "signals": [], + "reason": "Router composition / helpers; decisions are recorded for each mounted family.", + "route_signatures": [] + }, + "adminEvents/logo.js": { + "decision": "partial", + "signals": [ + "branding" + ], + "reason": "Successful admin logo operation only; image/filename/content excluded.", + "route_signatures": [ + "POST /:id/logo", + "DELETE /:id/logo" + ] + }, + "adminEvents/qr.js": { + "decision": "partial", + "signals": [ + "gallery_sharing" + ], + "reason": "Admin QR generation only; no scans, tokens or URLs.", + "route_signatures": [ + "GET /:id/qr", + "GET /:id/qr-print" + ] + }, + "adminEvents/resets.js": { + "decision": "partial", + "signals": [ + "galleries", + "gallery_sharing" + ], + "reason": "Admin gallery reset/sharing capability only; no password, recipient, token or reset statistics.", + "route_signatures": [ + "POST /:id/reset-password", + "POST /:id/resend-email" + ] + }, + "adminEvents/slideshow.js": { + "decision": "partial", + "signals": [ + "slideshow" + ], + "reason": "Admin generate/disable/configure only, never kiosk viewers or slide advances.", + "route_signatures": [ + "POST /:id/slideshow/generate", + "POST /:id/slideshow/disable", + "PATCH /:id/slideshow" + ] + }, + "adminEventTypes.js": { + "decision": "partial", + "signals": [ + "event_types" + ], + "reason": "Admin event-type CRUD; preset contents/names excluded.", + "route_signatures": [ + "GET /", + "GET /active", + "GET /:id", + "POST /", + "PUT /:id", + "DELETE /:id", + "POST /reorder" + ] + }, + "adminExpenses.js": { + "decision": "partial", + "signals": [ + "accounting", + "accounting_expenses", + "accounting_incoming_invoices" + ], + "reason": "Admin expense/inbound-invoice operations; no financial values, suppliers, mileage/location, dates, receipt files or OCR text.", + "route_signatures": [ + "GET /categories", + "POST /categories", + "PATCH /categories/:id", + "DELETE /categories/:id", + "POST /inbound", + "GET /inbound", + "GET /inbound/pending-summary", + "POST /inbound/bill-pending", + "GET /inbound/by-customer/:customerAccountId", + "GET /inbound/:id/file", + "GET /inbound/:id/page/:n", + "GET /inbound/:id", + "PATCH /inbound/:id", + "POST /inbound/:id/categorize", + "POST /inbound/:id/rebill", + "POST /inbound/:id/supplier-payment", + "GET /", + "POST /", + "GET /:id/proof", + "GET /:id", + "PATCH /:id", + "POST /:id/invoice", + "POST /:id/paid" + ] + }, + "adminExternalMedia.js": { + "decision": "partial", + "signals": [ + "share_mounts" + ], + "reason": "Only admin import operation; status/list/browse are not use. Snapshot checks external-path presence, never reports a path.", + "route_signatures": [ + "GET /list", + "POST /events/:id/import-external" + ] + }, + "adminFeatureFlags.js": { + "decision": "configuration", + "signals": [ + "crm", + "crm_quotes", + "crm_invoices", + "crm_contracts", + "crm_projects", + "crm_calendar", + "crm_hours", + "customer_portal", + "accounting", + "workflows", + "newsletters", + "face_recognition", + "slideshow", + "transfers", + "messaging", + "reminder_emails", + "accounting_incoming_invoices", + "accounting_expenses", + "accounting_tax_report", + "accounting_ledger", + "admin_management", + "analytics_dashboard" + ], + "reason": "Only allowlisted effective capability booleans. No marker from reading or saving feature flags. Disabled roadmap/developer flags excluded.", + "route_signatures": [ + "GET /", + "PUT /" + ] + }, + "adminFeedback.js": { + "decision": "partial", + "signals": [ + "feedback_moderation", + "gallery_feedback_likes", + "gallery_feedback_ratings", + "gallery_feedback_comments", + "gallery_feedback_favorites", + "gallery_feedback_reactions", + "gallery_feedback_color_labels", + "gallery_guest_accounts" + ], + "reason": "Admin moderation/word-filter operations only. Visitor feedback is not observed. Master-enabled per-gallery feedback-option booleans only; no contents, ratings, likes, colors, identities or word lists.", + "route_signatures": [ + "GET /events/:eventId/feedback-settings", + "PUT /events/:eventId/feedback-settings", + "GET /events/:eventId/feedback", + "PUT /feedback/:feedbackId/:action", + "DELETE /feedback/:feedbackId", + "GET /events/:eventId/feedback-analytics", + "GET /events/:eventId/feedback/export", + "GET /feedback/pending-moderation", + "GET /word-filters", + "POST /word-filters", + "PUT /word-filters/:id", + "DELETE /word-filters/:id" + ] + }, + "adminGuests.js": { + "decision": "partial", + "signals": [ + "guest_management" + ], + "reason": "Admin guest management/export initiation only. No guest names, invitations, tokens, contact data, guest counts or visitor interactions.", + "route_signatures": [ + "GET /events/:eventId/guests", + "GET /events/:eventId/guests/aggregate", + "GET /events/:eventId/guests/invites", + "POST /events/:eventId/guests/invites", + "DELETE /events/:eventId/guests/invites/:inviteId", + "GET /events/:eventId/guests/export-all", + "GET /events/:eventId/guests/:guestId", + "GET /events/:eventId/guests/:guestId/export", + "DELETE /events/:eventId/guests/:guestId", + "POST /events/:eventId/guests/:keepId/merge" + ] + }, + "adminImageSecurity.js": { + "decision": "configuration", + "signals": [ + "gallery_image_protection" + ], + "reason": "Only gallery/global technical protection configuration existence. No security events, blocked IPs, request counts, threat scores or admin monitoring access.", + "route_signatures": [ + "GET /settings", + "PUT /settings", + "GET /dashboard", + "GET /logs", + "GET /events/:eventId/access-logs", + "POST /block-ip", + "DELETE /logs/cleanup", + "GET /export" + ] + }, + "adminInvoices.js": { + "decision": "partial", + "signals": [ + "crm", + "crm_invoices", + "crm_invoice_import" + ], + "reason": "Admin invoice operations only; no amounts, VAT/customer/payment values or payment-check responses. v3 adds only: crm_invoice_import. Exact definitions are in features.v3.json; configuration-only signals never observe the public surface.", + "route_signatures": [ + "GET /", + "GET /:id", + "POST /", + "POST /import", + "PUT /:id", + "GET /:id/rebill-proofs", + "POST /:id/send", + "POST /:id/mark-paid", + "POST /:id/send-reminder", + "POST /:id/test-payment-check", + "POST /:id/reissue", + "POST /:id/release-for-delivery", + "POST /:id/cancel", + "GET /:id/pdf", + "POST /preview", + "GET /:id/payment-log" + ] + }, + "adminLedger.js": { + "decision": "partial", + "signals": [ + "accounting", + "accounting_ledger" + ], + "reason": "Admin ledger-account/VAT/mapping edits and ledger export initiation only; no account/currency/VAT identifiers or exported records.", + "route_signatures": [ + "GET /accounts", + "POST /accounts", + "PATCH /accounts/:id", + "DELETE /accounts/:id", + "GET /vat-codes", + "POST /vat-codes", + "PATCH /vat-codes/:id", + "DELETE /vat-codes/:id", + "GET /mappings", + "PATCH /mappings/category/:id", + "PATCH /mappings/settings", + "GET /export" + ] + }, + "adminNewsletters.js": { + "decision": "partial", + "signals": [ + "newsletters" + ], + "reason": "Admin campaign changes/test/queue/cancel only. Recipient resolution, previews, subscriptions/unsubscribes, delivery/open/click data and automatic sending excluded.", + "route_signatures": [ + "GET /", + "GET /:id", + "GET /:id/recipients", + "POST /", + "PUT /:id", + "DELETE /:id", + "POST /:id/preview", + "POST /:id/recipients/resolve", + "POST /:id/test", + "POST /:id/queue", + "POST /:id/cancel" + ] + }, + "adminNotifications.js": { + "decision": "excluded", + "signals": [], + "reason": "Bootstrap, passwords/MFA/session/profile, per-person notifications, developer helpers and operational health/update/log polling are outside the prioritization purpose.", + "route_signatures": [ + "GET /", + "PUT /:id/read", + "PUT /read-all", + "DELETE /clear-all" + ] + }, + "adminPhotoDimensions.js": { + "decision": "partial", + "signals": [ + "photo_processing" + ], + "reason": "Admin repair/regenerate/configuration initiation, never status polling or processing totals.", + "route_signatures": [ + "POST /repair-dimensions", + "GET /repair-dimensions/status", + "POST /repair-capture-dates", + "GET /repair-capture-dates/status", + "POST /repair-orientation", + "GET /repair-orientation/status" + ] + }, + "adminPhotoExport.js": { + "decision": "partial", + "signals": [ + "photo_exports", + "photo_xmp_export" + ], + "reason": "Admin export initiation only; export filters, selected files, sizes and contents excluded. v3 adds only: photo_xmp_export. Exact definitions are in features.v3.json; configuration-only signals never observe the public surface.", + "route_signatures": [ + "GET /:eventId/filtered", + "GET /:eventId/filter-summary", + "POST /:eventId/export", + "GET /export-formats" + ] + }, + "adminPhotos.js": { + "decision": "partial", + "signals": [ + "photo_management", + "photo_exports", + "photo_processing", + "video_uploads", + "camera_raw_uploads", + "s3_storage", + "s3_photo_storage", + "photo_replacement", + "photo_admin_marks" + ], + "reason": "Successful admin edits/exports and accepted upload evidence only. Chunk init/status, failed uploads and public downloads excluded. Only video/RAW/S3 booleans survive, never file metadata/EXIF/content. v3 adds only: photo_replacement, photo_admin_marks. Exact definitions are in features.v3.json; configuration-only signals never observe the public surface.", + "route_signatures": [ + "POST /:eventId/upload", + "GET /uploads/:upload_id/status", + "GET /uploads/:upload_id/stream", + "POST /photos/:photoId/retry", + "DELETE /:eventId/photos/:photoId", + "PUT /:eventId/photos/:photoId/mark", + "PATCH /:eventId/photos/:photoId", + "POST /:eventId/photos/bulk-delete", + "POST /:eventId/photos/bulk-update", + "GET /:eventId/photos/:photoId/download", + "GET /:eventId/photos", + "GET /:eventId/photo/:photoId", + "GET /:eventId/thumbnail/:photoId", + "GET /:eventId/preview/:photoId", + "GET /:eventId/debug", + "POST /:eventId/chunked-upload/init", + "POST /:eventId/chunked-upload/:uploadId/chunk/:chunkIndex", + "POST /:eventId/chunked-upload/:uploadId/complete", + "GET /:eventId/chunked-upload/:uploadId/status", + "DELETE /:eventId/chunked-upload/:uploadId" + ] + }, + "adminProjects.js": { + "decision": "partial", + "signals": [ + "crm", + "crm_projects" + ], + "reason": "Admin project operations only; project/person names, business performance, metadata and totals excluded.", + "route_signatures": [ + "GET /", + "POST /", + "GET /:id", + "PUT /:id", + "POST /:id/events", + "POST /:id/quotes", + "POST /:id/contracts", + "GET /:id/overview", + "GET /email/:emailId/preview", + "POST /email/:emailId/resend", + "POST /email/:emailId/cancel", + "POST /email/:emailId/retry", + "POST /email/:emailId/send-now" + ] + }, + "adminQuotes.js": { + "decision": "partial", + "signals": [ + "crm", + "crm_quotes", + "document_templates", + "crm_document_conversion" + ], + "reason": "Admin quote/preset operations only; no quote content, prices, customer acceptance or signatures. v3 adds only: crm_document_conversion. Exact definitions are in features.v3.json; configuration-only signals never observe the public surface.", + "route_signatures": [ + "GET /", + "GET /:id", + "POST /", + "PUT /:id", + "POST /:id/send", + "POST /:id/duplicate", + "POST /:id/accept", + "POST /:id/decline", + "POST /:id/convert", + "POST /:id/convert-to-invoice", + "POST /:id/convert-to-contract", + "GET /:id/pdf", + "POST /preview", + "GET /presets/line-items", + "POST /presets/line-items", + "PUT /presets/line-items/:id", + "DELETE /presets/line-items/:id", + "GET /presets/payment-terms", + "POST /presets/payment-terms", + "PUT /presets/payment-terms/:id", + "DELETE /presets/payment-terms/:id", + "GET /presets/payment-net-days", + "POST /presets/payment-net-days", + "PUT /presets/payment-net-days/:id", + "DELETE /presets/payment-net-days/:id", + "GET /presets/payment-timing", + "POST /presets/payment-timing", + "PUT /presets/payment-timing/:id", + "DELETE /presets/payment-timing/:id" + ] + }, + "adminRestore.js": { + "decision": "partial", + "signals": [ + "restore" + ], + "reason": "Admin restore initiation only, never file selection, content, progress, errors or timing.", + "route_signatures": [ + "GET /status", + "POST /validate", + "POST /start", + "GET /progress", + "GET /run/:id", + "GET /run/:id/report", + "GET /available-backups", + "POST /list-backups", + "GET /settings", + "PUT /settings" + ] + }, + "adminRoles.js": { + "decision": "partial", + "signals": [ + "admin_management" + ], + "reason": "Admin account/role management capability; no names, permissions, role labels, password reset operations or active-user counts. Auth/self-profile endpoints excluded.", + "route_signatures": [ + "GET /", + "GET /permissions", + "POST /", + "POST /:id/clone", + "PUT /:id", + "DELETE /:id" + ] + }, + "adminSettings.js": { + "decision": "partial", + "signals": [ + "custom_css", + "oauth", + "smtp", + "backup", + "s3_storage", + "video_uploads", + "camera_raw_uploads", + "public_site", + "branding", + "seo_customization", + "slideshow", + "download_resolution_picker", + "gallery_watermarks", + "database_backup", + "s3_auto_import", + "download_original_filenames" + ], + "reason": "Only specified configuration presence/booleans and explicit branding/SEO/slideshow operations. Generic settings reads, security policies, passwords, storage data, SMTP/OIDC credentials, custom HTML/CSS/SEO values excluded. v3 adds only: s3_auto_import, download_original_filenames. Exact definitions are in features.v3.json; configuration-only signals never observe the public surface.", + "route_signatures": [ + "GET /", + "GET /:type", + "GET /customer-surface", + "PUT /customer-surface", + "PUT /accounting", + "PUT /slideshow", + "GET /downloads", + "PUT /downloads", + "GET /sso", + "PUT /sso", + "POST /sso/test", + "GET /:type", + "GET /password/complexity", + "PUT /branding", + "POST /logo", + "DELETE /logo", + "POST /branding/watermark-logo", + "PUT /theme", + "PUT /general", + "PUT /security", + "PUT /analytics", + "PUT /seo", + "GET /storage/info", + "POST /favicon", + "PUT /security/rate-limit", + "GET /public-site/default", + "POST /public-site/reset" + ] + }, + "adminShortUrls.js": { + "decision": "partial", + "signals": [ + "gallery_sharing", + "short_links" + ], + "reason": "Admin short-link creation/deletion only; link/token/click metadata excluded.", + "route_signatures": [ + "GET /events/:eventId/short-urls", + "POST /events/:eventId/short-urls", + "DELETE /short-urls/:id" + ] + }, + "adminSystem.js": { + "decision": "excluded", + "signals": [], + "reason": "Bootstrap, passwords/MFA/session/profile, per-person notifications, developer helpers and operational health/update/log polling are outside the prioritization purpose.", + "route_signatures": [ + "GET /version", + "GET /updates", + "GET /updates/whatsnew", + "POST /updates/whatsnew/seen", + "GET /updates/changelog", + "GET /updates/instructions", + "GET /status", + "GET /database", + "GET /updates/notifications", + "PUT /updates/notifications", + "POST /updates/notifications/send", + "POST /updates/notifications/check" + ] + }, + "adminSystemHealth.js": { + "decision": "excluded", + "signals": [], + "reason": "Bootstrap, passwords/MFA/session/profile, per-person notifications, developer helpers and operational health/update/log polling are outside the prioritization purpose.", + "route_signatures": [ + "GET /backup-integrity", + "GET /backup-coverage", + "GET /failures", + "POST /failures/email/:id/retry", + "DELETE /failures/email/:id" + ] + }, + "adminTaxReport.js": { + "decision": "partial", + "signals": [ + "accounting", + "accounting_tax_report" + ], + "reason": "Admin tax report generation/export only; no totals, dates, tax regimes, geography or currency.", + "route_signatures": [ + "GET /", + "GET /pdf", + "GET /csv" + ] + }, + "adminThumbnails.js": { + "decision": "partial", + "signals": [ + "photo_processing" + ], + "reason": "Admin repair/regenerate/configuration initiation, never status polling or processing totals.", + "route_signatures": [ + "GET /settings", + "PUT /settings", + "POST /regenerate", + "POST /regenerate-previews", + "GET /regenerate/status" + ] + }, + "adminTransfers.js": { + "decision": "partial", + "signals": [ + "transfers", + "transfer_upload_links" + ], + "reason": "Admin transfer CRUD/files/link management/download only. Public recipients, received-file data, upload and download statistics excluded. v3 adds only: transfer_upload_links. Exact definitions are in features.v3.json; configuration-only signals never observe the public surface.", + "route_signatures": [ + "GET /", + "POST /", + "GET /:id", + "PATCH /:id", + "DELETE /:id", + "POST /:id/files", + "DELETE /:id/files/:fileId", + "POST /:id/upload-files", + "DELETE /:id/extra-files/:extraId", + "GET /:id/extra-files/:extraId/download", + "POST /:id/upload-link", + "DELETE /:id/upload-link", + "GET /:id/download", + "GET /:id/uploads/:uploadId/download" + ] + }, + "adminUsage.js": { + "decision": "excluded", + "signals": [], + "reason": "Consent, inspection, export, feedback, voting, deletion and abandoning an unsignable deletion are explicit protocol operations; not product-use signals. Activity only triggers a due fixed report.", + "route_signatures": [ + "POST /activity", + "GET /", + "POST /dismiss", + "POST /enable", + "POST /consent", + "POST /disable", + "POST /abandon", + "POST /retry", + "GET /preview", + "GET /export", + "PUT /feedback-preferences", + "POST /feedback", + "POST /vote", + "POST /portal-session" + ] + }, + "adminUsers.js": { + "decision": "partial", + "signals": [ + "admin_management" + ], + "reason": "Admin account/role management capability; no names, permissions, role labels, password reset operations or active-user counts. Auth/self-profile endpoints excluded.", + "route_signatures": [ + "GET /me/permissions", + "GET /", + "GET /roles", + "GET /invitations", + "POST /invite", + "DELETE /invitations/:id", + "GET /:id", + "PUT /:id", + "POST /:id/deactivate", + "POST /:id/activate", + "DELETE /:id", + "POST /:id/reset-password" + ] + }, + "adminVatCodes.js": { + "decision": "excluded", + "signals": [], + "reason": "Business identity/bank/tax-address configuration and VAT-code helper surface are not separate usage signals. Billing/accounting capabilities are covered without profiling the business.", + "route_signatures": [ + "GET /" + ] + }, + "adminWebhooks.js": { + "decision": "partial", + "signals": [ + "webhooks" + ], + "reason": "Active configuration existence plus successful admin manual test/replay enqueue. Actual network delivery/results/subscriptions/destinations excluded.", + "route_signatures": [ + "GET /", + "POST /", + "GET /:id", + "PUT /:id", + "DELETE /:id", + "POST /:id/test", + "GET /:id/deliveries", + "GET /:id/deliveries/:deliveryId", + "POST /:id/deliveries/:deliveryId/replay" + ] + }, + "adminWhatsapp.js": { + "decision": "partial", + "signals": [ + "whatsapp" + ], + "reason": "Effective configured sender and successful manual test only. No automated deliveries, phone numbers, templates or delivery statuses.", + "route_signatures": [ + "GET /config", + "PUT /config", + "POST /test" + ] + }, + "adminWorkflows.js": { + "decision": "partial", + "signals": [ + "workflows", + "workflow_automation_enabled" + ], + "reason": "Admin workflow authoring/approval/test initiation only. Runtime triggers, payloads, execution frequency/results and public approvals excluded. v3 adds only: workflow_automation_enabled. Exact definitions are in features.v3.json; configuration-only signals never observe the public surface.", + "route_signatures": [ + "GET /approvals", + "POST /approvals/:id/:action", + "GET /runs/:runId/steps", + "GET /:id/runs", + "POST /:id/test-run", + "GET /", + "GET /:id", + "POST /", + "PUT /:id", + "PATCH /:id/enabled", + "DELETE /:id" + ] + }, + "analyticsTrackerProxy.js": { + "decision": "excluded", + "signals": [], + "reason": "Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers.", + "route_signatures": [] + }, + "auth.js": { + "decision": "partial", + "signals": [ + "oauth" + ], + "reason": "Only successful admin OIDC callback sets oauth. Password/gallery authentication, MFA, account claims and provider details excluded.", + "route_signatures": [ + "POST /admin/login", + "POST /admin/login/mfa", + "POST /logout", + "POST /gallery/verify", + "POST /gallery/:slug/client-login", + "POST /gallery/share-login", + "POST /gallery/logout", + "GET /session", + "POST /admin/change-password", + "POST /password-strength", + "GET /admin/sso/login", + "GET /admin/sso/callback" + ] + }, + "customer.js": { + "decision": "excluded", + "signals": [], + "reason": "Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers.", + "route_signatures": [ + "GET /events", + "GET /events/:slug/access-token", + "GET /profile", + "PUT /profile", + "GET /profile/marketing", + "PUT /profile/marketing", + "POST /profile/password", + "GET /quotes", + "GET /invoices", + "GET /quotes/:id/pdf", + "GET /invoices/:id/pdf", + "GET /contracts", + "GET /contracts/:id/pdf" + ] + }, + "customerAuth.js": { + "decision": "excluded", + "signals": [], + "reason": "Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers.", + "route_signatures": [ + "POST /login", + "POST /logout", + "GET /session", + "GET /invite/:token", + "POST /accept-invite", + "GET /password-reset/:token", + "POST /password-reset" + ] + }, + "gallery.js": { + "decision": "excluded", + "signals": [], + "reason": "Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers.", + "route_signatures": [ + "GET /resolve/:identifier", + "GET /:slug/verify-token/:token", + "GET /:slug/info", + "GET /:slug/show/:token/session", + "GET /:slug/show/:token/state", + "GET /:slug/photos", + "GET /:slug/people", + "PATCH /:slug/photos/:photoId/visibility", + "PATCH /:slug/photos/visibility/bulk", + "GET /:slug/download/:photoId", + "GET /:slug/download-all", + "POST /:slug/download-selected", + "POST /:slug/download-jobs", + "GET /:slug/download-jobs/:token", + "GET /:slug/download-jobs/:token/file", + "POST /:slug/photo/:photoId/view", + "GET /:slug/photo/:photoId", + "GET /:slug/thumbnail/:photoId", + "GET /:slug/hero/:photoId", + "GET /:slug/preview/:photoId", + "GET /:slug/stats", + "POST /:eventId/upload", + "GET /:slug/uploads/status", + "GET /:slug/css-template" + ] + }, + "galleryFeedback.js": { + "decision": "excluded", + "signals": [], + "reason": "Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers.", + "route_signatures": [ + "GET /:slug/feedback-settings", + "GET /:slug/photos/:photoId/feedback", + "POST /:slug/photos/:photoId/feedback", + "GET /:slug/feedback-summary", + "GET /:slug/my-feedback" + ] + }, + "galleryGuests.js": { + "decision": "excluded", + "signals": [], + "reason": "Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers.", + "route_signatures": [ + "POST /:slug/guest", + "GET /:slug/guest/me", + "DELETE /:slug/guest/me", + "POST /:slug/guest/recover", + "POST /:slug/guest/verify", + "POST /:slug/guest/redeem" + ] + }, + "protectedImages.js": { + "decision": "excluded", + "signals": [], + "reason": "Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers.", + "route_signatures": [ + "GET /:slug/photo/:photoId/view", + "POST /:slug/photo/:photoId/generate-secure-token", + "POST /:slug/photo/:photoId/generate-url", + "GET /:slug/photo/:photoId/signed/:token" + ] + }, + "publicCMS.js": { + "decision": "excluded", + "signals": [], + "reason": "Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers.", + "route_signatures": [ + "GET /pages/:slug" + ] + }, + "publicContracts.js": { + "decision": "excluded", + "signals": [], + "reason": "Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers.", + "route_signatures": [ + "GET /:token", + "POST /:token/sign", + "POST /:token/upload-signed-pdf", + "GET /:token/pdf" + ] + }, + "publicFonts.js": { + "decision": "excluded", + "signals": [], + "reason": "Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers.", + "route_signatures": [ + "GET /" + ] + }, + "publicNewsletter.js": { + "decision": "excluded", + "signals": [], + "reason": "Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers.", + "route_signatures": [ + "GET /unsubscribe/:token", + "POST /unsubscribe/:token" + ] + }, + "publicPaymentCheck.js": { + "decision": "excluded", + "signals": [], + "reason": "Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers.", + "route_signatures": [ + "GET /:token", + "POST /:token" + ] + }, + "publicQuotes.js": { + "decision": "excluded", + "signals": [], + "reason": "Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers.", + "route_signatures": [ + "GET /:token", + "POST /:token/respond" + ] + }, + "publicSettings.js": { + "decision": "excluded", + "signals": [], + "reason": "Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers.", + "route_signatures": [ + "GET /" + ] + }, + "publicTransfer.js": { + "decision": "excluded", + "signals": [], + "reason": "Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers.", + "route_signatures": [ + "GET /:token", + "GET /:token/download", + "GET /:token/download/:fileId" + ] + }, + "publicTransferUpload.js": { + "decision": "excluded", + "signals": [], + "reason": "Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers.", + "route_signatures": [ + "GET /:token", + "POST /:token" + ] + }, + "publicWorkflowApprovals.js": { + "decision": "excluded", + "signals": [], + "reason": "Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers.", + "route_signatures": [ + "GET /:token/:action", + "POST /:token/:action" + ] + }, + "secureImages.js": { + "decision": "excluded", + "signals": [], + "reason": "Public/customer/gallery/visitor surface or existing optional third-party analytics proxy: no product-usage middleware, callbacks, counters or report triggers.", + "route_signatures": [ + "POST /:slug/generate-token", + "GET /:slug/secure/:photoId/:token", + "GET /:slug/secure-download/:photoId/:token", + "GET /security/stats" + ] + }, + "setup.js": { + "decision": "excluded", + "signals": [], + "reason": "Bootstrap, passwords/MFA/session/profile, per-person notifications, developer helpers and operational health/update/log polling are outside the prioritization purpose.", + "route_signatures": [ + "GET /status", + "POST /verify-token", + "POST /admin", + "POST /complete" + ] + }, + "v1/events.js": { + "decision": "partial", + "signals": [ + "api_integration" + ], + "reason": "Single bit after successful admin-owned scoped API authentication. No request/response values; API requests do not trigger reports.", + "route_signatures": [ + "POST /events", + "GET /events", + "GET /event-types", + "GET /events/:id", + "POST /events/:id/photos", + "GET /events/:id/share-link", + "GET /events/:id/photos" + ] + } + }, + "feature_flags": { + "accounting": { + "signals": [ + "accounting", + "accounting_ledger" + ], + "reason": "Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean." + }, + "analytics": { + "signals": [ + "analytics_dashboard" + ], + "reason": "Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean." + }, + "bills": { + "signals": [ + "crm_invoices", + "crm_invoice_import", + "crm_monthly_billing_manual" + ], + "reason": "Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean." + }, + "calendar": { + "signals": [ + "crm_calendar" + ], + "reason": "Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean." + }, + "calendarBooking": { + "signals": [], + "reason": "Excluded: disabled roadmap placeholder, not an implemented booking capability." + }, + "clients": { + "signals": [ + "crm" + ], + "reason": "Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean." + }, + "contracts": { + "signals": [ + "crm_contracts" + ], + "reason": "Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean." + }, + "crmDevelopment": { + "signals": [], + "reason": "Excluded: internal development/test helpers, not product adoption." + }, + "customerPortal": { + "signals": [ + "customer_portal" + ], + "reason": "Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean." + }, + "expenses": { + "signals": [ + "accounting_expenses" + ], + "reason": "Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean." + }, + "faces": { + "signals": [ + "face_recognition" + ], + "reason": "Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean." + }, + "galleries": { + "signals": [ + "galleries" + ], + "reason": "Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean." + }, + "hoursLogging": { + "signals": [ + "crm_hours" + ], + "reason": "Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean." + }, + "incomingInvoices": { + "signals": [ + "accounting_incoming_invoices", + "crm_combined_billing" + ], + "reason": "Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean." + }, + "incomingMail": { + "signals": [ + "incoming_mail" + ], + "reason": "Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean." + }, + "messaging": { + "signals": [ + "messaging" + ], + "reason": "Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean." + }, + "newsletters": { + "signals": [ + "newsletters" + ], + "reason": "Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean." + }, + "projects": { + "signals": [ + "crm_projects" + ], + "reason": "Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean." + }, + "quotes": { + "signals": [ + "crm_quotes", + "crm_document_conversion" + ], + "reason": "Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean." + }, + "reminderEmails": { + "signals": [ + "reminder_emails" + ], + "reason": "Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean." + }, + "slideshow": { + "signals": [ + "slideshow" + ], + "reason": "Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean." + }, + "taxReport": { + "signals": [ + "accounting_tax_report" + ], + "reason": "Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean." + }, + "transfers": { + "signals": [ + "transfers", + "transfer_upload_links" + ], + "reason": "Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean." + }, + "userManagement": { + "signals": [ + "admin_management" + ], + "reason": "Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean." + }, + "whatsapp": { + "signals": [ + "whatsapp" + ], + "reason": "Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean." + }, + "workflows": { + "signals": [ + "workflows", + "workflow_automation_enabled" + ], + "reason": "Only effective configuration boolean; dependency rules apply, no flag values/history beyond this boolean." + } + }, + "excluded_runtime": [ + "Gallery/customer/public events and optional website analytics", + "Automated newsletter, reminder, WhatsApp, webhook and IMAP jobs", + "Security/audit logs, biometric embeddings and recognition results", + "Operational health, migration, update and polling metrics", + "Business/customer/user identities, geography, financial amounts and document contents; only explicit v3 inventory totals are permitted.", + "Disabled calendarBooking and internal crmDevelopment; hosted future product #1111", + "Image fragmentation: removed from current PicPeak, not a live capability" + ], + "configuration_only": [ + "reminder_emails", + "public_site", + "gallery_feedback_likes", + "gallery_feedback_ratings", + "gallery_feedback_comments", + "gallery_feedback_favorites", + "gallery_feedback_reactions", + "gallery_feedback_color_labels", + "gallery_guest_accounts", + "gallery_guest_uploads", + "gallery_downloads", + "download_resolution_picker", + "gallery_client_access", + "gallery_watermarks", + "gallery_image_protection", + "gallery_reveal", + "gallery_expiration", + "gallery_folders", + "transfer_upload_links", + "workflow_automation_enabled", + "s3_auto_import", + "gallery_capture_date_sort", + "download_original_filenames" + ], + "inventory_totals": { + "galleries": { + "name": { + "en": "Stored galleries", + "de": "Gespeicherte Galerien" + }, + "description": { + "en": "Current number of gallery records, including drafts, inactive and archived galleries. Deleted galleries are excluded. One total for the installation, no breakdown or identifiers.", + "de": "Aktuelle Anzahl gespeicherter Galerien einschließlich Entwürfen, inaktiven und archivierten Galerien. Gelöschte Galerien zählen nicht. Eine Gesamtzahl der Installation, ohne Aufschlüsselung oder Kennungen." + } + }, + "photos": { + "name": { + "en": "Stored photo records", + "de": "Gespeicherte Fotoeinträge" + }, + "description": { + "en": "Current number of non-video photo records, including RAW, guest uploads and records of archived galleries. One total for the installation; not unique files, thumbnails, processing success or photo contents. Deleted records are excluded.", + "de": "Aktuelle Anzahl der Fotoeinträge ohne Videos, einschließlich RAW, Gast-Uploads und Einträgen archivierter Galerien. Eine Gesamtzahl der Installation; keine eindeutigen Dateien, Vorschaubilder, Verarbeitungserfolge oder Fotoinhalte. Gelöschte Einträge zählen nicht." + } + } + } +} diff --git a/frontend/src/features/settings/UsageCatalog.tsx b/frontend/src/features/settings/UsageCatalog.tsx index 4eaa25bb..c64c0686 100644 --- a/frontend/src/features/settings/UsageCatalog.tsx +++ b/frontend/src/features/settings/UsageCatalog.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import catalog from './usageFeatures.v2.json'; +import catalog from './usageFeatures.v3.json'; /** Local, static disclosure: opening it never contacts the collector. */ export function UsageCatalog() { @@ -12,6 +12,13 @@ export function UsageCatalog() {
{t('productUsage.catalogTitle')}

{t('productUsage.catalogExplanation')}

+
+

{t('productUsage.inventoryTitle')}

+

{t('productUsage.inventoryExplanation')}

+ {Object.keys(catalog.inventory).map((key) =>

+ {t(`productUsage.inventory.${key}.name`)}: {t(`productUsage.inventory.${key}.description`)} +

)} +