first commit

This commit is contained in:
Rutra committed 2026-10-12 14:25:22 +02:00
commit 3c0479faf0
169 files changed
+27617

No files matched your search

+182
View File
@@ -0,0 +1,182 @@
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));
}
}
}