Zone-by-zone toothbrush timer PWA with a teeth diagram and audio/visual cues. Works offline. Self-hosts on a DigitalOcean droplet with Docker + Caddy, automatic HTTPS and auto-updates.
HealthJavaScript · ShellGitHub @norbsmaqOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗A small, dependency-free web app that guides you through brushing your teeth zone by zone.
Brushing order: upper right → upper left, then lower left → lower right.
git clone https://github.com/norbsmaq/toothbrush-timer /opt/toothbrush-timer
cd /opt/toothbrush-timer
sudo ./setup-droplet.sh
The script asks for your domain name(s) and whether to enable HSTS (see below). Then it installs Docker if needed, opens ports 80 and 443 if ufw is active, turns on automatic updates, installs GoAccess for visitor stats, and starts the app. It finishes by printing the address to open.The container restarts automatically after reboots (restart: unless-stopped).
Point a DNS A record for each domain at the droplet's IP. For example, add records for both example.com and www.example.com. Then enter the domains when setup-droplet.sh asks, separated by spaces or commas:
Domains [none]: example.com www.example.com
The script warns you if a domain doesn't point at the droplet yet. Caddy gets and renews a Let's Encrypt certificate for each domain automatically, and serves the app on all of them. HTTPS is needed for the browser's keep-screen-awake feature and for installing the app on your phone. Everything else works over plain HTTP.
If you list both a domain and its www. version, the script also offers to redirect www.example.com to example.com (keeping the rest of the address). That way everyone ends up on one address, so an installed app and its saved settings always use the same one.
To change domains later, run sudo ./setup-droplet.sh again. It shows your current settings as defaults, so press Enter to keep any of them. Or edit .env directly (SITE_ADDRESS="example.com, www.example.com", REDIRECT_WWW=true) and run docker compose up -d.
By default the site sends an HSTS header. It tells browsers to always use HTTPS for your domain for the next year. Browsers ignore it over plain HTTP or an IP address, so it does no harm there.
If you're using a domain but might switch it back to plain HTTP later, answer n when the setup script asks about HSTS. Or set ENABLE_HSTS=false in .env and run docker compose up -d. Turning it off doesn't undo HSTS in browsers that have already received it; they keep it until it expires.
setup-droplet.sh turns on three kinds of automatic updates:
| What | How | When |
|---|---|---|
| Ubuntu security updates | unattended-upgrades | Daily. If an update needs a reboot, the droplet reboots at 04:00 and the app starts back up on its own. |
| Docker Engine | unattended-upgrades, from Docker's apt repository | Daily, along with the OS updates |
| The app and its Caddy web server | toothbrush-timer-update.timer runs update.sh | Daily around 03:30 |
update.sh makes the server's copy match the main branch on GitHub exactly, rebuilds on the newest Caddy image, restarts the container and removes old images. Anything pushed to main goes live on the next run. Any edits made directly to the repo's files on the server are overwritten. Your .env settings aren't part of the repo, so they're kept.
To update right away, run sudo ./update.sh. To see when the update last ran and when it runs next:
systemctl list-timers toothbrush-timer-update.timer
journalctl -u toothbrush-timer-update.service -n 50
To turn off the app auto-update: sudo systemctl disable --now toothbrush-timer-update.timer
.env is git-ignored, so your settings are kept across updates.
Caddy writes an access log for every request to logs/access.log in the app folder on the droplet. It starts a new file at 10 MB, compresses the old ones and deletes them after 90 days. setup-droplet.sh installs GoAccess, which turns the logs into a stats dashboard in your terminal. Nothing is published on the website.
To see the stats, SSH into the droplet and run:
cd /opt/toothbrush-timer
sudo ./stats.sh
It shows visitors, pages, browsers, operating systems and referrers per day, across the current and older log files. Use the arrow keys and Tab to move around, and press q to quit. Extra options are passed to GoAccess. For example, sudo ./stats.sh --ignore-crawlers hides search engine bots.
The logs include visitors' IP addresses and browser details. They stay on the droplet, readable only by root.
The app is a Progressive Web App (PWA). Once installed, it opens full-screen from your home screen and works offline.
Installing requires HTTPS, so set up a domain first (see above). Over plain http://IP it still works in the browser, but it won't install.
When you deploy a new version, an installed app picks it up the next time it's opened online.
To try the app or test changes without a server, get the code with git clone https://github.com/norbsmaq/toothbrush-timer (or Code → Download ZIP on GitHub), then either:
public/index.html in your browser. Everything works except offline mode and installing as an app.docker compose up, then open http://localhost.cd public && python3 -m http.server 8000, then open http://localhost:8000.public/ the app (index.html, style.css, app.js, icons)
manifest.webmanifest PWA manifest
sw.js service worker (offline support)
Caddyfile web server config
Dockerfile Caddy image with the app baked in
docker-compose.yml
setup-droplet.sh one-time droplet setup
update.sh pull + rebuild (run daily by a systemd timer)
stats.sh visitor stats from the access logs (GoAccess)
logs/ access logs on the server (git-ignored)