Files
pardis/README.md
2026-09-19 21:53:10 +02:00

195 lines
9.8 KiB
Markdown
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.
# TimeLog
Offline-first PWA for tracking hours on site. While the app is running, it can start
the day automatically and prompt for its end. Each task begins where the previous
one ended. Closed days have concise summaries ready to copy into a paper logbook.
Built for use outdoors: high contrast, large targets, usable with gloves on. All data
stays on the device (IndexedDB); there is no backend and nothing is uploaded.
## Development
```sh
npm install
npm run dev # http://localhost:5173
npm test # unit and IndexedDB regression tests
npm run check # svelte-check
npm run build # production build into dist/
```
## Exporter et importer les données
Dans **Réglages → Données**, **Exporter les données** télécharge un fichier
`timelog-AAAA-MM-JJ.json`. Il contient toutes les journées conservées, leurs postes
et les réglages enregistrés. L'export fonctionne hors ligne et ne modifie pas les
données de l'appareil. Les identifiants internes des postes sont recréés à l'import.
Pour restaurer une sauvegarde, choisissez **Importer un fichier…** puis vérifiez
le nom du fichier, les compteurs et les éventuels ajustements signalés. Deux modes
sont disponibles :
- **Fusionner** ajoute seulement les dates absentes. Une date déjà présente est
conservée intégralement, même si le fichier contient une autre version de ses
postes. Les réglages locaux sont conservés. Importer deux fois le même fichier
ne crée pas de doublons.
- **Remplacer tout** demande une confirmation, puis remplace toutes les journées,
tous les postes et les réglages, y compris une journée locale en cours. Si le
fichier ne contient pas de réglages, les valeurs par défaut sont rétablies.
Les journées importées sont enregistrées comme terminées. Pour une journée
exportée en cours, le dernier poste est arrêté à l'heure de l'export : le temps
écoulé est conservé. Les intervalles sans travail déjà présents sont conservés.
Une sauvegarde vide est valide et peut servir à restaurer un appareil vide.
Le format JSON est identifié par `format: "timelog-export"` et `version: 1`.
Les fichiers étrangers, versions non prises en charge, dates en double et réglages
invalides sont refusés. Les postes inutilisables et les corrections de plages
horaires sont signalés avant confirmation. La limite de taille est de 20 Mo.
L'écriture est transactionnelle : un échec annule le remplacement ou la fusion
dans son intégralité. Le récapitulatif, le thème et les suggestions sont ensuite
actualisés sans recharger la page.
L'import ne déclenche pas immédiatement de purge. La durée de conservation
s'applique de nouveau à la prochaine ouverture ou au changement de jour ; le
bouton **Purger maintenant** reste disponible.
Les tests d'import/export couvrent le format, les réglages, la fidélité des durées,
les fichiers invalides, la fusion, le remplacement et l'annulation transactionnelle
sur une IndexedDB simulée. `src/lib/fixtures/backup.json` est le jeu d'essai fourni ;
il n'est pas publié avec l'application.
## Journées, reprise et historique
- Une journée oubliée est clôturée au retour dans l'application, à son heure de
fin prévue (prolongations comprises), sans jamais précéder la dernière activité
enregistrée. Cette heure est une estimation : l'application ne mesure pas le
travail lorsqu'elle est fermée. Le dernier poste conserve ainsi sa durée jusqu'à
cette limite, au lieu d'être ramené à zéro.
- Une journée terminée propose **Reprendre la journée**. Cela annule sa clôture et
continue le dernier poste, client et déjeuner conservés. Le temps écoulé depuis
l'arrêt est compté dans ce poste : la reprise corrige une clôture prématurée.
- La création, la clôture et la reprise sont transactionnelles. Les actions
simultanées sur le même poste ne créent pas de doublons ; une journée clôturée
ne peut pas recevoir un nouveau poste actif. La purge épargne les journées
actives et conserve toujours la dernière journée terminée.
- Les horaires de déjeuner affichent un rappel puis une heure indicative de
reprise, calculée depuis le début réel de la pause. Les transitions restent
manuelles : aucun temps de travail n'est transformé automatiquement en pause.
- Dans **Récap**, le sélecteur **Journée à consulter** permet d'afficher et de copier
chaque journée terminée encore conservée, y compris les journées importées.
Les tests couvrent notamment les clôtures oubliées, les reprises, les appels
concurrents, les retours d'horloge, les échecs transactionnels et la conservation.
## Corriger les postes et les journées terminées
Le **Total journée** mesure la durée entre le début et la fin, repas compris.
Le récapitulatif et le texte copié distinguent aussi le travail hors repas et le
déjeuner. Les totaux suivent le réglage d'arrondi existant ; les éventuels trous
importés restent inclus dans l'amplitude de la journée.
Dans **Journée**, touchez un poste dans **Postes de la journée** pour modifier
son début, sa fin, sa tâche, son client ou son type (travail, transport, repas).
Les horaires sont saisis à la minute, avec leur date, et enregistrés ensemble
avec le bouton **Enregistrer**. Annuler ne modifie rien. Le début du premier
poste et la fin du dernier définissent les bornes de la journée. Modifier une
limite intermédiaire ajuste la limite du poste voisin, sans chevauchement.
La fin d'un poste encore en cours reste pilotée par les boutons de changement
de tâche ou de clôture.
**Ajouter un poste oublié** permet de choisir une plage existante puis les
horaires du poste à insérer dans cette plage. Les portions avant et après sont
conservées avec leur tâche et leur client. Par exemple, un poste 08:00–12:00
peut devenir 08:00–09:00, une tâche oubliée 09:00–10:00, puis 10:00–12:00.
Le type **Repas** permet aussi d'ajouter une pause oubliée et de renseigner
panier ou restaurant. Pour ajouter juste après une frontière existante,
choisissez le poste qui commence à cette frontière.
Dans **Récap**, choisissez la date puis **Corriger cette journée** : les mêmes
actions sont disponibles sans réouverture, même si une autre journée est en
cours. Le récapitulatif et son texte à copier sont recalculés après chaque
enregistrement. La suppression d'un poste demande confirmation ; le voisin
absorbe sa durée, et le dernier poste d'une journée ne peut pas être supprimé.
Chaque correction est transactionnelle. Les horaires inversés, les insertions
hors de la plage choisie et les formulaires devenus périmés après une autre
modification sont refusés sans écriture partielle.
In dev the console exposes `timelog.seed()` to generate 20 days of realistic history
(useful for exercising the suggestion engine) and `timelog.wipe()` to clear it.
## Deployment
The stack is a single container: a multi-stage build compiles the app and Caddy serves
the static output, obtaining and renewing a Let's Encrypt certificate on its own.
```sh
cp .env.example .env
$EDITOR .env # set SITE_ADDRESS and ACME_EMAIL
docker compose up -d --build
```
Requirements:
- `SITE_ADDRESS` must already resolve to the server.
- Ports **80 and 443** must be reachable from the internet. Port 80 is not optional —
it is what the ACME challenge uses to prove domain ownership.
Certificates are stored in the `caddy_data` volume. Keep it: without it Caddy re-issues
on every recreate and will hit the Let's Encrypt rate limit after a handful of restarts.
To update, pull and rebuild:
```sh
git pull && docker compose up -d --build
```
Running clients do not reload on their own. Each app checks hourly for a new build and
offers an **"Installer"** banner — but only while no working day is in progress, so an
update can never interrupt someone mid-day.
### HTTPS is not optional
Browsers only register a service worker in a secure context. Served over plain `http://`
to an IP or hostname, the app still loads but has **no offline support, cannot be
installed, and never updates** — which removes the point of the design. `localhost` is
the only exception, which is why `npm run dev` works without certificates.
### Behind an existing reverse proxy
If something upstream already terminates TLS, set `SITE_ADDRESS=:80`, drop the `443`
port mappings, and let the proxy forward to port 80. `ACME_EMAIL` is then unused but
must still be non-empty — Caddy will not parse a blank value.
The app must be served from the root of its domain. `start_url` and the service worker
scope are both `/`, so hosting it under a subpath will not work without changing the
manifest and Vite's `base`.
## Architecture
Two decisions shape everything else.
**Raw timestamps are stored; rounding is applied only at render.** Changing the rounding
setting therefore corrects past days too. Adjacent entries share one stored instant —
entry *N*'s end *is* entry *N+1*'s start — so rounding can never open a gap between
them. The quick time picker follows the rounding grid. The detailed entry editor
stores minute-level corrections; summaries apply the selected rounding at render.
**Suggestions are derived from history, not from counter tables.** There is nothing to
keep in sync; the model is rebuilt in memory from the entries. The tradeoff is that a
short retention window degrades predictions, which is why retention defaults to two
years.
```
src/lib/
time.js local dates, rounding, formatting (pure)
predict.js the suggestion engine (pure)
resume.js logbook text builder (pure)
db.js Dexie schema and the chain invariant
day.svelte.js day lifecycle state machine
```
Pure helpers and database/lifecycle operations have regression tests. `db.js`
keeps day and entry changes in shared transactions, including creation, closure,
reopening and boundary corrections.