apps.header.allAppsCount
pocketwatch
apps.detail.types.app from winnicodes' Repository
apps.detail.sections.overview
Readme
View on GitHubpocketwatch
Time tracking for freelancers and the self-employed. One click to start, one PDF to invoice — and every byte stays on your own machine.

🚀 Try it in 30 seconds
▶ Open the live demo — or download it (right-click → save) and double-click the file to run it offline.
One single HTML file: the whole app with demo data, no server, no Docker, no internet. Start, stop, search, filter, edit, export PDF and CSV — all of it real. The demo saves nothing; a reload puts it back to where it started.
The file is not a mockup, it is a build of the app:
cd webapp && npm ci && npm run standalone # writes ../docs/index.html
💡 Why pocketwatch?
| 🔒 Your data stays yours | Two JSON files on your own disk. No account, no subscription, no tracking, not a single request leaving the machine — even the fonts ship inside the container. |
| ⚡ As fast as a pocket calculator | No spinner, no database. Start and stop are one click, the timer keeps counting to the second and survives a reload. |
| 🧾 Ready to invoice | A PDF report with your name on it, or CSV for your spreadsheet and bookkeeping — either the filtered view or a date range you pick. |
| 📱 Works in either hand | The same app on a 27-inch monitor and on a phone: two columns here, cards and a drawer there. |
| 🐳 One container, done | docker compose up -d --build. Runs on a NAS, a Raspberry Pi or your laptop. |
🎬 The app, feature by feature
⏱️ Track — client, activity, go

- Client autocomplete — most recently used clients first, pick with the arrow keys
- Clock In starts immediately; the activity text can still be typed while the timer runs
- Live timer to the second, with Today and Week totals right below it
- Forgot the client name? The entry lands under “No client” — nothing gets lost
- Timer running for more than 8 hours → “forgot to clock out?” hint (can be turned off)
📜 History — grouped by day, totalled at the bottom

- Grouped by day with a date heading, total duration of the current selection always visible in the footer
- Full-text search across client and activity, filtering as you type
- Day headings stick to the top while you scroll (can be turned off)
- Long activity texts are clipped and expand on “more”
- Large histories load in chunks — the totals still count every match, not just the visible rows
🗓️ Period — day, week, month, year, and step back through time

Click Day / Week / Month / Year, then step backwards with the arrows — the total is recalculated for every period. “All” brings back the complete history.
✏️ Edit — fix it, or add what you forgot

- Client, activity, start and end by date and time — the duration updates live
- Custom date and time pickers instead of browser chrome: same design, fully keyboard-operable
- Invalid times are rejected (end before start, a half-filled end)
- Deleting always asks first — and unsaved changes ask before the dialog closes
📤 Export — PDF for the client, CSV for the office

- PDF: formatted report with your name, page numbers, optionally the export date in the footer
- CSV: semicolon-separated with a BOM — Excel opens it without mangling accented characters
- Current view (inherits search and period filter) or a custom date range
- Newest first and times only, no activities for the compact list
- The dialog tells you up front how many entries and how many hours will end up in the file
⚙️ Settings — including rounding

- Name for the header of the PDF export
- German / English and 12h / 24h — date formats follow along
- Week total in the header and sticky day headings on or off
- Round times to a fixed interval (1–60 min), optionally always upwards — with a live example (“00:37 becomes 00:30”). Rounding applies everywhere: history, totals, PDF, CSV.
- Reminder for timers left running
📱 Responsive — same app, different hand
Below 1024 px the two-column layout becomes one view at a time, switched through the drawer. Search, filter and export move into the header, table rows turn into cards and dialogs into full-screen views — no shrunk-down desktop, no pinch-zooming, no second app.
![]() Track — running timer |
![]() History — cards, not a table |
![]() Drawer — switch sections |
![]() Filter — period as pills |
![]() Edit — full screen |
![]() Export — same options |
![]() Settings — all of them |
Manifest, icons and theme colour are included: “Add to Home Screen” gives pocketwatch its own icon that opens without a browser bar. |
✨ Every feature at a glance
Tracking
- One-click start/stop, live timer accurate to the second
- A running entry survives a reload or a device switch (it lives in
times.json) - Client autocomplete, activity editable at any time
- Totals for today and the current week
- Warning for timers running longer than 8 hours
History
- Grouped by day, total duration of the filtered selection in the footer
- Full-text search across client and activity
- Period filter day/week/month/year/all with stepping backwards and forwards
- Sticky day headings, expandable activity texts, chunked loading
Editing
- Client, activity, start and end (date + time), duration calculated live
- Custom date/time pickers, fully keyboard-operable
- Validation of impossible times, confirmation before deleting or discarding
Export
- PDF report (name, page numbers, optional export date)
- CSV for Excel and bookkeeping (semicolon + BOM)
- Current view or custom date range, sort direction, “times only” variant
Settings
- German/English, 12h/24h
- Rounding to 1–60 min, to the nearest interval or always upwards
- Week total, sticky day headings, long-run reminder
- Name for the PDF export
Under the hood
- Storage in
data/times.jsonanddata/config.json, written atomically - Writes are debounced; nothing is lost when the tab is closed
- Docker container (Nginx + PHP-FPM on Alpine)
- Self-hosted fonts, no CDN, no telemetry
- Keyboard-operable, ARIA roles for switches, listboxes and dialogs
🐳 Install with Docker (recommended, Windows + Linux)
A ready-built image is on the GitHub Container Registry, so there is nothing to compile and you do not need Node on the host:
docker run -d --name pocketwatch -p 8080:80 \
-v "$(pwd)/data:/var/www/html/data" \
ghcr.io/winnicodes/pocketwatch:latest
Open http://localhost:8080 — your time entries live in the mounted data/ folder.
Tagged releases are published under their version as well, e.g.
ghcr.io/winnicodes/pocketwatch:1.0.1. Pin that if you would rather decide
yourself when to update.
Unraid
Docker → Add Container, then:
| Field | Value |
|---|---|
| Repository | ghcr.io/winnicodes/pocketwatch:latest |
| Port | 8080 (host) → 80 (container) |
| Path | /mnt/user/appdata/pocketwatch (host) → /var/www/html/data (container) |
The image is linux/amd64, which is what Unraid runs.
Build it yourself
The container compiles the frontend from source — still no Node on the host.
git clone https://github.com/winnicodes/pocketwatch.git
cd pocketwatch
docker compose up -d --build
Your time entries then live in the data/ folder next to docker-compose.yaml.
Without docker-compose
Linux / macOS:
docker build -t pocketwatch-app .
docker run -d -p 8080:80 -v "$(pwd)/data:/var/www/html/data" --name pocketwatch pocketwatch-app
Windows (PowerShell):
docker build -t pocketwatch-app .
docker run -d -p 8080:80 -v "${PWD}/data:/var/www/html/data" --name pocketwatch pocketwatch-app
Docker does not accept relative paths like
./datafor-v— hence$(pwd)/${PWD}.
The container fixes the permissions on data/ at startup, so a folder created by root is
not a problem.
PowerShell helpers
Small wrappers for Windows are included: _docker-build.ps1, _docker-run.ps1,
_docker-stop.ps1, _docker-restart.ps1, _docker-logs.ps1, _docker-cleanup.ps1.
🛡️ Security & privacy
pocketwatch collects nothing, sends nothing and loads nothing. No analytics, no fonts from Google, no external API — the container only ever talks to itself.
Important: the API deliberately has no authentication — pocketwatch is meant to run on your own LAN. Do not expose the container to the open internet: whoever reaches the URL can read and change every time entry. If you need access from outside, put a reverse proxy with authentication or a VPN in front of it.
💾 Data persistence
Everything lives in the mounted data/ folder:
data/times.json— all time entriesdata/config.json— settings (name, language, time format, rounding …)
Writes go to a temporary file and are then renamed into place: an interrupted write cannot
destroy a good file. As long as the folder is mounted on the host, your data survives every
update and every container rebuild. _docker-cleanup.ps1 deliberately leaves data/ alone.
For a backup, copying the data/ folder is enough.
📁 Project layout
pocketwatch/
├── webapp/ # React frontend (Vite)
│ ├── src/
│ ├── public/
│ │ ├── api/ # PHP API (read.php / write.php)
│ │ ├── fonts/ # Instrument Sans / Space Mono (self-hosted)
│ │ └── locales/ # de.json / en.json
│ ├── scripts/ # demo-data.mjs, standalone.mjs
│ ├── dist/ # build output (not in Git)
│ └── package.json
│
├── data/ # persistent data (not in Git)
│ ├── times.json # time entries
│ └── config.json # settings
│
├── docs/ # published via GitHub Pages
│ ├── index.html # single-file demo (npm run standalone)
│ └── *.png # screenshots for this README
│
├── Dockerfile # production container (Nginx + PHP + dist)
├── nginx.conf # Nginx configuration
├── docker-compose.yaml # the recommended way to start
└── README.md
🛠️ Local development
Requires Node.js 20+.
cd webapp
npm ci
npm run dev
Vite serves http://localhost:5173 with hot reload. No container needed: Vite cannot run
PHP, but vite.config.ts reimplements api/read.php and api/write.php for development
and works on the same data/ folder as the container. PHP remains authoritative in
production.
More scripts:
npm test # tests for the time validation (node:test, no extra dependencies)
npm run build # typecheck (tsc) + production build into webapp/dist
npm run demo-data -- --running # demo data into data/ (--force overwrites, --en for English)
npm run standalone # single-file demo (see above)
🔁 Updates
git pull
docker compose up -d --build
🚀 Tech stack
React 19 · TypeScript · Tailwind CSS 4 · Vite 7 · date-fns · react-day-picker · jsPDF + jsPDF-AutoTable · PHP 8.4 (FPM) · Nginx · Alpine Linux
❤️ Support
Please file issues and feature requests through GitHub Issues.
If pocketwatch saves you time, you can buy me a coffee: ko-fi.com/winnicodes — the same link sits at the bottom left inside the app.
📜 License
MIT — see LICENSE. Take it, use it, change it.
apps.detail.sections.mediaGallery
1 / 3apps.marketingCta.appInstallTitle
apps.marketingCta.appInstallDescription
apps.detail.sections.categories
apps.detail.sections.related
apps.detail.related.exploreCategories
apps.detail.related.exploreAllapps.detail.sections.links
apps.detail.sections.details
ghcr.io/winnicodes/pocketwatch:latestapps.detail.sections.runtime
- apps.detail.details.webui
http://[IP]:[PORT:80]- apps.detail.details.network
bridge- apps.detail.details.shell
sh- apps.detail.details.privileged
- false
apps.detail.sections.configuration
HTTP port for the WebUI.
- apps.detail.config.target
- 80
- apps.detail.config.default
- 8080
times.json and config.json live here. Back this folder up.
- apps.detail.config.target
- /var/www/html/data
- apps.detail.config.default
- /mnt/user/appdata/pocketwatch






