Files
pardis/README.md

6.2 KiB

TimeLog

Offline-first PWA for tracking hours on site. The day starts and ends on its own, each task begins where the previous one ended, and the app's main output is a concise summary of the last working day, 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

npm install
npm run dev      # http://localhost:5173
npm test         # unit tests for the time, prediction, resume and boundary logic
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.

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.

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:

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. Times a worker sets by hand are snapped to the grid on write, so a chosen value reads back exactly as chosen.

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

The four pure modules hold the logic worth testing and carry the unit tests. db.js concentrates every write that touches a boundary into closeAndStart and setEntryStart, so the timeline cannot drift out of order from anywhere else.