Files
pardis/README.md
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

397 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](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](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](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](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/).