fix(usage): introduce consented v4 without changing historical reports

This commit is contained in:
Paul Nothaft
2026-09-06 21:34:04 +02:00
parent 3cc893126d
commit ef8a52f02c
21 changed files with 5046 additions and 197 deletions
+4 -4
View File
@@ -201,13 +201,13 @@ test('public/gallery paths and failed/unauthenticated admin operations never set
expect(JSON.stringify(service.markUsed.mock.calls)).not.toContain('42');
});
test('consent upgrade accepts exactly the explicit v2 choice, never extra fields', async () => {
for (const data of [{}, { consent_version: 'usage-consent.v1' }, { consent_version: 'usage-consent.v2', user: 'PRIVATE' }])
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' }])
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')}`)
.send({ consent_version: 'usage-consent.v2' }).expect(200);
expect(service.command).toHaveBeenCalledWith('consent', { consent_version: 'usage-consent.v2' });
.send({ consent_version }).expect(200);
expect(service.command).toHaveBeenCalledWith('consent', { consent_version });
});
test('only a backup that writes to the configured destination flags S3', () => {
@@ -1,8 +1,8 @@
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const catalog = require('../../src/usage/features.v3.json');
const inventory = require('../../../docs/usage-coverage.v3.json');
const catalog = require('../../src/usage/features.v4.json');
const inventory = require('../../../docs/usage-coverage.v4.json');
const protocol = require('../../src/usage/schema.cjs');
const { RULES_V2, capabilityKeys } = require('../../src/usage/capabilityRules');
const { acceptedUpload, capabilityEvidence } = require('../../src/usage/capabilityEvidence');
@@ -52,16 +52,22 @@ test('all current settings tabs have an explicit scope decision', () => {
}
});
test('v1 wire validation is immutable; catalog, UI and translated descriptions agree', () => {
test('v1/v2/v3 wire validation is immutable; v4 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(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).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.v3.json')))).toEqual(catalog);
expect(JSON.parse(fs.readFileSync(path.join(frontend, 'src/features/settings/usageFeatures.v4.json')))).toEqual(catalog);
for (const lang of ['en', 'de']) {
const translated = JSON.parse(fs.readFileSync(path.join(frontend, `src/i18n/locales/${lang}.json`))).productUsage.catalog;
for (const [key, value] of Object.entries(catalog.features)) {
@@ -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'])('%s receiver compatibility never loosens the PicPeak sender', version => {
describe.each(['usage.v1', 'usage.v2', 'usage.v3', 'usage.v4'])('%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: [],
...(version === 'usage.v3' ? { inventory: { galleries: 0, photos: 0 } } : {}),
...(['usage.v3', 'usage.v4'].includes(version) ? { inventory: { galleries: 0, photos: 0 } } : {}),
}, version);
const envelope = p.signPacket(packet, id, new Date(now));
expect(p.verifyEnvelope(envelope, now)).toEqual(packet);
+35 -64
View File
@@ -62,7 +62,7 @@ for (const engine of ['sqlite3', ...(process.env.PICPEAK_PG_TEST_URL ? ['pg'] :
expect(queries.filter(sql => /from ["`]photos["`]/.test(sql))).toEqual([expect.stringMatching(/select count\(\*\)/)]);
expect(JSON.stringify(report)).not.toContain('PRIVATE');
const identity = p.generateIdentity();
const envelope = p.signPacket(p.makePacket(identity, 'report', 1, report), identity, new Date(now));
const envelope = p.signPacket(p.makePacket(identity, 'report', 1, report, 'usage.v3'), identity, new Date(now));
expect(p.verifyEnvelope(envelope, now).payload).toEqual(report);
await db('photos').where({ id: 1 }).delete();
await db('events').where({ id: 1 }).delete();
@@ -126,69 +126,6 @@ for (const engine of ['sqlite3', ...(process.env.PICPEAK_PG_TEST_URL ? ['pg'] :
expect((await snap({})).gallery_folders.configured).toBe(false);
});
test('gallery_downloads_restricted counts galleries with downloads switched off, and v2 keeps its old key', async () => {
// allow_downloads ships true, so the v2 key was true on every install
// with a gallery. Only switching downloads off is a decision.
const snap = version => expandSnapshot(db, { features: p.emptyFeatures('usage.v1'), flags: {}, used: new Set(), now, version });
expect((await snap('usage.v3')).gallery_downloads_restricted).toEqual({ configured: false });
expect(await snap('usage.v3')).not.toHaveProperty('gallery_downloads');
await db('events').insert([{ allow_downloads: true }, { allow_downloads: true }]);
expect((await snap('usage.v3')).gallery_downloads_restricted.configured).toBe(false);
expect((await snap('usage.v2')).gallery_downloads).toEqual({ configured: true });
expect(await snap('usage.v2')).not.toHaveProperty('gallery_downloads_restricted');
await db('events').insert({ allow_downloads: false });
expect((await snap('usage.v3')).gallery_downloads_restricted.configured).toBe(true);
expect((await snap('usage.v2')).gallery_downloads.configured).toBe(true);
});
test('a report queued under the replaced catalog is rebuilt in place, keeping its packet id', async () => {
const identity = p.generateIdentity();
const posted = [];
const service = new UsageService(db, {
now: () => now, secret: 'v3-test-only-secret'.repeat(3), endpoint: 'http://127.0.0.1:9/',
fetch: async (_url, init) => { posted.push(JSON.parse(init.body).packet); throw new Error('collector unreachable'); },
});
service.binding = async () => 'b'.repeat(64);
const report = (features) => ({
picpeak_version: '1.0.0', report_date: '2026-09-05', generated_at: new Date(now).toISOString(),
features, gallery_layouts: [], inventory: { galleries: 0, photos: 0 },
});
// The v3 catalog as it stood before gallery_downloads_restricted replaced gallery_downloads.
const { gallery_downloads_restricted, ...rest } = p.emptyFeatures('usage.v3');
const stale = { ...rest, gallery_downloads: gallery_downloads_restricted };
const seed = (payload) => db('product_usage_state').where({ id: 1 }).update({
status: 'active', installation_id: identity.installation_id, public_key: identity.public_key,
private_key_encrypted: service.encrypt(identity.private_key), instance_binding: 'b'.repeat(64),
sequence: 1, last_error: null, attempts: 3, next_attempt_at: now + 60_000,
pending_packet: JSON.stringify(p.makePacket(identity, 'report', 2, payload, 'usage.v3')),
});
await seed(report(stale));
const queued = JSON.parse((await db('product_usage_state').where({ id: 1 }).first()).pending_packet);
await service.deliver(await db('product_usage_state').where({ id: 1 }).first());
// Sent once, under the current catalog, as the same packet.
expect(posted).toHaveLength(1);
expect(posted[0].packet_id).toBe(queued.packet_id);
expect(posted[0].sequence).toBe(2);
expect(posted[0].payload.features).toHaveProperty('gallery_downloads_restricted');
expect(posted[0].payload.features).not.toHaveProperty('gallery_downloads');
// The rebuilt packet is what stays queued for the ordinary retry path.
let row = await db('product_usage_state').where({ id: 1 }).first();
const retained = JSON.parse(row.pending_packet);
expect(retained.packet_id).toBe(queued.packet_id);
expect(retained.payload.features).toHaveProperty('gallery_downloads_restricted');
expect(row.status).toBe('active');
expect(row.last_error).toBe('DELIVERY_FAILED');
// Narrow: a report that still validates is sent as queued, payload untouched.
await seed(report(p.emptyFeatures('usage.v3')));
await service.deliver(await db('product_usage_state').where({ id: 1 }).first());
expect(posted).toHaveLength(2);
expect(posted[1].payload.report_date).toBe('2026-09-05');
row = await db('product_usage_state').where({ id: 1 }).first();
expect(JSON.parse(row.pending_packet).payload.report_date).toBe('2026-09-05');
});
test('ML recognition is already represented without querying faces or results', async () => {
await db('feature_flags').insert({ key: 'faces', value: true });
await client.markUsed(['face_recognition']);
@@ -197,5 +134,39 @@ for (const engine of ['sqlite3', ...(process.env.PICPEAK_PG_TEST_URL ? ['pg'] :
expect((await client.snapshot()).features.face_recognition).toEqual({ configured: false, used: true });
// No faces, people, embeddings or recognition-result tables exist in this fixture.
});
test.each([
[[], false, false], [[true], true, false], [[false], false, true],
[[true, false], true, true], [[null], false, false],
])('v4 measures explicit restrictions independently from legacy allowed downloads: %p', async (values, allowed, restricted) => {
if (values.length) await db('events').insert(values.map(allow_downloads => ({ allow_downloads })));
const queries = [];
db.on('query', q => queries.push(q));
for (const version of ['usage.v1', 'usage.v2', 'usage.v3', 'usage.v4']) {
await db('product_usage_state').where({ id: 1 }).update({ consent_version: p.CONSENT_VERSIONS[version] });
queries.length = 0;
const report = await client.preview();
const downloadQueries = queries.filter(q => /where ["`]allow_downloads["`] =/.test(q.sql));
if (version === 'usage.v4') {
expect(report.features.gallery_downloads_restricted).toEqual({ configured: restricted });
expect(report.features).not.toHaveProperty('gallery_downloads');
expect(report.inventory).toEqual({ galleries: values.length, photos: 0 });
expect(downloadQueries).toHaveLength(1);
expect(downloadQueries[0].sql).toMatch(/select 1 as present/);
expect(Number(downloadQueries[0].bindings[0])).toBe(0);
} else {
expect(report.features).not.toHaveProperty('gallery_downloads_restricted');
if (version === 'usage.v1') expect(downloadQueries).toHaveLength(0);
else {
expect(report.features.gallery_downloads).toEqual({ configured: allowed });
expect(downloadQueries).toHaveLength(1);
expect(Number(downloadQueries[0].bindings[0])).toBe(1);
}
}
const identity = p.generateIdentity();
const envelope = p.signPacket(p.makePacket(identity, 'report', 1, report, version), identity, new Date(now));
expect(p.verifyEnvelope(envelope, now).payload).toEqual(report);
}
});
});
}
+1 -1
View File
@@ -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'].includes(schemaForConsent(req.body.consent_version)))
if (!req.body || Object.keys(req.body).length !== 1 || !['usage.v2', 'usage.v3', 'usage.v4'].includes(schemaForConsent(req.body.consent_version)))
throw new ValidationError('Explicit usage consent is required');
res.json(await service.command('consent', { consent_version: req.body.consent_version }));
})
+12 -26
View File
@@ -539,6 +539,9 @@ class UsageService {
return value;
}
async deliver(state) {
// A lost receipt may mean this packet is already stored by the collector.
// Preserve its original schema, date and payload across binary upgrades;
// only re-sign transport metadata, never rebuild under the same packet ID.
const packet = JSON.parse(state.pending_packet);
if (
packet.action !== 'delete' &&
@@ -564,31 +567,14 @@ class UsageService {
});
return null;
}
const identity = {
public_key: state.public_key,
private_key: this.decrypt(state.private_key_encrypted)
};
let envelope;
try {
envelope = signPacket(packet, identity, new Date(this.now()));
} catch (error) {
// A report queued under a catalog this build no longer ships — the
// upgrade replaced a key in the same wire version — fails local
// validation before anything is sent, and retrying cannot repair it.
// Left as it was it blocked every operation behind it for good. A
// report's payload is derived state, so rebuild it from the current
// snapshot in place. The packet ID and sequence are kept: a re-signed
// retry must reuse them so a lost acknowledgement does not duplicate
// data. Reports only — a stale registration, deletion or command is a
// genuine conflict and keeps the handling below.
if (error.code !== 'INVALID_PACKET' || packet.action !== 'report') throw error;
packet.payload = await this.snapshot(packet.schema_version);
state.pending_packet = JSON.stringify(packet);
await this.db('product_usage_state')
.where({ id: 1, status: 'active' })
.update({ pending_packet: state.pending_packet });
envelope = signPacket(packet, identity, new Date(this.now()));
}
const envelope = signPacket(
packet,
{
public_key: state.public_key,
private_key: this.decrypt(state.private_key_encrypted)
},
new Date(this.now())
);
// Last check before anything leaves. The guard at the top of this
// method runs before the binding lookup above, which is asynchronous —
// so a withdrawal that COMPLETED during it would previously still have
@@ -974,7 +960,7 @@ class UsageService {
generated_at: now,
features: expanded,
gallery_layouts: [...layouts].sort(),
...(version === 'usage.v3' ? { inventory: await require('./inventorySnapshot').inventorySnapshot(this.db) } : {})
...(['usage.v3', 'usage.v4'].includes(version) ? { inventory: await require('./inventorySnapshot').inventorySnapshot(this.db) } : {})
};
}
+2 -7
View File
@@ -85,12 +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);
// allow_downloads ships true — column default and the create route both set
// it — so "at least one gallery allows downloads" is true on every install
// with a gallery and says nothing. v2 consented to that key under that
// description, so v2 keeps sending it unchanged. v3 asks the question that
// is actually a decision: has anyone switched downloads off.
if (version === 'usage.v3') {
if (version === 'usage.v4') {
result.gallery_downloads_restricted.configured = await exists('events', ['allow_downloads'], (query) =>
query.where('allow_downloads', formatBoolean(false)));
} else {
@@ -127,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 (version === 'usage.v3') {
if (['usage.v3', 'usage.v4'].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',
+6 -6
View File
@@ -1182,18 +1182,18 @@
},
"used": null
},
"gallery_downloads_restricted": {
"gallery_downloads": {
"category": "gallery_configuration",
"since": "usage.v3",
"since": "usage.v2",
"measurement": "configuration",
"configuration": "configuration",
"name": {
"en": "Gallery downloads restricted",
"de": "Galerie-Downloads eingeschränkt"
"en": "Gallery downloads allowed",
"de": "Galerie-Downloads erlaubt"
},
"configured": {
"en": "At least one gallery has downloads switched off; only existence across the installation, never gallery IDs or counts.",
"de": "Mindestens eine Galerie hat Downloads abgeschaltet; nur installationsweite Existenz, niemals Galeriekennungen oder Anzahlen."
"en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts.",
"de": "In der betreffenden Galerie-/Globalkonfiguration aktiviert; nur installationsweite Existenz, niemals Galeriekennungen oder Anzahlen."
},
"used": null
},
File diff suppressed because it is too large Load Diff
+10 -6
View File
@@ -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.v3.json");
const CATALOGS = { "usage.v2": require("./features.v2.json"), "usage.v3": CATALOG };
const CONSENT_VERSIONS = { "usage.v1": "usage-consent.v1", "usage.v2": "usage-consent.v2", "usage.v3": "usage-consent.v3" };
const CURRENT_SCHEMA_VERSION = "usage.v3";
const 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 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);
@@ -19,6 +19,10 @@ const LEGACY_FEATURE_KEYS = [
"whatsapp", "backup", "s3_storage", "share_mounts",
];
const FEATURE_KEYS = Object.keys(CATALOG.features);
// Historical questions remain independently visible after a newer schema
// stops asking them. Never rename or invert a retained report's values.
const ALL_FEATURES = Object.assign({}, ...Object.values(CATALOGS).map(c => c.features));
const ALL_FEATURE_KEYS = Object.keys(ALL_FEATURES);
const LAYOUTS = ["grid", "masonry", "carousel", "timeline", "mosaic", "gallery-premium", "gallery-story", "other"];
const object = (properties, required = Object.keys(properties)) => ({
type: "object", additionalProperties: false, properties, required,
@@ -45,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 } },
...(version === "usage.v3" ? { inventory: object(Object.fromEntries(INVENTORY_KEYS.map(key => [key,
...(["usage.v3", "usage.v4"].includes(version) ? { inventory: object(Object.fromEntries(INVENTORY_KEYS.map(key => [key,
{ type: "integer", minimum: 0, maximum: MAX_INVENTORY_COUNT }
]))) } : {}),
});
@@ -109,7 +113,7 @@ const ingressEnvelopeSchemas = Object.fromEntries(Object.entries(envelopeSchemas
return [version, schema];
}));
module.exports = {
FEATURE_KEYS, LEGACY_FEATURE_KEYS, LAYOUTS, CATALOG, CATALOGS, CONSENT_VERSIONS,
FEATURE_KEYS, ALL_FEATURES, ALL_FEATURE_KEYS, LEGACY_FEATURE_KEYS, LAYOUTS, CATALOG, CATALOGS, CONSENT_VERSIONS,
schemaForConsent, schemaRank, INVENTORY_KEYS, MAX_INVENTORY_COUNT, CURRENT_SCHEMA_VERSION,
CURRENT_CONSENT_VERSION, featureKeysFor, observesUse, emptyFeatures,
envelopeSchema, envelopeSchemas, ingressEnvelopeSchemas, payloads, payloadsByVersion,
+36 -37
View File
@@ -1,19 +1,21 @@
# Product-usage coverage: usage.v3
# Product-usage coverage: usage.v4
Reviewed PicPeak baseline: a5ff9264 (3.124.1-beta.0), plus the usage integration.
The inventory covers 81 backend route families (80 product families plus usage),
all 26 feature flags and all current settings tabs. This is capability coverage,
not instrumentation of every UI field. Source of truth: `usage-coverage.v3.json`.
The prior `usage-coverage.v2.json` and v2 wire catalog remain available unchanged.
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.
## Data scope
There are 86 capabilities: the original 19 in v1, 54 added in v2, and 13 added in
v3. 63 have configured/used booleans; 23 are configuration-only and omit `used`.
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.
v3 additionally reports exactly two installation-wide integers under `inventory`:
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.
@@ -36,25 +38,22 @@ records can include guest uploads without observing individual upload actions.
## Consent and version transition
- v1 and v2 keep their exact wire schemas and feature allowlists. Updating code
does not grant consent or collect v3 markers/inventory for an older participant.
- The local EN/DE dialog lists all 86 capabilities and both inventory definitions.
An unchecked checkbox requires explicit consent to `usage-consent.v3`.
- A signed v3 consent command upgrades v1 or v2 without changing identity/history.
Prior queued operations finish first. Only a matching accepted receipt changes
local consent and atomically clears previous local usage markers. Lost receipts
remain retryable; a withdrawal always wins over a late upgrade receipt.
- Consent cannot downgrade. Older clients may continue sending their already
consented older report schema. Old reports retain their original raw envelopes.
- Collector first, client second. Older collectors reject v3 rather than accepting
undisclosed fields. No second report on the same UTC day; the first v3 report
may be on the next day of admin activity.
- Summary/history count the latest report per reporter (per period for history).
Missing older fields are unknown. Feature denominators use only supplied fields.
Inventory has `{ total, reported }` per key; zero with `reported=0` means unknown,
while zero with a positive denominator is a reported empty inventory. Never sum
every daily report as if it were a different installation. Opt-out removes
current and historical contributions, including these totals.
- 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 reported capability
@@ -129,7 +128,7 @@ Definitions are shipped byte-identically in both applications as
| `gallery_feedback_color_labels` — Gallery color labels enabled / Galerie-Farblabels aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | Not collected: configuration only. |
| `gallery_guest_accounts` — Guest identities enabled / Gastidentitäten aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | Not collected: configuration only. |
| `gallery_guest_uploads` — Guest uploads enabled / Gast-Uploads aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | Not collected: configuration only. |
| `gallery_downloads_restricted` — Gallery downloads restricted / Galerie-Downloads eingeschränkt | usage.v3 | At least one gallery has downloads switched off; only existence across the installation, never gallery IDs or counts. Replaces v2's `gallery_downloads`, which was true on every installation with a gallery because downloads ship enabled. | Not collected: configuration only. |
| `gallery_downloads_restricted` — Gallery downloads restricted / Galerie-Downloads eingeschränkt | 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 / Download-Auflösungswahl aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | Not collected: configuration only. |
| `gallery_client_access` — Client access enabled / Client-Zugang aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | Not collected: configuration only. |
| `gallery_watermarks` — Watermarks enabled / Wasserzeichen aktiviert | usage.v2 | Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts. | Not collected: configuration only. |
@@ -167,11 +166,11 @@ Definitions are shipped byte-identically in both applications as
| `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.v3.json; configuration-only signals never observe the public surface. |
| `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.v3.json; configuration-only signals never observe the public surface. |
| `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.v3.json; configuration-only signals never observe the public surface. |
| `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. |
@@ -179,7 +178,7 @@ Definitions are shipped byte-identically in both applications as
| `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, and replaces gallery_downloads with gallery_downloads_restricted (downloads ship enabled, so only switching them off is a decision). Exact definitions are in features.v3.json; configuration-only signals never observe the public surface. |
| `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. |
@@ -195,30 +194,30 @@ Definitions are shipped byte-identically in both applications as
| `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.v3.json; configuration-only signals never observe the public surface. |
| `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.v3.json; configuration-only signals never observe the public surface. |
| `adminPhotos.js` | partial: `photo_management`, `photo_exports`, `photo_processing`, `video_uploads`, `camera_raw_uploads`, `s3_storage`, `s3_photo_storage`, `photo_replacement`, `photo_admin_marks` | Successful admin edits/exports and accepted upload evidence only. Chunk init/status, failed uploads and public downloads excluded. Only video/RAW/S3 booleans survive, never file metadata/EXIF/content. v3 adds only: photo_replacement, photo_admin_marks. Exact definitions are in features.v3.json; configuration-only signals never observe the public surface. |
| `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.v3.json; configuration-only signals never observe the public surface. |
| `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.v3.json; configuration-only signals never observe the public surface. |
| `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.v3.json; configuration-only signals never observe the public surface. |
| `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.v3.json; configuration-only signals never observe the public surface. |
| `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. |
@@ -247,6 +246,6 @@ Definitions are shipped byte-identically in both applications as
- 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.
- 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
+14 -9
View File
@@ -1,23 +1,28 @@
# Optional product usage and feedback (#1110)
Current scope: **usage.v3**. The expanded catalog contains 86 capabilities
Current scope: **usage.v4**. v4 replaces `gallery_downloads` with
`gallery_downloads_restricted` after explicit `usage-consent.v4` consent.
All v1/v2/v3 schemas and queued packets remain immutable. Historical views
keep both questions separate; neither can be inferred by inverting the other.
The inventory and other capabilities are unchanged from v3. The expanded catalog contains 86 capabilities
(including ML face recognition and invoice import) and exactly two inventory
totals: stored gallery records and non-video photo records, including drafts
and retained archive records. No content, identifiers, per-gallery breakdowns,
biometric results, financial values or visitor actions.
Existing v1/v2 participants retain their previous scope until explicit signed
v3 consent is confirmed. New count queries and markers do not run before that
confirmation. Collector must be deployed first. v1/v2 wire schemas and raw
Existing v1/v2/v3 participants retain their previous scope until explicit signed
v4 consent is confirmed. The new restriction query does not run before that
confirmation. Collector must be deployed first. v1/v2/v3 wire schemas and raw
history remain unchanged. See [current coverage](FEATURE_COVERAGE.md) for all
definitions and [v3 inventory](usage-coverage.v3.json) for code boundaries.
definitions and [v4 inventory](usage-coverage.v4.json) for code boundaries.
The sections below also document the historical v1/v2 implementation. Any
statements excluding all gallery/photo counts describe those earlier versions;
v3 adds only the two installation totals above.
Backward compatibility is required for future changes. The collector continues
to accept v1/v2/v3 reports, including omitted or null measurements, using their
to accept v1/v2/v3/v4 reports, including omitted or null measurements, using their
declared schema and original reporting day. Missing values remain unknown in
aggregates and histories. PicPeak still emits complete reports through the
unchanged sender schemas; only reception is more tolerant. Consent, field
@@ -205,8 +210,8 @@ Public voting uses a backend-authorized 15-minute session, never the lookup hash
## Contract
The closed v1/v2 schemas are in `backend/src/usage/schema.cjs`, with signing in
`protocol.cjs`. Keep these and `features.v2.json` byte-identical to the collector's `protocol/` copies.
The closed v1/v2/v3/v4 schemas are in `backend/src/usage/schema.cjs`, with signing in
`protocol.cjs`. Keep these and all versioned `features.v*.json` catalogs byte-identical to the collector's `protocol/` copies.
The collector serves its schema and complete source archive publicly. Aggregate
projections and the complete dataset are accessible to participating
installations only; raw reports require the installation's confidential lookup
@@ -224,7 +229,7 @@ cursor. Deletion removes the source publication; operators must also remove
any externally copied content and follow the documented backup/log policies.
Used flags represent successful allowlisted admin capability calls since
consent to the current schema (v1: joining; v2: joining or explicit upgrade),
consent to the current schema (v1: joining; v2/v3/v4: joining or explicit upgrade),
not visitor behavior or counts. OAuth marks successful admin SSO;
applied CSS is observed during report generation. Gallery layouts are controlled
enums extracted from event themes without IDs or counts. Other signals use the
+3 -3
View File
@@ -22,7 +22,7 @@
"signals": [
"galleries",
"gallery_guest_uploads",
"gallery_downloads_restricted",
"gallery_downloads",
"gallery_client_access",
"gallery_watermarks",
"gallery_image_protection",
@@ -569,7 +569,7 @@
"signals": [
"galleries",
"gallery_guest_uploads",
"gallery_downloads_restricted",
"gallery_downloads",
"gallery_client_access",
"gallery_watermarks",
"gallery_reveal",
@@ -1774,7 +1774,7 @@
"gallery_feedback_color_labels",
"gallery_guest_accounts",
"gallery_guest_uploads",
"gallery_downloads_restricted",
"gallery_downloads",
"download_resolution_picker",
"gallery_client_access",
"gallery_watermarks",
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,6 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import catalog from './usageFeatures.v3.json';
import catalog from './usageFeatures.v4.json';
/** Local, static disclosure: opening it never contacts the collector. */
export function UsageCatalog() {
@@ -69,7 +69,7 @@ beforeEach(() => {
};
});
afterEach(cleanup);
it('shows every v2 signal locally before participation, without collector calls', async () => {
it('shows every v4 signal locally before participation, without collector calls', async () => {
mount();
await screen.findByText('productUsage.catalogTitle');
expect(screen.getAllByRole('heading', { level: 4, hidden: true })).toHaveLength(87);
@@ -77,8 +77,8 @@ it('shows every v2 signal locally before participation, without collector calls'
expect(service.preview).not.toHaveBeenCalled();
expect(service.upgradeConsent).not.toHaveBeenCalled();
});
it('existing v1 requires renewed unchecked consent; cancellation keeps v1 unchanged', async () => {
vi.mocked(service.status).mockResolvedValue({ ...status, status: 'active', consent_update_available: true });
it.each(['usage.v1', 'usage.v2', 'usage.v3'])('existing %s requires renewed unchecked consent; cancellation keeps its scope unchanged', async (schema_version) => {
vi.mocked(service.status).mockResolvedValue({ ...status, status: 'active', schema_version, consent_update_available: true });
vi.mocked(service.upgradeConsent).mockResolvedValue({ delivered: false, queued: true, state: { ...status, status: 'active', pending_action: 'consent' } });
mount();
fireEvent.click(await screen.findByText('productUsage.reviewUpgrade'));
@@ -1182,18 +1182,18 @@
},
"used": null
},
"gallery_downloads_restricted": {
"gallery_downloads": {
"category": "gallery_configuration",
"since": "usage.v3",
"since": "usage.v2",
"measurement": "configuration",
"configuration": "configuration",
"name": {
"en": "Gallery downloads restricted",
"de": "Galerie-Downloads eingeschränkt"
"en": "Gallery downloads allowed",
"de": "Galerie-Downloads erlaubt"
},
"configured": {
"en": "At least one gallery has downloads switched off; only existence across the installation, never gallery IDs or counts.",
"de": "Mindestens eine Galerie hat Downloads abgeschaltet; nur installationsweite Existenz, niemals Galeriekennungen oder Anzahlen."
"en": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts.",
"de": "In der betreffenden Galerie-/Globalkonfiguration aktiviert; nur installationsweite Existenz, niemals Galeriekennungen oder Anzahlen."
},
"used": null
},
File diff suppressed because it is too large Load Diff
+12 -8
View File
@@ -1,17 +1,17 @@
{
"productUsage": {
"fields": "usage.v3-Berichte enthalten einen Installationsfingerabdruck, PicPeak-Version, UTC-Berichtsdatum und Erstellungszeit, Schema-/Signaturmetadaten, feste Galerie-Layouts, 86 Funktionssignale (63 Konfiguriert/Genutzt-Paare und 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 Aktionsanzahlen oder Besucherbeobachtung.",
"catalogTitle": "Vollständiger Katalog: 86 Funktionssignale und 2 Bestandszahlen (usage.v3)",
"fields": "usage.v4-Berichte enthalten einen Installationsfingerabdruck, PicPeak-Version, UTC-Berichtsdatum und Erstellungszeit, Schema-/Signaturmetadaten, feste Galerie-Layouts, 86 Funktionssignale (63 Konfiguriert/Genutzt-Paare und 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 Aktionsanzahlen oder Besucherbeobachtung.",
"catalogTitle": "Vollständiger Katalog: 86 Funktionssignale und 2 Bestandszahlen (usage.v4)",
"catalogExplanation": "Konfiguriert beschreibt die aktuelle technische Verfügbarkeit oder Einrichtung. Integriert bedeutet verfügbar, nicht genutzt. Genutzt ist ein installationsweites Ja/Nein seit Zustimmung zum Berichtsschema; angenommene Aufträge gelten als gestartet, nicht zwingend abgeschlossen. Reine Konfigurationssignale enthalten kein Genutzt-Feld. Der Bestand enthält nur aktuelle Gesamtzahlen der Galerie-/Fotoeinträge, ohne Aufschlüsselung nach Galerien. Nutzungsmarker speichern keine Personen, Objektkennungen, Aktionszeiten oder Häufigkeiten.",
"catalogSearch": "Funktionsname oder Schlüssel suchen",
"catalogEmpty": "Keine passenden Funktionen.",
"configuredLabel": "Konfiguriert",
"usedLabel": "Genutzt",
"configurationOnly": "Nur Konfiguration — tatsächliche Nutzung wird nicht erfasst.",
"versionDisclosure": "Diese Zustimmung gilt für usage.v3 / usage-consent.v3. Bestehende v1- und v2-Teilnahmen behalten ihre bisherigen 19 bzw. 73 Fähigkeiten ohne Bestandszahlen bis zur ausdrücklichen Erweiterung. Identität und Rohhistorie bleiben erhalten; lokale Nutzungsmarker beginnen erst nach Bestätigung durch den Collector neu. Höchstens ein Bericht pro UTC-Tag wird angenommen, der erste v3-Bericht kann daher am nächsten aktiven Tag folgen. Neue Marker und Bestandszahlen werden vor Bestätigung 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/v3-Teilnahmen behalten ihren bisherigen Umfang bis zur ausdrücklichen Erweiterung. Identität und Rohhistorie bleiben erhalten; wartende Pakete werden vor dem Upgrade unverändert zugestellt. Lokale Nutzungsmarker beginnen erst nach Bestätigung neu. Der erste v4-Bericht kann am nächsten aktiven UTC-Tag folgen. Das neue Signal wird vor Bestätigung nicht erfasst.",
"currentSchema": "Aktuelles Berichtsschema: {{schema}}",
"reviewUpgrade": "Erweiterten Umfang von usage.v3 prüfen",
"upgrade": "usage.v3 ausdrücklich zustimmen",
"reviewUpgrade": "Erweiterten Umfang von usage.v4 prüfen",
"upgrade": "usage.v4 ausdrücklich zustimmen",
"upgradeExplanation": "Deine bestehende Teilnahme behält ihren bisherigen Umfang. Prüfe den erweiterten Katalog und die beiden Bestandszahlen, bevor du dich entscheidest. Ablehnen beendet die Teilnahme nicht.",
"upgradePending": "Die signierte Erweiterung wartet auf Bestätigung. Es wird nur der bisher bestätigte Umfang erfasst. Wiederhole den Versuch, wenn der Collector erreichbar ist, oder deaktiviere die Teilnahme zum Stoppen und Löschen.",
"catalog": {
@@ -335,9 +335,9 @@
"name": "Gast-Uploads aktiviert",
"configured": "In der betreffenden Galerie-/Globalkonfiguration aktiviert; nur installationsweite Existenz, niemals Galeriekennungen oder Anzahlen."
},
"gallery_downloads_restricted": {
"name": "Galerie-Downloads eingeschränkt",
"configured": "Mindestens eine Galerie hat Downloads abgeschaltet; nur installationsweite Existenz, niemals Galeriekennungen oder Anzahlen."
"gallery_downloads": {
"name": "Galerie-Downloads erlaubt",
"configured": "In der betreffenden Galerie-/Globalkonfiguration aktiviert; nur installationsweite Existenz, niemals Galeriekennungen oder Anzahlen."
},
"download_resolution_picker": {
"name": "Download-Auflösungswahl aktiviert",
@@ -421,6 +421,10 @@
"download_original_filenames": {
"name": "Originaldateinamen für Downloads aktiviert",
"configured": "Der Schalter für Originaldateinamen beim Download ist aktiviert; keine Dateinamen oder Downloads werden gelesen oder gesendet."
},
"gallery_downloads_restricted": {
"name": "Galerie-Downloads eingeschränkt",
"configured": "Mindestens eine Galerie hat Downloads abgeschaltet; nur installationsweite Existenz, niemals Galeriekennungen oder Anzahlen."
}
},
"auditTitle": "Export- und Löschquittungen",
+12 -8
View File
@@ -1,17 +1,17 @@
{
"productUsage": {
"fields": "usage.v3 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.v3)",
"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)",
"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",
"usedLabel": "Used",
"configurationOnly": "Configuration only — actual use is not collected.",
"versionDisclosure": "This consent covers usage.v3 / usage-consent.v3. Existing v1 and v2 participants retain their previous 19 or 73 capabilities without inventory totals until they explicitly upgrade. Identity and raw history remain; local usage markers restart only after the collector confirms the upgrade. At most one report per UTC day is accepted, so the first v3 report may be on the next active day. New markers and totals are not collected before confirmation.",
"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.",
"currentSchema": "Current reporting schema: {{schema}}",
"reviewUpgrade": "Review expanded usage.v3 scope",
"upgrade": "Explicitly agree to usage.v3",
"reviewUpgrade": "Review expanded usage.v4 scope",
"upgrade": "Explicitly agree to usage.v4",
"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": {
@@ -335,9 +335,9 @@
"name": "Guest uploads enabled",
"configured": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts."
},
"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."
"gallery_downloads": {
"name": "Gallery downloads allowed",
"configured": "Enabled in applicable gallery/global configuration; only existence across the installation, never gallery IDs or counts."
},
"download_resolution_picker": {
"name": "Download resolution picker enabled",
@@ -421,6 +421,10 @@
"download_original_filenames": {
"name": "Original download filenames enabled",
"configured": "The original-download-filenames switch is enabled; no filenames or downloads are read or sent."
},
"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."
}
},
"auditTitle": "Export and deletion receipts",
@@ -49,12 +49,12 @@ export const productUsageService = {
async enable(): Promise<UsageStatus> {
return (
await api.post('/admin/usage/enable', {
consent_version: 'usage-consent.v3'
consent_version: 'usage-consent.v4'
})
).data;
},
async upgradeConsent(): Promise<{ delivered: boolean; queued: boolean; state: UsageStatus }> {
return (await api.post('/admin/usage/consent', { consent_version: 'usage-consent.v3' })).data;
return (await api.post('/admin/usage/consent', { consent_version: 'usage-consent.v4' })).data;
},
async disable(): Promise<UsageStatus> {
return (await api.post('/admin/usage/disable')).data;