348 lines
20 KiB
Markdown
348 lines
20 KiB
Markdown
# 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. 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/
|
||
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 → Données**, **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.
|
||
|
||
La purge automatique est désactivée. **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 la télécharger après restauration. Elle reste sur le même appareil : un export externe demeure 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 travaillé** exclut le repas, qui 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. La case **Pause déjeuner** distingue
|
||
le repas du travail : il n'y a plus de menu « Type de 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.
|
||
La case **Pause déjeuner** 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é.
|
||
Supprimer un repas réaffecte sa durée au poste voisin et peut donc changer le
|
||
total travaillé.
|
||
|
||
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 et ne transmet aucune donnée.
|
||
|
||
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 :
|
||
|
||
- Le travail hors repas, les pauses, le transport identifié, les nombres de
|
||
journées, postes, tâches et clients.
|
||
- Les heures par client et par tâche, leurs parts du travail, 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 du travail.
|
||
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 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, ACME_EMAIL and TRUST_PROXY
|
||
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 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:
|
||
|
||
```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
|
||
|
||
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
|
||
travaillé, 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.
|
||
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](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. Aucune donnée
|
||
n’est envoyée à un serveur ; conservez le fichier hors de cet appareil.
|
||
|
||
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. 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.
|