Files
pardis/README.md
Adrien W. b11fe56993
Some checks failed
Tests et déploiement / validate (push) Failing after 3m25s
Tests et déploiement / deploy (push) Has been skipped
More UI / UX changes
Added about page
2026-10-11 10:19:41 +02:00

23 KiB
Raw Blame History

Pardis

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. Data is saved locally in IndexedDB. Optional end-to-end encrypted synchronization shares it between authorized devices without e-mail or password registration.

Development

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/
npm run validate # tests, Svelte check and full PWA build
npx playwright install chromium # install the browser for UI tests
npm run test:e2e # fresh profiles, synthetic fixtures and offline PWA test
npm run validate:all # include browser tests in full validation

Exporter et importer les données

Dans Réglages → Gérer les données et sauvegardes, l’écran Données et sauvegardes regroupe l’export, l’import, la vérification des sauvegardes et la durée de conservation. Le rappel Sauvegarder ouvre aussi cet écran.

Les données restent disponibles dans le navigateur sur cet appareil. La synchronisation chiffrée peut être activée séparément pour les partager avec vos autres appareils. Effacer les données du site ou perdre l’appareil peut faire perdre l’historique : gardez une copie vérifiée du fichier exporté sur un autre support.

Exporter les données télécharge un fichier pardis-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: 2. Le format v1 reste importable. Les anciennes applications refusent le format v2 pour éviter de transformer les pauses en travail. 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.

Dans Durée de conservation, Conserver (jours) détermine les journées anciennes proposées à la purge. Modifier cette durée ne supprime rien ; la purge automatique est désactivée. L’historique conservé alimente les suggestions et les statistiques. La dernière journée terminée et les journées actives sont conservées.

Purger maintenant présente les dates concernées et demande confirmation. Une sauvegarde locale est conservée avant la suppression ; Récupérer la sauvegarde avant purge permet de la fusionner ou de remplacer les données, puis de l’exporter après restauration. Cette copie est remplacée à la prochaine purge et disparaît si les données du site sont effacées : un fichier externe reste nécessaire.

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 aujourd’hui propose Reprendre la journée avec confirmation : prolonger le dernier poste, ou créer une pause jusqu’à maintenant (choix initial). Un dernier repas ou une pause garde sa nature ; le travail reprend dans un nouveau poste. Les journées passées se corrigent par Voir les détails → Corriger cette journé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. Les transitions restent manuelles sauf si l'option Pause déjeuner est activée dans les automatismes.
  • 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.

Automatismes

Dans Réglages → Automatismes, chaque comportement a son interrupteur :

Option Comportement
Ouverture Démarrer aux horaires et jours prévus. Activée par défaut, comme auparavant.
Début de journée Au bouton « Arrivé sur site », proposer la dernière tâche de travail de la précédente journée enregistrée, hors repas et transport.
Pause déjeuner Ouvrir la pause à l'heure habituelle et reprendre la tâche précédente après la durée réglée. Reprendre le type de repas et le restaurant de la précédente journée enregistrée.
Arrivée sur chantier Lors du repas automatique, transformer un transport initial non renseigné en un transport de 1/5/15/30 minutes, selon l'arrondi, suivi de la tâche précédente.
Fin de demi-journée À la pause ou à la clôture, remplacer une plage automatique non renseignée par la dernière tâche de travail connue. Conserver tout poste manuel.
Fin de journée Clôturer à l'heure prévue sans confirmation.

Les cinq nouvelles options sont désactivées par défaut. Elles sont conservées dans les sauvegardes et rétablies lors d'un import en remplacement. L'option Arrivée sur chantier intervient seulement avec un repas automatique. Sans cette estimation de transport, Fin de demi-journée peut remplacer le transport initial non renseigné par du travail sur toute la plage. Une tâche de travail déjà présente suffit : aucun doublon n'est ajouté à la demi-journée.

« Précédente » désigne la dernière journée terminée enregistrée avant la date courante, y compris après un week-end. La complétion utilise la dernière tâche de travail connue ; sans historique de travail, elle utilise Travaux sur site. Les postes produits automatiquement portent la mention Auto. Les corriger les rend manuels et les protège des déductions suivantes. Les saisies anciennes sans cette information sont considérées comme manuelles.

Une pause commencée ou corrigée manuellement n'est pas terminée par l'automatisme de déjeuner. Un choix manuel du type ou du restaurant est conservé. Une pause supprimée n'est pas recréée. Désactiver l'option arrête les transitions suivantes ; cela ne supprime pas les postes déjà enregistrés. Une durée de déjeuner nulle n'ajoute aucune pause automatique. La durée d'une pause automatique déjà commencée est conservée si le réglage change ensuite.

L'app vérifie l'horloge toutes les 30 secondes et au retour à l'écran. Une PWA fermée ou suspendue ne peut pas exécuter du code à heure fixe : à la réouverture, les étapes manquées sont rattrapées avec leurs horaires prévus, sans écraser une action manuelle plus tardive. Les journées déjà clôturées restent intactes et les dates entièrement absentes ne sont pas créées. Pour une journée sans intervention, activer les options voulues et ouvrir l'app au moins une fois pour cette journée.

Les prolongations et la reprise d'une journée sont respectées. Un démarrage manuel laisse au moins une heure avant une clôture automatique. Les transitions automatiques sont transactionnelles : une erreur annule toute l'opération et des appels simultanés ne créent pas de doublons.

Jours non travaillés

Dans Réglages → Jours non travaillés, ajouter les dates à bloquer à l'avance. La liste est triée par date ; chaque date peut être modifiée ou supprimée. Les doublons sont refusés. Ces exceptions priment sur les jours de semaine habituellement travaillés et empêchent l'ouverture automatique, y compris lors du rattrapage à la réouverture de l'app ou hors ligne.

Aucune journée ni aucun poste n'est créé pour une date bloquée. Les journées déjà commencées ou terminées restent conservées. Dans Journée, le bouton Démarrer quand même permet un démarrage manuel exceptionnel. Retirer une date rétablit les règles habituelles de démarrage automatique.

La liste est enregistrée avec les réglages et incluse dans l'export des données. Un import en remplacement rétablit la liste de la sauvegarde ; un import en fusion conserve la liste locale, comme les autres réglages. Une ancienne sauvegarde sans cette liste la réinitialise à vide lors d'un remplacement.

Corriger les postes et les journées terminées

Le Total des heures de présence exclut les pauses et le repas. Le repas reste affiché séparément dans le récapitulatif et le texte copié. Les totaux suivent le réglage d’arrondi existant.

Dans Journée, touchez un poste dans Postes de la journée pour modifier son début, sa fin, sa tâche ou son client. Les boutons Poste ordinaire, Repas et Pause permettent de choisir la nature du poste. Le type interne d'un transport déjà enregistré est conservé quand on corrige ses horaires ou son libellé. Les sélecteurs d'heure ne proposent pas de date : ils conservent la date du poste. Les minutes et les boutons d'ajustement suivent l'arrondi choisi dans les réglages (1, 5, 15 ou 30 minutes). Les tâches, clients et restaurants utilisent les mêmes suggestions et la même saisie libre que l'écran de la journée. Les corrections sont enregistrées ensemble avec Enregistrer. Annuler ne modifie rien ; ouvrir puis valider un horaire inchangé conserve sa valeur enregistrée. 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. Le début et la fin sont préremplis avec les deux bornes connues (jusqu'à l'heure actuelle pour un poste en cours). Il suffit de changer une seule borne pour corriger le début ou la fin de la 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 bouton Repas permet aussi d’ajouter un déjeuner oublié et de renseigner panier ou restaurant ; Pause ajoute une pause hors repas. Pour ajouter juste après une frontière existante, choisissez le poste qui commence à cette frontière.

Dans Récap, choisissez la date puis Voir les détails → 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é. Supprimer un repas réaffecte sa durée au poste voisin et peut donc changer le total des heures de présence.

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.

Statistiques

Ouvrez Réglages → Consulter les statistiques. Le bouton Stats de la barre du bas n'apparaît que sur cette page ; les autres écrans gardent leurs trois boutons habituels. La page fonctionne hors ligne. Les calculs de statistiques restent sur l’appareil ; la synchronisation facultative peut partager leur historique source chiffré.

Les filtres couvrent tout l'historique, le mois, l'année, les 30 derniers jours ou une plage de dates inclusive. Les journées en cours sont exclues par défaut. L'option pour les inclure affiche des cumuls et détails provisoires. Les moyennes de « Repères et observations », la médiane, les extrêmes et les profils par jour de semaine restent calculés sur les seules journées terminées enregistrées.

Les statistiques présentent :

  • Les heures de présence hors repas et pauses, les pauses, le transport identifié, les nombres de journées, postes, tâches et clients.
  • Les heures de présence par client et par tâche, leurs parts des heures de présence, les nombres de journées concernées et les détails croisés, accessibles en ouvrant une ligne.
  • L'évolution par mois ou semaine, sans inventer des heures pour les jours absents.
  • Les journées avec repas panier ou restaurant, les restaurants fréquentés, les durées de pause et les journées sans pause enregistrée. Plusieurs fragments de repas le même jour ne créent pas plusieurs journées de repas.
  • Les moyennes, la médiane, les journées extrêmes, les principales concentrations de temps par client et tâche, et le détail quotidien.
  • Les clients ou restaurants non renseignés, les postes à zéro après arrondi, les journées vides, les plages non couvertes et les chevauchements éventuels.

Le calcul reprend l'arrondi du récapitulatif et exclut toujours le repas et les pauses des heures de présence. Seules les données encore conservées sont analysées : les jours absents ou purgés ne permettent pas de conclure à une absence de travail. Le transport identifié repose sur le type enregistré, pas sur une déduction à partir du nom de la tâche.

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 contains the static app served by Caddy and an optional-use synchronization API backed by SQLite in a persistent volume. Caddy obtains and renews the HTTPS certificate. Synchronization stays disabled in each client until explicitly activated. See SYNC.md for pairing, encryption, recovery, quotas and server backups.

cp .env.example .env
$EDITOR .env          # set SITE_ADDRESS, ACME_EMAIL and TRUST_PROXY
docker compose up -d --build

Requirements:

  • SITE_ADDRESS must already resolve to the server.
  • ACME_EMAIL is required and must be a valid contact address.
  • 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 bind-mounted ./caddy/data directory. 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

The current Caddy configuration terminates TLS itself and accepts PROXY protocol from the address or CIDR in TRUST_PROXY. Set this value to the real upstream proxy address; 192.0.2.10/32 in .env.example is a documentation placeholder. LISTEN_IP selects the local interface for the published ports and defaults to 0.0.0.0. A deployment that terminates TLS elsewhere needs its own Caddy and port configuration; changing SITE_ADDRESS alone does not configure that mode.

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. Time pickers follow the selected rounding grid; opening an unchanged time preserves its stored timestamp. Summaries apply the same 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.

Pauses et nouveaux parcours

Journée → Commencer une pause ferme le poste courant. Reprendre le travail propose la dernière tâche de travail et son client, immédiatement modifiables. Une pause a ses propres horaires et son libellé, sans client ni type de repas. Le détail montre tous les postes ; la consultation reste séparée de la correction. Le récapitulatif et sa copie excluent les pauses. Le transport entre dans le total des heures de présence, les repas et pauses en sont exclus. Seules les statistiques montrent le cumul des pauses, séparément des repas. Les intervalles vides importés sont préservés.

Réglages → Gérer les automatismes et Gérer les jours non travaillés ouvrent des pages dédiées ; le bouton Retour du navigateur fonctionne via les routes hash. Gérer les données et sauvegardes ouvre aussi une page dédiée, avec retour aux réglages, conservation de la durée après rechargement et accès hors ligne. Les réglages sont écrits par transaction sur la dernière valeur persistée pour préserver les modifications d’autres onglets. Les erreurs de démarrage et actualisation sont affichées avec une action Réessayer.

La checklist avant production et les validations restant à exécuter sont suivies dans PRODUCTION.md.

Sauvegarde vérifiée et validation navigateur

Exporter les données prépare le fichier et enregistre son empreinte complète. Vérifier le fichier exporté… relit le fichier téléchargé, contrôle qu’il est importable et compare son contenu à l’export. La date de la dernière sauvegarde vérifiée est distincte de celle du dernier export préparé. Après sept jours sans export vérifié, Pardis affiche un rappel pendant son utilisation. L’export reste local ; conservez le fichier hors de cet appareil. La synchronisation facultative utilise un coffre distinct et ne remplace pas cette sauvegarde.

La purge affiche une liste figée de journées. Sauvegarde et suppression partagent une transaction ; une correction concurrente bloque la purge et demande une nouvelle consultation. Les autres journées récemment importées restent intactes.

Les tests Playwright utilisent des contextes neufs et des données synthétiques, à 320 px. Ils vérifient consultation/correction, focus, Retour, reprise à deux onglets, export vérifié, purge/restauration et rechargement hors ligne. Ils couvrent aussi le nouvel écran Données, le retour du champ de conservation à sa valeur persistée après un échec, le refus d’un export modifié et l’annulation complète d’une purge en erreur, avec préservation de la sauvegarde précédente. Traces, vidéos et captures sont désactivées. Pour un Chrome déjà installé, définir PARDIS_CHROME_PATH avec son chemin ; sinon installer Chromium avec Playwright. La CI installe Chromium Headless Shell et ses dépendances avant de lancer ces tests. Elle télécharge uniquement la variante utilisée par les tests sans interface et accorde 180 secondes à chaque connexion de téléchargement, avec les nouvelles tentatives intégrées à Playwright. L’installation dispose de 20 minutes et le job de validation de 30 minutes. Une erreur d’installation bloque le déploiement.

Synchronisation facultative entre appareils

Données et sauvegardes → Synchronisation et appareils permet de créer un coffre chiffré sans e-mail ni mot de passe, puis de lier des appareils avec un code temporaire ou un QR code et une confirmation sur l’écran dédié. La saisie reste locale et fonctionne hors ligne. Les journées concurrentes sont conservées pour résolution. Un document de secours permet de retrouver l’accès après perte des appareils.

Les règles métier sont partagées ; le thème et le suivi des sauvegardes restent locaux. Un appareil est responsable des automatismes, avec proposition de transfert à l’ouverture sur un appareil secondaire. Les demandes d’association ne produisent aucune alerte ailleurs dans l’application.

Le protocole, les limites de sécurité, le fonctionnement des suppressions et les commandes de sauvegarde/restauration du serveur sont documentés dans SYNC.md. Pour le développement, lancer aussi npm run sync:server avec Node.js 24.