Files
big-brother/web/src/app/pages/backups/backups.ts
T
2026-10-12 14:25:22 +02:00

183 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
import { Api } from '../../core/api';
import { Feedback } from '../../core/feedback';
import { errorMessage, formatBytes, formatDateTime, formatRelative } from '../../core/format';
import type { BackupFile, BackupSchedule } from '../../core/models';
import { Icon } from '../../shared/icon';
import { JobConsole } from '../../shared/job-console';
@Component({
selector: 'app-backups',
imports: [Icon, JobConsole],
template: `
<div class="page">
<header class="page-header">
<div>
<h1>Sauvegardes</h1>
<p>
Export de toutes les bases MariaDB et archive de <span class="mono">/srv</span> (sans
node_modules).
</p>
</div>
<button type="button" class="btn primary" (click)="run()" [disabled]="starting()">
<app-icon name="archive" /> Lancer une sauvegarde
</button>
</header>
@if (error()) {
<div class="alert critical" role="alert">
<app-icon name="critical" />
<p>{{ error() }}</p>
</div>
}
<div class="alert warning">
<app-icon name="alert" />
<p>
Les sauvegardes restent sur le VPS (<span class="mono">{{ dir() }}</span
>). Télécharge-les régulièrement : si le serveur est perdu, elles le sont aussi.
</p>
</div>
@if (jobId(); as id) {
<app-job-console [jobId]="id" (finished)="load()" />
}
<section class="grid-tiles">
<div class="card tile">
<span class="tile-label">Sauvegarde automatique</span>
<span class="tile-value small">{{
schedule()?.dailyAt ? 'Chaque jour à ' + schedule()!.dailyAt : 'Désactivée'
}}</span>
<span class="tile-sub">Variable BACKUP_DAILY_AT</span>
</div>
<div class="card tile">
<span class="tile-label">Conservation</span>
<span class="tile-value small">{{ schedule()?.keep ?? '–' }} de chaque type</span>
<span class="tile-sub">Les plus anciennes sont supprimées</span>
</div>
<div class="card tile">
<span class="tile-label">Dernière sauvegarde</span>
<span class="tile-value small">{{
files()[0] ? relative(files()[0].createdAt) : 'Aucune'
}}</span>
<span class="tile-sub">{{ files()[0] ? dateTime(files()[0].createdAt) : '' }}</span>
</div>
</section>
<section class="card">
<div class="card-body flush table-wrap">
<table class="table">
<thead>
<tr>
<th>Fichier</th>
<th>Contenu</th>
<th class="num">Taille</th>
<th>Date</th>
<th class="actions"><span class="sr-only">Actions</span></th>
</tr>
</thead>
<tbody>
@for (file of files(); track file.name) {
<tr>
<td class="mono">{{ file.name }}</td>
<td>{{ file.type === 'db' ? 'Bases de données' : 'Dossier /srv' }}</td>
<td class="num">{{ bytes(file.size) }}</td>
<td class="nowrap">{{ dateTime(file.createdAt) }}</td>
<td class="actions">
<a
class="btn small"
[href]="api.backupUrl(file.name)"
[attr.download]="file.name"
>
<app-icon name="download" /> Télécharger
</a>
<button
type="button"
class="btn small icon-only danger"
(click)="remove(file)"
[attr.aria-label]="'Supprimer ' + file.name"
>
<app-icon name="trash" />
</button>
</td>
</tr>
} @empty {
<tr>
<td colspan="5" class="empty">Aucune sauvegarde pour l’instant.</td>
</tr>
}
</tbody>
</table>
</div>
</section>
</div>
`,
styles: `
.tile-value.small {
font-size: 18px;
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BackupsPage {
protected api = inject(Api);
private feedback = inject(Feedback);
protected readonly files = signal<BackupFile[]>([]);
protected readonly schedule = signal<BackupSchedule | null>(null);
protected readonly dir = signal('');
protected readonly error = signal<string | null>(null);
protected readonly starting = signal(false);
protected readonly jobId = signal<number | null>(null);
protected readonly bytes = formatBytes;
protected readonly dateTime = formatDateTime;
protected readonly relative = formatRelative;
constructor() {
this.load();
}
async load() {
try {
const result = await this.api.backups();
this.files.set(result.files);
this.schedule.set(result.schedule);
this.dir.set(result.dir);
this.error.set(null);
} catch (error) {
this.error.set(errorMessage(error));
}
}
protected async run() {
this.starting.set(true);
try {
const { jobId } = await this.api.runBackup();
this.jobId.set(jobId);
} catch (error) {
this.feedback.error(errorMessage(error));
} finally {
this.starting.set(false);
}
}
protected async remove(file: BackupFile) {
const ok = await this.feedback.confirm({
title: 'Supprimer cette sauvegarde ?',
message: `${file.name} sera supprimé du serveur. C’est définitif.`,
confirmLabel: 'Supprimer',
danger: true,
});
if (!ok) return;
try {
await this.api.deleteBackup(file.name);
this.feedback.success('Sauvegarde supprimée.');
await this.load();
} catch (error) {
this.feedback.error(errorMessage(error));
}
}
}