Skip to content

Commit ce34775

Browse files
authored
fix: Datenblattlinks fuer 35 Katalogeintraege, Moderationsstand im Upload (#151)
Belegt (Herstellerseite/-PDF des Modells, geoeffnet und Modell darauf gefunden): Angenieux Optimo 24-290/28-76/15-40/17-80 und alle zwoelf Optimo Primes, Leica SL 24-70/24-90/90-280/50, Canon CJ12ex4.3B/ CJ17ex6.2B/CJ45ex13.6B/HJ17ex6.2B, CR-N500/CR-N300 (Objektiv und CR-N300 weiss), Sony BRC-X400/SRG-X120/BRC-H800/SRG-XB25, AIDA UHD-100A/ PTZ4K-NDI-X30, Viltrox AF 24/1.8 FE und AF 50/1.8 FE. An den Belegen korrigiert: HJ17ex6.2B 6.2-106 mm (stand 105), CJ12ex4.3B 4.3-52 mm (stand 51.6), Optimo Prime 18 mm heisst T2 und 200 mm T2.2 (die Blendenwerte standen schon so, der Name sagte T1.8). Offen, kein Herstellerbeleg: Panasonic VariCam AJ-HDC27, Canon HJ11ex4.7B, Sony FR7 (Platzhalter ohne Objektiv), Lomo Anamorphic 35/50/75/100. Client aus av-device-library b3b259e: `moderation` je Upload-Ergebnis; Anzeige danach, der automatische Lauf schickt Wartendes erneut mit.
1 parent 2295d7f commit ce34775

9 files changed

Lines changed: 103 additions & 55 deletions

File tree

‎README.md‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,12 @@ MultiCam Planner is designed for quick, intuitive camera planning with essential
8989
next version instead of a second device. *Upload own devices
9090
automatically* (Settings, on by default) uploads on start and a few seconds
9191
after a change; *Sync now* uploads everything. Unchanged entries (by hash of
92-
what was sent last) are not re-sent automatically. Under the selector every
93-
own entry shows its last result — waiting for moderation, live, in sync,
94-
blocked (with the reason, e.g. *datasheet link missing*) or failed — and
92+
what was sent last) are not re-sent automatically — except those still
93+
waiting for moderation: the server reports `moderation: pending | approved`
94+
with every result (also `in-sync`), so the status turns *live* once a
95+
moderator approves. Under the selector every own entry shows its last
96+
result — waiting for moderation, live, blocked (with the reason, e.g.
97+
*datasheet link missing*) or failed — and
9598
*Upload…*, which asks for the datasheet link, stores it on the entry
9699
(`manufacturerUrl`) and uploads at once. Not signed in, it leads to the
97100
sign-in. Changed community guidelines (`guidelines-outdated`) have to be

‎src/__tests__/deviceLibrary.test.ts‎

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ describe('store against a mocked server', () => {
224224
fetchMock
225225
.mockResolvedValueOnce(json({ user: { id: 'u', email: 'a@b.de', username: 'l' } }, { 'set-auth-token': 'T' }))
226226
.mockResolvedValueOnce(json({ planner: 'multicam', results: [
227-
{ localId: eigeneKamera.id, state: 'edit-proposed', slug: 'sony-pxw-fx9' },
227+
{ localId: eigeneKamera.id, state: 'edit-proposed', slug: 'sony-pxw-fx9', moderation: 'pending' },
228228
{ localId: 'custom-lens-1', state: 'blocked', findings: [{ kind: 'no-source', blocking: true }] },
229229
] }))
230230
.mockResolvedValueOnce(json(antwort(0, [])));
@@ -247,19 +247,27 @@ describe('store against a mocked server', () => {
247247
expect(r['custom-lens-1']).toMatchObject({ state: 'blocked', findings: ['no-source'] });
248248
expect(JSON.parse(speicher['multicam-device-library-uploads']).records[eigeneKamera.id].state).toBe('edit-proposed');
249249

250-
// … unveraendert geht automatisch nichts erneut hoch, nur der Abgleich:
250+
// … automatisch geht unveraendert nur noch mit, was in der Moderation
251+
// wartet (die Kamera), nicht das Blockierte — und die Freigabe kommt an:
252+
fetchMock
253+
.mockResolvedValueOnce(json({ results: [{ localId: eigeneKamera.id, state: 'in-sync', moderation: 'approved' }] }))
254+
.mockResolvedValueOnce(json(antwort(0, [])));
255+
await useDeviceLibrary.getState().syncAll();
256+
expect(JSON.parse(String((fetchMock.mock.calls[3][1] as RequestInit).body)).items.map((i: { localId: string }) => i.localId)).toEqual([eigeneKamera.id]);
257+
expect(useDeviceLibrary.getState().uploads.records[eigeneKamera.id]).toMatchObject({ state: 'in-sync', moderation: 'approved', slug: 'sony-pxw-fx9' });
258+
259+
// … danach, freigegeben und unveraendert, nur noch der Abgleich:
251260
fetchMock.mockResolvedValueOnce(json(antwort(0, [])));
252261
await useDeviceLibrary.getState().syncAll();
253-
expect(fetchMock.mock.calls[3][0]).toContain('/api/sync');
262+
expect(fetchMock.mock.calls[5][0]).toContain('/api/sync');
254263

255264
// … „Sync now" schickt alles, dann erst den Abgleich.
256265
fetchMock
257-
.mockResolvedValueOnce(json({ results: [{ localId: eigeneKamera.id, state: 'in-sync' }, { localId: 'custom-lens-1', state: 'blocked' }] }))
266+
.mockResolvedValueOnce(json({ results: [{ localId: eigeneKamera.id, state: 'in-sync', moderation: 'approved' }, { localId: 'custom-lens-1', state: 'blocked' }] }))
258267
.mockResolvedValueOnce(json(antwort(0, [])));
259268
await useDeviceLibrary.getState().syncAll({ manual: true });
260-
expect(fetchMock.mock.calls[4][0]).toContain('/api/upload');
261-
expect(fetchMock.mock.calls[5][0]).toContain('/api/sync');
262-
expect(useDeviceLibrary.getState().uploads.records[eigeneKamera.id]).toMatchObject({ state: 'in-sync', slug: 'sony-pxw-fx9' });
269+
expect(fetchMock.mock.calls[6][0]).toContain('/api/upload');
270+
expect(fetchMock.mock.calls[7][0]).toContain('/api/sync');
263271
});
264272

265273
it('automatic upload can be switched off; manual sync still uploads', async () => {
@@ -324,7 +332,10 @@ describe('upload ledger', () => {
324332
const { pendingUploads, applyUploadResults, emptyLedger, hashOf, toUploadItem } = await import('../library/upload');
325333
const erst = pendingUploads([kamera], emptyLedger('https://s'));
326334
expect(erst).toHaveLength(1);
327-
const ledger = applyUploadResults(emptyLedger('https://s'), erst, [{ localId: eigeneKamera.id, state: 'created', slug: 'x' }], '2026-09-27T00:00:00Z');
335+
const wartend = applyUploadResults(emptyLedger('https://s'), erst, [{ localId: eigeneKamera.id, state: 'created', slug: 'x', moderation: 'pending' }], '2026-09-27T00:00:00Z');
336+
// Wartet in der Moderation: geht erneut mit, damit die Freigabe ankommt.
337+
expect(pendingUploads([kamera], wartend)).toHaveLength(1);
338+
const ledger = applyUploadResults(wartend, erst, [{ localId: eigeneKamera.id, state: 'in-sync', moderation: 'approved' }], '2026-09-27T00:00:00Z');
328339
expect(pendingUploads([kamera], ledger)).toHaveLength(0);
329340
expect(pendingUploads([kamera], ledger, true)).toHaveLength(1);
330341
const umsortiert = Object.fromEntries(Object.entries(eigeneKamera).reverse()) as unknown as Camera;
@@ -336,6 +347,17 @@ describe('upload ledger', () => {
336347
expect(hashOf({ a: 1, b: 2 })).toBe(hashOf({ b: 2, a: 1 }));
337348
});
338349

350+
it('display bucket follows moderation, older records fall back to the state', async () => {
351+
const { uploadBucket } = await import('../library/upload');
352+
const r = (state: string, moderation?: 'pending' | 'approved') => ({ hash: 'h', state, at: 'z', ...(moderation ? { moderation } : {}) }) as never;
353+
expect(uploadBucket(r('in-sync', 'pending'))).toBe('waiting');
354+
expect(uploadBucket(r('in-sync', 'approved'))).toBe('live');
355+
expect(uploadBucket(r('created'))).toBe('waiting');
356+
expect(uploadBucket(r('in-sync'))).toBe('live');
357+
expect(uploadBucket(r('blocked', 'pending'))).toBe('blocked');
358+
expect(uploadBucket(r('error'))).toBe('failed');
359+
});
360+
339361
it('a ledger of another server is not reused; deleted entries fall out', async () => {
340362
const { readLedger, pruneLedger, emptyLedger } = await import('../library/upload');
341363
const l = { server: 'https://a', records: { x: { hash: 'h', state: 'created' as const, at: 'z' } } };

‎src/components/Settings/DeviceLibrarySection.tsx‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import { useDeviceLibrary } from '../../library/store';
1212
import LibraryErrorLine from '../Library/LibraryErrorLine';
1313
import { usesKeychain } from '../../library/tokenStore';
14+
import { uploadBucket } from '../../library/upload';
1415

1516
const feld = 'block w-full border border-bc-border bg-bc-dark text-xs text-bc-text-bright';
1617
const knopf = 'border border-bc-border text-xs text-bc-text transition-colors hover:bg-bc-panel-raised disabled:opacity-50';
@@ -28,7 +29,7 @@ export default function DeviceLibrarySection() {
2829

2930
const busy = lib.phase === 'signing-in' || lib.phase === 'checking' || lib.phase === 'syncing' || lib.phase === 'uploading';
3031
const records = Object.values(lib.uploads.records);
31-
const zaehle = (...states: string[]) => records.filter((r) => states.includes(r.state)).length;
32+
const zaehle = (bucket: ReturnType<typeof uploadBucket>) => records.filter((r) => uploadBucket(r) === bucket).length;
3233
const cameras = lib.cache.entries.filter((e) => e.kind === 'camera').length;
3334
const lenses = lib.cache.entries.length - cameras;
3435

@@ -224,12 +225,12 @@ export default function DeviceLibrarySection() {
224225
{records.length > 0 && (
225226
<p className="mt-1 text-xs text-bc-muted">
226227
{format(
227-
t('library.upload.summary', 'Own entries: {live} live or in sync, {waiting} waiting for moderation, {blocked} blocked, {failed} failed.'),
228+
t('library.upload.summary', 'Own entries: {live} live, {waiting} waiting for moderation, {blocked} blocked, {failed} failed.'),
228229
{
229-
live: zaehle('approved', 'in-sync'),
230-
waiting: zaehle('created', 'edit-proposed', 'pending-updated'),
230+
live: zaehle('live'),
231+
waiting: zaehle('waiting'),
231232
blocked: zaehle('blocked'),
232-
failed: zaehle('error'),
233+
failed: zaehle('failed'),
233234
},
234235
)}
235236
</p>

‎src/data/cameras.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -578,13 +578,13 @@ export const CAMERAS: Camera[] = [
578578
{ id: 'datavideo-ptc-305', manufacturerUrl: 'https://datavideo.com/product/PTC-305', manufacturer: 'Datavideo', model: 'PTC-305', sensor: { name: '1/1.8" (7.44×5.58)', widthMm: 7.44, heightMm: 5.58, cropFactor: 4.84 }, mount: 'integrated', resolutions: ['4K','HD'], type: 'ptz' },
579579
{ id: 'datavideo-ptc-145', manufacturerUrl: 'https://datavideo.com/product/PTC-145', manufacturer: 'Datavideo', model: 'PTC-145', sensor: { name: '1/2.8" (5.37×4.04)', widthMm: 5.37, heightMm: 4.04, cropFactor: 6.7 }, mount: 'integrated', resolutions: ['HD'], type: 'ptz' },
580580
{ id: 'avonic-cm93-ip', manufacturerUrl: 'https://avonic.com/cm93/', manufacturer: 'Avonic', model: 'CM93-IP', sensor: { name: '1/1.8" (7.44×5.58)', widthMm: 7.44, heightMm: 5.58, cropFactor: 4.84 }, mount: 'integrated', resolutions: ['4K','HD'], type: 'ptz' },
581-
{ id: 'sony-srg-a30-b', manufacturer: 'Sony', model: 'SRG-XB25', sensor: { name: '1/2.5" (5.76×4.29)', widthMm: 5.76, heightMm: 4.29, cropFactor: 6.25 }, mount: 'integrated', resolutions: ['4K','HD'], type: 'ptz' },
582-
{ id: 'canon-cr-n300w', manufacturer: 'Canon', model: 'CR-N300 (weiß)', sensor: { name: '1/2.3" (6.17×4.55)', widthMm: 6.17, heightMm: 4.55, cropFactor: 5.64 }, mount: 'integrated', resolutions: ['4K','HD'], type: 'ptz' },
581+
{ id: 'sony-srg-a30-b', manufacturerUrl: 'https://pro.sony/ue_US/products/ptz-network-cameras/srg-xb25', manufacturer: 'Sony', model: 'SRG-XB25', sensor: { name: '1/2.5" (5.76×4.29)', widthMm: 5.76, heightMm: 4.29, cropFactor: 6.25 }, mount: 'integrated', resolutions: ['4K','HD'], type: 'ptz' },
582+
{ id: 'canon-cr-n300w', manufacturerUrl: 'https://www.canon-europe.com/ptz-cameras/cr-n300/specifications/', manufacturer: 'Canon', model: 'CR-N300 (weiß)', sensor: { name: '1/2.3" (6.17×4.55)', widthMm: 6.17, heightMm: 4.55, cropFactor: 5.64 }, mount: 'integrated', resolutions: ['4K','HD'], type: 'ptz' },
583583
{ id: 'ptzoptics-move-4k-ndi', manufacturerUrl: 'https://docs.ptzoptics.com/docs/cameras/move-4k/technical-specs', manufacturer: 'PTZOptics', model: 'Move 4K SDI', sensor: { name: '1/2.5" (5.76×4.29)', widthMm: 5.76, heightMm: 4.29, cropFactor: 6.25 }, mount: 'integrated', resolutions: ['4K','HD'], type: 'ptz' },
584584
{ id: 'lumens-vc-b11u', manufacturerUrl: 'https://www.mylumens.com/en/Products_detail/1047/VC-B11U-Video-Conference-Camera', manufacturer: 'Lumens', model: 'VC-B11U', sensor: { name: '1/2.8" (5.37×4.04)', widthMm: 5.37, heightMm: 4.04, cropFactor: 6.7 }, mount: 'integrated', resolutions: ['4K','HD'], type: 'ptz' },
585585
{ id: 'marshall-cv730-ndi', manufacturerUrl: 'https://marshall-usa.com/cameras/CV730-NDI/', manufacturer: 'Marshall', model: 'CV730-NDI', sensor: { name: '1/1.8" (7.44×5.58)', widthMm: 7.44, heightMm: 5.58, cropFactor: 4.84 }, mount: 'integrated', resolutions: ['4K','HD'], type: 'ptz' },
586-
{ id: 'aida-uhd-100a', manufacturer: 'AIDA', model: 'UHD-100A', sensor: { name: '1/2.5" (5.76×4.29)', widthMm: 5.76, heightMm: 4.29, cropFactor: 6.25 }, mount: 'C/CS', resolutions: ['4K','HD'], type: 'broadcast' },
587-
{ id: 'aida-ptz4k-ndi', manufacturer: 'AIDA', model: 'PTZ4K-NDI-X30', sensor: { name: '1/1.8" (7.44×5.58)', widthMm: 7.44, heightMm: 5.58, cropFactor: 4.84 }, mount: 'integrated', resolutions: ['4K','HD'], type: 'ptz' },
586+
{ id: 'aida-uhd-100a', manufacturerUrl: 'https://aidaimaging.com/uhd-100a/', manufacturer: 'AIDA', model: 'UHD-100A', sensor: { name: '1/2.5" (5.76×4.29)', widthMm: 5.76, heightMm: 4.29, cropFactor: 6.25 }, mount: 'C/CS', resolutions: ['4K','HD'], type: 'broadcast' },
587+
{ id: 'aida-ptz4k-ndi', manufacturerUrl: 'https://aidaimaging.com/ptz4k-ndi-x30/', manufacturer: 'AIDA', model: 'PTZ4K-NDI-X30', sensor: { name: '1/1.8" (7.44×5.58)', widthMm: 7.44, heightMm: 5.58, cropFactor: 4.84 }, mount: 'integrated', resolutions: ['4K','HD'], type: 'ptz' },
588588

589589
// ── Marshall POV ──
590590
{ id: 'marshall-cv568', manufacturerUrl: 'https://marshall-usa.com/cameras/CV568/', manufacturer: 'Marshall', model: 'CV568', sensor: { name: '1/1.8" (7.44×5.58)', widthMm: 7.44, heightMm: 5.58, cropFactor: 4.84 }, mount: 'M12', resolutions: ['HD'], type: 'broadcast', notes: 'POV camera, global shutter' },

0 commit comments

Comments
 (0)