Offline-first PWA for work shift management, optimized for mobile devices.
Italian documentation is available in README_IT.md.
- Create, edit, and delete single and recurring shifts (daily, weekly, monthly, yearly)
- List view with date search and pagination, plus monthly calendar view with swipe gestures
- Overtime tracking and multiple allowances per shift
- Statistics dashboard with period summaries for hours, overtime, and allowances
- JSON backup/restore with validation, including password-protected encrypted backups
- Light/dark theme with automatic system preference detection
- Italian/English localization
- Local data encryption with 256-bit AES-GCM
- Service worker with offline-first caching and PWA update detection
- Local notifications on native platforms (Android via Capacitor)
| Technology | Version |
|---|---|
| Angular | 21.2.5 |
| TypeScript | 5.9.3 |
| Tailwind CSS | 4.2.2 |
| Capacitor | 8.3.0 |
| Jest | 30.3.0 |
| Cypress | 15.13.0 |
| Playwright | 1.58.2 |
# Requires Node.js 22+
# Install dependencies
npm install
# Development server (port 3000)
npm run dev
# Production build
npm run build
# Preview the production build
npm run previewThe development app is available by default at http://localhost:3000/.
# Linting and formatting
npm run lint # Check ESLint issues
npm run lint:fix # Fix auto-fixable ESLint issues
npm run format # Format with Prettier
npm run format:check # Verify formatting
# Tests
npm test # Jest unit tests (single run)
npm run test:watch # Jest in watch mode
npm run test:coverage # Coverage report (output in coverage/)
npm run e2e # Start dev server + run Cypress E2E tests
npm run test:pw:install # Install Chromium for Playwright
npm run test:pw # Playwright browser smoke/flows
npm run test:pw:ui # Playwright UI mode
npm run test:pw:headed # Playwright headed mode
npx tsc --noEmit # Standalone type check
# Mobile (Capacitor)
npm run build:mobile # Build + Capacitor sync
npm run android:dev # Open Android StudioThe application uses Angular standalone components with signal-based state management and OnPush change detection.
src/
app.component.ts/html # Main component, state, and modals
shift.model.ts # Shift, Repetition, and Allowance interfaces
components/
calendar.component.ts # Calendar view with touch gestures
shift-list-item.component.ts # Shift card
toast-container.component.ts # Toast notifications
services/
shift.service.ts # Shift CRUD, recurrences, encrypted persistence
crypto.service.ts # 256-bit AES-GCM encryption
calendar.service.ts # Calendar navigation and day grid
translation.service.ts # Internationalization
notification.service.ts # Native local notifications
toast.service.ts # Toast UI
sw-update.service.ts # PWA update handling
pipes/
translate.pipe.ts # Translation pipe
date-format.pipe.ts # Localized date pipe
assets/i18n/ # Translation JSON files (it.json, en.json)
- Data encrypted in
localStoragewith 256-bit AES-GCM - Device key persisted preferably in IndexedDB; legacy fallback to
localStorageonly when IndexedDB is unavailable - Exportable backups encrypted with a user password (
PBKDF2+AES-GCM) - Hardened Content Security Policy (CSP), with
unsafe-inlinestill allowed in local development forng servecompatibility - Automatic migration of legacy unencrypted data
- Data at rest is encrypted with a per-device AES-GCM 256-bit key. When IndexedDB is available the key is stored as a non-extractable
CryptoKey(extractable: false): the raw key bytes never leave the browser cryptography context. A small ciphertext header (ETBLOB1:) marks each encrypted record so legacy formats can be migrated transparently. - The device key is unique to this browser profile on this device. It is not derived from a password and is not synchronized to any backend. Clearing the browser data, switching device or losing the storage destroys the key — and with it the ability to read the encrypted shifts. There is no recovery path other than restoring a previously exported backup.
- Encrypted backups are produced with
PBKDF2-SHA256(600 000 iterations) + AES-GCM keyed from the user-supplied password. Backups created before the 600 000-iter bump (e.g. 250 000 iter) are still decryptable thanks to the per-payloaditerationsfield, but new exports always use the current parameters. The app always requires a password ≥ 12 characters at export time: there is no plaintext export path. - Imports accept either the password-protected format (most cases) or, only for backward compatibility, a legacy unencrypted JSON file. After import the shifts are immediately re-saved through the encrypted on-disk format.
- Server-side hardening: the deployed app ships
_headers(Cloudflare Pages / Netlify) with strict CSP, HSTS (preload+includeSubDomains),X-Content-Type-Options, COOP / CORPsame-origin, restrictivePermissions-Policy, andframe-ancestors 'none'. The PWA cannot be embedded in iframes. - Recommendation: export an encrypted backup as soon as you accumulate non-trivial data. The first time you reach 5 shifts the app shows a one-time reminder; you can repeat the export from Settings any time. Without a backup, a lost device or a cleared profile equals lost data.
Important note:
- Local storage encryption is useful against casual data exposure, but it is not strong protection against attackers who gain access to the browser execution context.
- The local encryption key is still managed in the same client context as the data, so its real protection level is lower than that of password-protected encrypted backups.
- The two recommended architectural evolutions to close this gap are:
- user password for local storage, with a key derived at each unlock and kept only in memory;
- a secret kept outside
localStorage, such as an authenticated backend or Capacitor secure native storage.
- Of the two, the first is the recommended direction for this project in terms of simplicity, security, and reliability.
- Build verified with Angular 21.2.5, TypeScript 5.9.3, and Tailwind CSS 4.2.2 (821.62 KB initial bundle)
- Unit tests: 319/319 passing (82% statement coverage)
- Cypress E2E: 55/55 passing
- Playwright browser flows: 13/13 passing
- Lint, type check, formatting, and local builds verified
- Full internationalization: all user-facing strings use translation keys (Italian/English)
- Password modal for encrypted backup export/import (replaces
window.prompt()) - Main remaining open items: native notification validation on a physical device and a future review of the local storage key strategy
See P.md for detailed status, open gaps, and the operational plan.
The project uses two browser testing layers:
- Cypress for the broader existing E2E suite
- Playwright for fast Chromium smoke/browser flows with automatic server management
Main files:
playwright.config.ts- runner, browser, and web server configurationplaywright/tests/smoke.spec.ts- smoke tests for app bootstrap, calendar toggle, and shift creationplaywright/tests/app-flows.spec.ts- additional browser flows: persistence, basic CRUD, theme/language, calendar, reset, recurring edit/delete, statistics rendering, encrypted backup/import with password, and wrong-password import error handlingplaywright/tests/helpers.ts- shared helpers for bootstrap and shift creation
MIT - Copyright (c) 2025 Leonardo



