- PHP 100%
| data | ||
| favicon | ||
| lang | ||
| .gitignore | ||
| .htaccess | ||
| api.php | ||
| config.php.sample | ||
| favicon.ico | ||
| index.php | ||
| README.de.md | ||
| README.md | ||
Post Office Box Tracker (PF)
A tiny multi-user web app for tracking visits to a shared post office box (Postfach). On every visit, log a timestamped count of what was inside — letters, magazines, advertising, and any additional media types you define.
- Single-file PHP app (
index.php+api.php), no build step, no JS framework. - Flat-file storage (
data/*.json,data/*.txt) — no database. - Persistent login. "Stay logged in" checkbox with secure HttpOnly cookie (30 days). Sessions persist across browser restarts until explicit logout.
- Lowercase usernames. Usernames are always normalised to lowercase on login and user creation.
- Trusted, read-everything model: every logged-in user can see the full history.
- Admin tab for user management, dynamic media-type configuration, and history deletion.
- Super admin is protected: cannot be demoted, renamed, or deleted.
- Read-only users: can view history only — no check submissions, no CSV export.
- English / German / French, selectable from the header (
**= auto-detect from browser). - CSV export of the full history.
- Optional comment per check (shown in history only when non-empty).
- "Last Check" banner on both Today and History tabs with relative time ("3 days, 2 hours, 15 minutes ago").
- PWA-ready: full favicon set, manifest,
theme-colorDeutsche Post yellow (#FFCC00). - CLI version check:
php index.php --version
Skeleton borrowed from the sibling med project.
Screenshot of the idea
┌─ Postfach-Tracker ──────────── ** ▾ v0.1.9 ────┐
│ Admin ▾ │
├─────────────────────────────────────────────────┤
│ [ Today ] [ History ] [ Admin ] │
│ │
│ ┌ Last Check: 2026-04-22 08:42 ─────────────┐ │
│ │ 3 days, 2 hours, 15 minutes ago │ │
│ └────────────────────────────────────────────┘ │
│ │
│ ┌ Log a Post Office Check ───────────────────┐ │
│ │ Letters − [ 3 ] + │ │
│ │ Magazines − [ 0 ] + │ │
│ │ Advertising − [ 1 ] + │ │
│ │ │ │
│ │ Comment: [Found a parcel notice too ] │ │
│ │ [ Submit Check ] │ │
│ └────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
Installation
- Copy the project to your webroot (e.g.
/var/www/html/pf). PHP 7.4+ is enough. cp config.php.sample config.phpand edit the default users / super admin.- Make
data/writable by the PHP user:chmod -R ug+rwX data/ && chown -R www-data:www-data data/. - Visit
/pf/in a browser. The first request createsdata/users.jsonanddata/types.jsonfrom the defaults. - Log in with your super admin account and change the password immediately.
Install on Android (Hermit WebApp)
The app is a mobile-friendly webapp with a real favicon set + site.webmanifest, so it installs cleanly via Hermit:
- Install Hermit from the Play Store.
- In Hermit: + Create Lite App → paste your URL (e.g.
https://example.com/pf/). - Hermit picks up the 512×512 icon and the yellow theme color from
site.webmanifest. - Optional: enable "Authorize login cookies" so the session survives between launches.
Deutsche Post yellow makes the launcher icon hard to miss. 📮
Roles
| Role | Today | History | CSV Export | Admin | Change Password |
|---|---|---|---|---|---|
| Super admin | ✅ | ✅ + delete rows | ✅ | ✅ (protected) | ✅ |
| Admin | ✅ | ✅ + delete rows | ✅ | ✅ | ✅ |
| Regular user | ✅ | ✅ (view only) | ✅ | ❌ | ✅ |
| Read-only user | ❌ (hidden) | ✅ (view only) | ❌ | ❌ | ✅ |
- Super admin (
super_admininconfig.php): cannot be demoted, renamed, or deleted. Username field is disabled when editing. - Admin: full user management + media-type management. Can select and delete history rows.
- Regular user: submit checks, view complete history, export CSV, change own password. Can see every user's entries — this app is for a trusted group sharing one P.O. box.
- Read-only user (
isReadOnlyflag): can only view history. No Today tab, no CSV export, no check submissions.
Data File Format
data/history.txt
Appended log of every check, semicolon-separated, one row per check. Column order is documented by the most recent #cols header above the data row:
#cols;date;time;user;letter;magazine;advertising
2026-04-22;08:42;admin1;3;0;1;Found a parcel notice too
2026-04-22;17:05;user1;0;1;2;
#cols;date;time;user;letter;magazine;advertising;parcel
2026-04-23;09:10;user2;1;0;0;1;
- A fresh
#colsline is written whenever the admin adds or removes a media type, so historic rows survive column changes. - The comment is always the last field after all type columns (not listed in
#cols). Semicolons in comments are replaced with commas. - The CSV export (
/api.php?action=export_csv) unifies all column sets into one table: current types first, then any historic-only ids, missing cells left empty. Comment column is always included. - An "empty visit" is a valid entry — all zeroes still records that someone checked the box.
- The History table only shows the Comment column when at least one row has a non-empty comment.
- Admin deletion: admins can select rows via checkboxes and delete them. Deletion rewrites
history.txtwithout the selected lines (preserving#colsheaders).
data/types.json
[
{ "id": "letter", "label": { "en": "Letters", "de": "Briefe", "fr": "Lettres" } },
{ "id": "magazine", "label": { "en": "Magazines", "de": "Zeitschriften", "fr": "Magazines" } },
{ "id": "advertising", "label": { "en": "Advertising", "de": "Werbung", "fr": "Publicité" } }
]
idis the stable column name used inhistory.txt. Must match^[A-Za-z][A-Za-z0-9_]*$. Do not rename without accepting that old rows keep the old column name.labelis what the UI displays per language. Editing labels is always safe.- Auto-seeded from
data/types_empty.jsonon first request.
data/users.json
{
"admin": {
"realname": "Administrator",
"password": "$2y$...hashed...",
"isAdmin": true,
"isReadOnly": false,
"prefs": { "lang": "de" }
}
}
- Auto-seeded from
config.phpon first request if absent. - Delete this file to re-seed from defaults.
- Language preferences are stored inline under each user's
prefsobject (no per-user directories).
Deploying
Copy the project to your webroot via rsync, scp, git pull, or whatever fits your workflow. The key points:
config.phpcontains secrets and is not tracked in git. Copy it fromconfig.php.sampleon first setup, then keep it in sync manually when the sample changes (e.g. new version, new functions).data/holds runtime state (users, types, history). Back it up before overwriting with a fresh deploy.- To check the deployed version from the command line:
php index.php --version
License
Same permissive spirit as the rest of franik.net — do what you want, don't blame me.