feat: Admin Area System: Database Backup/Restore and Danger Zone

This commit is contained in:
OpenVelo Agent
2026-07-23 12:10:37 +00:00
parent 785d56c02d
commit 7b9be24013
34 changed files with 3378 additions and 53 deletions
@@ -1,4 +1,4 @@
import { DestroyRef, Injectable, computed, inject, signal } from '@angular/core';
import { DestroyRef, Injectable, computed, effect, inject, signal } from '@angular/core';
import { Observable } from 'rxjs';
import {
BoardCard,
@@ -17,11 +17,13 @@ import {
} from './challenges.pure';
import { ChallengesApiService } from './challenges.service';
import { EventSourceLike } from '../../core/services/event-status.pure';
import { SystemDataChangeService } from '../../core/services/system-data-change.service';
@Injectable({ providedIn: 'root' })
export class ChallengesStore {
private readonly api: ChallengesApiService;
private readonly destroyRef: DestroyRef;
private readonly dataChanges: SystemDataChangeService | null;
private readonly _board = signal<BoardResponse | null>(null);
private readonly _eventState = signal<EventStatePayloadLike['state']>('unconfigured');
@@ -35,10 +37,35 @@ export class ChallengesStore {
private readonly solveListeners = new Map<string, Set<(p: SolveEventLivePayload) => void>>();
constructor(api?: ChallengesApiService, destroyRef?: DestroyRef) {
constructor(api?: ChallengesApiService, destroyRef?: DestroyRef, dataChanges?: SystemDataChangeService) {
this.api = api ?? inject(ChallengesApiService);
this.destroyRef = destroyRef ?? inject(DestroyRef);
this.dataChanges = dataChanges ?? null;
this.destroyRef.onDestroy(() => this.stop());
this.installDataChangeListeners();
}
private installDataChangeListeners(): void {
if (!this.dataChanges) return;
const lastSeen = { ts: 0 };
effect(() => {
const evts = this.dataChanges!.events();
const latest = evts[evts.length - 1];
if (!latest || latest.ts === lastSeen.ts) return;
lastSeen.ts = latest.ts;
if (latest.kind === 'challenges-wiped' || latest.kind === 'restore-completed' || latest.kind === 'scores-reset') {
this.reset();
}
});
}
/**
* Exposed for callers that want to clear the cached board/selected detail
* after a destructive operation (e.g. challenge wipe). The signal
* subscription above re-evaluates when `events()` mutates.
*/
invalidateForSystemChange(_kind: 'scores-reset' | 'challenges-wiped' | 'restore-completed'): void {
this.reset();
}
readonly board = this._board.asReadonly();