Ship It — Polish, Backups & What's Next
mysqldump backup script + cron (filling in the empty tools/db-backup.php stub). A security pass: delete leftover scripts, keep secrets out of git, lock down cookies, get on HTTPS. Then unlock the next module.Flash messages — the finishing UX touch
You've got forms everywhere now, and good apps confirm actions: "Task added," "Track saved." The clean way pairs perfectly with the POST-Redirect-GET you've used all module — stash a one-shot message in the session before redirecting, show-and-delete it on the next page:
// after a successful action, before the redirect:
$_SESSION['flash'] = 'Saved.';
header('Location: ' . tabUrl('work'));
exit;
// near the top of the page render:
if (!empty($_SESSION['flash'])) {
echo '<div class="flash">' . e($_SESSION['flash']) . '</div>';
unset($_SESSION['flash']); // one-shot: gone after one view
}
It appears exactly once and survives a refresh-free moment, then vanishes — because we unset it right after showing it. Tiny feature, big "this feels finished" payoff. Style .flash to match the dashboard's neon and you're done.
Honor the no-JS foundation
Back in chapter 2 we promised progressive enhancement: the site works without JavaScript, and gets nicer with it. Let's actually verify that promise instead of assuming it. Open your browser dev tools, disable JavaScript, and reload:
- The clock shows the server time (frozen, but correct at load) — good.
- The timer shows a static display — degraded but not broken.
- Tasks, login, editing — all still work, because they're plain forms and redirects.
- The audio player can't play (it needs JS), but nothing throws and the page is intact.
If anything is actually broken (not just degraded) with JS off, that's a bug worth fixing — it means you leaned on JavaScript somewhere you didn't need to. This is the test that proves your architecture is sound.
Backups — the empty stub finally gets filled
There's a tools/db-backup.php file in the project that's been empty this whole time. Now that you have a database worth protecting, fill it in. The job is a mysqldump on a schedule:
# a daily backup, kept for a week — run from cron on the Lubuntu server
mysqldump --single-transaction dashboard \
| gzip > /home/erictey/backups/dashboard-$(date +%F).sql.gz
gunzip < backup.sql.gz | mariadb dashboard_test) and confirm your data's there. The day you need a backup is the worst possible day to discover it was empty or corrupt. Test restores now, while it's calm.Wire a cron entry (crontab -e) to run it nightly, and add a line to prune backups older than a week so the disk doesn't fill. The same machine you set up in Part 1 is doing the work — it all comes full circle.
The security checklist — run it, don't just read it
Before you call anything done, walk this list end to end. Each item is a real foot-gun this module could have left behind:
- Delete
register.phpif it's still there. (Chapter 8 told you to. Double-check.) - Keep secrets out of git. Your DB password should come from
getenv('DB_PASS'), not a committed file. Add any config-with-secrets to.gitignoreand rotate the password if it ever got committed. - Session cookies:
httponly+samesite=Laxon, andsecureon once you're on HTTPS. - HTTPS: finish what Part 1 started — a login over plain HTTP sends passwords in the clear on your network.
- Least-privilege DB user:
dashboard_userhas only SELECT/INSERT/UPDATE/DELETE, never root, never DROP. Confirm it. - Escape on output everywhere user content renders — the
e()helper. One unescaped echo is an XSS hole.
What's next — unlock the locked tracks
Look at the Programming module grid: PYTHON, JAVASCRIPT, LINUX, and DEVOPS are sitting there locked. Unlocking one is genuinely just flipping 'locked' to 'unlocked' in the $modules array in index.php and giving it a chapters file — the exact same five-step wiring that added this module (require + $MODULE_SUBS + $MODULE_CHAPTERS + grid card + $MODULE_META). You've now seen how a module is built from the inside, so you can author your own.
That's the quiet victory of finishing this module: you didn't just complete the dashboard, you learned the machine that makes the dashboard. Adding the next track is no longer a mystery — it's a checklist you've already run.
tools/db-backup.php (or a cron mysqldump) produces a gzipped dump you've test-restored; every box on the security checklist is ticked; and you know exactly how to unlock the next module. There are no // FAKE comments left in index.php. That last one means the site is, genuinely, finished.
▣ Mini Project: The Ship Checklist
One last pass to cross the finish line. This isn't new features — it's the professional polish that separates "it works on my machine" from "it's done." Work the list top to bottom and enjoy ticking the final boxes on a site you took from mockup to real.
- Add the flash-message helper and use it after every successful action.
- Do the JS-off test; fix anything that's broken (not just degraded).
- Fill in
tools/db-backup.php(or a cronmysqldump), run it, and test-restore the dump into a scratch DB. - Run the entire security checklist and fix every miss.
- Unlock one locked module: flip it to
'unlocked', add a chapters file, and confirm it appears with working progress.
Stretch goals:
- Wire the last two fake Home stats (MOON, WEATHER) to a free public API.
- Add log rotation and a tiny uptime check that pings the site and alerts you if it's down.
- Write your own first chapter for one of the unlocked tracks — you know the format cold now.
What you flexed: one-shot flash messages, verifying progressive enhancement instead of assuming it, real backups with tested restores, a security checklist run end to end, and the full module-wiring pattern. You started with a beautiful mockup and you finished the site. That's the whole game — go build the next track.