No description
Find a file
2026-05-15 22:54:03 +02:00
data initial commit 20260422-144217 2026-04-22 14:42:17 +02:00
favicon initial commit 20260422-144217 2026-04-22 14:42:17 +02:00
lang Actual State 20260515-225403 2026-05-15 22:54:03 +02:00
.gitignore Actual State 20260515-225403 2026-05-15 22:54:03 +02:00
.htaccess initial commit 20260422-144217 2026-04-22 14:42:17 +02:00
api.php Actual State 20260515-225403 2026-05-15 22:54:03 +02:00
config.php.sample Actual State 20260515-225403 2026-05-15 22:54:03 +02:00
favicon.ico initial commit 20260422-144217 2026-04-22 14:42:17 +02:00
index.php Actual State 20260515-225403 2026-05-15 22:54:03 +02:00
README.de.md Actual State 20260515-225403 2026-05-15 22:54:03 +02:00
README.md Actual State 20260515-225403 2026-05-15 22:54:03 +02:00

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-color Deutsche 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

  1. Copy the project to your webroot (e.g. /var/www/html/pf). PHP 7.4+ is enough.
  2. cp config.php.sample config.php and edit the default users / super admin.
  3. Make data/ writable by the PHP user: chmod -R ug+rwX data/ && chown -R www-data:www-data data/.
  4. Visit /pf/ in a browser. The first request creates data/users.json and data/types.json from the defaults.
  5. 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:

  1. Install Hermit from the Play Store.
  2. In Hermit: + Create Lite App → paste your URL (e.g. https://example.com/pf/).
  3. Hermit picks up the 512×512 icon and the yellow theme color from site.webmanifest.
  4. 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_admin in config.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 (isReadOnly flag): 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 #cols line 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.txt without the selected lines (preserving #cols headers).

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é" } }
]
  • id is the stable column name used in history.txt. Must match ^[A-Za-z][A-Za-z0-9_]*$. Do not rename without accepting that old rows keep the old column name.
  • label is what the UI displays per language. Editing labels is always safe.
  • Auto-seeded from data/types_empty.json on first request.

data/users.json

{
  "admin": {
    "realname": "Administrator",
    "password": "$2y$...hashed...",
    "isAdmin": true,
    "isReadOnly": false,
    "prefs": { "lang": "de" }
  }
}
  • Auto-seeded from config.php on first request if absent.
  • Delete this file to re-seed from defaults.
  • Language preferences are stored inline under each user's prefs object (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.php contains secrets and is not tracked in git. Copy it from config.php.sample on 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.