Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Daily Pushups (Progressive Web App)

A phone-first workout tracker. Tap an exercise, punch the rep count into a big keypad, and it adds to your running total for the day. A second screen charts reps per day so you can see whether you're actually keeping it up.

The shell is fixed to the viewport — the nav bar stays pinned to the bottom of the screen and each screen scrolls inside it. Light and dark are both selected designs; the app follows your device by default and Settings can override it.

Plain HTML, CSS and vanilla JavaScript. No framework, no build step, no npm, no backend.


Run it locally

Service workers and ES modules need a real origin — opening index.html from the file system will not work. Serve the folder:

python3 -m http.server 8080

Then open http://localhost:8080.

Deploy to Netlify

Drag and drop — go to https://app.netlify.com/drop and drag this folder onto the page. That's the whole deploy. You get an HTTPS URL immediately, which is required for the app to install to a home screen.

From Git — connect the repo. netlify.toml already sets the publish directory to the project root with no build command.

On each redeploy, bump CACHE_VERSION in sw.js so returning visitors drop their old cache.

Install on your phone

  • iPhone — open the URL in Safari, tap Share, then Add to Home Screen.
  • Android — Chrome offers Install app, or use the menu.

Once installed it launches full screen and works with no connection.


Where your data lives

In this browser, on this device. Nowhere else. There is no account and no server, which is why it's free to run and instant to use — and also why:

  • Your phone and your laptop keep separate, unrelated histories.
  • Clearing site data, resetting the device, or "Clear History and Website Data" in Safari erases everything permanently.
  • iOS can evict storage for sites you haven't opened in a while.

Export a backup from Settings now and then. It downloads a JSON file, and importing it merges entries back in — importing never deletes anything, so it's safe to import onto a device that already has data. That file is the only way to move history between devices or recover from a wipe.


How it works

File Role
index.html Markup for the three screens, plus the inline pre-paint theme script
styles.css Dark-first styling; light mode is a separate selected set of steps, keyed off [data-theme]
app.js State, storage, theming, rendering, the SVG chart
sw.js Offline cache
manifest.webmanifest Install metadata
tools/make_icons.py Regenerates icons/ — stdlib only, not deployed

Storage shape

Two localStorage keys. The workout data lives in workout-tracker-v1:

{
  "version": 1,
  "exercises": [{ "id": "e1", "name": "Push-ups", "goal": 100 }],
  "entries": [
    { "id": "k3f9a", "exerciseId": "e1", "reps": 25, "ts": 1753228800000, "day": "2026-07-23" }
  ]
}

The appearance preference lives separately in workout-tracker-theme, holding one of auto, light or dark. It is deliberately not part of state and therefore not part of a backup export: it describes this device, and restoring a backup from another phone should not repaint this one.

Exercise ids never change, so renaming an exercise in Settings keeps its entire history attached. Anything read out of storage or an imported file is run through normalize(), so malformed data degrades to defaults instead of a blank screen.

Why an entry has both ts and day

They answer different questions and neither can be recomputed from the other.

ts is a UTC instant — it orders entries and prints the clock time beside each set. day is the local calendar day, frozen at the moment of logging.

Deriving the day from ts later would evaluate it in whatever timezone the device is in at that moment. Log a set at 00:30 in Berlin, open the app in New York, and the recomputed day lands on the previous date: history silently rewrites itself and a streak you actually earned breaks. Storing day pins the entry to the calendar day the user was living in. It is also the grouping key for every total, streak and chart bucket, so keeping it is a plain Map lookup instead of constructing a Date per entry on every render.

Day keys are always built from local calendar fields, never toISOString() — that is UTC and would misfile evening workouts for anyone behind it.

A note on entry size

A single entry is capped at 99 reps — two digits, which is what makes the auto-save-on-second-digit keypad work. Log a bigger set as two entries; the daily total is unbounded.

Undo

Deleting a set offers Undo in the toast. Adding one does not — the chip that appears in the keypad log removes it in a single tap, so a second affordance only ate the toast's dwell time.

The Progress chart

The chart carries no title or caption; the exercise tabs above it already name what you're looking at. A legend sits above the plot, and each key mirrors the mark it stands for — a filled rect for the bars, a dashed stroke for your goal line, a solid orange stroke for the average.

Tapping a bar opens a tooltip anchored to it with the date and rep count. It clamps inside the plot and flips below the bar when a tall one leaves no headroom overhead. Tap the same bar again, or anywhere else, to dismiss. The tooltip only ever enhances — every number in it is also in the data table under the chart, so nothing is reachable by pointer alone.

How the average is computed

Past 30 days avr — shown as a tile and drawn as the orange reference line — covers the 30 days before today, and counts only days that have records.

Both exclusions are deliberate. Today is a partial total while it is still in progress; averaging it in would drag the figure down every morning and creep it back up over the day. Rest days are excluded so the number reads as "what a session looks like" rather than being diluted by days you didn't train.

Appearance & theming

Settings has a three-way Auto / Light / Dark control. Auto is the default on a first visit and resolves from the device.

The stored value is the preference; what lands on <html data-theme> is the resolved theme. That split is what keeps the stylesheet to a single light rule and lets an explicit choice beat the device in both directions — a prefers-color-scheme media query cannot express "Light, even though this phone is set to dark".

Three things follow from that design:

  • Resolution happens twice, on purpose. An inline synchronous script in <head> applies the theme before first paint, because app.js is a deferred module and would repaint after the dark default was already on screen. Both copies carry a comment pointing at the other — change the rule in one and you must change it in the other.
  • Auto stays live. A change listener on the media query repaints when you flip your OS appearance mid-session, but only while the preference is Auto.
  • Switching re-renders the chart. Bar colours are read from the stylesheet at render time and baked into the SVG fill, so a live chart would otherwise keep its old steps.

theme-color is a single meta tag set from the computed --plane, rather than the usual pair of media-scoped tags, for the same override reason. The static theme_color in manifest.webmanifest is read by the OS at install time and cannot follow the setting, so an installed PWA set to Light still shows a dark splash screen.

Colours

The three exercise colours are the validated categorical slots violet / orange / aqua, with separate steps chosen for the dark and light surfaces. They clear colour-blind separation and contrast checks against this app's own surfaces in both modes. (The light-mode aqua sits just under 3:1 contrast; the data table under the chart is what relieves that, so don't remove it.) If you change them, keep identity carried by more than hue alone — the chart's value labels, the data table, and the exercise names all back up the colour.

The average line reuses the orange slot as an annotation colour, so on the Crunches tab the line and the bars share a hue. Form separates them there — vertical columns against one horizontal rule, with matching shapes in the legend — but hue alone does not. Worth knowing before adding a fourth exercise.

Regenerating icons

python3 tools/make_icons.py

Writes icons/icon-192.png, icon-512.png, icon-maskable-512.png, apple-touch-icon-180.png and icon.svg. Pure standard library — no Pillow, no ImageMagick.


Copyright & licensing

© 2026 Stan Kosyakov. All rights reserved.

The source code, design, and content of this application are the intellectual property of the copyright holder. You may view the code and run your own personal copy.

Commercial use requires prior written permission. Anyone intending to replicate, redistribute, resell, or build on this app — in whole or in part — for any commercial purpose must request and obtain written permission first. Contact video_wavy.7k@icloud.com before doing so.

About

Progressive Web App that helps you track your daily pushups and other exercises during short breaks.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages