Nested UI for data & storage options
All checks were successful
Tests et déploiement / validate (push) Successful in 1m55s
Tests et déploiement / deploy (push) Successful in 37s

This commit is contained in:
2026-10-10 12:08:06 +02:00
parent 0cb1e1678e
commit 485116f2da
6 changed files with 180 additions and 71 deletions

View File

@@ -13,6 +13,7 @@
import Automations from './routes/Automations.svelte';
import NonWorkingSettings from './routes/NonWorkingSettings.svelte';
import Stats from './routes/Stats.svelte';
import DataSettings from './routes/DataSettings.svelte';
let ready = $state(false);
let bootError = $state('');
@@ -58,8 +59,8 @@
{#if !ready}
<div class="boot">{#if bootError}<div class="stack"><p role="alert">{bootError}</p><button onclick={() => (retry += 1)}>Réessayer</button></div>{:else}<p>Chargement…</p>{/if}</div>
{:else}
{#if backupState.loaded && backupReminderDue(backupState.status, clockNow) && view !== 'settings'}
<div class="update"><span>Sauvegarde à vérifier</span><button disabled={dayState.importing} onclick={() => go('settings')}>Sauvegarder</button></div>
{#if backupState.loaded && backupReminderDue(backupState.status, clockNow) && !['settings', 'data'].includes(view)}
<div class="update"><span>Sauvegarde à vérifier</span><button disabled={dayState.importing} onclick={() => go('data')}>Sauvegarder</button></div>
{/if}
{#if needsRefresh.value && !dayState.day}
<!-- Only offered while no day is running, so an update can never interrupt
@@ -77,6 +78,8 @@
<Automations />
{:else if view === 'non-working'}
<NonWorkingSettings />
{:else if view === 'data'}
<DataSettings />
{:else if ['settings', 'automations', 'non-working'].includes(view)}
<Settings />
{:else if view === 'today'}
@@ -104,9 +107,9 @@
>
<button
type="button"
class:sel={['settings', 'automations', 'non-working'].includes(view)}
class:sel={['settings', 'automations', 'non-working', 'data'].includes(view)}
disabled={dayState.importing}
aria-current={['settings', 'automations', 'non-working'].includes(view)}
aria-current={['settings', 'automations', 'non-working', 'data'].includes(view)}
onclick={() => go('settings')}>Réglages</button
>
{#if view === 'stats'}

View File

@@ -126,7 +126,7 @@
</script>
<section class="card stack" aria-busy={busy}>
<h2>Données</h2>
<h2>Sauvegarde et restauration</h2>
<button type="button" disabled={busy} onclick={exportAll}>Exporter les données</button>

View File

@@ -6,7 +6,7 @@
* worker should land on.
*/
const ROUTES = ['resume', 'today', 'settings', 'stats', 'details', 'automations', 'non-working'];
const ROUTES = ['resume', 'today', 'settings', 'stats', 'details', 'automations', 'non-working', 'data'];
function fromHash() {
const [path, query = ''] = location.hash.replace(/^#\/?/, '').split('?');

View File

@@ -0,0 +1,121 @@
<script>
import BackupPanel from '../components/BackupPanel.svelte';
import Sheet from '../components/Sheet.svelte';
import { go } from '../lib/router.svelte.js';
import { purgeCandidates, purgeReviewedDays } from '../lib/backup-safety.js';
import { dayState, refresh } from '../lib/day.svelte.js';
import { settings, settingsError, updateSettings } from '../lib/settings.svelte.js';
let purged = $state(null);
let purgeOpen = $state(false);
let purgeError = $state('');
let purgeBusy = $state(false);
let reviewedPurge = $state([]);
let reviewedRetention = $state(0);
function reviewPurge() {
reviewedRetention = settings.retentionDays;
reviewedPurge = purgeCandidates(dayState.closedDays, reviewedRetention).map(day => ({
day: { ...day }, entries: dayState.historyEntries.filter(entry => entry.date === day.date).sort((a, b) => a.seq - b.seq).map(entry => ({ ...entry })),
}));
purgeError = ''; purgeOpen = true;
}
async function saveRetention(event) {
const input = event.currentTarget;
await updateSettings({ retentionDays: Number(input.value) });
input.value = String(settings.retentionDays);
}
async function purge() {
if (purgeBusy || dayState.importing) return;
purgeBusy = true; purgeError = '';
try {
purged = await purgeReviewedDays($state.snapshot(reviewedPurge), reviewedRetention);
await refresh(); purgeOpen = false;
} catch (error) { purgeError = error.message; }
finally { purgeBusy = false; }
}
</script>
<fieldset class="screen" disabled={dayState.importing}>
<h1>Données et sauvegardes</h1>
<button type="button" onclick={() => go('settings')}>Retour aux réglages</button>
<section class="card stack">
<h2>Vos données sur cet appareil</h2>
<p class="small">Vos journées, postes et réglages sont enregistrés dans ce navigateur, sur cet appareil, et restent accessibles hors ligne. Ils ne sont pas synchronisés entre vos appareils.</p>
<p class="small">Effacer les données du navigateur ou perdre cet appareil peut vous faire perdre votre historique. Exportez régulièrement un fichier, vérifiez-le puis gardez une copie ailleurs : vous pourrez le réimporter pour retrouver vos données ou les transférer sur un autre appareil.</p>
</section>
{#if settingsError.message}
<p class="error card" role="alert">{settingsError.message}</p>
{/if}
<BackupPanel />
<section class="card stack">
<h2>Durée de conservation</h2>
<label>
<span>Conserver (jours)</span>
<input
type="number"
min="30"
step="30"
value={settings.retentionDays}
onchange={saveRetention}
/>
</label>
<p class="dim small">
Cette durée détermine les journées anciennes proposées à la purge. La suppression
se fait uniquement avec « Purger maintenant », après confirmation ; changer la durée
ne supprime rien. La dernière journée terminée est toujours conservée.
</p>
<p class="dim small">
L’historique alimente les suggestions et les statistiques. La purge conserve une
sauvegarde récupérable sur cet appareil, remplacée à la prochaine purge. Exportez
aussi un fichier pour garder une copie ailleurs.
</p>
<button type="button" onclick={reviewPurge}>Purger maintenant</button>
{#if purged !== null}
<p class="dim small">
{purged === 0
? 'Aucune journée plus ancienne que la durée de conservation.'
: `${purged} journée${purged > 1 ? 's' : ''} supprimée${purged > 1 ? 's' : ''}.`}
</p>
{/if}
</section>
</fieldset>
<Sheet open={purgeOpen} title="Confirmer la purge" onclose={() => { if (!purgeBusy) purgeOpen = false; }}>
<p>Journées concernées : {reviewedPurge.length ? reviewedPurge.map(item => item.day.date).join(', ') : 'aucune'}.</p>
<p>Une sauvegarde récupérable sera conservée sur cet appareil avant la suppression.</p>
{#if purgeError}<p role="alert">{purgeError}</p>{/if}
<button disabled={purgeBusy || dayState.importing || !reviewedPurge.length} class="btn-danger" onclick={purge}>Confirmer la purge</button>
<button disabled={purgeBusy} onclick={() => (purgeOpen = false)}>Annuler</button>
</Sheet>
<style>
fieldset.screen {
border: 0;
margin: 0;
min-width: 0;
}
label {
display: flex;
flex-direction: column;
gap: var(--gap);
font-weight: 700;
}
.small {
font-size: 0.85rem;
}
.error {
color: var(--danger-fg);
background: var(--danger);
border-color: var(--danger);
font-weight: 700;
}
</style>

View File

@@ -1,9 +1,6 @@
<script>
import BackupPanel from '../components/BackupPanel.svelte';
import { go } from '../lib/router.svelte.js';
import Sheet from '../components/Sheet.svelte';
import { purgeCandidates, purgeReviewedDays } from '../lib/backup-safety.js';
import { dayState, refresh } from '../lib/day.svelte.js';
import { dayState } from '../lib/day.svelte.js';
import {
ROUNDING_CHOICES,
settings,
@@ -13,20 +10,6 @@
WEEKDAY_LABELS,
} from '../lib/settings.svelte.js';
let purged = $state(null);
let purgeOpen = $state(false);
let purgeError = $state('');
let purgeBusy = $state(false);
let reviewedPurge = $state([]);
let reviewedRetention = $state(0);
function reviewPurge() {
reviewedRetention = settings.retentionDays;
reviewedPurge = purgeCandidates(dayState.closedDays, reviewedRetention).map(day => ({
day: { ...day }, entries: dayState.historyEntries.filter(entry => entry.date === day.date).sort((a, b) => a.seq - b.seq).map(entry => ({ ...entry })),
}));
purgeError = ''; purgeOpen = true;
}
function toggleWorkday(value) {
return setWorkingDay(value, !settings.workdays.includes(value));
}
@@ -36,16 +19,6 @@
await updateSettings({ [key]: numeric ? Number(input.value) : input.value });
input.value = String(settings[key]);
}
async function purge() {
if (purgeBusy || dayState.importing) return;
purgeBusy = true; purgeError = '';
try {
purged = await purgeReviewedDays($state.snapshot(reviewedPurge), reviewedRetention);
await refresh(); purgeOpen = false;
} catch (error) { purgeError = error.message; }
finally { purgeBusy = false; }
}
</script>
<fieldset class="screen" disabled={dayState.importing}>
@@ -175,45 +148,13 @@
</div>
</section>
<BackupPanel />
<section class="card stack">
<h2>Historique</h2>
<label>
<span>Conserver (jours)</span>
<input
type="number"
min="30"
step="30"
value={settings.retentionDays}
onchange={(e) => saveField(e, 'retentionDays', true)}
/>
</label>
<p class="dim small">
L'historique alimente les suggestions : plus il est court, moins les propositions
sont justes. La dernière journée close est toujours conservée.
</p>
<p class="dim small">
La purge nécessite une confirmation et conserve une sauvegarde locale récupérable.
</p>
<button type="button" onclick={reviewPurge}>Purger maintenant</button>
{#if purged !== null}
<p class="dim small">
{purged === 0
? 'Aucune journée plus ancienne que la durée de conservation.'
: `${purged} journée${purged > 1 ? 's' : ''} supprimée${purged > 1 ? 's' : ''}.`}
</p>
{/if}
<h2>Données et sauvegardes</h2>
<p class="dim small">Sauvegarder, exporter ou importer les données et régler leur durée de conservation.</p>
<button type="button" onclick={() => go('data')}>Gérer les données et sauvegardes</button>
</section>
</fieldset>
<Sheet open={purgeOpen} title="Confirmer la purge" onclose={() => { if (!purgeBusy) purgeOpen = false; }}>
<p>Journées concernées : {reviewedPurge.length ? reviewedPurge.map(item => item.day.date).join(', ') : 'aucune'}.</p>
<p>Une sauvegarde récupérable sera conservée sur cet appareil avant la suppression.</p>
{#if purgeError}<p role="alert">{purgeError}</p>{/if}
<button disabled={purgeBusy || dayState.importing || !reviewedPurge.length} class="btn-danger" onclick={purge}>Confirmer la purge</button>
<button disabled={purgeBusy} onclick={() => (purgeOpen = false)}>Annuler</button>
</Sheet>
<style>
fieldset.screen {
border: 0;