first commit
This commit is contained in:
commit
3c0479faf0
169 files changed
+27617
No files matched your search
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user