diff --git a/backend/__tests__/routes/adminUsage.test.js b/backend/__tests__/routes/adminUsage.test.js index d69b504d..de92918e 100644 --- a/backend/__tests__/routes/adminUsage.test.js +++ b/backend/__tests__/routes/adminUsage.test.js @@ -201,8 +201,8 @@ test('public/gallery paths and failed/unauthenticated admin operations never set expect(JSON.stringify(service.markUsed.mock.calls)).not.toContain('42'); }); -test.each(['usage-consent.v2', 'usage-consent.v3', 'usage-consent.v4'])('consent upgrade accepts exactly the explicit %s choice, never extra fields', async (consent_version) => { - for (const data of [{}, { consent_version: 'usage-consent.v1' }, { consent_version: 'usage-consent.v5' }, { consent_version, user: 'PRIVATE' }]) +test.each(['usage-consent.v2', 'usage-consent.v3', 'usage-consent.v4', 'usage-consent.v5'])('consent upgrade accepts exactly the explicit %s choice, never extra fields', async (consent_version) => { + for (const data of [{}, { consent_version: 'usage-consent.v1' }, { consent_version: 'usage-consent.v6' }, { consent_version, user: 'PRIVATE' }]) await request(app).post('/api/admin/usage/consent').set('Authorization', `Bearer ${token('admin')}`).send(data).expect(400); expect(service.command).not.toHaveBeenCalled(); await request(app).post('/api/admin/usage/consent').set('Authorization', `Bearer ${token('admin')}`) diff --git a/backend/__tests__/routes/usageAdoptionEvidence.test.js b/backend/__tests__/routes/usageAdoptionEvidence.test.js new file mode 100644 index 00000000..fb1d2a1e --- /dev/null +++ b/backend/__tests__/routes/usageAdoptionEvidence.test.js @@ -0,0 +1,131 @@ +const express = require('express'); +const request = require('supertest'); +const knex = require('knex'); +const { changedFields, settingsChanged } = require('../../src/usage/adoptionEvidence'); + +jest.mock('../../src/middleware/auth', () => ({ adminAuth: (req, res, next) => { req.admin = { id: 1 }; next(); } })); +jest.mock('../../src/middleware/permissions', () => ({ requirePermission: () => (req, res, next) => next() })); +jest.mock('../../src/middleware/requireFeatureFlag', () => ({ requireFeatureFlag: () => (req, res, next) => next() })); +jest.mock('../../src/services/productUsageService', () => ({ markUsed: jest.fn().mockResolvedValue() })); +jest.mock('../../src/services/emailProcessor', () => ({ + htmlToText: body => body, wrapEmailHtml: jest.fn(async body => body), buildSignatureTextFor: jest.fn(async () => ''), +})); +jest.mock('../../src/services/businessProfileService', () => ({ getEmailSignature: jest.fn(async () => null) })); + +describe('v5 evidence comes from real edits, not the generic successful-route marker', () => { + let db, app; + const marker = require('../../src/services/productUsageService').markUsed; + const recorded = () => marker.mock.calls.flatMap(([keys]) => keys).filter(key => /_editing$/.test(key)); + beforeAll(async () => { + db = knex({ client: 'sqlite3', connection: { filename: ':memory:' }, useNullAsDefault: true }); + jest.doMock('../../src/database/db', () => ({ db, logActivity: jest.fn(async () => {}) })); + await db.schema.createTable('cms_pages', t => { + t.increments('id'); t.string('slug'); + for (const key of ['title_en', 'title_de', 'content_en', 'content_de', 'logo_url', 'external_url']) t.text(key); + t.boolean('use_external_url').defaultTo(false); t.boolean('show_in_footer').defaultTo(true); t.timestamp('updated_at'); + }); + await db.schema.createTable('email_templates', t => { t.increments('id'); t.string('template_key'); t.timestamp('updated_at'); }); + await db.schema.createTable('email_template_translations', t => { + t.increments('id'); t.integer('template_id'); t.string('language'); + for (const key of ['subject', 'body_html', 'body_text']) t.text(key); + t.timestamp('updated_at'); t.timestamp('created_at'); + }); + await db.schema.createTable('app_settings', t => { t.string('setting_key').primary(); t.text('setting_value'); }); + await db.schema.createTable('event_types', t => { + t.increments('id'); for (const key of ['name', 'slug_prefix', 'emoji', 'theme_preset', 'theme_config']) t.text(key); + t.integer('display_order'); t.boolean('is_active'); t.boolean('is_system'); t.timestamp('updated_at'); t.timestamp('created_at'); + }); + await db.schema.createTable('events', t => { t.increments('id'); t.integer('created_by'); }); + await db.schema.createTable('event_category_order', t => { t.integer('event_id'); t.integer('category_id'); t.integer('position'); }); + await db.schema.createTable('photo_categories', t => { + t.increments('id'); t.text('name'); t.text('slug'); t.integer('hero_photo_id'); t.integer('event_id'); + t.integer('display_order'); t.boolean('allow_downloads'); t.boolean('is_folder'); t.boolean('is_global'); + }); + app = express(); app.use(express.json()); + app.use(require('../../src/middleware/productUsage').productUsage); + app.use('/cms', require('../../src/routes/adminCMS')); + app.use('/email', require('../../src/routes/adminEmail')); + app.use('/event-types', require('../../src/routes/adminEventTypes')); + app.use('/categories', require('../../src/routes/adminCategories')); + }); + beforeEach(async () => { + marker.mockClear(); + for (const table of ['cms_pages', 'email_templates', 'email_template_translations', 'app_settings', 'event_types', 'photo_categories', 'events', 'event_category_order']) await db(table).delete(); + await db('cms_pages').insert({ slug: 'privacy', title_en: 'Privacy', content_en: 'Seeded content' }); + await db('email_templates').insert({ id: 1, template_key: 'PRIVATE-template' }); + await db('email_template_translations').insert({ template_id: 1, language: 'en', subject: 'Seeded subject', body_html: 'Seeded body', body_text: '' }); + }); + afterAll(() => db.destroy()); + test('CMS reads, unchanged saves and external content do not imply internal content editing', async () => { + await request(app).get('/cms/pages').expect(200); + await request(app).put('/cms/pages/privacy').send({ content_en: 'Seeded content' }).expect(200); + await request(app).put('/cms/pages/privacy').send({ use_external_url: true, external_url: 'https://example.test/privacy', content_en: 'Other' }).expect(200); + expect(recorded()).toEqual([]); + await request(app).put('/cms/pages/privacy').send({ use_external_url: false, content_en: 'PRIVATE real content' }).expect(200); + expect(recorded()).toEqual(['cms_content_editing']); + expect(JSON.stringify(marker.mock.calls)).not.toMatch(/PRIVATE|example\.test|privacy/); + }); + test('template preview, empty and unchanged saves are excluded; actual content changes count', async () => { + await request(app).post('/email/templates/PRIVATE-template/preview').send({}).expect(200); + await request(app).put('/email/templates/PRIVATE-template').send({ translations: {} }).expect(200); + const translation = { subject: 'Seeded subject', body_html: 'Seeded body', body_text: '' }; + await request(app).put('/email/templates/PRIVATE-template').send({ translations: { en: translation } }).expect(200); + expect(recorded()).toEqual([]); + await request(app).put('/email/templates/PRIVATE-template').send({ translations: { en: { ...translation, subject: 'PRIVATE custom subject' } } }).expect(200); + expect(recorded()).toEqual(['email_template_editing']); + expect(JSON.stringify(marker.mock.calls)).not.toContain('PRIVATE'); + }); + test('failed saves do not count; new nonempty templates do', async () => { + await request(app).put('/email/templates/missing').send({ translations: {} }).expect(404); + await request(app).post('/email/templates').send({ template_key: 'empty', translations: {} }).expect(201); + expect(recorded()).toEqual([]); + await request(app).post('/email/templates').send({ template_key: 'custom', translations: { de: { subject: 'Privat' } } }).expect(201); + expect(recorded()).toEqual(['email_template_editing']); + }); + test('seeded event types and categories count only after a real edit, not identical saves', async () => { + await db('event_types').insert({ id: 1, name: 'Wedding', slug_prefix: 'wedding', is_active: true, is_system: true }); + await db('photo_categories').insert({ id: 1, name: 'All', slug: 'all', is_global: true, is_folder: false }); + await request(app).get('/event-types').expect(200); + await request(app).get('/categories/global').expect(200); + await request(app).put('/event-types/1').send({ name: 'Wedding', is_active: true }).expect(200); + await request(app).put('/categories/1').send({ name: 'All', is_folder: false }).expect(200); + expect(recorded()).toEqual([]); + await request(app).put('/event-types/1').send({ name: 'PRIVATE event type' }).expect(200); + await request(app).put('/categories/1').send({ name: 'PRIVATE category' }).expect(200); + expect(recorded()).toEqual(['event_type_editing', 'category_editing']); + expect(JSON.stringify(marker.mock.calls)).not.toContain('PRIVATE'); + }); + test('reordering event types and global categories counts only when the order changes', async () => { + await db('event_types').insert([ + { id: 1, name: 'A', slug_prefix: 'a', display_order: 1, is_active: true, is_system: true }, + { id: 2, name: 'B', slug_prefix: 'b', display_order: 2, is_active: true, is_system: true }, + ]); + await db('photo_categories').insert([ + { id: 1, name: 'A', slug: 'a', is_global: true, is_folder: false, display_order: 1 }, + { id: 2, name: 'B', slug: 'b', is_global: true, is_folder: false, display_order: 2 }, + ]); + await db('events').insert({ id: 1 }); + await request(app).post('/event-types/reorder').send({ orderedIds: [1, 2] }).expect(200); + await request(app).post('/categories/reorder-global').send({ orderedIds: [1, 2] }).expect(200); + await request(app).delete('/categories/reorder/1').expect(200); // no override to reset + expect(recorded()).toEqual([]); + await request(app).post('/event-types/reorder').send({ orderedIds: [2, 1] }).expect(200); + await request(app).post('/categories/reorder-global').send({ orderedIds: [2, 1] }).expect(200); + await request(app).post('/categories/reorder').send({ event_id: 1, orderedIds: [2, 1] }).expect(200); + expect(recorded()).toEqual(['event_type_editing', 'category_editing', 'category_editing']); + marker.mockClear(); + await request(app).post('/categories/reorder').send({ event_id: 1, orderedIds: [2, 1] }).expect(200); // same override again + expect(recorded()).toEqual([]); + await request(app).delete('/categories/reorder/1').expect(200); + expect(recorded()).toEqual(['category_editing']); + }); + test('settings compare persisted values, not timestamps, JSON order or defaults materialized as rows', async () => { + await db('app_settings').insert({ setting_key: 'theme_config', setting_value: JSON.stringify({ a: 1, b: 2 }) }); + expect(await settingsChanged(db, { theme_config: { b: 2, a: 1 } }, ['theme_config'])).toBe(false); + expect(await settingsChanged(db, { theme_config: { b: 2, a: 2 } }, ['theme_config'])).toBe(true); + expect(await settingsChanged(db, { missing: 'fallback' }, ['missing'])).toBe(false); + expect(await settingsChanged(db, { secret: 'PRIVATE' }, ['theme_config'])).toBe(false); + expect(changedFields({ enabled: 1, updated_at: 'old' }, { enabled: true, updated_at: 'new' }, ['enabled'])).toBe(false); + expect(changedFields({ body: '' }, { body: null }, ['body'])).toBe(false); + }); +}); diff --git a/backend/__tests__/services/usageCoverageInventory.test.js b/backend/__tests__/services/usageCoverageInventory.test.js index 3295f20e..6845e71b 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.v4.json'); -const inventory = require('../../../docs/usage-coverage.v4.json'); +const catalog = require('../../src/usage/features.v5.json'); +const inventory = require('../../../docs/usage-coverage.v5.json'); const protocol = require('../../src/usage/schema.cjs'); const { RULES_V2, capabilityKeys } = require('../../src/usage/capabilityRules'); const { acceptedUpload, capabilityEvidence } = require('../../src/usage/capabilityEvidence'); @@ -52,22 +52,22 @@ test('all current settings tabs have an explicit scope decision', () => { } }); -test('v1/v2/v3 wire validation is immutable; v4 catalog, UI and translated descriptions agree', () => { +test('v1/v2/v3 wire validation is immutable; v5 catalog, UI and translated descriptions agree', () => { expect(crypto.createHash('sha256').update(JSON.stringify(protocol.envelopeSchemas['usage.v1'].properties)).digest('hex')) .toBe('cc8d0a865d21e36d2b24d23ca6aa8dd8d48000cb17aef83996786f70755bc922'); expect(crypto.createHash('sha256').update(JSON.stringify(protocol.envelopeSchemas['usage.v2'].properties)).digest('hex')) .toBe('159821cf45c1951016d33a4ed9ca55a0a7ee1b60dd715b803fcfed33e5c8a846'); - expect(protocol.FEATURE_KEYS).toHaveLength(86); + expect(protocol.FEATURE_KEYS).toHaveLength(87); expect(crypto.createHash('sha256').update(JSON.stringify(protocol.envelopeSchemas['usage.v3'].properties)).digest('hex')) .toBe('93214702c79f47823f154544ebad6612dd313604f69e60b86de4c0e4c904571a'); expect(protocol.FEATURE_KEYS).toContain('gallery_downloads_restricted'); expect(protocol.FEATURE_KEYS).not.toContain('gallery_downloads'); - expect(protocol.ALL_FEATURE_KEYS).toHaveLength(87); + expect(protocol.ALL_FEATURE_KEYS).toHaveLength(94); expect(protocol.ALL_FEATURE_KEYS).toContain('gallery_downloads'); expect(protocol.LEGACY_FEATURE_KEYS).toHaveLength(19); expect(inventory.configuration_only).toHaveLength(23); const frontend = path.resolve(__dirname, '../../../frontend'); - expect(JSON.parse(fs.readFileSync(path.join(frontend, 'src/features/settings/usageFeatures.v4.json')))).toEqual(catalog); + expect(JSON.parse(fs.readFileSync(path.join(frontend, 'src/features/settings/usageFeatures.v5.json')))).toEqual(catalog); // The catalog is source, and source is English only: its strings are the // en locale verbatim. Every other language lives in its locale file and // must cover every key and field, but says whatever its translator chose. @@ -85,11 +85,11 @@ test('v1/v2/v3 wire validation is immutable; v4 catalog, UI and translated descr }); 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', + const explicit = ['cms_content_editing', 'email_template_editing', 'email_template_delivery', 'branding_editing', 'seo_editing', 'event_type_editing', 'category_editing', 'custom_css', 'oauth', 'smtp', 'email_webhook', 'whatsapp', 'incoming_mail', '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((key) => protocol.observesUse(key)).filter((key) => !covered.has(key))).toEqual([]); - for (const key of covered) expect(protocol.observesUse(key)).toBe(true); + for (const key of covered) expect(protocol.ALL_FEATURES[key].used).toBeTruthy(); }); test.each([ diff --git a/backend/__tests__/services/usageIngressCompatibility.test.js b/backend/__tests__/services/usageIngressCompatibility.test.js index 0fc3bb5d..89385309 100644 --- a/backend/__tests__/services/usageIngressCompatibility.test.js +++ b/backend/__tests__/services/usageIngressCompatibility.test.js @@ -7,13 +7,13 @@ const signHistorical = (packet, id) => { return { ...signed, signature: crypto.sign(null, Buffer.from(p.canonical(signed)), id.private_key).toString('base64url') }; }; -describe.each(['usage.v1', 'usage.v2', 'usage.v3', 'usage.v4'])('%s receiver compatibility never loosens the PicPeak sender', version => { +describe.each(['usage.v1', 'usage.v2', 'usage.v3', 'usage.v4', 'usage.v5'])('%s receiver compatibility never loosens the PicPeak sender', version => { test('complete original reports still sign and verify', () => { const id = p.generateIdentity(); const packet = p.makePacket(id, 'report', 1, { picpeak_version: '1.0.0', report_date: '2026-09-06', generated_at: new Date(now).toISOString(), features: p.emptyFeatures(version), gallery_layouts: [], - ...(['usage.v3', 'usage.v4'].includes(version) ? { inventory: { galleries: 0, photos: 0 } } : {}), + ...(['usage.v3', 'usage.v4', 'usage.v5'].includes(version) ? { inventory: { galleries: 0, photos: 0 } } : {}), }, version); const envelope = p.signPacket(packet, id, new Date(now)); expect(p.verifyEnvelope(envelope, now)).toEqual(packet); diff --git a/backend/__tests__/services/usageOutboundOnly.test.js b/backend/__tests__/services/usageOutboundOnly.test.js index 426337de..ddb3f489 100644 --- a/backend/__tests__/services/usageOutboundOnly.test.js +++ b/backend/__tests__/services/usageOutboundOnly.test.js @@ -67,6 +67,14 @@ test('the collector has no way in: no inbound route and no scheduled pull', () = for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { if (!entry.isFile() || !entry.name.endsWith('.js')) continue; if (entry.name === 'productUsageService.js') continue; + if (entry.name === 'emailProcessor.js') { + // v5 explicitly consents to one local background-mail bit, never a send + // to the collector. No mail details may be passed into the usage API. + const email = fs.readFileSync(path.join(dir, entry.name), 'utf8'); + expect(email).toContain(".markUsed(['email_template_delivery'])"); + expect(email).not.toMatch(/productUsageService'\)\.(?:tick|enable|command|deliver)/); + continue; + } expect(fs.readFileSync(path.join(dir, entry.name), 'utf8')) .not.toContain('productUsageService'); } diff --git a/backend/__tests__/services/usageTemplateDelivery.test.js b/backend/__tests__/services/usageTemplateDelivery.test.js new file mode 100644 index 00000000..96fe02a4 --- /dev/null +++ b/backend/__tests__/services/usageTemplateDelivery.test.js @@ -0,0 +1,82 @@ +const knex = require('knex'); +jest.mock('nodemailer', () => ({ createTransport: jest.fn() })); +jest.mock('../../src/services/emailWebhookTransport', () => ({ isEnabled: jest.fn(), send: jest.fn() })); +jest.mock('../../src/services/productUsageService', () => ({ markUsed: jest.fn().mockResolvedValue() })); +jest.mock('../../src/services/businessProfileService', () => ({ getEmailSignature: jest.fn(async () => null) })); + +describe('template delivery is a coarse transport-acceptance bit', () => { + let db, sendTemplateEmail, queueEmail, processEmailQueue; + const sendMail = jest.fn(); + const webhook = require('../../src/services/emailWebhookTransport'); + const marker = require('../../src/services/productUsageService').markUsed; + beforeAll(async () => { + db = knex({ client: 'sqlite3', connection: { filename: ':memory:' }, useNullAsDefault: true }); + jest.doMock('../../src/database/db', () => ({ db })); + await db.schema.createTable('email_configs', t => { + t.increments('id'); for (const key of ['smtp_host', 'smtp_user', 'smtp_pass', 'from_name', 'from_email']) t.string(key); + t.integer('smtp_port'); t.boolean('smtp_secure'); t.boolean('tls_reject_unauthorized'); + }); + await db('email_configs').insert({ smtp_host: 'smtp.example.test', smtp_port: 587, from_email: 'sender@example.test' }); + await db.schema.createTable('email_templates', t => { t.increments('id'); t.string('template_key'); t.text('subject'); t.text('body_html'); }); + await db('email_templates').insert({ template_key: 'PRIVATE-template', subject: 'PRIVATE subject', body_html: '
PRIVATE body
' }); + await db.schema.createTable('app_settings', t => { t.string('setting_key').primary(); t.text('setting_value'); }); + await db.schema.createTable('email_queue', t => { + t.increments('id'); t.integer('event_id'); t.integer('campaign_id'); t.string('recipient_email'); t.string('email_type'); + t.text('email_data'); t.string('status'); t.integer('retry_count'); t.text('error_message'); t.text('rendered_html'); + t.timestamp('scheduled_at'); t.timestamp('created_at'); t.timestamp('sent_at'); + }); + require('nodemailer').createTransport.mockReturnValue({ sendMail, verify: jest.fn(async () => true) }); + ({ sendTemplateEmail, queueEmail, processEmailQueue } = require('../../src/services/emailProcessor')); + }); + beforeEach(() => { + marker.mockClear(); marker.mockResolvedValue(); webhook.isEnabled.mockReturnValue(false); + sendMail.mockReset(); sendMail.mockResolvedValue({ messageId: 'PRIVATE-message', accepted: ['PRIVATE@example.test'] }); + }); + afterAll(() => db.destroy()); + const send = options => sendTemplateEmail('PRIVATE@example.test', 'PRIVATE-template', { __language: 'en' }, options); + test('successful real SMTP send transmits only the fixed capability key', async () => { + await send(); + expect(marker).toHaveBeenCalledWith(['email_template_delivery']); + expect(JSON.stringify(marker.mock.calls)).not.toContain('PRIVATE'); + }); + test('rejected recipients, failed sends, missing templates and explicit tests do not count', async () => { + await send({ usageEligible: false }); + sendMail.mockResolvedValueOnce({ messageId: 'private', accepted: [] }); await send(); + sendMail.mockRejectedValueOnce(new Error('mail failed')); await expect(send()).rejects.toThrow('mail failed'); + await expect(sendTemplateEmail('private@example.test', 'missing', { __language: 'en' })).rejects.toThrow('not found'); + expect(marker).not.toHaveBeenCalled(); + }); + test('a queued test message keeps its exclusion through the queue processor; a queued real one counts', async () => { + await queueEmail(null, 'PRIVATE@example.test', 'PRIVATE-template', { __language: 'en' }, { usageEligible: false }); + await processEmailQueue(); + expect(sendMail).toHaveBeenCalledTimes(1); + expect(marker).not.toHaveBeenCalled(); + await queueEmail(null, 'PRIVATE@example.test', 'PRIVATE-template', { __language: 'en' }); + await processEmailQueue(); + expect(sendMail).toHaveBeenCalledTimes(2); + expect(marker).toHaveBeenCalledWith(['email_template_delivery']); + expect(await db('email_queue').where('status', 'sent').count({ n: '*' }).first()).toMatchObject({ n: 2 }); + }); + test('a workflow test run (engine.testRun, __test) queues a test message; a real run counts', async () => { + require('../../src/services/workflows/actions'); + const sendEmail = require('../../src/services/workflows/registry').getAction('send_email'); + const ctx = (vars) => ({ node: { config: { to: 'PRIVATE@example.test', emailType: 'PRIVATE-template', recipientClass: 'admin' } }, vars }); + await sendEmail(ctx({ __test: true, emailData: { __language: 'en' } })); + await processEmailQueue(); + expect(sendMail).toHaveBeenCalledTimes(1); + expect(marker).not.toHaveBeenCalled(); + await sendEmail(ctx({ emailData: { __language: 'en' } })); + await processEmailQueue(); + expect(sendMail).toHaveBeenCalledTimes(2); + expect(marker).toHaveBeenCalledWith(['email_template_delivery']); + }); + test('webhook success counts; failure does not; marker failure never retries successful mail', async () => { + webhook.isEnabled.mockReturnValue(true); webhook.send.mockResolvedValue({ messageId: 'private' }); + marker.mockRejectedValueOnce(new Error('usage unavailable')); + await expect(send()).resolves.toMatchObject({ success: true }); + expect(webhook.send).toHaveBeenCalledTimes(1); + marker.mockClear(); webhook.send.mockRejectedValueOnce(new Error('webhook failed')); + await expect(send()).rejects.toThrow('webhook failed'); + expect(marker).not.toHaveBeenCalled(); + }); +}); diff --git a/backend/src/routes/adminCMS.js b/backend/src/routes/adminCMS.js index 0df5902b..4ae1f6c2 100644 --- a/backend/src/routes/adminCMS.js +++ b/backend/src/routes/adminCMS.js @@ -1,3 +1,4 @@ +const { changedEvidence } = require('../usage/adoptionEvidence'); const express = require('express'); const path = require('path'); const fs = require('fs').promises; @@ -144,6 +145,8 @@ router.put('/pages/:slug', adminAuth, requirePermission('cms.edit'), [ { type: 'admin', id: req.admin.id, name: req.admin.username } ); + if (!updated.use_external_url) changedEvidence(res, 'cms_content_editing', page, updated, + ['title_en', 'title_de', 'content_en', 'content_de']); res.json(updated); } catch (error) { logger.error('Error updating CMS page:', error); diff --git a/backend/src/routes/adminCategories.js b/backend/src/routes/adminCategories.js index 4461a76d..4adfbfbd 100644 --- a/backend/src/routes/adminCategories.js +++ b/backend/src/routes/adminCategories.js @@ -1,3 +1,5 @@ +const { changedEvidence } = require('../usage/adoptionEvidence'); +const { capabilityEvidence } = require('../usage/capabilityEvidence'); const express = require('express'); const { body, validationResult } = require('express-validator'); const { safeValidationErrors } = require('../utils/routeHelpers'); @@ -123,6 +125,7 @@ router.post('/', adminAuth, requirePermission('settings.edit'), [ { type: 'admin', id: req.admin.id, name: req.admin.username } ); + capabilityEvidence(res, 'category_editing'); res.json(category); } catch (error) { logger.error('Error creating category:', error); @@ -206,6 +209,8 @@ router.put('/:id', adminAuth, requirePermission('settings.edit'), [ { type: 'admin', id: req.admin.id, name: req.admin.username } ); + changedEvidence(res, 'category_editing', category, updated, + ['name', 'slug', 'hero_photo_id', 'allow_downloads', 'is_folder']); res.json(updated); } catch (error) { logger.error('Error updating category:', error); @@ -260,6 +265,7 @@ router.put('/:id/hero', adminAuth, requirePermission('settings.edit'), [ { type: 'admin', id: req.admin.id, name: req.admin.username } ); + changedEvidence(res, 'category_editing', category, updated, ['hero_photo_id']); res.json(updated); } catch (error) { logger.error('Error updating category hero:', error); @@ -294,6 +300,7 @@ router.delete('/:id', adminAuth, requirePermission('settings.edit'), async (req, { type: 'admin', id: req.admin.id, name: req.admin.username } ); + capabilityEvidence(res, 'category_editing'); res.json({ message: 'Category deleted successfully' }); } catch (error) { logger.error('Error deleting category:', error); @@ -346,12 +353,14 @@ router.post('/reorder', adminAuth, requirePermission('settings.edit'), [ return res.status(400).json({ error: 'One or more categories are not available for this event' }); } + const before = await db('event_category_order').where('event_id', eventId).orderBy('position', 'asc').pluck('category_id'); await db.transaction(async (trx) => { await trx('event_category_order').where('event_id', eventId).del(); await trx('event_category_order').insert( orderedIds.map((id, i) => ({ event_id: eventId, category_id: id, position: i + 1 })) ); }); + changedEvidence(res, 'category_editing', { order: before }, { order: orderedIds }, ['order']); // Log activity after commit (avoids a SQLite in-transaction global write). await logActivity('event_category_order_set', @@ -371,7 +380,8 @@ router.post('/reorder', adminAuth, requirePermission('settings.edit'), [ router.delete('/reorder/:eventId', adminAuth, requirePermission('settings.edit'), requireEventOwnership, async (req, res) => { try { const eventId = parseInt(req.params.eventId, 10); - await db('event_category_order').where('event_id', eventId).del(); + const removed = await db('event_category_order').where('event_id', eventId).del(); + if (removed > 0) capabilityEvidence(res, 'category_editing'); await logActivity('event_category_order_reset', { eventId }, @@ -400,7 +410,8 @@ router.post('/reorder-global', adminAuth, requirePermission('settings.edit'), [ const orderedIds = req.body.orderedIds.map((id) => parseInt(id, 10)); - const globals = await db('photo_categories').where('is_global', formatBoolean(true)).pluck('id'); + const globals = await db('photo_categories').where('is_global', formatBoolean(true)) + .orderBy('display_order', 'asc').orderBy('name', 'asc').pluck('id'); const globalsSet = new Set(globals); const invalid = orderedIds.filter((id) => !globalsSet.has(id)); if (invalid.length > 0) { @@ -423,6 +434,7 @@ router.post('/reorder-global', adminAuth, requirePermission('settings.edit'), [ .where('is_global', formatBoolean(true)) .orderBy('display_order', 'asc') .orderBy('name', 'asc'); + changedEvidence(res, 'category_editing', { order: globals }, { order: categories.map((category) => category.id) }, ['order']); res.json(categories); } catch (error) { logger.error('Error reordering global categories:', error); diff --git a/backend/src/routes/adminDev.js b/backend/src/routes/adminDev.js index 0aaaa621..0152d95e 100644 --- a/backend/src/routes/adminDev.js +++ b/backend/src/routes/adminDev.js @@ -443,7 +443,7 @@ router.post( const frontendUrl = await getAbsoluteFrontendUrl(req); const payload = await buildPayloadFor(req.body.templateKey, req.admin.id, frontendUrl); - await emailProcessor.queueEmail(null, admin.email, req.body.templateKey, payload); + await emailProcessor.queueEmail(null, admin.email, req.body.templateKey, payload, { usageEligible: false }); return successResponse(res, { sent: true, diff --git a/backend/src/routes/adminEmail.js b/backend/src/routes/adminEmail.js index 440335bb..fa047d3f 100644 --- a/backend/src/routes/adminEmail.js +++ b/backend/src/routes/adminEmail.js @@ -1,3 +1,4 @@ +const { changedFields } = require('../usage/adoptionEvidence'); const express = require('express'); const { capabilityEvidence } = require('../usage/capabilityEvidence'); const nodemailer = require('nodemailer'); @@ -1029,6 +1030,7 @@ router.put('/templates/:key', [ return res.status(400).json({ error: 'translations object is required' }); } + let contentChanged = false; // Upsert each language translation for (const [language, data] of Object.entries(translations)) { if (!data || typeof data !== 'object') continue; @@ -1044,6 +1046,7 @@ router.put('/templates/:key', [ updated_at: new Date(), }; + contentChanged ||= changedFields(existing, row, ['subject', 'body_html', 'body_text']); if (existing) { await db('email_template_translations') .where({ template_id: template.id, language }) @@ -1091,6 +1094,7 @@ router.put('/templates/:key', [ { type: 'admin', id: req.admin.id, name: req.admin.username } ); + if (contentChanged) capabilityEvidence(res, 'email_template_editing'); res.json({ message: 'Email template updated successfully' }); } catch (error) { errorResponse(res, error, 500, 'Failed to update email template'); @@ -1178,6 +1182,8 @@ router.post('/templates', [ null, { type: 'admin', id: req.admin.id, name: req.admin.username }); + if (Object.values(translations).some(content => content && changedFields({}, content, + ['subject', 'body_html', 'body_text']))) capabilityEvidence(res, 'email_template_editing'); return res.status(201).json({ template_key: templateKey, id: templateId }); } catch (error) { return errorResponse(res, error, 500, 'Failed to create email template'); diff --git a/backend/src/routes/adminEventTypes.js b/backend/src/routes/adminEventTypes.js index 23a5a2fd..af4416d1 100644 --- a/backend/src/routes/adminEventTypes.js +++ b/backend/src/routes/adminEventTypes.js @@ -1,3 +1,5 @@ +const { changedEvidence } = require('../usage/adoptionEvidence'); +const { capabilityEvidence } = require('../usage/capabilityEvidence'); /** * Admin Event Types Routes * CRUD operations for managing customizable event types @@ -123,6 +125,7 @@ router.post('/', adminAuth, requirePermission('event_types.manage'), [ { type: 'admin', id: req.admin.id, name: req.admin.username } ); + capabilityEvidence(res, 'event_type_editing'); res.status(201).json(eventType); } catch (error) { logger.error('Error creating event type:', { error: error.message }); @@ -163,7 +166,10 @@ router.put('/:id', adminAuth, requirePermission('event_types.manage'), [ const { id } = req.params; const updates = req.body; + const before = await eventTypeService.getEventTypeById(parseInt(id)); const eventType = await eventTypeService.updateEventType(parseInt(id), updates); + changedEvidence(res, 'event_type_editing', before, eventType, + ['name', 'slug_prefix', 'emoji', 'theme_preset', 'theme_config', 'display_order', 'is_active']); // Log activity await logActivity('event_type_updated', @@ -210,6 +216,7 @@ router.delete('/:id', adminAuth, requirePermission('event_types.manage'), [ { type: 'admin', id: req.admin.id, name: req.admin.username } ); + capabilityEvidence(res, 'event_type_editing'); res.json({ message: 'Event type deleted successfully' }); } catch (error) { logger.error('Error deleting event type:', { error: error.message }); @@ -240,7 +247,9 @@ router.post('/reorder', adminAuth, requirePermission('event_types.manage'), [ } const { orderedIds } = req.body; + const before = (await eventTypeService.getAllEventTypes()).map((type) => type.id); const eventTypes = await eventTypeService.reorderEventTypes(orderedIds); + changedEvidence(res, 'event_type_editing', { order: before }, { order: eventTypes.map((type) => type.id) }, ['order']); // Log activity await logActivity('event_types_reordered', diff --git a/backend/src/routes/adminEvents/logo.js b/backend/src/routes/adminEvents/logo.js index 2e33fee6..75366368 100644 --- a/backend/src/routes/adminEvents/logo.js +++ b/backend/src/routes/adminEvents/logo.js @@ -1,3 +1,4 @@ +const { capabilityEvidence } = require('../../usage/capabilityEvidence'); // Extracted verbatim from the original routes/adminEvents.js (see ./index.js). // Exports a register function; ./index.js calls the sub-routers in the original // registration order so Express route matching is unchanged. @@ -89,6 +90,7 @@ module.exports = (router) => { { type: 'admin', id: req.admin.id, name: req.admin.username } ); + capabilityEvidence(res, 'branding_editing'); res.json({ message: 'Event logo uploaded successfully', hero_logo_url: logoUrl @@ -135,6 +137,7 @@ module.exports = (router) => { { type: 'admin', id: req.admin.id, name: req.admin.username } ); + if (event.hero_logo_url || event.hero_logo_path) capabilityEvidence(res, 'branding_editing'); res.json({ message: 'Event logo removed successfully' }); } catch (error) { errorResponse(res, error, 500, 'Failed to delete event logo'); diff --git a/backend/src/routes/adminSettings.js b/backend/src/routes/adminSettings.js index bc405dcf..9ed8a9ec 100644 --- a/backend/src/routes/adminSettings.js +++ b/backend/src/routes/adminSettings.js @@ -1,3 +1,7 @@ +const { settingsChanged } = require('../usage/adoptionEvidence'); +const { capabilityEvidence } = require('../usage/capabilityEvidence'); +const SEO_USAGE_KEYS = ['seo_allow_indexing', 'seo_block_ai_crawlers', 'seo_block_social_bots', + 'seo_blocked_ai_agents', 'seo_custom_rules', 'seo_meta_noindex', 'seo_meta_nofollow', 'seo_meta_noai', 'seo_sitemap_url']; const express = require('express'); const multer = require('multer'); const path = require('path'); @@ -1036,6 +1040,9 @@ router.put('/branding', adminAuth, requirePermission('settings.edit'), async (re ...(promo_alignment !== undefined && { promo_alignment: normalizedPromoAlignment }) }; + const brandingUpdates = Object.fromEntries(Object.entries(brandingSettings).map(([key, value]) => [`branding_${key}`, value])); + const brandingChanged = await settingsChanged(db, brandingUpdates, Object.keys(brandingUpdates)); + // Handle favicon deletion if empty string or null is provided if (favicon_url === '' || favicon_url === null || favicon_url === undefined) { // Get current favicon path to delete file @@ -1125,6 +1132,7 @@ router.put('/branding', adminAuth, requirePermission('settings.edit'), async (re metadata: JSON.stringify({ company_name }) }); + if (brandingChanged) capabilityEvidence(res, 'branding_editing'); clearPublicSiteCache(); // Check if watermark settings changed and trigger regeneration @@ -1229,6 +1237,7 @@ router.post('/logo', adminAuth, requirePermission('settings.edit'), upload.singl updated_at: new Date() }); + capabilityEvidence(res, 'branding_editing'); res.json({ message: 'Logo uploaded successfully', logoUrl: publicPath @@ -1247,6 +1256,7 @@ router.delete('/logo', adminAuth, requirePermission('settings.edit'), async (req const pathKey = isDark ? 'branding_logo_path_dark' : 'branding_logo_path'; const urlKey = isDark ? 'branding_logo_url_dark' : 'branding_logo_url'; + const logoChanged = await settingsChanged(db, { [pathKey]: '', [urlKey]: '' }, [pathKey, urlKey]); const pathSetting = await db('app_settings').where('setting_key', pathKey).first(); if (pathSetting && pathSetting.setting_value) { try { @@ -1261,6 +1271,7 @@ router.delete('/logo', adminAuth, requirePermission('settings.edit'), async (req .whereIn('setting_key', [pathKey, urlKey]) .update({ setting_value: JSON.stringify(''), updated_at: new Date() }); + if (logoChanged) capabilityEvidence(res, 'branding_editing'); res.json({ message: 'Logo removed' }); } catch (error) { errorResponse(res, error, 500, 'Failed to remove logo'); @@ -1346,6 +1357,7 @@ router.post('/branding/watermark-logo', adminAuth, requirePermission('settings.e watermarkRegenerationStarted = true; } + capabilityEvidence(res, 'branding_editing'); res.json({ message: 'Watermark logo uploaded successfully', watermarkLogoUrl: publicPath, @@ -1360,6 +1372,7 @@ router.post('/branding/watermark-logo', adminAuth, requirePermission('settings.e router.put('/theme', adminAuth, requirePermission('settings.edit'), async (req, res) => { try { const themeSettings = req.body; + const themeChanged = await settingsChanged(db, { theme_config: themeSettings }, ['theme_config']); // Save theme settings await db('app_settings') @@ -1386,6 +1399,7 @@ router.put('/theme', adminAuth, requirePermission('settings.edit'), async (req, clearPublicSiteCache(); + if (themeChanged) capabilityEvidence(res, 'branding_editing'); res.json({ message: 'Theme settings updated successfully' }); } catch (error) { errorResponse(res, error, 500, 'Failed to update theme settings'); @@ -1709,6 +1723,7 @@ router.put('/seo', adminAuth, requirePermission('settings.edit'), async (req, re } } + const seoChanged = await settingsChanged(db, settings, SEO_USAGE_KEYS); // Update or insert each setting for (const [key, value] of Object.entries(settings)) { await db('app_settings') @@ -1738,6 +1753,7 @@ router.put('/seo', adminAuth, requirePermission('settings.edit'), async (req, re metadata: JSON.stringify({ settings_count: Object.keys(settings).length }) }); + if (seoChanged) capabilityEvidence(res, 'seo_editing'); res.json({ message: 'SEO settings updated successfully' }); } catch (error) { errorResponse(res, error, 500, 'Failed to update SEO settings'); @@ -2035,6 +2051,7 @@ router.post('/favicon', adminAuth, requirePermission('settings.edit'), faviconUp { type: 'admin', id: req.admin.id, name: req.admin.username } ); + capabilityEvidence(res, 'branding_editing'); res.json({ faviconUrl }); } catch (error) { errorResponse(res, error, 500, 'Failed to upload favicon'); diff --git a/backend/src/routes/adminUsage.js b/backend/src/routes/adminUsage.js index 5bd419c9..159cc5ca 100644 --- a/backend/src/routes/adminUsage.js +++ b/backend/src/routes/adminUsage.js @@ -82,7 +82,7 @@ router.post( router.post( '/consent', wrap(async (req, res) => { - if (!req.body || Object.keys(req.body).length !== 1 || !['usage.v2', 'usage.v3', 'usage.v4'].includes(schemaForConsent(req.body.consent_version))) + if (!req.body || Object.keys(req.body).length !== 1 || !schemaForConsent(req.body.consent_version) || req.body?.consent_version === 'usage-consent.v1') throw new ValidationError('Explicit usage consent is required'); res.json(await service.command('consent', { consent_version: req.body.consent_version })); }) diff --git a/backend/src/services/emailProcessor.js b/backend/src/services/emailProcessor.js index 513467c3..f6a6c093 100644 --- a/backend/src/services/emailProcessor.js +++ b/backend/src/services/emailProcessor.js @@ -906,7 +906,7 @@ async function buildSignatureTextFor(language) { } } -async function sendTemplateEmail(to, templateKey, variables) { +async function sendTemplateEmail(to, templateKey, variables, { usageEligible = true } = {}) { try { // Webhook transport (#1225) replaces SMTP entirely when configured, so an // instance using it has no SMTP settings to initialise and must not be @@ -987,6 +987,17 @@ async function sendTemplateEmail(to, templateKey, variables) { ? await emailWebhookTransport.send(mail) : await transporter.sendMail(mail); + // Transport acceptance is the measured event, not rendering or inbox + // delivery. The service accepts only the fixed bit under confirmed v5 + // consent; marker failure must never retry an already-sent message. + if (usageEligible && (viaWebhook || info.accepted?.length > 0)) { + try { + await require('./productUsageService').markUsed(['email_template_delivery']); + } catch { + logger.warn('Product usage mail marker could not be recorded'); + } + } + logger.info(`Email sent successfully: ${info.messageId} (${language})`); // Return the rendered HTML so the queue processor can persist the ACTUAL // sent body (email_queue.rendered_html) for the Project Overview preview. @@ -1275,7 +1286,8 @@ async function processEmailQueue({ ignoreSchedule = false, limit = 10, onlyId = sendResult = await sendTemplateEmail( email.recipient_email, email.email_type, - emailData + emailData, + { usageEligible: emailData.__usageEligible !== false } ); } @@ -1436,6 +1448,9 @@ async function queueEmail(eventId, recipientEmail, emailType, emailData, options try { // Add eventId to emailData for language detection emailData.eventId = eventId; + // An explicit test message (dev tools' send-test-email) must not count as + // template delivery when the queue processor sends it later. + if (options.usageEligible === false) emailData.__usageEligible = false; const row = { event_id: eventId, recipient_email: recipientEmail, diff --git a/backend/src/services/updateNotificationService.js b/backend/src/services/updateNotificationService.js index 053d3b9f..f9cf3e09 100644 --- a/backend/src/services/updateNotificationService.js +++ b/backend/src/services/updateNotificationService.js @@ -231,7 +231,7 @@ async function sendTestUpdateNotification() { current_version: updateInfo.current, channel: channelLabel, recipient_email: email - }); + }, { usageEligible: false }); successCount++; } catch (error) { errorCount++; diff --git a/backend/src/services/workflows/actions.js b/backend/src/services/workflows/actions.js index 91412acf..801f6a2e 100644 --- a/backend/src/services/workflows/actions.js +++ b/backend/src/services/workflows/actions.js @@ -53,7 +53,8 @@ registry.registerAction('send_email', async (ctx) => { // INTERNAL/admin = immediate; EXTERNAL/customer = business-hours floor. const respectBusinessHours = !isInternal; - await emailProcessor.queueEmail(eventId, to, emailType, emailData, { respectBusinessHours }); + // A workflow test run (engine.testRun, __test) is a test message for usage. + await emailProcessor.queueEmail(eventId, to, emailType, emailData, { respectBusinessHours, usageEligible: !ctx.vars?.__test }); return { sent_to: to, recipientClass, respectBusinessHours }; }); @@ -120,7 +121,7 @@ registry.registerAction('escalate_to_collections', async (ctx) => { due_date: invoice.due_date ? String(invoice.due_date).slice(0, 10) : '', reminder_level: invoice.reminder_level || 0, attachments, - }, { respectBusinessHours: false }); // internal/admin → immediate + }, { respectBusinessHours: false, usageEligible: !ctx.vars?.__test }); // internal/admin → immediate return { collections_handoff_to: adminEmail, outstanding }; }); diff --git a/backend/src/usage/UsageService.js b/backend/src/usage/UsageService.js index cb8318de..d9a7e647 100644 --- a/backend/src/usage/UsageService.js +++ b/backend/src/usage/UsageService.js @@ -16,7 +16,7 @@ const { verifyEnvelope, digest, canonical, - FEATURE_KEYS, + ALL_FEATURE_KEYS, LEGACY_FEATURE_KEYS, CATALOG, CURRENT_SCHEMA_VERSION, @@ -796,7 +796,7 @@ class UsageService { async markUsed(features, { destinationBackup = false, legacyFeatures } = {}) { let allowed = [...new Set([...features, ...(legacyFeatures || [])])].filter((f) => - FEATURE_KEYS.includes(f) + ALL_FEATURE_KEYS.includes(f) ); if (!allowed.length) return; // Single-transaction status check prevents opt-out racing a late marker. @@ -960,7 +960,7 @@ class UsageService { generated_at: now, features: expanded, gallery_layouts: [...layouts].sort(), - ...(['usage.v3', 'usage.v4'].includes(version) ? { inventory: await require('./inventorySnapshot').inventorySnapshot(this.db) } : {}) + ...(['usage.v3', 'usage.v4', 'usage.v5'].includes(version) ? { inventory: await require('./inventorySnapshot').inventorySnapshot(this.db) } : {}) }; } diff --git a/backend/src/usage/adoptionEvidence.js b/backend/src/usage/adoptionEvidence.js new file mode 100644 index 00000000..c006bb3b --- /dev/null +++ b/backend/src/usage/adoptionEvidence.js @@ -0,0 +1,52 @@ +'use strict'; +const { isDeepStrictEqual } = require('node:util'); +const { capabilityEvidence } = require('./capabilityEvidence'); + +// Compare values already handled locally by an admin operation. Never retain +// these values, hashes, IDs or a before/after record in the usage subsystem. +function normalized(value) { + if (value === undefined || value === null || value === '') return null; + if (typeof value === 'string' && (value.startsWith('[') || value.startsWith('{'))) { + try { return normalized(JSON.parse(value)); } catch { return value; } + } + // Array.from, not .map: a row array from the sqlite binding belongs to the + // outer realm under Jest and isDeepStrictEqual rejects it on prototype alone. + if (Array.isArray(value)) return Array.from(value, normalized); + if (value && typeof value === 'object') return Object.fromEntries( + Object.entries(value).map(([key, item]) => [key, normalized(item)]) + ); + return value; +} +function changedFields(before, after, fields) { + return fields.some(key => { + if (after[key] === undefined) return false; + let left = before?.[key], right = after[key]; + if (typeof left === 'boolean' && (right === 0 || right === 1)) right = Boolean(right); + if (typeof right === 'boolean' && (left === 0 || left === 1)) left = Boolean(left); + return !isDeepStrictEqual(normalized(left), normalized(right)); + }); +} +function changedEvidence(res, key, before, after, fields) { + if (changedFields(before, after, fields)) capabilityEvidence(res, key); +} + +// Restrict comparisons to named product settings. Missing rows are unknown, +// not evidence of customization: persisting a fallback for the first time must +// not turn a default-only installation into an observed customization. +async function settingsChanged(db, updates, allowedKeys) { + const keys = allowedKeys.filter(key => updates[key] !== undefined); + if (!keys.length) return false; + try { + const rows = await db('app_settings').whereIn('setting_key', keys).select('setting_key', 'setting_value'); + return rows.some(row => { + let previous = row.setting_value; + try { previous = JSON.parse(previous); } catch { /* legacy plain value */ } + return changedFields({ value: previous }, { value: updates[row.setting_key] }, ['value']); + }); + } catch { + // Optional evidence must not prevent the product operation from running. + require('../utils/logger').warn('Product usage settings comparison unavailable'); + return false; + } +} +module.exports = { changedFields, changedEvidence, settingsChanged }; diff --git a/backend/src/usage/capabilityEvidence.js b/backend/src/usage/capabilityEvidence.js index 747f6d1f..8545389d 100644 --- a/backend/src/usage/capabilityEvidence.js +++ b/backend/src/usage/capabilityEvidence.js @@ -1,5 +1,5 @@ 'use strict'; -const { FEATURE_KEYS, observesUse } = require('./schema.cjs'); +const { ALL_FEATURE_KEYS, ALL_FEATURES } = require('./schema.cjs'); // Trusted route handlers call this AFTER their business operation succeeds. // Only fixed, allowlisted keys reach finish middleware. It still requires an @@ -7,7 +7,7 @@ const { FEATURE_KEYS, observesUse } = require('./schema.cjs'); function capabilityEvidence(res, ...keys) { res.locals.productUsageFeatures = [...new Set([ ...(res.locals.productUsageFeatures || []), - ...keys.filter((key) => FEATURE_KEYS.includes(key) && observesUse(key)) + ...keys.filter((key) => ALL_FEATURE_KEYS.includes(key) && ALL_FEATURES[key]?.used) ])]; } function acceptedUpload(res, { video = false, raw = false, s3 = false } = {}) { diff --git a/backend/src/usage/expandedSnapshot.js b/backend/src/usage/expandedSnapshot.js index 46428d12..cc3d34ec 100644 --- a/backend/src/usage/expandedSnapshot.js +++ b/backend/src/usage/expandedSnapshot.js @@ -85,7 +85,7 @@ async function expandSnapshot(db, { features, flags, used, now, version = 'usage gallery_guest_uploads: 'allow_user_uploads', gallery_client_access: 'client_access_enabled', gallery_watermarks: 'watermark_downloads' })) result[key].configured = await enabled('events', column); - if (version === 'usage.v4') { + if (['usage.v4', 'usage.v5'].includes(version)) { result.gallery_downloads_restricted.configured = await exists('events', ['allow_downloads'], (query) => query.where('allow_downloads', formatBoolean(false))); } else { @@ -122,7 +122,7 @@ async function expandSnapshot(db, { features, flags, used, now, version = 'usage 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 (['usage.v3', 'usage.v4'].includes(version)) { + if (['usage.v3', 'usage.v4', 'usage.v5'].includes(version)) { 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', diff --git a/backend/src/usage/features.v5.json b/backend/src/usage/features.v5.json new file mode 100644 index 00000000..61009cb2 --- /dev/null +++ b/backend/src/usage/features.v5.json @@ -0,0 +1,1313 @@ +{ + "schema_version": "usage.v5", + "consent_version": "usage-consent.v5", + "features": { + "crm": { + "category": "crm", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "clients", + "name": { + "en": "Client management" + }, + "configured": { + "en": "The clients capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "crm_quotes": { + "category": "crm", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "quotes", + "name": { + "en": "Quotes" + }, + "configured": { + "en": "The quotes capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "crm_invoices": { + "category": "crm", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "bills", + "name": { + "en": "Invoices" + }, + "configured": { + "en": "The bills capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "crm_contracts": { + "category": "crm", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "contracts", + "name": { + "en": "Contracts" + }, + "configured": { + "en": "The contracts capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "crm_projects": { + "category": "crm", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "projects", + "name": { + "en": "Projects" + }, + "configured": { + "en": "The projects capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "crm_calendar": { + "category": "crm", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "calendar", + "name": { + "en": "Admin calendar" + }, + "configured": { + "en": "The calendar capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "crm_hours": { + "category": "crm", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "hoursLogging", + "name": { + "en": "Hours logging" + }, + "configured": { + "en": "The hoursLogging capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "customer_portal": { + "category": "crm", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "customerPortal", + "name": { + "en": "Customer portal" + }, + "configured": { + "en": "The customerPortal capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "accounting": { + "category": "accounting", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "accounting", + "name": { + "en": "Accounting" + }, + "configured": { + "en": "The accounting capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "workflows": { + "category": "automation", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "workflows", + "name": { + "en": "Workflows" + }, + "configured": { + "en": "The workflows capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "newsletters": { + "category": "communication", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "newsletters", + "name": { + "en": "Newsletters" + }, + "configured": { + "en": "The newsletters capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "face_recognition": { + "category": "gallery", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "faces", + "name": { + "en": "ML face recognition" + }, + "configured": { + "en": "The faces capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "custom_css": { + "category": "appearance", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Custom CSS" + }, + "configured": { + "en": "Custom CSS is configured globally or applied through a gallery/theme/template; CSS text is not sent." + }, + "used": { + "en": "Applied CSS observed after consent, without observing visitors." + } + }, + "oauth": { + "category": "integration", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Admin SSO" + }, + "configured": { + "en": "Admin OIDC is enabled and issuer/client configuration is present; no provider or credential values." + }, + "used": { + "en": "Successful admin SSO login; no account, identity-provider or session details." + } + }, + "smtp": { + "category": "communication", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "SMTP delivery" + }, + "configured": { + "en": "An outgoing SMTP host is configured; no host, account, address or credentials." + }, + "used": { + "en": "A successful explicitly initiated admin SMTP test/send; no recipients or messages." + } + }, + "whatsapp": { + "category": "communication", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "WhatsApp integration" + }, + "configured": { + "en": "The WhatsApp capability is enabled and a usable configuration is present; no phone number, token or template." + }, + "used": { + "en": "Successful admin integration test; no recipient, message or delivery history." + } + }, + "backup": { + "category": "operations", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Backups" + }, + "configured": { + "en": "A full or database backup schedule is enabled; no schedule, path, storage sizes or backup names." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "s3_storage": { + "category": "integration", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "S3 storage" + }, + "configured": { + "en": "S3 is configured for media or backups; no bucket, endpoint, credentials or object keys." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "share_mounts": { + "category": "integration", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "External folders" + }, + "configured": { + "en": "At least one gallery uses an external folder; only existence, no folder paths or gallery identifiers." + }, + "used": { + "en": "An admin initiated an accepted external-folder import; no scanned paths, files or counts." + } + }, + "galleries": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Gallery management" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "photo_management": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Media management" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "photo_exports": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Admin media export" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "photo_processing": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Media maintenance tools" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "archive_management": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Gallery archives" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "gallery_sharing": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Gallery sharing and QR" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "short_links": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Short links" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "category_editing": { + "category": "gallery", + "since": "usage.v5", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Category customization" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "An admin created, changed or deleted a category since consent. Seeded categories, reading and unchanged saves do not count. No names, memberships or identifiers are retained." + }, + "replaces": "gallery_categories" + }, + "event_type_editing": { + "category": "gallery", + "since": "usage.v5", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Event type customization" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "An admin created, changed or deleted an event type since consent. Seeded presets, reading and unchanged saves do not count. No names, presets or identifiers are retained." + }, + "replaces": "event_types" + }, + "slideshow": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "slideshow", + "name": { + "en": "Live slideshow" + }, + "configured": { + "en": "The slideshow capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "transfers": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "transfers", + "name": { + "en": "PicTransfer" + }, + "configured": { + "en": "The transfers capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "video_uploads": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Admin video uploads" + }, + "configured": { + "en": "Video extensions are allowed in global upload settings; no uploaded-file metadata." + }, + "used": { + "en": "At least one admin video file was successfully stored/accepted; no names, formats, lengths, sizes or processing/visitor history." + } + }, + "camera_raw_uploads": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Admin camera RAW uploads" + }, + "configured": { + "en": "Camera RAW (DNG) is allowed in global upload settings; no camera models or EXIF." + }, + "used": { + "en": "At least one admin camera RAW upload was stored/accepted; only the capability bit, no filename or metadata." + } + }, + "messaging": { + "category": "communication", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "messaging", + "name": { + "en": "Messaging tools" + }, + "configured": { + "en": "The messaging capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "incoming_mail": { + "category": "communication", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "IMAP intake" + }, + "configured": { + "en": "Incoming mail is enabled and an IMAP configuration is present; no mailbox, server, folders or credentials." + }, + "used": { + "en": "A successful explicit admin connection test or non-skipped manual poll; no background intake, messages, attachments or counts." + } + }, + "reminder_emails": { + "category": "communication", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "flag": "reminderEmails", + "name": { + "en": "Automatic event reminders" + }, + "configured": { + "en": "The reminderEmails capability switch is effectively enabled; only a boolean." + }, + "used": null + }, + "email_template_editing": { + "category": "communication", + "since": "usage.v5", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Email template customization" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "An admin created a nonempty template or saved a real subject/body change since consent. Defaults, unchanged saves, previews and sending are excluded. This does not establish the current customization of templates edited before consent." + }, + "replaces": "email_templates" + }, + "email_webhook": { + "category": "communication", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Email webhook transport" + }, + "configured": { + "en": "Both email webhook settings are present; no URL or secret." + }, + "used": { + "en": "Successful explicitly initiated admin send/test through the webhook transport; no recipients, messages or automatic deliveries." + } + }, + "accounting_incoming_invoices": { + "category": "accounting", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "incomingInvoices", + "name": { + "en": "Incoming invoices" + }, + "configured": { + "en": "The incomingInvoices capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "accounting_expenses": { + "category": "accounting", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "expenses", + "name": { + "en": "Expenses" + }, + "configured": { + "en": "The expenses capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "accounting_tax_report": { + "category": "accounting", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "taxReport", + "name": { + "en": "Tax reports" + }, + "configured": { + "en": "The taxReport capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "accounting_ledger": { + "category": "accounting", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "accounting", + "name": { + "en": "Ledger and accounting export" + }, + "configured": { + "en": "The accounting capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "crm_installments": { + "category": "crm", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Installment-plan tools" + }, + "configured": { + "en": "Quotes or invoices are enabled; no actual payment plans, amounts or statuses are inspected." + }, + "used": { + "en": "An admin saved an installment plan; no dates, amounts, currencies, payment status or document IDs." + } + }, + "document_templates": { + "category": "crm", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Document presets and blocks" + }, + "configured": { + "en": "Quotes or contracts are enabled, making document presets/blocks available; no template content." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "cms_content_editing": { + "category": "appearance", + "since": "usage.v5", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "CMS content editing" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "An admin saved a real change to an internal CMS page title or body since consent. Unchanged saves, external links, logos, seeded pages and page views do not count. This does not measure whether anyone read the page." + }, + "replaces": "cms" + }, + "public_site": { + "category": "appearance", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Public landing page" + }, + "configured": { + "en": "The public landing-page setting is enabled; no page HTML, texts, domains or visitors." + }, + "used": null + }, + "branding_editing": { + "category": "appearance", + "since": "usage.v5", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Branding customization" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "An admin changed branding settings, a theme or a logo since consent. Reading settings and unchanged saves do not count. A change can also restore a default; this is not a claim about the current design." + }, + "replaces": "branding" + }, + "seo_editing": { + "category": "appearance", + "since": "usage.v5", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "SEO customization" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "An admin saved a real change to an allowlisted SEO setting since consent. Defaults, reading and unchanged saves do not count; no rules, paths or search-engine activity are collected." + }, + "replaces": "seo_customization" + }, + "admin_management": { + "category": "operations", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "userManagement", + "name": { + "en": "Admin and role management" + }, + "configured": { + "en": "The userManagement capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "api_integration": { + "category": "integration", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "HTTP API integration" + }, + "configured": { + "en": "An unrevoked, unexpired API credential exists; no tokens, names, scopes or owner data." + }, + "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." + } + }, + "webhooks": { + "category": "integration", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Outbound webhooks" + }, + "configured": { + "en": "At least one active webhook is configured; no destinations, subscriptions, secrets or delivery logs." + }, + "used": { + "en": "Successful explicit admin webhook test/replay; no automatic or visitor-triggered deliveries." + } + }, + "restore": { + "category": "operations", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Restore" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "portable_backup": { + "category": "operations", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Portable PicPeak export/import" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "database_backup": { + "category": "operations", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Database backups" + }, + "configured": { + "en": "Scheduled database backups are enabled; no schedules, file names or database contents." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "s3_photo_storage": { + "category": "integration", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "S3 media storage" + }, + "configured": { + "en": "S3 is the configured media backend and required credentials are present; no values are sent." + }, + "used": { + "en": "Successful admin media storage/accepted upload to S3; no buckets, objects or sizes." + } + }, + "s3_backups": { + "category": "integration", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "S3 backup destination" + }, + "configured": { + "en": "The configured backup destination is S3 with a bucket present; no bucket or credentials." + }, + "used": { + "en": "An admin started a backup to the configured S3 destination or a successful S3 test upload; local exports never imply S3 use." + } + }, + "analytics_dashboard": { + "category": "operations", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "analytics", + "name": { + "en": "Existing analytics module" + }, + "configured": { + "en": "The analytics capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "feedback_moderation": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Feedback moderation" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "guest_management": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Guest administration tools" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "gallery_feedback_likes": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery likes enabled" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "gallery_feedback_ratings": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery star ratings enabled" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "gallery_feedback_comments": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery comments enabled" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "gallery_feedback_favorites": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery favorites enabled" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "gallery_feedback_reactions": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery reactions enabled" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "gallery_feedback_color_labels": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery color labels enabled" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "gallery_guest_accounts": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Guest identities enabled" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "gallery_guest_uploads": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Guest uploads enabled" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "gallery_downloads_restricted": { + "category": "gallery_configuration", + "since": "usage.v4", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery downloads restricted" + }, + "configured": { + "en": "At least one gallery has downloads switched off; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "download_resolution_picker": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Download resolution picker enabled" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "gallery_client_access": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Client access enabled" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "gallery_watermarks": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Watermarks enabled" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "gallery_image_protection": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Image protection enabled" + }, + "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." + }, + "used": null + }, + "gallery_reveal": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery reveal enabled" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "gallery_expiration": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery expiration configured" + }, + "configured": { + "en": "At least one gallery has an expiry configured; no dates, gallery IDs or counts." + }, + "used": null + }, + "photo_xmp_export": { + "category": "gallery", + "since": "usage.v3", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "XMP export" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "An admin successfully generated an XMP export; no sidecars, filenames, ratings, selections or counts." + } + }, + "photo_replacement": { + "category": "gallery", + "since": "usage.v3", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Photo replacement" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "An admin upload actually replaced a photo successfully; no filenames, matching values, IDs or counts." + } + }, + "photo_admin_marks": { + "category": "gallery", + "since": "usage.v3", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Photographer marks" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "An admin successfully saved their own photo mark; no rating, color, photo or admin identity." + } + }, + "gallery_folders": { + "category": "gallery_configuration", + "since": "usage.v3", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery folders configured" + }, + "configured": { + "en": "An applicable global or gallery category is configured as a folder; no names, contents, counts or visitor activity." + }, + "used": null + }, + "transfer_upload_links": { + "category": "gallery_configuration", + "since": "usage.v3", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "PicTransfer upload links enabled" + }, + "configured": { + "en": "PicTransfer is enabled and a non-deleted transfer allows unexpired uploads; no links, tokens, dates, recipients or uploads." + }, + "used": null + }, + "workflow_automation_enabled": { + "category": "automation", + "since": "usage.v3", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Workflow automation enabled" + }, + "configured": { + "en": "The workflows module and at least one workflow are enabled; no names, graphs, triggers, decisions or runs." + }, + "used": null + }, + "s3_auto_import": { + "category": "integration", + "since": "usage.v3", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "S3 automatic import enabled" + }, + "configured": { + "en": "S3 media storage is configured and STORAGE_AUTO_IMPORT is enabled; no bucket, prefix, credentials, polling or imported objects." + }, + "used": null + }, + "crm_invoice_import": { + "category": "crm", + "since": "usage.v3", + "measurement": "configuration_and_use", + "configuration": "flag", + "name": { + "en": "Invoice import" + }, + "configured": { + "en": "The required product capabilities are effectively enabled; only a boolean." + }, + "used": { + "en": "An admin successfully imported an existing invoice; no PDF, invoice number, amount, currency, customer or payment status." + }, + "flag": "bills" + }, + "crm_combined_billing": { + "category": "crm", + "since": "usage.v3", + "measurement": "configuration_and_use", + "configuration": "capability", + "name": { + "en": "Combined billing" + }, + "configured": { + "en": "The required product capabilities are effectively enabled; only a boolean." + }, + "used": { + "en": "An admin successfully created a combined bill; no hours, expenses, customer, documents or financial values." + } + }, + "crm_monthly_billing_manual": { + "category": "crm", + "since": "usage.v3", + "measurement": "configuration_and_use", + "configuration": "flag", + "name": { + "en": "Manual monthly billing" + }, + "configured": { + "en": "The required product capabilities are effectively enabled; only a boolean." + }, + "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." + }, + "flag": "bills" + }, + "crm_document_conversion": { + "category": "crm", + "since": "usage.v3", + "measurement": "configuration_and_use", + "configuration": "capability", + "name": { + "en": "Document conversion" + }, + "configured": { + "en": "The required product capabilities are effectively enabled; only a boolean." + }, + "used": { + "en": "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": { + "category": "gallery_configuration", + "since": "usage.v3", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Capture-date sorting configured" + }, + "configured": { + "en": "A gallery defaults to sorting by capture date; no capture dates, EXIF or visitor sorting actions." + }, + "used": null + }, + "download_original_filenames": { + "category": "gallery_configuration", + "since": "usage.v3", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Original download filenames enabled" + }, + "configured": { + "en": "The original-download-filenames switch is enabled; no filenames or downloads are read or sent." + }, + "used": null + }, + "email_template_delivery": { + "category": "integration", + "since": "usage.v5", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Emails sent using templates" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "At least one real template email was accepted by SMTP or the configured mail webhook since consent, including background sends. Previews, test messages and template-free messages are excluded. Acceptance does not prove receipt or reading. No template key, recipient, contents, message identifier, send time or count is stored in usage markers." + } + } + }, + "inventory": { + "galleries": { + "name": { + "en": "Stored galleries" + }, + "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." + } + }, + "photos": { + "name": { + "en": "Stored photo records" + }, + "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." + } + } + } +} diff --git a/backend/src/usage/schema.cjs b/backend/src/usage/schema.cjs index 685e6d7f..70417999 100644 --- a/backend/src/usage/schema.cjs +++ b/backend/src/usage/schema.cjs @@ -2,10 +2,10 @@ // Vendored byte-identical in PicPeak. Existing wire versions stay immutable; // every expansion requires explicit consent to its own version. -const CATALOG = require("./features.v4.json"); -const CATALOGS = { "usage.v2": require("./features.v2.json"), "usage.v3": require("./features.v3.json"), "usage.v4": CATALOG }; -const CONSENT_VERSIONS = { "usage.v1": "usage-consent.v1", "usage.v2": "usage-consent.v2", "usage.v3": "usage-consent.v3", "usage.v4": "usage-consent.v4" }; -const CURRENT_SCHEMA_VERSION = "usage.v4"; +const CATALOG = require("./features.v5.json"); +const CATALOGS = { "usage.v2": require("./features.v2.json"), "usage.v3": require("./features.v3.json"), "usage.v4": require("./features.v4.json"), "usage.v5": CATALOG }; +const CONSENT_VERSIONS = { "usage.v1": "usage-consent.v1", "usage.v2": "usage-consent.v2", "usage.v3": "usage-consent.v3", "usage.v4": "usage-consent.v4", "usage.v5": "usage-consent.v5" }; +const CURRENT_SCHEMA_VERSION = "usage.v5"; 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); @@ -49,7 +49,7 @@ 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 } }, - ...(["usage.v3", "usage.v4"].includes(version) ? { inventory: object(Object.fromEntries(INVENTORY_KEYS.map(key => [key, + ...(["usage.v3", "usage.v4", "usage.v5"].includes(version) ? { inventory: object(Object.fromEntries(INVENTORY_KEYS.map(key => [key, { type: "integer", minimum: 0, maximum: MAX_INVENTORY_COUNT } ]))) } : {}), }); diff --git a/docs/FEATURE_COVERAGE.md b/docs/FEATURE_COVERAGE.md index ce02eecd..85f9f6b2 100644 --- a/docs/FEATURE_COVERAGE.md +++ b/docs/FEATURE_COVERAGE.md @@ -1,251 +1,158 @@ -# Product-usage coverage: usage.v4 +# Product-usage coverage: usage.v5 -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.v4.json`. -The prior v2/v3 inventories and all v1/v2/v3 wire catalogs/schemas remain unchanged. +## What the numbers mean -## Data scope +The 86 v4 capabilities have all been reviewed for the distinction between +availability, a present configuration, and observed use. v5 asks 87 questions: +six broad admin-management questions are replaced by precisely defined edit +signals, and one question measures real template-mail transport acceptance. +There are 64 configured/used pairs and 23 configuration-only signals. +Historical views retain all 94 keys separately. No old value is renamed, +backfilled, reinterpreted, or combined with its replacement. -There are 86 capabilities: the original 19 in v1, 54 added in v2, and 13 added in -v3. v4 replaces `gallery_downloads` with `gallery_downloads_restricted`; the active -catalog remains at 86. 63 have configured/used booleans; 23 are configuration-only -and omit `used`. Historical views retain both questions separately (87 total keys). -ML face recognition was already included: only effective availability and a -successful authenticated admin capability operation, never biometric results. +- **Built in** is availability, never an adoption percentage. This applies to + gallery/media management, export/maintenance/archive/share/short-link tools, + restore/import, moderation, guest administration, XMP, replacement and marks, + as well as the built-in editors. An operation remains the evidence of use. +- **Enabled / configuration present** can be the shipped default, an inherited + value or an explicit choice. Flags and configuration-only signals do not + establish deliberate setup, successful delivery, or visitor activity. +- **Observed since consent** is one monotonic boolean. It is not frequency, + recent activity, the present configuration, or proof of a job's completion. + Existing operation signals keep their documented boundaries (including + explicit connection tests, accepted jobs and admin calendar/analytics reads). +- The six new edit signals compare accepted, persisted product values. Empty + requests, timestamps, previews, unchanged saves and automatically seeded + records do not establish editing. Restoring a different default does count + as an edit. Missing setting baselines are conservatively not inferred. +- CMS editing means a real internal title/body change. Changing only a logo or + external link does not establish internal content editing; no page-view + hooks are added. Earlier customization is not inferred from row existence, + creation/update timestamps, activity logs or current private contents. +- Template editing and template sending are independent. An unchanged shipped + template can be sent. Sending means SMTP accepted at least one recipient or + the configured mail webhook accepted the real message. Preview, test, raw + composer and newsletter messages do not set this bit. It includes queued + and background template mail, but proves neither receipt nor reading. -v3 and v4 report 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. +A false edit/use bit means no qualifying observation since accepted consent; +it must never be presented as proof of unchanged defaults or lifetime non-use. +Old reporters' missing new questions are unknown, not false. -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. +## Privacy and consent -`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. +New observations contain only fixed allowlisted booleans. The existing two +installation inventory totals (galleries and non-video photo records) are +unchanged. No content, recipient, template name/key, category/event identifiers, +logo, business value, per-action count, timestamp or content hash is stored in +usage markers or transmitted. Comparisons take place locally in the existing +admin operation; no content is scanned to reconstruct past usage. -## Consent and version transition +Reports retain the existing stable installation fingerprint, PicPeak version, +daily date/generation time and gallery-layout enum. They are pseudonymous, +not fully anonymous. Existing retention, access controls and deletion apply. -- v1/v2/v3 retain their exact sender and receiver contracts. Updating code does - not grant consent to v4 or start collecting the restricted-downloads signal. -- v4 asks whether at least one gallery has downloads disabled, instead of the - v2/v3 question whether at least one gallery allows them. These questions are - not complements: mixed galleries can make both true. Never invert old values. -- The EN/DE dialog explains the change and all 86 capabilities plus both totals. - An unchecked checkbox requires explicit `usage-consent.v4` consent. -- Prior queued packets finish unchanged: preserve packet ID, sequence, payload, - logical day and digest. Only issue time, nonce and signature change on retry. - Never rebuild an existing packet with a new snapshot under its old packet ID. -- Signed v4 consent can upgrade v1/v2/v3 without changing identity or history. - Only the matching accepted receipt changes local scope and resets local markers. - Lost receipts remain retryable; opt-out always wins over a late response. -- Collector first, client second. v1/v2/v3 remain accepted even after v4 consent. - Missing fields stay unknown. Historical aggregation uses the union of known - questions, while the active v4 sender still has exactly 86 fields. +Every v1–v4 wire schema/catalog remains immutable. New evidence is retained only +under active, confirmed usage-consent.v5. The signed consent upgrade preserves +identity and raw history, finishes pending old packets unchanged and resets +markers only after the matching receipt. Opt-out wins over late receipts. +A marker write failure must not retry a successfully sent email. +Deploy the collector before the PicPeak client; existing clients keep working. -## Every reported capability +## Full audit -Definitions are shipped byte-identically in both applications as -`features.v4.json`, served at `/schema/features.v4.json`. The catalog is English only; the German labels live in the frontend locale file (`productUsage.catalog`). +The table is the decision for every active v5 capability. The route/flag/settings +inventory is `usage-coverage.v5.json`; historical inventories remain unchanged. +Exact definitions are served at `/schema/features.v5.json` and disclosed in the +PicPeak EN/DE consent catalog. A retired broad management signal remains under +its original definition in the collector's earlier-measurements view/history. -| Key (EN) | Since | Configured | Used | -| --- | --- | --- | --- | -| `crm` — Client management | usage.v1 | The clients 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. | -| `crm_quotes` — Quotes | usage.v1 | The quotes 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. | -| `crm_invoices` — Invoices | usage.v1 | The bills 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. | -| `crm_contracts` — Contracts | usage.v1 | The contracts 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. | -| `crm_projects` — Projects | usage.v1 | The projects 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. | -| `crm_calendar` — Admin calendar | usage.v1 | The calendar 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. | -| `crm_hours` — Hours logging | usage.v1 | The hoursLogging 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. | -| `customer_portal` — Customer portal | usage.v1 | The customerPortal 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. | -| `accounting` — Accounting | 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 | 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 | 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` — ML face recognition | 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 | 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 | 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 | 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. | -| `whatsapp` — WhatsApp integration | usage.v1 | The WhatsApp capability is enabled and a usable configuration is present; no phone number, token or template. | Successful admin integration test; no recipient, message or delivery history. | -| `backup` — Backups | usage.v1 | A full or database backup schedule is enabled; no schedule, path, storage sizes or backup names. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | -| `s3_storage` — S3 storage / S3-Speicher | usage.v1 | S3 is configured for media or backups; no bucket, endpoint, credentials or object keys. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | -| `share_mounts` — External folders | usage.v1 | At least one gallery uses an external folder; only existence, no folder paths or gallery identifiers. | An admin initiated an accepted external-folder import; no scanned paths, files or counts. | -| `galleries` — Gallery management | 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. | -| `photo_management` — Media management | 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. | -| `photo_exports` — Admin media export | 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. | -| `photo_processing` — Media maintenance tools | 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. | -| `archive_management` — Gallery archives | 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_sharing` — Gallery sharing and QR | 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. | -| `short_links` — Short links | 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_categories` — Photo categories | 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. | -| `event_types` — Event types and presets | 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. | -| `slideshow` — Live slideshow | usage.v2 | The slideshow 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. | -| `transfers` — PicTransfer | usage.v2 | The transfers 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. | -| `video_uploads` — Admin video uploads | usage.v2 | Video extensions are allowed in global upload settings; no uploaded-file metadata. | At least one admin video file was successfully stored/accepted; no names, formats, lengths, sizes or processing/visitor history. | -| `camera_raw_uploads` — Admin camera 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 | 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 | 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 | usage.v2 | The reminderEmails capability switch is effectively enabled; only a boolean. | Not collected: configuration only. | -| `email_templates` — Email templates | 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 | 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 | 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. | -| `accounting_expenses` — Expenses | usage.v2 | The expenses 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. | -| `accounting_tax_report` — Tax reports | usage.v2 | The taxReport 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. | -| `accounting_ledger` — Ledger and accounting export | usage.v2 | 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. | -| `crm_installments` — Installment-plan tools | 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 | 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 | 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 | usage.v2 | The public landing-page setting is enabled; no page HTML, texts, domains or visitors. | Not collected: configuration only. | -| `branding` — Branding settings | 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 | 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 | 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. | -| `api_integration` — HTTP API integration | usage.v2 | An unrevoked, unexpired API credential exists; no tokens, names, scopes or owner data. | Successful authenticated HTTP API capability call; only this bit, never URLs, request values, token/owner IDs or call counts. Does not trigger a report. | -| `webhooks` — Outbound webhooks | usage.v2 | At least one active webhook is configured; no destinations, subscriptions, secrets or delivery logs. | Successful explicit admin webhook test/replay; no automatic or visitor-triggered deliveries. | -| `restore` — Restore | 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. | -| `portable_backup` — Portable PicPeak export/import | 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. | -| `database_backup` — Database backups | usage.v2 | Scheduled database backups are enabled; no schedules, file names or database contents. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | -| `s3_photo_storage` — S3 media storage / S3-Medienspeicher | usage.v2 | S3 is the configured media backend and required credentials are present; no values are sent. | Successful admin media storage/accepted upload to S3; no buckets, objects or sizes. | -| `s3_backups` — S3 backup destination / S3-Sicherungsziel | usage.v2 | The configured backup destination is S3 with a bucket present; no bucket or credentials. | An admin started a backup to the configured S3 destination or a successful S3 test upload; local exports never imply S3 use. | -| `analytics_dashboard` — Existing analytics module | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | Not collected: configuration only. | -| `gallery_downloads_restricted` — Gallery downloads restricted | usage.v4 | At least one gallery has downloads switched off; only existence across the installation, never gallery IDs or counts. | Not collected (configuration only). | -| `download_resolution_picker` — Download resolution picker enabled | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | usage.v3 | The original-download-filenames switch is enabled; no filenames or downloads are read or sent. | Not collected: configuration only. | - -## Inventory totals - -- `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. - -## Route-family decisions - -| Family | Coverage | Boundary | +| Capability | Availability/configuration | Evidence of use | | --- | --- | --- | -| `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: 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: 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`, `gallery_folders` | Admin category CRUD; no names, descriptions, colors or ordering values. v3 adds only: gallery_folders. Exact definitions are in features.v4.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`, `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.v4.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`, `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.v4.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: 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_restricted`, `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.v4.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: 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. | -| `adminEvents/slideshow.js` | partial: `slideshow` | Admin generate/disable/configure only, never kiosk viewers or slide advances. | -| `adminEventTypes.js` | partial: `event_types` | Admin event-type CRUD; preset contents/names excluded. | -| `adminExpenses.js` | partial: `accounting`, `accounting_expenses`, `accounting_incoming_invoices` | Admin expense/inbound-invoice operations; no financial values, suppliers, mileage/location, dates, receipt files or OCR text. | -| `adminExternalMedia.js` | partial: `share_mounts` | Only admin import operation; status/list/browse are not use. Snapshot checks external-path presence, never reports a path. | -| `adminFeatureFlags.js` | configuration: `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` | Only allowlisted effective capability booleans. No marker from reading or saving feature flags. Disabled roadmap/developer flags excluded. | -| `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`, `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.v4.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: 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`, `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.v4.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.v4.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`, `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.v4.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`, `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.v4.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: 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`, `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.v4.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: 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`, `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.v4.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: 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. | - -## 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/v4 inventory totals are permitted. -- Disabled calendarBooking and internal crmDevelopment; hosted future product #1111 -- Image fragmentation: removed from current PicPeak, not a live capability +| `crm` — Client management | Enabled switch/capability; may be a default. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `crm_quotes` — Quotes | Enabled switch/capability; may be a default. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `crm_invoices` — Invoices | Enabled switch/capability; may be a default. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `crm_contracts` — Contracts | Enabled switch/capability; may be a default. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `crm_projects` — Projects | Enabled switch/capability; may be a default. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `crm_calendar` — Admin calendar | Enabled switch/capability; may be a default. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `crm_hours` — Hours logging | Enabled switch/capability; may be a default. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `customer_portal` — Customer portal | Enabled switch/capability; may be a default. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `accounting` — Accounting | Enabled switch/capability; may be a default. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `workflows` — Workflows | Enabled switch/capability; may be a default. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `newsletters` — Newsletters | Enabled switch/capability; may be a default. | 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 | Enabled switch/capability; may be a default. | 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 | Technical configuration exists; may be a default, not activity. | Applied CSS observed after consent, without observing visitors. | +| `oauth` — Admin SSO | Technical configuration exists; may be a default, not activity. | Successful admin SSO login; no account, identity-provider or session details. | +| `smtp` — SMTP delivery | Technical configuration exists; may be a default, not activity. | A successful explicitly initiated admin SMTP test/send; no recipients or messages. | +| `whatsapp` — WhatsApp integration | Technical configuration exists; may be a default, not activity. | Successful admin integration test; no recipient, message or delivery history. | +| `backup` — Backups | Technical configuration exists; may be a default, not activity. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `s3_storage` — S3 storage | Technical configuration exists; may be a default, not activity. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `share_mounts` — External folders | Technical configuration exists; may be a default, not activity. | An admin initiated an accepted external-folder import; no scanned paths, files or counts. | +| `galleries` — Gallery management | Built in; display a label, not a percentage. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `photo_management` — Media management | Built in; display a label, not a percentage. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `photo_exports` — Admin media export | Built in; display a label, not a percentage. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `photo_processing` — Media maintenance tools | Built in; display a label, not a percentage. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `archive_management` — Gallery archives | Built in; display a label, not a percentage. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `gallery_sharing` — Gallery sharing and QR | Built in; display a label, not a percentage. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `short_links` — Short links | Built in; display a label, not a percentage. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `category_editing` — Category customization | Built in; display a label, not a percentage. | An admin created, changed or deleted a category since consent. Seeded categories, reading and unchanged saves do not count. No names, memberships or identifiers are retained. | +| `event_type_editing` — Event type customization | Built in; display a label, not a percentage. | An admin created, changed or deleted an event type since consent. Seeded presets, reading and unchanged saves do not count. No names, presets or identifiers are retained. | +| `slideshow` — Live slideshow | Enabled switch/capability; may be a default. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `transfers` — PicTransfer | Enabled switch/capability; may be a default. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `video_uploads` — Admin video uploads | Technical configuration exists; may be a default, not activity. | At least one admin video file was successfully stored/accepted; no names, formats, lengths, sizes or processing/visitor history. | +| `camera_raw_uploads` — Admin camera RAW uploads | Technical configuration exists; may be a default, not activity. | At least one admin camera RAW upload was stored/accepted; only the capability bit, no filename or metadata. | +| `messaging` — Messaging tools | Enabled switch/capability; may be a default. | 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 | Technical configuration exists; may be a default, not activity. | A successful explicit admin connection test or non-skipped manual poll; no background intake, messages, attachments or counts. | +| `reminder_emails` — Automatic event reminders | Technical configuration exists; may be a default, not activity. | Not collected. No visitor/customer activity inferred. | +| `email_template_editing` — Email template customization | Built in; display a label, not a percentage. | An admin created a nonempty template or saved a real subject/body change since consent. Defaults, unchanged saves, previews and sending are excluded. This does not establish the current customization of templates edited before consent. | +| `email_webhook` — Email webhook transport | Technical configuration exists; may be a default, not activity. | Successful explicitly initiated admin send/test through the webhook transport; no recipients, messages or automatic deliveries. | +| `accounting_incoming_invoices` — Incoming invoices | Enabled switch/capability; may be a default. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `accounting_expenses` — Expenses | Enabled switch/capability; may be a default. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `accounting_tax_report` — Tax reports | Enabled switch/capability; may be a default. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `accounting_ledger` — Ledger and accounting export | Enabled switch/capability; may be a default. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `crm_installments` — Installment-plan tools | Technical configuration exists; may be a default, not activity. | An admin saved an installment plan; no dates, amounts, currencies, payment status or document IDs. | +| `document_templates` — Document presets and blocks | Technical configuration exists; may be a default, not activity. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `cms_content_editing` — CMS content editing | Built in; display a label, not a percentage. | An admin saved a real change to an internal CMS page title or body since consent. Unchanged saves, external links, logos, seeded pages and page views do not count. This does not measure whether anyone read the page. | +| `public_site` — Public landing page | Technical configuration exists; may be a default, not activity. | Not collected. No visitor/customer activity inferred. | +| `branding_editing` — Branding customization | Built in; display a label, not a percentage. | An admin changed branding settings, a theme or a logo since consent. Reading settings and unchanged saves do not count. A change can also restore a default; this is not a claim about the current design. | +| `seo_editing` — SEO customization | Built in; display a label, not a percentage. | An admin saved a real change to an allowlisted SEO setting since consent. Defaults, reading and unchanged saves do not count; no rules, paths or search-engine activity are collected. | +| `admin_management` — Admin and role management | Enabled switch/capability; may be a default. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `api_integration` — HTTP API integration | Technical configuration exists; may be a default, not activity. | Successful authenticated HTTP API capability call; only this bit, never URLs, request values, token/owner IDs or call counts. Does not trigger a report. | +| `webhooks` — Outbound webhooks | Technical configuration exists; may be a default, not activity. | Successful explicit admin webhook test/replay; no automatic or visitor-triggered deliveries. | +| `restore` — Restore | Built in; display a label, not a percentage. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `portable_backup` — Portable PicPeak export/import | Built in; display a label, not a percentage. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `database_backup` — Database backups | Technical configuration exists; may be a default, not activity. | A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts. | +| `s3_photo_storage` — S3 media storage | Technical configuration exists; may be a default, not activity. | Successful admin media storage/accepted upload to S3; no buckets, objects or sizes. | +| `s3_backups` — S3 backup destination | Technical configuration exists; may be a default, not activity. | An admin started a backup to the configured S3 destination or a successful S3 test upload; local exports never imply S3 use. | +| `analytics_dashboard` — Existing analytics module | Enabled switch/capability; may be a default. | 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 | Built in; display a label, not a percentage. | 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 | Built in; display a label, not a percentage. | 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 | Technical configuration exists; may be a default, not activity. | Not collected. No visitor/customer activity inferred. | +| `gallery_feedback_ratings` — Gallery star ratings enabled | Technical configuration exists; may be a default, not activity. | Not collected. No visitor/customer activity inferred. | +| `gallery_feedback_comments` — Gallery comments enabled | Technical configuration exists; may be a default, not activity. | Not collected. No visitor/customer activity inferred. | +| `gallery_feedback_favorites` — Gallery favorites enabled | Technical configuration exists; may be a default, not activity. | Not collected. No visitor/customer activity inferred. | +| `gallery_feedback_reactions` — Gallery reactions enabled | Technical configuration exists; may be a default, not activity. | Not collected. No visitor/customer activity inferred. | +| `gallery_feedback_color_labels` — Gallery color labels enabled | Technical configuration exists; may be a default, not activity. | Not collected. No visitor/customer activity inferred. | +| `gallery_guest_accounts` — Guest identities enabled | Technical configuration exists; may be a default, not activity. | Not collected. No visitor/customer activity inferred. | +| `gallery_guest_uploads` — Guest uploads enabled | Technical configuration exists; may be a default, not activity. | Not collected. No visitor/customer activity inferred. | +| `gallery_downloads_restricted` — Gallery downloads restricted | Technical configuration exists; may be a default, not activity. | Not collected. No visitor/customer activity inferred. | +| `download_resolution_picker` — Download resolution picker enabled | Technical configuration exists; may be a default, not activity. | Not collected. No visitor/customer activity inferred. | +| `gallery_client_access` — Client access enabled | Technical configuration exists; may be a default, not activity. | Not collected. No visitor/customer activity inferred. | +| `gallery_watermarks` — Watermarks enabled | Technical configuration exists; may be a default, not activity. | Not collected. No visitor/customer activity inferred. | +| `gallery_image_protection` — Image protection enabled | Technical configuration exists; may be a default, not activity. | Not collected. No visitor/customer activity inferred. | +| `gallery_reveal` — Gallery reveal enabled | Technical configuration exists; may be a default, not activity. | Not collected. No visitor/customer activity inferred. | +| `gallery_expiration` — Gallery expiration configured | Technical configuration exists; may be a default, not activity. | Not collected. No visitor/customer activity inferred. | +| `photo_xmp_export` — XMP export | Built in; display a label, not a percentage. | An admin successfully generated an XMP export; no sidecars, filenames, ratings, selections or counts. | +| `photo_replacement` — Photo replacement | Built in; display a label, not a percentage. | An admin upload actually replaced a photo successfully; no filenames, matching values, IDs or counts. | +| `photo_admin_marks` — Photographer marks | Built in; display a label, not a percentage. | An admin successfully saved their own photo mark; no rating, color, photo or admin identity. | +| `gallery_folders` — Gallery folders configured | Technical configuration exists; may be a default, not activity. | Not collected. No visitor/customer activity inferred. | +| `transfer_upload_links` — PicTransfer upload links enabled | Technical configuration exists; may be a default, not activity. | Not collected. No visitor/customer activity inferred. | +| `workflow_automation_enabled` — Workflow automation enabled | Technical configuration exists; may be a default, not activity. | Not collected. No visitor/customer activity inferred. | +| `s3_auto_import` — S3 automatic import enabled | Technical configuration exists; may be a default, not activity. | Not collected. No visitor/customer activity inferred. | +| `crm_invoice_import` — Invoice import | Enabled switch/capability; may be a default. | An admin successfully imported an existing invoice; no PDF, invoice number, amount, currency, customer or payment status. | +| `crm_combined_billing` — Combined billing | Enabled switch/capability; may be a default. | An admin successfully created a combined bill; no hours, expenses, customer, documents or financial values. | +| `crm_monthly_billing_manual` — Manual monthly billing | Enabled switch/capability; may be a default. | 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 | Enabled switch/capability; may be a default. | 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 | Technical configuration exists; may be a default, not activity. | Not collected. No visitor/customer activity inferred. | +| `download_original_filenames` — Original download filenames enabled | Technical configuration exists; may be a default, not activity. | Not collected. No visitor/customer activity inferred. | +| `email_template_delivery` — Emails sent using templates | Built in; display a label, not a percentage. | At least one real template email was accepted by SMTP or the configured mail webhook since consent, including background sends. Previews, test messages and template-free messages are excluded. Acceptance does not prove receipt or reading. No template key, recipient, contents, message identifier, send time or count is stored in usage markers. | diff --git a/docs/usage-coverage.v5.json b/docs/usage-coverage.v5.json new file mode 100644 index 00000000..66216ef5 --- /dev/null +++ b/docs/usage-coverage.v5.json @@ -0,0 +1,2160 @@ +{ + "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_restricted", + "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_type_editing" + ], + "reason": "General admin event-type capability; no names or preset contents." + }, + "categories": { + "signals": [ + "category_editing", + "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." + }, + "email": { + "signals": [ + "smtp", + "incoming_mail", + "messaging", + "email_template_editing", + "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_editing" + ], + "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_template_editing" + ], + "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." + }, + "branding": { + "signals": [ + "branding_editing", + "gallery_watermarks" + ], + "reason": "Branding operation and watermark configuration only; no branding text, logos or colors." + }, + "cms": { + "signals": [ + "cms_content_editing", + "public_site" + ], + "reason": "Admin page editing capability/public-site enabled only; no HTML, slugs or traffic." + } + }, + "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": [ + "category_editing", + "gallery_folders" + ], + "reason": "Admin category CRUD; no names, descriptions, colors or ordering values. v3 adds only: gallery_folders. Exact definitions are in features.v4.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_content_editing" + ], + "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.v4.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.v4.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_template_editing", + "email_webhook", + "reminder_emails", + "email_template_delivery" + ], + "reason": "v5 records real admin template content changes separately from successful real template mail transport acceptance, including background sends; no preview/test/no-op markers or message details.", + "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_restricted", + "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.v4.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_editing" + ], + "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_type_editing" + ], + "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.v4.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.v4.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.v4.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.v4.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_editing", + "seo_editing", + "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.v4.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.v4.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.v4.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_restricted", + "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" + }, + "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." + } + }, + "photos": { + "name": { + "en": "Stored photo records" + }, + "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." + } + } + }, + "adoption_audit": { + "crm": { + "configuration_decision": "Enabled switch/capability; may be a default.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "crm_quotes": { + "configuration_decision": "Enabled switch/capability; may be a default.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "crm_invoices": { + "configuration_decision": "Enabled switch/capability; may be a default.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "crm_contracts": { + "configuration_decision": "Enabled switch/capability; may be a default.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "crm_projects": { + "configuration_decision": "Enabled switch/capability; may be a default.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "crm_calendar": { + "configuration_decision": "Enabled switch/capability; may be a default.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "crm_hours": { + "configuration_decision": "Enabled switch/capability; may be a default.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "customer_portal": { + "configuration_decision": "Enabled switch/capability; may be a default.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "accounting": { + "configuration_decision": "Enabled switch/capability; may be a default.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "workflows": { + "configuration_decision": "Enabled switch/capability; may be a default.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "newsletters": { + "configuration_decision": "Enabled switch/capability; may be a default.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "face_recognition": { + "configuration_decision": "Enabled switch/capability; may be a default.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "custom_css": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Applied CSS observed after consent, without observing visitors." + }, + "oauth": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Successful admin SSO login; no account, identity-provider or session details." + }, + "smtp": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "A successful explicitly initiated admin SMTP test/send; no recipients or messages." + }, + "whatsapp": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Successful admin integration test; no recipient, message or delivery history." + }, + "backup": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "s3_storage": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "share_mounts": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "An admin initiated an accepted external-folder import; no scanned paths, files or counts." + }, + "galleries": { + "configuration_decision": "Built in; display a label, not a percentage.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "photo_management": { + "configuration_decision": "Built in; display a label, not a percentage.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "photo_exports": { + "configuration_decision": "Built in; display a label, not a percentage.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "photo_processing": { + "configuration_decision": "Built in; display a label, not a percentage.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "archive_management": { + "configuration_decision": "Built in; display a label, not a percentage.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "gallery_sharing": { + "configuration_decision": "Built in; display a label, not a percentage.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "short_links": { + "configuration_decision": "Built in; display a label, not a percentage.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "category_editing": { + "configuration_decision": "Built in; display a label, not a percentage.", + "use_decision": "An admin created, changed or deleted a category since consent. Seeded categories, reading and unchanged saves do not count. No names, memberships or identifiers are retained." + }, + "event_type_editing": { + "configuration_decision": "Built in; display a label, not a percentage.", + "use_decision": "An admin created, changed or deleted an event type since consent. Seeded presets, reading and unchanged saves do not count. No names, presets or identifiers are retained." + }, + "slideshow": { + "configuration_decision": "Enabled switch/capability; may be a default.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "transfers": { + "configuration_decision": "Enabled switch/capability; may be a default.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "video_uploads": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "At least one admin video file was successfully stored/accepted; no names, formats, lengths, sizes or processing/visitor history." + }, + "camera_raw_uploads": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "At least one admin camera RAW upload was stored/accepted; only the capability bit, no filename or metadata." + }, + "messaging": { + "configuration_decision": "Enabled switch/capability; may be a default.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "incoming_mail": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "A successful explicit admin connection test or non-skipped manual poll; no background intake, messages, attachments or counts." + }, + "reminder_emails": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Not collected. No visitor/customer activity inferred." + }, + "email_template_editing": { + "configuration_decision": "Built in; display a label, not a percentage.", + "use_decision": "An admin created a nonempty template or saved a real subject/body change since consent. Defaults, unchanged saves, previews and sending are excluded. This does not establish the current customization of templates edited before consent." + }, + "email_webhook": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Successful explicitly initiated admin send/test through the webhook transport; no recipients, messages or automatic deliveries." + }, + "accounting_incoming_invoices": { + "configuration_decision": "Enabled switch/capability; may be a default.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "accounting_expenses": { + "configuration_decision": "Enabled switch/capability; may be a default.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "accounting_tax_report": { + "configuration_decision": "Enabled switch/capability; may be a default.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "accounting_ledger": { + "configuration_decision": "Enabled switch/capability; may be a default.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "crm_installments": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "An admin saved an installment plan; no dates, amounts, currencies, payment status or document IDs." + }, + "document_templates": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "cms_content_editing": { + "configuration_decision": "Built in; display a label, not a percentage.", + "use_decision": "An admin saved a real change to an internal CMS page title or body since consent. Unchanged saves, external links, logos, seeded pages and page views do not count. This does not measure whether anyone read the page." + }, + "public_site": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Not collected. No visitor/customer activity inferred." + }, + "branding_editing": { + "configuration_decision": "Built in; display a label, not a percentage.", + "use_decision": "An admin changed branding settings, a theme or a logo since consent. Reading settings and unchanged saves do not count. A change can also restore a default; this is not a claim about the current design." + }, + "seo_editing": { + "configuration_decision": "Built in; display a label, not a percentage.", + "use_decision": "An admin saved a real change to an allowlisted SEO setting since consent. Defaults, reading and unchanged saves do not count; no rules, paths or search-engine activity are collected." + }, + "admin_management": { + "configuration_decision": "Enabled switch/capability; may be a default.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "api_integration": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Successful authenticated HTTP API capability call; only this bit, never URLs, request values, token/owner IDs or call counts. Does not trigger a report." + }, + "webhooks": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Successful explicit admin webhook test/replay; no automatic or visitor-triggered deliveries." + }, + "restore": { + "configuration_decision": "Built in; display a label, not a percentage.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "portable_backup": { + "configuration_decision": "Built in; display a label, not a percentage.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "database_backup": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "s3_photo_storage": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Successful admin media storage/accepted upload to S3; no buckets, objects or sizes." + }, + "s3_backups": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "An admin started a backup to the configured S3 destination or a successful S3 test upload; local exports never imply S3 use." + }, + "analytics_dashboard": { + "configuration_decision": "Enabled switch/capability; may be a default.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "feedback_moderation": { + "configuration_decision": "Built in; display a label, not a percentage.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "guest_management": { + "configuration_decision": "Built in; display a label, not a percentage.", + "use_decision": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + }, + "gallery_feedback_likes": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Not collected. No visitor/customer activity inferred." + }, + "gallery_feedback_ratings": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Not collected. No visitor/customer activity inferred." + }, + "gallery_feedback_comments": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Not collected. No visitor/customer activity inferred." + }, + "gallery_feedback_favorites": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Not collected. No visitor/customer activity inferred." + }, + "gallery_feedback_reactions": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Not collected. No visitor/customer activity inferred." + }, + "gallery_feedback_color_labels": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Not collected. No visitor/customer activity inferred." + }, + "gallery_guest_accounts": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Not collected. No visitor/customer activity inferred." + }, + "gallery_guest_uploads": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Not collected. No visitor/customer activity inferred." + }, + "gallery_downloads_restricted": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Not collected. No visitor/customer activity inferred." + }, + "download_resolution_picker": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Not collected. No visitor/customer activity inferred." + }, + "gallery_client_access": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Not collected. No visitor/customer activity inferred." + }, + "gallery_watermarks": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Not collected. No visitor/customer activity inferred." + }, + "gallery_image_protection": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Not collected. No visitor/customer activity inferred." + }, + "gallery_reveal": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Not collected. No visitor/customer activity inferred." + }, + "gallery_expiration": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Not collected. No visitor/customer activity inferred." + }, + "photo_xmp_export": { + "configuration_decision": "Built in; display a label, not a percentage.", + "use_decision": "An admin successfully generated an XMP export; no sidecars, filenames, ratings, selections or counts." + }, + "photo_replacement": { + "configuration_decision": "Built in; display a label, not a percentage.", + "use_decision": "An admin upload actually replaced a photo successfully; no filenames, matching values, IDs or counts." + }, + "photo_admin_marks": { + "configuration_decision": "Built in; display a label, not a percentage.", + "use_decision": "An admin successfully saved their own photo mark; no rating, color, photo or admin identity." + }, + "gallery_folders": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Not collected. No visitor/customer activity inferred." + }, + "transfer_upload_links": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Not collected. No visitor/customer activity inferred." + }, + "workflow_automation_enabled": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Not collected. No visitor/customer activity inferred." + }, + "s3_auto_import": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Not collected. No visitor/customer activity inferred." + }, + "crm_invoice_import": { + "configuration_decision": "Enabled switch/capability; may be a default.", + "use_decision": "An admin successfully imported an existing invoice; no PDF, invoice number, amount, currency, customer or payment status." + }, + "crm_combined_billing": { + "configuration_decision": "Enabled switch/capability; may be a default.", + "use_decision": "An admin successfully created a combined bill; no hours, expenses, customer, documents or financial values." + }, + "crm_monthly_billing_manual": { + "configuration_decision": "Enabled switch/capability; may be a default.", + "use_decision": "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": { + "configuration_decision": "Enabled switch/capability; may be a default.", + "use_decision": "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": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Not collected. No visitor/customer activity inferred." + }, + "download_original_filenames": { + "configuration_decision": "Technical configuration exists; may be a default, not activity.", + "use_decision": "Not collected. No visitor/customer activity inferred." + }, + "email_template_delivery": { + "configuration_decision": "Built in; display a label, not a percentage.", + "use_decision": "At least one real template email was accepted by SMTP or the configured mail webhook since consent, including background sends. Previews, test messages and template-free messages are excluded. Acceptance does not prove receipt or reading. No template key, recipient, contents, message identifier, send time or count is stored in usage markers." + } + } +} diff --git a/frontend/src/features/settings/UsageCatalog.tsx b/frontend/src/features/settings/UsageCatalog.tsx index 61ea6472..3615d4c2 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.v4.json'; +import catalog from './usageFeatures.v5.json'; /** Local, static disclosure: opening it never contacts the collector. */ export function UsageCatalog() { @@ -29,7 +29,7 @@ export function UsageCatalog() {{key} · {definition.since}
{t('productUsage.configuredLabel')}: {t(`productUsage.catalog.${key}.configured`)}
+{t(definition.configuration === 'builtin' ? 'productUsage.builtinLabel' : definition.configuration === 'flag' || definition.configuration === 'capability' ? 'productUsage.enabledLabel' : 'productUsage.configuredLabel')}: {t(`productUsage.catalog.${key}.configured`)}
{definition.used ? `${t('productUsage.usedLabel')}: ${t(`productUsage.catalog.${key}.used`)}` : t('productUsage.configurationOnly')}
diff --git a/frontend/src/features/settings/__tests__/ProductUsageTab.test.tsx b/frontend/src/features/settings/__tests__/ProductUsageTab.test.tsx index a6b677c4..8e8c39db 100644 --- a/frontend/src/features/settings/__tests__/ProductUsageTab.test.tsx +++ b/frontend/src/features/settings/__tests__/ProductUsageTab.test.tsx @@ -69,10 +69,10 @@ beforeEach(() => { }; }); afterEach(cleanup); -it('shows every v4 signal locally before participation, without collector calls', async () => { +it('shows every v5 signal locally before participation, without collector calls', async () => { mount(); await screen.findByText('productUsage.catalogTitle'); - expect(screen.getAllByRole('heading', { level: 4, hidden: true })).toHaveLength(87); + expect(screen.getAllByRole('heading', { level: 4, hidden: true })).toHaveLength(88); expect(service.enable).not.toHaveBeenCalled(); expect(service.preview).not.toHaveBeenCalled(); expect(service.upgradeConsent).not.toHaveBeenCalled(); diff --git a/frontend/src/features/settings/usageFeatures.v5.json b/frontend/src/features/settings/usageFeatures.v5.json new file mode 100644 index 00000000..61009cb2 --- /dev/null +++ b/frontend/src/features/settings/usageFeatures.v5.json @@ -0,0 +1,1313 @@ +{ + "schema_version": "usage.v5", + "consent_version": "usage-consent.v5", + "features": { + "crm": { + "category": "crm", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "clients", + "name": { + "en": "Client management" + }, + "configured": { + "en": "The clients capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "crm_quotes": { + "category": "crm", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "quotes", + "name": { + "en": "Quotes" + }, + "configured": { + "en": "The quotes capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "crm_invoices": { + "category": "crm", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "bills", + "name": { + "en": "Invoices" + }, + "configured": { + "en": "The bills capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "crm_contracts": { + "category": "crm", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "contracts", + "name": { + "en": "Contracts" + }, + "configured": { + "en": "The contracts capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "crm_projects": { + "category": "crm", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "projects", + "name": { + "en": "Projects" + }, + "configured": { + "en": "The projects capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "crm_calendar": { + "category": "crm", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "calendar", + "name": { + "en": "Admin calendar" + }, + "configured": { + "en": "The calendar capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "crm_hours": { + "category": "crm", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "hoursLogging", + "name": { + "en": "Hours logging" + }, + "configured": { + "en": "The hoursLogging capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "customer_portal": { + "category": "crm", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "customerPortal", + "name": { + "en": "Customer portal" + }, + "configured": { + "en": "The customerPortal capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "accounting": { + "category": "accounting", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "accounting", + "name": { + "en": "Accounting" + }, + "configured": { + "en": "The accounting capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "workflows": { + "category": "automation", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "workflows", + "name": { + "en": "Workflows" + }, + "configured": { + "en": "The workflows capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "newsletters": { + "category": "communication", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "newsletters", + "name": { + "en": "Newsletters" + }, + "configured": { + "en": "The newsletters capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "face_recognition": { + "category": "gallery", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "faces", + "name": { + "en": "ML face recognition" + }, + "configured": { + "en": "The faces capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "custom_css": { + "category": "appearance", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Custom CSS" + }, + "configured": { + "en": "Custom CSS is configured globally or applied through a gallery/theme/template; CSS text is not sent." + }, + "used": { + "en": "Applied CSS observed after consent, without observing visitors." + } + }, + "oauth": { + "category": "integration", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Admin SSO" + }, + "configured": { + "en": "Admin OIDC is enabled and issuer/client configuration is present; no provider or credential values." + }, + "used": { + "en": "Successful admin SSO login; no account, identity-provider or session details." + } + }, + "smtp": { + "category": "communication", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "SMTP delivery" + }, + "configured": { + "en": "An outgoing SMTP host is configured; no host, account, address or credentials." + }, + "used": { + "en": "A successful explicitly initiated admin SMTP test/send; no recipients or messages." + } + }, + "whatsapp": { + "category": "communication", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "WhatsApp integration" + }, + "configured": { + "en": "The WhatsApp capability is enabled and a usable configuration is present; no phone number, token or template." + }, + "used": { + "en": "Successful admin integration test; no recipient, message or delivery history." + } + }, + "backup": { + "category": "operations", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Backups" + }, + "configured": { + "en": "A full or database backup schedule is enabled; no schedule, path, storage sizes or backup names." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "s3_storage": { + "category": "integration", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "S3 storage" + }, + "configured": { + "en": "S3 is configured for media or backups; no bucket, endpoint, credentials or object keys." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "share_mounts": { + "category": "integration", + "since": "usage.v1", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "External folders" + }, + "configured": { + "en": "At least one gallery uses an external folder; only existence, no folder paths or gallery identifiers." + }, + "used": { + "en": "An admin initiated an accepted external-folder import; no scanned paths, files or counts." + } + }, + "galleries": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Gallery management" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "photo_management": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Media management" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "photo_exports": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Admin media export" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "photo_processing": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Media maintenance tools" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "archive_management": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Gallery archives" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "gallery_sharing": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Gallery sharing and QR" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "short_links": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Short links" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "category_editing": { + "category": "gallery", + "since": "usage.v5", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Category customization" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "An admin created, changed or deleted a category since consent. Seeded categories, reading and unchanged saves do not count. No names, memberships or identifiers are retained." + }, + "replaces": "gallery_categories" + }, + "event_type_editing": { + "category": "gallery", + "since": "usage.v5", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Event type customization" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "An admin created, changed or deleted an event type since consent. Seeded presets, reading and unchanged saves do not count. No names, presets or identifiers are retained." + }, + "replaces": "event_types" + }, + "slideshow": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "slideshow", + "name": { + "en": "Live slideshow" + }, + "configured": { + "en": "The slideshow capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "transfers": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "transfers", + "name": { + "en": "PicTransfer" + }, + "configured": { + "en": "The transfers capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "video_uploads": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Admin video uploads" + }, + "configured": { + "en": "Video extensions are allowed in global upload settings; no uploaded-file metadata." + }, + "used": { + "en": "At least one admin video file was successfully stored/accepted; no names, formats, lengths, sizes or processing/visitor history." + } + }, + "camera_raw_uploads": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Admin camera RAW uploads" + }, + "configured": { + "en": "Camera RAW (DNG) is allowed in global upload settings; no camera models or EXIF." + }, + "used": { + "en": "At least one admin camera RAW upload was stored/accepted; only the capability bit, no filename or metadata." + } + }, + "messaging": { + "category": "communication", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "messaging", + "name": { + "en": "Messaging tools" + }, + "configured": { + "en": "The messaging capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "incoming_mail": { + "category": "communication", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "IMAP intake" + }, + "configured": { + "en": "Incoming mail is enabled and an IMAP configuration is present; no mailbox, server, folders or credentials." + }, + "used": { + "en": "A successful explicit admin connection test or non-skipped manual poll; no background intake, messages, attachments or counts." + } + }, + "reminder_emails": { + "category": "communication", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "flag": "reminderEmails", + "name": { + "en": "Automatic event reminders" + }, + "configured": { + "en": "The reminderEmails capability switch is effectively enabled; only a boolean." + }, + "used": null + }, + "email_template_editing": { + "category": "communication", + "since": "usage.v5", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Email template customization" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "An admin created a nonempty template or saved a real subject/body change since consent. Defaults, unchanged saves, previews and sending are excluded. This does not establish the current customization of templates edited before consent." + }, + "replaces": "email_templates" + }, + "email_webhook": { + "category": "communication", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Email webhook transport" + }, + "configured": { + "en": "Both email webhook settings are present; no URL or secret." + }, + "used": { + "en": "Successful explicitly initiated admin send/test through the webhook transport; no recipients, messages or automatic deliveries." + } + }, + "accounting_incoming_invoices": { + "category": "accounting", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "incomingInvoices", + "name": { + "en": "Incoming invoices" + }, + "configured": { + "en": "The incomingInvoices capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "accounting_expenses": { + "category": "accounting", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "expenses", + "name": { + "en": "Expenses" + }, + "configured": { + "en": "The expenses capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "accounting_tax_report": { + "category": "accounting", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "taxReport", + "name": { + "en": "Tax reports" + }, + "configured": { + "en": "The taxReport capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "accounting_ledger": { + "category": "accounting", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "accounting", + "name": { + "en": "Ledger and accounting export" + }, + "configured": { + "en": "The accounting capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "crm_installments": { + "category": "crm", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Installment-plan tools" + }, + "configured": { + "en": "Quotes or invoices are enabled; no actual payment plans, amounts or statuses are inspected." + }, + "used": { + "en": "An admin saved an installment plan; no dates, amounts, currencies, payment status or document IDs." + } + }, + "document_templates": { + "category": "crm", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Document presets and blocks" + }, + "configured": { + "en": "Quotes or contracts are enabled, making document presets/blocks available; no template content." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "cms_content_editing": { + "category": "appearance", + "since": "usage.v5", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "CMS content editing" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "An admin saved a real change to an internal CMS page title or body since consent. Unchanged saves, external links, logos, seeded pages and page views do not count. This does not measure whether anyone read the page." + }, + "replaces": "cms" + }, + "public_site": { + "category": "appearance", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Public landing page" + }, + "configured": { + "en": "The public landing-page setting is enabled; no page HTML, texts, domains or visitors." + }, + "used": null + }, + "branding_editing": { + "category": "appearance", + "since": "usage.v5", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Branding customization" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "An admin changed branding settings, a theme or a logo since consent. Reading settings and unchanged saves do not count. A change can also restore a default; this is not a claim about the current design." + }, + "replaces": "branding" + }, + "seo_editing": { + "category": "appearance", + "since": "usage.v5", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "SEO customization" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "An admin saved a real change to an allowlisted SEO setting since consent. Defaults, reading and unchanged saves do not count; no rules, paths or search-engine activity are collected." + }, + "replaces": "seo_customization" + }, + "admin_management": { + "category": "operations", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "userManagement", + "name": { + "en": "Admin and role management" + }, + "configured": { + "en": "The userManagement capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "api_integration": { + "category": "integration", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "HTTP API integration" + }, + "configured": { + "en": "An unrevoked, unexpired API credential exists; no tokens, names, scopes or owner data." + }, + "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." + } + }, + "webhooks": { + "category": "integration", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Outbound webhooks" + }, + "configured": { + "en": "At least one active webhook is configured; no destinations, subscriptions, secrets or delivery logs." + }, + "used": { + "en": "Successful explicit admin webhook test/replay; no automatic or visitor-triggered deliveries." + } + }, + "restore": { + "category": "operations", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Restore" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "portable_backup": { + "category": "operations", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Portable PicPeak export/import" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "database_backup": { + "category": "operations", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "Database backups" + }, + "configured": { + "en": "Scheduled database backups are enabled; no schedules, file names or database contents." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "s3_photo_storage": { + "category": "integration", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "S3 media storage" + }, + "configured": { + "en": "S3 is the configured media backend and required credentials are present; no values are sent." + }, + "used": { + "en": "Successful admin media storage/accepted upload to S3; no buckets, objects or sizes." + } + }, + "s3_backups": { + "category": "integration", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "presence", + "name": { + "en": "S3 backup destination" + }, + "configured": { + "en": "The configured backup destination is S3 with a bucket present; no bucket or credentials." + }, + "used": { + "en": "An admin started a backup to the configured S3 destination or a successful S3 test upload; local exports never imply S3 use." + } + }, + "analytics_dashboard": { + "category": "operations", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "flag", + "flag": "analytics", + "name": { + "en": "Existing analytics module" + }, + "configured": { + "en": "The analytics capability switch is effectively enabled; only a boolean." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "feedback_moderation": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Feedback moderation" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "guest_management": { + "category": "gallery", + "since": "usage.v2", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Guest administration tools" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "A documented successful authenticated admin capability operation was observed since consent to this schema. No actor, operation history, parameters or counts." + } + }, + "gallery_feedback_likes": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery likes enabled" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "gallery_feedback_ratings": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery star ratings enabled" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "gallery_feedback_comments": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery comments enabled" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "gallery_feedback_favorites": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery favorites enabled" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "gallery_feedback_reactions": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery reactions enabled" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "gallery_feedback_color_labels": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery color labels enabled" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "gallery_guest_accounts": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Guest identities enabled" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "gallery_guest_uploads": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Guest uploads enabled" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "gallery_downloads_restricted": { + "category": "gallery_configuration", + "since": "usage.v4", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery downloads restricted" + }, + "configured": { + "en": "At least one gallery has downloads switched off; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "download_resolution_picker": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Download resolution picker enabled" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "gallery_client_access": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Client access enabled" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "gallery_watermarks": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Watermarks enabled" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "gallery_image_protection": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Image protection enabled" + }, + "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." + }, + "used": null + }, + "gallery_reveal": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery reveal enabled" + }, + "configured": { + "en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts." + }, + "used": null + }, + "gallery_expiration": { + "category": "gallery_configuration", + "since": "usage.v2", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery expiration configured" + }, + "configured": { + "en": "At least one gallery has an expiry configured; no dates, gallery IDs or counts." + }, + "used": null + }, + "photo_xmp_export": { + "category": "gallery", + "since": "usage.v3", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "XMP export" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "An admin successfully generated an XMP export; no sidecars, filenames, ratings, selections or counts." + } + }, + "photo_replacement": { + "category": "gallery", + "since": "usage.v3", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Photo replacement" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "An admin upload actually replaced a photo successfully; no filenames, matching values, IDs or counts." + } + }, + "photo_admin_marks": { + "category": "gallery", + "since": "usage.v3", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Photographer marks" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "An admin successfully saved their own photo mark; no rating, color, photo or admin identity." + } + }, + "gallery_folders": { + "category": "gallery_configuration", + "since": "usage.v3", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Gallery folders configured" + }, + "configured": { + "en": "An applicable global or gallery category is configured as a folder; no names, contents, counts or visitor activity." + }, + "used": null + }, + "transfer_upload_links": { + "category": "gallery_configuration", + "since": "usage.v3", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "PicTransfer upload links enabled" + }, + "configured": { + "en": "PicTransfer is enabled and a non-deleted transfer allows unexpired uploads; no links, tokens, dates, recipients or uploads." + }, + "used": null + }, + "workflow_automation_enabled": { + "category": "automation", + "since": "usage.v3", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Workflow automation enabled" + }, + "configured": { + "en": "The workflows module and at least one workflow are enabled; no names, graphs, triggers, decisions or runs." + }, + "used": null + }, + "s3_auto_import": { + "category": "integration", + "since": "usage.v3", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "S3 automatic import enabled" + }, + "configured": { + "en": "S3 media storage is configured and STORAGE_AUTO_IMPORT is enabled; no bucket, prefix, credentials, polling or imported objects." + }, + "used": null + }, + "crm_invoice_import": { + "category": "crm", + "since": "usage.v3", + "measurement": "configuration_and_use", + "configuration": "flag", + "name": { + "en": "Invoice import" + }, + "configured": { + "en": "The required product capabilities are effectively enabled; only a boolean." + }, + "used": { + "en": "An admin successfully imported an existing invoice; no PDF, invoice number, amount, currency, customer or payment status." + }, + "flag": "bills" + }, + "crm_combined_billing": { + "category": "crm", + "since": "usage.v3", + "measurement": "configuration_and_use", + "configuration": "capability", + "name": { + "en": "Combined billing" + }, + "configured": { + "en": "The required product capabilities are effectively enabled; only a boolean." + }, + "used": { + "en": "An admin successfully created a combined bill; no hours, expenses, customer, documents or financial values." + } + }, + "crm_monthly_billing_manual": { + "category": "crm", + "since": "usage.v3", + "measurement": "configuration_and_use", + "configuration": "flag", + "name": { + "en": "Manual monthly billing" + }, + "configured": { + "en": "The required product capabilities are effectively enabled; only a boolean." + }, + "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." + }, + "flag": "bills" + }, + "crm_document_conversion": { + "category": "crm", + "since": "usage.v3", + "measurement": "configuration_and_use", + "configuration": "capability", + "name": { + "en": "Document conversion" + }, + "configured": { + "en": "The required product capabilities are effectively enabled; only a boolean." + }, + "used": { + "en": "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": { + "category": "gallery_configuration", + "since": "usage.v3", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Capture-date sorting configured" + }, + "configured": { + "en": "A gallery defaults to sorting by capture date; no capture dates, EXIF or visitor sorting actions." + }, + "used": null + }, + "download_original_filenames": { + "category": "gallery_configuration", + "since": "usage.v3", + "measurement": "configuration", + "configuration": "configuration", + "name": { + "en": "Original download filenames enabled" + }, + "configured": { + "en": "The original-download-filenames switch is enabled; no filenames or downloads are read or sent." + }, + "used": null + }, + "email_template_delivery": { + "category": "integration", + "since": "usage.v5", + "measurement": "configuration_and_use", + "configuration": "builtin", + "name": { + "en": "Emails sent using templates" + }, + "configured": { + "en": "Built-in capability is available; this is not evidence of use." + }, + "used": { + "en": "At least one real template email was accepted by SMTP or the configured mail webhook since consent, including background sends. Previews, test messages and template-free messages are excluded. Acceptance does not prove receipt or reading. No template key, recipient, contents, message identifier, send time or count is stored in usage markers." + } + } + }, + "inventory": { + "galleries": { + "name": { + "en": "Stored galleries" + }, + "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." + } + }, + "photos": { + "name": { + "en": "Stored photo records" + }, + "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." + } + } + } +} diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 3b823905..2d38c015 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -1,17 +1,17 @@ { "productUsage": { - "fields": "usage.v4-Berichte enthalten einen Installationsfingerabdruck, die PicPeak-Version, UTC-Berichtsdatum und Erstellungszeit, Schema- und Signaturmetadaten, die Galerie-Layouts aus einer festen Liste, 86 Funktionssignale (63 Paare aus Konfiguriert und Genutzt, 23 reine Konfigurationswerte) sowie zwei Gesamtzahlen der Installation: gespeicherte Galerien und Fotoeinträge ohne Videos. Entwürfe, archivierte Galerien und deren erhaltene Fotoeinträge zählen mit. Der Katalog unten erklärt jedes Feld. Keine Aktionszähler, keine Beobachtung von Besuchern.", - "catalogTitle": "Vollständiger Katalog: 86 Funktionssignale und 2 Bestandszahlen (usage.v4)", + "fields": "usage.v5-Berichte enthalten einen Installationsfingerabdruck, die PicPeak-Version, UTC-Berichtsdatum und Erstellungszeit, Schema- und Signaturmetadaten, die Galerie-Layouts aus einer festen Liste, 87 Funktionssignale (64 Paare aus Konfiguriert und Genutzt, 23 reine Konfigurationswerte) sowie zwei Gesamtzahlen der Installation: gespeicherte Galerien und Fotoeinträge ohne Videos. Entwürfe, archivierte Galerien und deren erhaltene Fotoeinträge zählen mit. Der Katalog unten erklärt jedes Feld. Keine Aktionszähler, keine Beobachtung von Besuchern.", + "catalogTitle": "Vollständiger Katalog: 87 Funktionssignale und 2 Bestandszahlen (usage.v5)", "catalogExplanation": "„Konfiguriert“ beschreibt, ob eine Funktion technisch verfügbar oder eingerichtet ist. „Integriert“ heißt: immer verfügbar, ohne Nutzungsnachweis. „Genutzt“ ist ein einzelnes Ja/Nein für die ganze Installation seit der Zustimmung zu diesem Schema; ein angenommener Auftrag zählt als gestartet, nicht als abgeschlossen. Reine Konfigurationssignale haben kein Genutzt-Feld. Der Bestand enthält nur die aktuellen Gesamtzahlen der Galerie- und Fotoeinträge, ohne Aufschlüsselung nach Galerien. Nutzungsmarker speichern keine Personen, Objektkennungen, Zeitpunkte oder Häufigkeiten.", "catalogSearch": "Funktionsname oder Schlüssel suchen", "catalogEmpty": "Keine passenden Funktionen.", - "configuredLabel": "Konfiguriert", + "configuredLabel": "Verfügbarkeit / Konfiguration", "usedLabel": "Genutzt", "configurationOnly": "Nur Konfiguration, die tatsächliche Nutzung wird nicht erfasst.", - "versionDisclosure": "Diese Zustimmung gilt für usage.v4 / usage-consent.v4. Sie ersetzt die Frage „Erlaubt mindestens eine Galerie Downloads?“ durch „Hat mindestens eine Galerie Downloads abgeschaltet?“. Bestehende v1-, v2- und v3-Teilnahmen behalten ihren bisherigen Umfang, bis du sie ausdrücklich erweiterst. Identität und Rohhistorie bleiben erhalten, wartende Pakete werden vor dem Upgrade unverändert zugestellt. Lokale Nutzungsmarker beginnen erst nach der Bestätigung durch den Collector neu. Der erste v4-Bericht kann am nächsten aktiven UTC-Tag folgen. Das neue Signal wird vor der Bestätigung nicht erfasst.", + "versionDisclosure": "Diese Zustimmung gilt für usage.v5 / usage-consent.v5: 87 Funktionen und dieselben zwei Bestandszahlen. Neue Ja/Nein-Werte unterscheiden echte CMS-, Vorlagen-, Branding-, SEO-, Kategorie- und Ereignistyp-Änderungen sowie die Annahme echter Vorlagen-E-Mails durch den Mailtransport (auch im Hintergrund) von Standards, unverändertem Speichern, Vorschauen und Tests. Inhalte, Empfänger, Vorlagenkennungen, Aktionszeitpunkte und Häufigkeiten werden nicht erfasst. Die Beobachtung beginnt erst nach bestätigter Zustimmung; Nein bedeutet nicht, dass noch Standards verwendet werden. Frühere Versionen behalten bis zum Upgrade Bedeutung und Umfang. Wartende Pakete bleiben unverändert; Markierungen starten nach Bestätigung neu.", "currentSchema": "Aktuelles Berichtsschema: {{schema}}", - "reviewUpgrade": "Erweiterten Umfang von usage.v4 ansehen", - "upgrade": "usage.v4 ausdrücklich zustimmen", + "reviewUpgrade": "Erweiterten Umfang von usage.v5 ansehen", + "upgrade": "usage.v5 ausdrücklich zustimmen", "upgradeExplanation": "Deine bestehende Teilnahme behält ihren bisherigen Umfang. Sieh dir den erweiterten Katalog und die beiden Bestandszahlen an, bevor du dich entscheidest. Ablehnen beendet die Teilnahme nicht.", "upgradePending": "Die signierte Erweiterung wartet auf die Bestätigung des Collectors. Bis dahin wird nur der bisher bestätigte Umfang erfasst. Versuche es erneut, wenn der Collector erreichbar ist, oder deaktiviere die Teilnahme, um zu stoppen und zu löschen.", "catalog": { @@ -425,6 +425,41 @@ "gallery_downloads_restricted": { "name": "Galerie-Downloads eingeschränkt", "configured": "Mindestens eine Galerie hat Downloads abgeschaltet; nur installationsweite Existenz, niemals Galeriekennungen oder Anzahlen." + }, + "cms_content_editing": { + "name": "CMS-Inhalte bearbeiten", + "configured": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis.", + "used": "Ein Admin hat seit Zustimmung den Titel oder Inhalt einer internen CMS-Seite tatsächlich geändert. Unverändertes Speichern, externe Links, Logos, Standardseiten und Seitenaufrufe zählen nicht. Ob jemand die Seite gelesen hat, wird nicht gemessen." + }, + "email_template_editing": { + "name": "E-Mail-Vorlagen anpassen", + "configured": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis.", + "used": "Ein Admin hat seit Zustimmung eine nicht leere Vorlage erstellt oder Betreff/Inhalt tatsächlich geändert. Standardvorlagen, unverändertes Speichern, Vorschauen und Versand zählen nicht. Frühere Anpassungen lassen sich daraus nicht ableiten." + }, + "branding_editing": { + "name": "Branding anpassen", + "configured": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis.", + "used": "Ein Admin hat seit Zustimmung Branding-Einstellungen, ein Theme oder Logo geändert. Lesen und unverändertes Speichern zählen nicht. Auch das Wiederherstellen eines Standards kann eine Änderung sein; dies beschreibt nicht das aktuelle Design." + }, + "seo_editing": { + "name": "SEO anpassen", + "configured": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis.", + "used": "Ein Admin hat seit Zustimmung eine festgelegte SEO-Einstellung tatsächlich geändert. Standards, Lesen und unverändertes Speichern zählen nicht; Regeln, Pfade und Suchmaschinenaktivität werden nicht erfasst." + }, + "event_type_editing": { + "name": "Ereignistypen anpassen", + "configured": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis.", + "used": "Ein Admin hat seit Zustimmung einen Ereignistyp erstellt, geändert oder gelöscht. Standardvorlagen, Lesen und unverändertes Speichern zählen nicht. Namen, Vorlagen und Kennungen werden nicht gespeichert." + }, + "category_editing": { + "name": "Kategorien anpassen", + "configured": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis.", + "used": "Ein Admin hat seit Zustimmung eine Kategorie erstellt, geändert oder gelöscht. Standardkategorien, Lesen und unverändertes Speichern zählen nicht. Namen, Zuordnungen und Kennungen werden nicht gespeichert." + }, + "email_template_delivery": { + "name": "E-Mails mit Vorlagen versendet", + "configured": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis.", + "used": "Seit Zustimmung wurde mindestens eine echte Vorlagen-E-Mail vom SMTP-Transport oder Mail-Webhook angenommen, auch bei Hintergrundversand. Vorschauen, Testmails und Nachrichten ohne Vorlage zählen nicht. Dies belegt weder Empfang noch Lesen. Vorlagenkennung, Empfänger, Inhalte, Nachrichtenkennung, Versandzeit und Anzahl werden nicht in Nutzungsmarkierungen gespeichert." } }, "auditTitle": "Export- und Löschquittungen", @@ -522,7 +557,9 @@ "name": "Gespeicherte Fotoeinträge", "description": "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." } - } + }, + "builtinLabel": "Fest integriert — Verfügbarkeit bedeutet keine Nutzung", + "enabledLabel": "Aktiviert — kann ein Standard sein" }, "userManagement": { "title": "Benutzerverwaltung", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 3db9fcad..34353930 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -1,17 +1,17 @@ { "productUsage": { - "fields": "usage.v4 reports contain an installation fingerprint, PicPeak version, UTC report date and generation time, schema/signing metadata, controlled gallery layouts, 86 fixed capability signals (63 configured/used pairs and 23 configuration-only booleans), and two installation totals: stored galleries and photo records excluding videos. Drafts and archived galleries and their retained photo records are included. The catalog below defines every field. There are no action counts or visitor observations.", - "catalogTitle": "Full catalog: 86 capability signals and 2 inventory totals (usage.v4)", + "fields": "usage.v5 reports contain an installation fingerprint, PicPeak version, UTC report date and generation time, schema/signing metadata, controlled gallery layouts, 87 fixed capability signals (64 configured/used pairs and 23 configuration-only booleans), and two installation totals: stored galleries and photo records excluding videos. Drafts and archived galleries and their retained photo records are included. The catalog below defines every field. There are no action counts or visitor observations.", + "catalogTitle": "Full catalog: 87 capability signals and 2 inventory totals (usage.v5)", "catalogExplanation": "Configured describes current technical availability or configuration. Built-in means available, not used. Used is one installation-wide yes/no bit since consent to the reporting schema; accepted jobs mean initiated, not necessarily completed. Configuration-only capabilities omit used. Inventory contains only current gallery/photo record totals, with no per-gallery breakdown. No actor, entity identifier, action time or frequency is stored in usage markers.", "catalogSearch": "Search capability name or key", "catalogEmpty": "No matching capabilities.", - "configuredLabel": "Configured", + "configuredLabel": "Availability / configuration", "usedLabel": "Used", "configurationOnly": "Configuration only — actual use is not collected.", - "versionDisclosure": "This consent covers usage.v4 / usage-consent.v4. It replaces the question “does any gallery allow downloads?” with “does any gallery have downloads switched off?”. Existing v1/v2/v3 participants keep their exact previous scope until they explicitly upgrade. Identity and raw history remain; pending packets are delivered unchanged before an upgrade. Local usage markers restart only after confirmation. The first v4 report may be on the next active UTC day. The new signal is never collected before confirmation.", + "versionDisclosure": "This consent covers usage.v5 / usage-consent.v5: 87 capabilities and the same two inventory totals. New booleans distinguish real CMS/template/branding/SEO/category/event-type edits and actual template-email transport acceptance, including background sends, from defaults, unchanged saves, previews and tests. No content, recipient, template key, action timestamp or frequency is collected. Observations begin only after accepted consent; false does not mean an installation still uses defaults. Previous versions keep their meanings and scope until upgrade; queued packets stay unchanged and markers restart after confirmation.", "currentSchema": "Current reporting schema: {{schema}}", - "reviewUpgrade": "Review expanded usage.v4 scope", - "upgrade": "Explicitly agree to usage.v4", + "reviewUpgrade": "Review expanded usage.v5 scope", + "upgrade": "Explicitly agree to usage.v5", "upgradeExplanation": "Your existing participation keeps its current scope. Review the expanded catalog and the two inventory totals before deciding whether to upgrade. Declining does not end participation.", "upgradePending": "The signed consent upgrade is pending confirmation. Only the previously accepted scope is collected. Retry when the collector is available, or disable participation to stop and delete.", "catalog": { @@ -425,6 +425,41 @@ "gallery_downloads_restricted": { "name": "Gallery downloads restricted", "configured": "At least one gallery has downloads switched off; only existence across the installation, never gallery IDs or counts." + }, + "cms_content_editing": { + "name": "CMS content editing", + "configured": "Built-in capability is available; this is not evidence of use.", + "used": "An admin saved a real change to an internal CMS page title or body since consent. Unchanged saves, external links, logos, seeded pages and page views do not count. This does not measure whether anyone read the page." + }, + "email_template_editing": { + "name": "Email template customization", + "configured": "Built-in capability is available; this is not evidence of use.", + "used": "An admin created a nonempty template or saved a real subject/body change since consent. Defaults, unchanged saves, previews and sending are excluded. This does not establish the current customization of templates edited before consent." + }, + "branding_editing": { + "name": "Branding customization", + "configured": "Built-in capability is available; this is not evidence of use.", + "used": "An admin changed branding settings, a theme or a logo since consent. Reading settings and unchanged saves do not count. A change can also restore a default; this is not a claim about the current design." + }, + "seo_editing": { + "name": "SEO customization", + "configured": "Built-in capability is available; this is not evidence of use.", + "used": "An admin saved a real change to an allowlisted SEO setting since consent. Defaults, reading and unchanged saves do not count; no rules, paths or search-engine activity are collected." + }, + "event_type_editing": { + "name": "Event type customization", + "configured": "Built-in capability is available; this is not evidence of use.", + "used": "An admin created, changed or deleted an event type since consent. Seeded presets, reading and unchanged saves do not count. No names, presets or identifiers are retained." + }, + "category_editing": { + "name": "Category customization", + "configured": "Built-in capability is available; this is not evidence of use.", + "used": "An admin created, changed or deleted a category since consent. Seeded categories, reading and unchanged saves do not count. No names, memberships or identifiers are retained." + }, + "email_template_delivery": { + "name": "Emails sent using templates", + "configured": "Built-in capability is available; this is not evidence of use.", + "used": "At least one real template email was accepted by SMTP or the configured mail webhook since consent, including background sends. Previews, test messages and template-free messages are excluded. Acceptance does not prove receipt or reading. No template key, recipient, contents, message identifier, send time or count is stored in usage markers." } }, "auditTitle": "Export and deletion receipts", @@ -522,7 +557,9 @@ "name": "Stored photo records", "description": "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." } - } + }, + "builtinLabel": "Built in — availability is not use", + "enabledLabel": "Enabled — may be a default" }, "userManagement": { "title": "User Management", diff --git a/frontend/src/services/productUsage.service.ts b/frontend/src/services/productUsage.service.ts index 2ce0021f..7b0344cc 100644 --- a/frontend/src/services/productUsage.service.ts +++ b/frontend/src/services/productUsage.service.ts @@ -49,12 +49,12 @@ export const productUsageService = { async enable(): Promise