import { z } from 'zod'; import type { BackupStatus, RestoreStatus } from './backup-status'; /** * Site-Admin system panel (issue #86): maintenance jobs, backup status, * audit trail, and storage overview — the operator's single glance for * instance health (operations.md §Maintenance jobs). */ export interface SystemJobView { name: string; cadenceSeconds: number; status: 'IDLE' | 'RUNNING' | 'FAILED'; lastRunAt: string | null; lastDurationMs: number | null; lastError: string | null; /** False for a database row whose job no longer registers in this build. */ registered: boolean; } export type JobTriggerOutcome = 'succeeded' | 'failed' | 'already_running'; export interface JobTriggerResult { outcome: JobTriggerOutcome; job: SystemJobView; } export interface SystemBackupView { /** False when no status.json exists (sidecar never ran / not deployed). */ available: boolean; /** Freshness verdict mirroring the readyz `backup` check (issue #85). */ fresh: boolean; status: BackupStatus | null; maxAgeHours: number; /** Whether a Nextcloud target is fully configured (issue #103). */ remoteConfigured: boolean; /** Progress/result of the last in-app restore, if any (issue #103). */ restore: RestoreStatus | null; } /** * Backup configuration surface of the admin settings page (issue #103). * The app password is write-only: the view only says whether one is stored. */ export interface BackupSettingsView { localRetentionDays: number | null; remoteRetentionDays: number; nextcloud: { enabled: boolean; baseUrl: string; username: string; folder: string; uploadSchedule: 'off' | 'daily' | 'weekly'; passwordSet: boolean; }; } export const backupSettingsInputSchema = z.object({ /** `null` = no override; the sidecar's env value stays authoritative. */ localRetentionDays: z.number().int().min(1).max(3650).nullable(), remoteRetentionDays: z.number().int().min(1).max(3650), nextcloud: z.object({ enabled: z.boolean(), baseUrl: z.string().trim().url({ message: 'validation.url' }).or(z.literal('')), username: z.string().trim().max(200), folder: z.string().trim().min(1).max(500), uploadSchedule: z.enum(['off', 'daily', 'weekly']), /** Empty or omitted = keep the stored app password. */ password: z.string().max(500).optional(), }), }); export type BackupSettingsInput = z.infer; export const backupConnectionTestInputSchema = z.object({ baseUrl: z.string().trim().url({ message: 'validation.url' }), username: z.string().trim().min(1).max(200), folder: z.string().trim().min(1).max(500), /** Empty or omitted = test with the stored app password. */ password: z.string().max(500).optional(), }); export type BackupConnectionTestInput = z.infer; export interface BackupConnectionTestResult { ok: boolean; /** Raw transport/server error for the admin, like the SMTP test (#80). */ error?: string; } /** One restorable set as offered in the restore picker (issue #103). */ export interface BackupSetView { backupId: string; /** UTC run start derived from the backup id. */ startedAt: string; /** Bundle size (remote) or dump+archive sum (local); null when unknown. */ sizeBytes: number | null; } export interface BackupSetsView { local: BackupSetView[]; remoteConfigured: boolean; remote: BackupSetView[]; /** Present when a configured remote target could not be listed. */ remoteError?: string; } export const backupIdSchema = z.string().regex(/^\d{8}-\d{6}$/); export const backupRestoreInputSchema = z.object({ source: z.enum(['local', 'remote']), backupId: backupIdSchema, /** Type-to-confirm safety: must repeat the backup id verbatim. */ confirm: z.string(), }); export type BackupRestoreInput = z.infer; /** Response of the public, maintenance-exempt restore status endpoint. */ export type RestoreStatusResponse = { state: 'idle' } | RestoreStatus; export interface AuditActorView { id: string; username: string; displayName: string; } export interface AuditEntryView { id: string; at: string; action: string; actor: AuditActorView | null; targetType: string | null; targetId: string | null; details: Record | null; } export const AUDIT_PAGE_SIZE = 50; export const auditListQuerySchema = z.object({ /** Exact username of the acting user. */ actor: z.string().trim().min(1).optional(), /** Action id or prefix, e.g. `grant.` matches created and deleted. */ action: z.string().trim().min(1).optional(), from: z.coerce.date().optional(), to: z.coerce.date().optional(), page: z.coerce.number().int().min(1).default(1), }); export type AuditListQuery = z.infer; export interface AuditListView { entries: AuditEntryView[]; page: number; pageCount: number; total: number; } export interface StoragePondView { pondId: string; name: string; slug: string; /** Lowercase like PondView's `type` (the api's wire convention). */ type: 'personal' | 'shared'; storageBytesUsed: number; } export interface StorageOverviewView { totalBytes: number; /** Top ponds by storage use, largest first. */ ponds: StoragePondView[]; }