feat(shared): backfill-status and usage wire schemas on the storage admin state

This commit is contained in:
jubnl
2026-08-20 22:30:19 +02:00
parent 18ca97ca14
commit eb998595d7
6 changed files with 75 additions and 0 deletions
@@ -32,6 +32,8 @@ function baseState(overrides: Partial<StorageAdminState> = {}): StorageAdminStat
health: { replicaFailures: [] },
encryptionReady: true,
seedFilePresent: false,
usage: null,
backfills: [],
...overrides,
};
}
@@ -48,6 +48,8 @@ const STATE: StorageAdminState = {
health: { replicaFailures: [] },
encryptionReady: true,
seedFilePresent: false,
usage: null,
backfills: [],
};
describe('settingsDocumentOf', () => {
@@ -30,6 +30,8 @@ function baseState(overrides: Partial<StorageAdminState> = {}): StorageAdminStat
health: { replicaFailures: [] },
encryptionReady: true,
seedFilePresent: false,
usage: null,
backfills: [],
...overrides,
};
}
@@ -57,6 +57,9 @@ export class StorageAdminService {
health: { replicaFailures: this.storage.health().replicaFailures.map((f) => ({ ...f })) },
encryptionReady: this.env.env().security.encryptionKeySet,
seedFilePresent: fs.existsSync(SEED_CONFIG_PATH),
// Filled by the stats/jobs services — backfill/stats spec.
usage: null,
backfills: [],
};
}
+37
View File
@@ -2,8 +2,11 @@ import {
STORAGE_BACKEND_TYPES,
STORAGE_BACKEND_TYPE_IDS,
STORAGE_CATEGORIES,
storageAdminStateSchema,
storageBackfillStatusSchema,
storageConfigSchema,
storageSecretFields,
storageUsageSchema,
type StorageBackendTypeId,
} from './storage.schema';
@@ -145,3 +148,37 @@ describe('storageConfigSchema wire compatibility', () => {
expect(result.success).toBe(false);
});
});
describe('backfill + usage wire shapes', () => {
it('storageBackfillStatusSchema round-trips a running and a finished status', () => {
const running = {
backend: 'backups-local-mirror',
status: 'running',
done: 3,
total: 10,
copied: 2,
skipped: 1,
failed: 0,
startedAt: 1_700_000_000_000,
};
expect(storageBackfillStatusSchema.parse(running)).toEqual(running);
const done = { ...running, status: 'done', done: 10, finishedAt: 1_700_000_100_000 };
expect(storageBackfillStatusSchema.parse(done)).toEqual(done);
expect(storageBackfillStatusSchema.safeParse({ ...running, status: 'paused' }).success).toBe(false);
});
it('storageUsageSchema carries all configurable categories plus the legacy photos bucket', () => {
const perCategory = { objects: 1, bytes: 2 };
const usage = {
computedAt: 1_700_000_000_000,
categories: Object.fromEntries(STORAGE_CATEGORIES.map((c) => [c, perCategory])),
legacyPhotos: { objects: 0, bytes: 0 },
};
expect(storageUsageSchema.parse(usage)).toEqual(usage);
});
it('the admin state embeds usage (nullable) and backfills', () => {
const stateShape = storageAdminStateSchema.pick({ usage: true, backfills: true });
expect(stateShape.parse({ usage: null, backfills: [] })).toEqual({ usage: null, backfills: [] });
});
});
+29
View File
@@ -212,6 +212,33 @@ export const storageReplicaFailureSchema = z.object({
at: z.number(),
});
// ── Backfill + usage (backfill/stats/notifications spec) ─────────────────────
export const storageBackfillStatusSchema = z.object({
/** The mirror's wire name — user-facing copy says "Sync", never this. */
backend: z.string(),
status: z.enum(['running', 'done', 'error', 'cancelled']),
/** Objects examined in the copy phase; total is 0 while enumerating. */
done: z.number(),
total: z.number(),
copied: z.number(),
skipped: z.number(),
failed: z.number(),
startedAt: z.number(),
finishedAt: z.number().optional(),
/** status 'error' only: the aborting (primary-side) error. */
error: z.string().optional(),
});
export type StorageBackfillStatus = z.infer<typeof storageBackfillStatusSchema>;
export const storageUsageSchema = z.object({
computedAt: z.number(),
categories: z.record(storageCategorySchema, z.object({ objects: z.number(), bytes: z.number() })),
/** The served-legacy photos directory — attributed to whatever backend `photos` resolves to. */
legacyPhotos: z.object({ objects: z.number(), bytes: z.number() }),
});
export type StorageUsage = z.infer<typeof storageUsageSchema>;
export const storageAdminStateSchema = z.object({
backends: z.array(
z.object({
@@ -231,6 +258,8 @@ export const storageAdminStateSchema = z.object({
health: z.object({ replicaFailures: z.array(storageReplicaFailureSchema) }),
encryptionReady: z.boolean(),
seedFilePresent: z.boolean(),
usage: storageUsageSchema.nullable(),
backfills: z.array(storageBackfillStatusSchema),
});
export type StorageAdminState = z.infer<typeof storageAdminStateSchema>;