Added fonts
All checks were successful
Tests et déploiement / validate (push) Successful in 2m54s
Tests et déploiement / deploy (push) Successful in 41s

Added license file and updated README
This commit is contained in:
2026-10-11 12:29:04 +02:00
parent e53ae6f8be
commit 48445ab328
11 changed files with 1560 additions and 482 deletions

613
README.md
View File

@@ -1,395 +1,394 @@
# 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.
An offline-first progressive web app (PWA) for tracking on-site working hours. While
the app is open, it can start a workday automatically and prompt to close it. Each
task starts when the previous one ends. 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.
Designed for outdoor use: high contrast, large touch targets, and usable with gloves.
Data is stored locally in IndexedDB. Optional end-to-end encrypted synchronization
shares it between authorized devices without requiring an email address or password.
French documentation: [README.fr.md](README.fr.md).
## 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
```
npm install
npm run dev # http://localhost:5173
npm test # unit and IndexedDB regression tests
npm run check # Svelte diagnostics
npm run build # production build into dist/
npm run validate # tests, Svelte diagnostics 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
## Exporting and importing data
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.
In **Settings (Réglages) → Manage data and backups (Gérer les données et
sauvegardes)**, the **Data and backups (Données et sauvegardes)** screen groups
export, import, backup verification, and retention settings. The **Back up
(Sauvegarder)** reminder also opens this screen.
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.
Data remains available in the browser on this device. Optional encrypted
synchronization can share it with your other devices. Clearing site data or losing
the device can result in lost history: keep a verified copy of the exported file on
another storage device.
**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.
**Export data (Exporter les données)** downloads a file named
pardis-YYYY-MM-DD.json. It contains all retained days, their entries, and saved
settings. Export works offline and does not modify the device's data. Internal entry
identifiers are regenerated on 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 :
To restore a backup, choose **Import file… (Importer un fichier…)**, then review the
file name, counts, and any reported adjustments. Two modes are available:
- **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.
- **Merge (Fusionner)** adds only dates that are not already present. An existing
date is kept in full even if the file contains a different version of its entries.
Local settings are preserved. Importing the same file twice creates no duplicates.
- **Replace all (Remplacer tout)** asks for confirmation, then replaces every day,
entry, and setting, including a local day currently in progress. If the file has no
settings, defaults are restored.
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.
Imported days are marked as closed. For a day exported while in progress, the last
entry is stopped at the export time, preserving elapsed time. Existing gaps between
work periods are preserved. An empty backup is valid and can restore an empty device.
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.
The JSON format is identified by format: "timelog-export" and version: 2. Version 1
files remain importable. Older app versions reject version 2 to avoid turning breaks
into work. Foreign files, unsupported versions, duplicate dates, and invalid
settings are rejected. Unusable entries and time-range corrections are reported
before confirmation. The size limit is 20 MB. Writes are transactional: a failure
cancels the entire merge or replacement. The summary, theme, and suggestions then
refresh without reloading the 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.
Under **Retention (Durée de conservation)**, **Keep (days) (Conserver (jours))**
determines which older days are offered for deletion. Changing this setting does not
delete anything; automatic deletion is disabled. Retained history feeds suggestions
and statistics. The latest closed day and active days are always kept.
**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.
**Delete now (Purger maintenant)** lists affected dates and asks for confirmation.
A local backup is kept before deletion. **Recover pre-deletion backup (Récupérer la
sauvegarde avant purge)** can merge or replace data, then export it after recovery.
This copy is replaced by the next deletion and is removed if site data is cleared; an
external file is still necessary.
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.
Import/export tests cover the format, settings, duration fidelity, invalid files,
merge, replacement, and transaction rollback using a simulated IndexedDB.
src/lib/fixtures/backup.json is the supplied test fixture; it is not published with
the app.
## Journées, reprise et historique
## Workdays, resuming, and history
- 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.
- If a workday was left open, it is closed when the app is next opened, at its planned
end time (including overtime), but never before the last recorded activity. This is
an estimate: the app does not measure work while it is closed. The final entry
keeps its duration up to that limit instead of being reset to zero.
- A day closed today offers **Resume workday (Reprendre la journée)** with
confirmation: continue the last entry, or create a break up to now (the default).
A final meal or break keeps its type; work resumes in a new entry. Past days can be
corrected via **View details → Edit this day (Voir les détails → Corriger cette
journée)**.
- Creating, closing, and resuming a day are transactional. Simultaneous actions on
the same entry cannot create duplicates; a closed day cannot receive a new active
entry. Deletion skips active days and always keeps the latest closed day.
- Lunch schedules show a reminder and an indicative return time. Transitions remain
manual unless **Lunch break (Pause déjeuner)** is enabled under automations.
- In **Summary (Récap)**, the **Day to view (Journée à consulter)** selector shows
and copies every retained closed day, including imported days.
Les tests couvrent notamment les clôtures oubliées, les reprises, les appels
concurrents, les retours d'horloge, les échecs transactionnels et la conservation.
Tests cover missed closures, resuming, concurrent calls, clock changes, transaction
failures, and data retention.
## Automatismes
## Automations
Dans **Réglages → Automatismes**, chaque comportement a son interrupteur :
In **Settings (Réglages) → Automations (Automatismes)**, each behavior has its own
switch:
| Option | Comportement |
| Option | Behavior |
| --- | --- |
| 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. |
| Opening | Start at the scheduled times and on scheduled days. Enabled by default, as before. |
| Start of day | When tapping “Arrived on site” (Arrivé sur site), suggest the last work task from the previous recorded day, excluding meals and travel. |
| Lunch break | Start a break at the usual time and resume the previous task after the configured duration. Reuse the meal type and restaurant from the previous recorded day. |
| Site arrival | During an automatic meal, turn an unspecified initial travel period into a travel entry of 1/5/15/30 minutes, according to the rounding setting, followed by the previous task. |
| Half-day end | At a break or day closure, replace an unspecified automatic period with the last known work task. Preserve any manual entry. |
| End of day | Close the day at the scheduled time without 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.
The five newer options are disabled by default. They are included in backups and are
restored by replacement imports. **Site arrival** only applies with an automatic
meal. Without this travel estimate, **Half-day end** may replace unspecified initial
travel with work for the entire period. If a work task is already present, no
duplicate is added to the half-day.
« 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.
“Previous” means the latest recorded closed day before the current date, including
after a weekend. Completion reuses the most recent known work task; when there is no
work history, it uses “On-site work” (Travaux sur site). Automatically created
entries are marked **Auto**. Editing one makes it manual and protects it from later
inference. Older entries without this marker are treated as manual.
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.
A manually started or edited break is not ended by the lunch automation. A manually
chosen meal type or restaurant is preserved. A deleted break is not recreated.
Disabling an option stops future transitions; it does not remove entries already
saved. A zero lunch duration does not add an automatic break. The duration of an
automatic break already in progress is preserved if the setting changes later.
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.
The app checks the clock every 30 seconds and when it returns to the foreground.
A closed or suspended PWA cannot run code at a scheduled time: on reopening, missed
steps are caught up at their planned times without overwriting a later manual action.
Already closed days remain unchanged, and entirely missing dates are not created.
For a day with no intervention, enable the desired options and open the app at least
once that day.
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.
Overtime and resumed days are respected. A manual start leaves at least one hour
before an automatic closure. Automatic transitions are transactional: an error
cancels the whole operation, and simultaneous calls cannot create duplicates.
## Jours non travaillés
## Non-working days
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.
In **Settings (Réglages) → Non-working days (Jours non travaillés)**, add dates to
block in advance. The list is sorted by date; each date can be edited or removed.
Duplicates are rejected. These exceptions take precedence over the usual weekdays
and prevent automatic opening, including catch-up when the app is reopened or used
offline.
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.
No day or entry is created for a blocked date. Days already started or closed are
preserved. **Start anyway (Démarrer quand même)** in **Workday (Journée)** allows an
exceptional manual start. Removing a date restores the usual automatic start rules.
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.
The list is saved with settings and included in data exports. A replacement import
restores the backup's list; a merge keeps the local list, like other settings. An
older backup without this list resets it to empty on replacement.
## Corriger les postes et les journées terminées
## Editing entries and closed days
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.
The **Total hours present (Total des heures de présence)** excludes breaks and meals.
Meals remain separate in the summary and copied text. Totals use the selected
rounding setting.
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.
In **Workday (Journée)**, tap an entry under **Day entries (Postes de la journée)**
to edit its start, end, task, or client. **Regular entry (Poste ordinaire)**, **Meal
(Repas)**, and **Break (Pause)** buttons select the entry type. When editing the
times or label of an existing travel entry, its internal type is preserved.
**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.
Time selectors do not include a date: they keep the entry's date. Minutes and
adjustment buttons follow the selected rounding interval (1, 5, 15, or 30 minutes).
Tasks, clients, and restaurants use the same suggestions and free-text entry as the
workday screen.
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.
Corrections are saved together with **Save (Enregistrer)**. Cancel makes no changes;
opening and saving an unchanged time preserves its stored value. The first entry's
start and last entry's end define the day boundaries. Editing an intermediate
boundary adjusts the adjacent entry's boundary without overlap. The end of an active
entry remains controlled by task changes or day closure.
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.
**Add a missed entry (Ajouter un poste oublié)** lets you choose an existing period,
then the times for an entry to insert into it. The start and end are prefilled with
the known boundaries (up to now for an active entry). Change either boundary to
adjust one side of the period. The portions before and after are preserved with
their original task and client. For example, 08:00–12:00 can become 08:00–09:00,
a missed task from 09:00–10:00, then 10:00–12:00. **Meal (Repas)** can add a missed
meal and set its type to packed or restaurant; **Break (Pause)** adds a non-meal
break. To add an entry immediately after an existing boundary, choose the entry
that starts at that boundary.
## Statistiques
In **Summary (Récap)**, choose a date, then **View details → Edit this day (Voir les
détails → Corriger cette journée)**. The same actions are available without resuming
the day, even while another day is in progress. The summary and copied text are
recalculated after each save. Deleting an entry requires confirmation; its neighbor
absorbs its duration, and the final entry of a day cannot be deleted. Deleting a meal
assigns its duration to the neighboring entry and can therefore change total hours
present.
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é.
Every correction is transactional. Reversed times, insertions outside the selected
period, and forms made stale by another edit are rejected without a partial write.
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.
## Statistics
Les statistiques présentent :
Open **Settings (Réglages) → View statistics (Consulter les statistiques)**. The
**Stats** button appears in the bottom bar only on this page; other screens keep
their usual three buttons. The page works offline. Statistics are calculated on the
device; optional synchronization may share their source history in encrypted form.
- 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.
Filters cover all history, the month, the year, the last 30 days, or an inclusive
date range. Days in progress are excluded by default. Enabling them shows
provisional totals and details. Averages under **Highlights and observations
(Repères et observations)**, the median, extremes, and weekday profiles use only
recorded closed days.
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.
Statistics include:
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.
- Hours present excluding meals and breaks, breaks, identified travel, and counts of
days, entries, tasks, and clients.
- Hours present by client and task, their shares of hours present, the number of
affected days, and cross-details available by opening a row.
- Monthly or weekly trends without inventing hours for missing days.
- Days with packed or restaurant meals, restaurants visited, break durations, and
days with no recorded break. Multiple meal fragments on one day count as one
meal day.
- Averages, median, extreme days, top client and task time concentrations, and
daily details.
- Unspecified clients or restaurants, entries rounded to zero, empty days, uncovered
periods, and possible overlaps.
Calculations use the summary's rounding and always exclude meals and breaks from
hours present. Only retained data is analyzed: missing or deleted days cannot be
taken as proof that no work happened. Identified travel uses the recorded entry type,
not an inference from the task name.
In development, the console exposes `timelog.seed()` to generate 20 days of realistic
history (useful for exercising suggestions) and `timelog.wipe()` to clear it.
## Deployment
The stack contains the static app served by Caddy and an optional-use synchronization
The stack consists of the static app served by Caddy and an optional 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](SYNC.md) for pairing, encryption, recovery, quotas and server backups.
certificate. Synchronization remains disabled in each client until explicitly
activated. See [SYNC.md](SYNC.md) for pairing, encryption, recovery, quotas, and
server backups.
```sh
cp .env.example .env
$EDITOR .env # set SITE_ADDRESS, ACME_EMAIL and TRUST_PROXY
docker compose up -d --build
```
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.
- Ports **80 and 443** must be reachable from the internet. Port 80 is mandatory:
the ACME challenge uses it 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.
Certificates are stored in the bind-mounted `./caddy/data` directory. Keep it:
without it, Caddy requests a new certificate after every recreation and will hit
Let's Encrypt rate limits after only a handful of restarts.
To update, pull and rebuild:
```sh
git pull && docker compose up -d --build
```
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.
Running clients do not reload automatically. Each app checks hourly for a new build
and offers an **Install (Installer)** banner, but only when no workday is in progress,
so an update cannot interrupt a workday.
### HTTPS is not optional
### HTTPS is required
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.
Browsers only register a service worker in a secure context. Served over plain
`http://` at an IP address or hostname, the app still loads but has **no offline
support, cannot be installed, and never updates**, defeating 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 current Caddy configuration terminates TLS itself and accepts the PROXY protocol
from the address or CIDR in `TRUST_PROXY`. Set this to the actual upstream proxy
address; `192.0.2.10/32` in `.env.example` is a documentation placeholder.
`LISTEN_IP` selects the local interface for 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`.
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.
**Raw timestamps are stored; rounding is applied only when rendering.** 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
when rendered.
**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.
**Suggestions are derived from history, not counter tables.** Nothing needs to be
kept in sync; the model is rebuilt in memory from 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
```
src/lib/
time.js local dates, rounding, formatting (pure)
predict.js suggestion engine (pure)
resume.js logbook text builder (pure)
db.js Dexie schema and 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.
Pure helpers and database/lifecycle operations have regression tests. `db.js` keeps
day and entry changes in shared transactions, including creation, closure, resuming,
and boundary corrections.
## Pauses et nouveaux parcours
## Breaks and newer workflows
**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.
**Workday → Start a break (Journée → Commencer une pause)** closes the current entry.
**Resume work (Reprendre le travail)** suggests the last work task and its client,
which can be edited immediately. A break has its own times and label, without a
client or meal type. Details show every entry; viewing is separate from editing.
Summaries and copied text exclude breaks. Travel is included in total hours present;
meals and breaks are excluded. Statistics show total break time separately from meals.
Imported gaps are preserved.
**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.
**Settings (Réglages) → Manage automations (Gérer les automatismes)** and
**Manage non-working days (Gérer les jours non travaillés)** open dedicated pages;
the browser Back button works through hash routes. **Manage data and backups (Gérer
les données et sauvegardes)** also opens a dedicated page, with a return to settings,
retention persistence after reload, and offline access.
La checklist avant production et les validations restant à exécuter sont suivies
dans [PRODUCTION.md](PRODUCTION.md).
Settings are written transactionally against the latest persisted value to preserve
changes made in other tabs. Startup and refresh errors are shown with a Retry action.
## Sauvegarde vérifiée et validation navigateur
The production checklist and remaining validation steps are tracked in
[PRODUCTION.md](PRODUCTION.md).
**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.
## Verified backups and browser validation
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.
**Export data (Exporter les données)** prepares a file and saves its full fingerprint.
**Verify exported file… (Vérifier le fichier exporté…)** reads the downloaded file,
checks that it can be imported, and compares its contents with the prepared export.
The date of the last verified backup is separate from the most recent prepared export.
After seven days without a verified export, Pardis shows a reminder while in use.
Export remains local; keep the file outside this device. Optional synchronization
uses a separate vault and does not replace backups.
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.
Deletion displays a fixed list of days. Backup and deletion share a transaction; a
concurrent correction blocks deletion and requires reviewing the list again. Other
recently imported days remain untouched.
Playwright tests use fresh contexts and synthetic data at 320 px. They cover
viewing/editing, focus, Back navigation, resuming in two tabs, verified export,
deletion/recovery, and offline reload. They also cover the dedicated Data screen,
restoring a retention field after a save failure, rejecting a modified export, and
fully rolling back a failed deletion while preserving the previous backup. Traces,
videos, and screenshots are disabled. To use an installed Chrome, set
`PARDIS_CHROME_PATH` to its path; otherwise install Chromium with Playwright.
CI installs Chromium Headless Shell and its dependencies before running the tests.
It downloads only the headless browser build used by the tests and gives each
download attempt 180 seconds, with Playwright's built-in retries. Browser setup has
a 20-minute limit and the validation job has a 30-minute limit. An installation
failure blocks deployment.
## Synchronisation facultative entre appareils
## Optional device synchronization
**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.
**Data and backups (Données et sauvegardes) → Synchronization and devices
(Synchronisation et appareils)** creates an encrypted vault without an email or
password, then links devices using a temporary code or QR code and confirmation
on the dedicated screen. Data entry stays local and works offline. Concurrent days
are retained for conflict resolution. A recovery document restores access after
losing the devices.
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.
Business rules are shared; the theme and backup tracking stay local. One device is
responsible for automations, with a transfer prompt when opening the app on a
secondary device. Pairing requests do not create alerts elsewhere in the app.
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](SYNC.md).
Pour le développement, lancer aussi `npm run sync:server` avec Node.js 24.
The protocol, security limits, deletion behavior, and server backup/restore commands
are documented in [SYNC.md](SYNC.md). For development, also run `npm run sync:server`
with Node.js 24.
## License and font credits
Pardis is distributed under the GNU General Public License, version 3 only
(GPL-3.0-only). See [LICENSE.md](LICENSE.md) for the full license text. The bundled font
files are separate works and remain under the SIL Open Font License, version 1.1;
their license and copyright notices are included in [public/fonts/OFL.txt](public/fonts/OFL.txt).
Copyright (C) 2026 EI Adrien Weiskopff.
- **Bebas Neue**, by Ryoichi Tsunekawa - SIL Open Font License 1.1.
- **Charis SIL** - SIL Open Font License 1.1. See the [official Charis SIL site](https://software.sil.org/fr/charis/).