Adrien W. 64f61004d1
All checks were successful
Tests et déploiement / validate (push) Successful in 2m43s
Tests et déploiement / deploy (push) Successful in 1m21s
version 1.0.0
2026-10-11 12:51:47 +02:00
2026-10-10 11:31:18 +02:00
2026-09-06 10:24:05 +02:00
2026-10-11 12:51:47 +02:00
2026-10-11 12:29:04 +02:00
2026-10-10 16:51:43 +02:00
2026-10-11 12:51:47 +02:00
2026-10-11 09:16:57 +02:00
2026-10-11 10:19:41 +02:00
2026-09-06 13:50:58 +02:00
2026-09-20 15:54:19 +02:00
2026-09-06 13:50:58 +02:00
2026-10-11 12:29:04 +02:00
2026-10-11 12:51:47 +02:00
2026-10-11 12:51:47 +02:00
2026-10-11 12:51:47 +02:00
2026-10-11 12:51:47 +02:00
2026-09-06 10:24:05 +02:00
2026-10-10 17:10:00 +02:00
2026-10-11 12:51:47 +02:00

Pardis

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.

Current application version: 1.0.0.

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.

Development

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

Exporting and importing data

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.

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.

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.

To restore a backup, choose Import file… (Importer un fichier…), then review the file name, counts, and any reported adjustments. Two modes are available:

  • 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.

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.

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.

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.

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.

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.

Workdays, resuming, and history

  • 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.

Tests cover missed closures, resuming, concurrent calls, clock changes, transaction failures, and data retention.

Automations

In Settings (Réglages) → Automations (Automatismes), each behavior has its own switch:

Option Behavior
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.

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.

“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.

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.

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.

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.

Non-working days

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.

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.

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.

Editing entries and closed days

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.

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.

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.

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.

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.

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.

Every correction is transactional. Reversed times, insertions outside the selected period, and forms made stale by another edit are rejected without a partial write.

Statistics

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.

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.

Statistics include:

  • 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 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 remains 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 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 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:

git pull && docker compose up -d --build

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 required

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 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.

Architecture

Two decisions shape everything else.

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 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     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, resuming, and boundary corrections.

Breaks and newer workflows

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.

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.

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.

The production checklist and remaining validation steps are tracked in PRODUCTION.md.

Verified backups and browser validation

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.

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.

Optional device synchronization

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.

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.

The protocol, security limits, deletion behavior, and server backup/restore commands are documented in 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 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. 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.
Description
No description provided
Readme 1.9 MiB
Languages
JavaScript 70.7%
Svelte 27.7%
CSS 1.1%
Dockerfile 0.4%
HTML 0.1%