Simple web app for creating packing lists for alpine adventures

sebastian b46705174a Show mapping counts on the items page and make the list sortable il y a 3 semaines
admin b46705174a Show mapping counts on the items page and make the list sortable il y a 3 semaines
img 053475e484 Make it installable as web application il y a 5 mois
migrations 7e2c7d34b6 Persist checklists offline, protect admin writes, harden config and links il y a 3 semaines
screenshots 45ab8ba776 Updating readme and adding screenshots il y a 5 mois
.gitignore 7e2c7d34b6 Persist checklists offline, protect admin writes, harden config and links il y a 3 semaines
.htaccess 7e2c7d34b6 Persist checklists offline, protect admin writes, harden config and links il y a 3 semaines
README.md 7e2c7d34b6 Persist checklists offline, protect admin writes, harden config and links il y a 3 semaines
config.sample.php 7e2c7d34b6 Persist checklists offline, protect admin writes, harden config and links il y a 3 semaines
db.php 7e2c7d34b6 Persist checklists offline, protect admin writes, harden config and links il y a 3 semaines
favicon.ico 053475e484 Make it installable as web application il y a 5 mois
index.php 7e2c7d34b6 Persist checklists offline, protect admin writes, harden config and links il y a 3 semaines
list.php 7e2c7d34b6 Persist checklists offline, protect admin writes, harden config and links il y a 3 semaines
manifest.webmanifest 053475e484 Make it installable as web application il y a 5 mois
offline.html 7e2c7d34b6 Persist checklists offline, protect admin writes, harden config and links il y a 3 semaines
packing.php 7e2c7d34b6 Persist checklists offline, protect admin writes, harden config and links il y a 3 semaines
schema.sql 7e2c7d34b6 Persist checklists offline, protect admin writes, harden config and links il y a 3 semaines
sw.js 7e2c7d34b6 Persist checklists offline, protect admin writes, harden config and links il y a 3 semaines

README.md

PackIt

PackIt is a smart packing list generator for outdoor activities. Select the activities you're planning — camping, skiing, kayaking, and more — and PackIt produces a deduplicated, grouped checklist of everything you need to bring.

Screenshots

Desktop

Activity selection on desktop

Mobile

Activity selection Packing list
Activity selection on mobile Packing list on mobile

How it works

  1. Choose your activities — Pick one or more activities from the activity selection page. Activities are organised into groups (e.g. Overnight, Water Activities, Winter Sports).
  2. Generate your list — PackIt merges all item requirements across your selected activities, removing duplicates, and groups the result by category (Clothing, Shelter, Navigation, etc.). When the same item is mapped to multiple activities with different quantities, the highest quantity wins.
  3. Pack and check off — Work through the interactive checklist, ticking items as you pack them. A progress bar tracks how far along you are, and your ticks are remembered on that device, so packing can be spread over several sittings.
  4. Share or print — Every list gets a permanent, shareable short URL. You can also print the list directly from the browser.

Features

  • Activity-based list generation — Items are mapped to activities in the database. Selecting multiple activities produces a single merged list with no duplicate entries.
  • Per-mapping quantities — Each activity-to-item mapping carries a quantity. Items needed in multiples (e.g. 2× Water bottle) are rendered with a leading multiplier, and merging across activities keeps the largest required quantity.
  • Grouped items — Items are organised into logical categories (Clothing, Shelter, Navigation, Medical, Water & Food, Safety Gear) for easy scanning.
  • Interactive checklist — Click any item to mark it as packed. Checked items are visually struck through and a progress bar updates in real time. Ticks are stored in the browser under the list's hash and restored on the next visit; a Reset button appears once something is ticked. Nothing is sent to the server, so each device keeps its own progress, and a browser with site data blocked simply falls back to not remembering.
  • Shareable short URLs — Each unique activity selection is stored under a deterministic 5-character hash, producing a permanent link short enough to share comfortably over chat or read out loud.
  • Installable PWA — A web app manifest, theme color, and maskable icons let PackIt be installed to the home screen on mobile or as a standalone app on desktop.
  • Works offline — A service worker (sw.js) caches the app shell and every list you have opened, which is what makes an installed PackIt useful at the trailhead. Pages are network-first, so an online visit always reflects the latest admin changes and falls back to the cached copy only when the network is gone. A list never opened on the device shows offline.html instead. The admin panel is deliberately never cached.
  • Collision-safe short links — Links are a 5-character SHA-256 prefix. If a prefix is ever claimed by a different selection, that selection transparently gets a 6-character link (then 7, …) rather than failing, so links stay short without ever pointing at the wrong list.
  • Print support — The packing list page has a print stylesheet that removes UI chrome and renders a clean black-and-white checklist.
  • Admin panel — A password-protected admin interface at /admin/ lets you manage all content:
    • Activity groups and activities
    • Item groups and items
    • Activity-to-item mappings (with quantities)
    • Inline editing on every list page — edit fields render in place within the table column, so the page no longer jumps to the top on each edit
    • Dashboard with counts of all entities
    • CSRF-protected: every form carries a per-session token that the matching handler verifies, so a page an admin merely visits cannot post changes on their behalf
    • Session cookie is HttpOnly, SameSite=Strict, scoped to /admin/, and marked Secure over HTTPS
    • Failed logins are logged and throttled (see below)

Tech stack

  • Backend: PHP with PDO
  • Database: MySQL (schema in schema.sql)
  • Frontend: Vanilla HTML/CSS/JS — no build step required
  • PWA: manifest.webmanifest plus icons in img/

Setup

  1. Create the database by running schema.sql:

    mysql -u <user> -p < schema.sql
    
  2. Copy the config template and fill in your database credentials:

    cp config.sample.php config.php
    

    config.php is git-ignored and excluded from deploy.sh — it never belongs in the repository. On a real server, place it one directory above the web root.

  3. Serve the project with any PHP-capable web server (Apache, Nginx, PHP built-in server). The included .htaccess ensures the .webmanifest MIME type is served correctly, denies HTTP access to config.php, config.sample.php and .sql files, and keeps sw.js out of the browser cache.

  4. Open the site in a browser and start selecting activities. On mobile, use the browser's "Add to Home Screen" option to install PackIt as an app.

Offline support needs a secure context: browsers only register a service worker over HTTPS (or on localhost). Everything else works over plain HTTP.

Error reporting

config.php sets $appEnv. Leave it at production on a live site: errors are written to the PHP error log and visitors see a generic message. Set it to development to get the detail on screen instead. Anything other than the exact string development is treated as production, so a typo fails closed.

Upgrading an existing install

Databases created before this change need one migration:

mysql -u <user> -p <your-database> < migrations/001_hardening.sql

It adds the login_attempts table, narrows selection_hashes.hash to VARCHAR(16), and indexes activity_ids. It also deletes any rows still holding a 64-character hash from the original link scheme — those links have been unreachable since links were shortened to 5 characters, and the table is only a cache of selection → hash, so anything deleted is recreated by re-picking the activities.

Admin password

Admin credentials live in the admin_users table, not in config.php. The password_hash column stores a bcrypt hash produced by PHP's password_hash(); admin/login.php verifies the submitted password against it with password_verify().

schema.sql seeds a default account:

Username Password
admin packit-admin

Change it after first login. There is no UI for this yet, so update the row directly — generate a new hash and replace it in the database:

php -r "echo password_hash('your-new-password', PASSWORD_BCRYPT), PHP_EOL;"
UPDATE admin_users
SET password_hash = '<paste-hash-here>'
WHERE username = 'admin';

Login throttling

Failed logins are recorded in login_attempts. After 10 failures from one IP within 15 minutes that IP is refused until the window passes. Repeated failures against a username add a growing delay (up to 5 seconds) instead of a lockout — locking the username would let anyone shut the real admin out of their own panel. A successful login clears that IP's history, and rows older than a day are pruned on each write.

Project structure

index.php              Activity selection page
list.php               Processes selection, stores hash, redirects
packing.php            Displays the shareable packing list
db.php                 Bootstrap: error handling + PDO connection helper
config.php             Database credentials (git-ignored, create from the sample)
config.sample.php      Documented config template
schema.sql             Database schema and sample data
migrations/            One-off SQL upgrades for existing databases
sw.js                  Service worker (offline support)
offline.html           Shown when an unvisited list is opened offline
manifest.webmanifest   PWA manifest (installable web app)
.htaccess              Manifest MIME type, blocks config/SQL, no-cache for sw.js
img/                   App icons (192/512, plus maskable variants)
admin/
  index.php            Dashboard
  activities.php       Manage activities
  activity_groups.php  Manage activity groups
  items.php            Manage items
  item_groups.php      Manage item groups
  mappings.php         Manage activity-to-item mappings (with quantities)
  layout.php           Shared admin layout
  login.php / auth.php Authentication, CSRF helpers, login throttling