Viva Sheet
Wake Up, Sleeping Beauty: an alarm clock that punishes oversleeping with unpredictable alarms.
The idea in one paragraph
You set alarms in the app. If you let one ring (or snooze it) for x minutes, then any alarm you set for 24 hours or more after that ring is moved by a hidden random amount, earlier or later, whose size grows with x. You can't see the real time. The only way out is to walk 10,000 steps (read from Apple Health) before the shifted alarm fires.
Stack
| Layer | Choice | Why |
|---|---|---|
| Client | Installable PWA: plain HTML/CSS/JS, SVG analog clock, Lorenz-attractor "chaos crown" | Brief asked for a web deployment; no framework needed for a small UI; light/dark via prefers-color-scheme |
| Alarm delivery | Service Worker + Web Push (VAPID, ES256, empty payload) | A web page can't ring in the background; a server push can wake the phone. Empty payload avoids payload encryption |
| Backend | Cloudflare Worker (JS) | Serverless, free tier, supports a cron every minute |
| Storage | Cloudflare D1 (SQLite) | Tiny relational state: alarms, chaos state, push subscriptions |
| Health data | iOS Shortcuts automation, calling the Worker's /api/steps | HealthKit has no web API, so a native Shortcut reads steps and pushes them to us |
| Auth | One secret token (single user) | Simplest thing that keeps strangers out |
| Tests | node:test, with node:sqlite standing in for D1 | Runs the real Worker code offline: 10 tests |
Architecture
iPhone PWA ──(token)──► Worker /api/* ◄──► D1 (alarms, chaos, subs)
▲ ▲ │
│ push notification │ └── cron (every minute): start due alarms,
│ │ send a push every ~30 s until Stop/Snooze
Apple push service ◄───────────┘
iOS Shortcut (Health steps) ──► POST /api/steps
The Worker is the only place the real fire time exists. /api/state never selects it, so the client cannot show or leak it (there is a test for that).
The chaos logic
x = minutes rung (snoozes included) minus a 3-minute reaction grace, never below 0. An alarm nobody answers auto-stops after 30 minutes, so x is capped at 27.
T1 = when that alarm started ringing. Alarms whose intended time is at or after T1 + 24h are "affected".
3. Draw the shift. Normal, centred on zero, so earlier and later are equally likely, with spread proportional to x and no hard cap:
fire = max(intended + s, now + 1 min)
now + 1 min floor only stops a shifted alarm landing in the past.
4. Keep it hidden and fair. The draw happens on the server: at creation for new alarms, or when an earlier alarm finishes (pending alarms are re-rolled with the new x). Several alarms get independent draws, but you can't see any of them.
5. Escape. If steps since
T1 reach 10,000 before the shifted alarm fires, the shift is cancelled and the alarm rings at the intended time (immediately, if that time has already passed).
6. Chain. Every alarm that rings replaces the chaos state (new T1, new x).
Worked example. Let an alarm ring 12 minutes: x = 12 − 3 = 9, σ = 135 min. Then P(moved by more than 30 min) ≈ 82%, more than 2 hours ≈ 37%, more than 6 hours ≈ 0.8%. A 1-minute reaction after the grace gives x = 0, so no shift at all.
Design decisions and trade-offs
- We own the alarms. iOS gives apps no access to the Clock app's alarms, so alarms are created in this app.
- Web, not native. A project constraint. The cost: no HealthKit and no true alarm ringing, so we use Shortcuts and a push "nag loop".
- Normal distribution. Symmetric, one parameter, unbounded tails (as requested), and easy to explain and test: mean 0, sd 15·x, checked statistically in a unit test.
- Empty-payload push. The service worker shows a fixed "Wake up!" notification, so there is nothing to encrypt, which removes the most error-prone part of Web Push.
- Cron every minute. Granularity is one minute, so an alarm may start up to 60 s late. A second push goes out 30 s after the first.
- D1 over KV. Strongly consistent reads and writes for a read-modify-write workload.
Honest limitations
- Push delivery on iOS can be delayed, and Silent mode or Focus will mute it. Keep a real backup alarm.
- Steps arrive when the Shortcut runs (e.g. every few hours), so a goal reached between runs may be noticed late. The number is sent by a Shortcut, so the check is honest-system, not tamper-proof.
- Setting a backup alarm earlier than T1 + 24h is unaffected by design.
- Single user, and the token lives in the URL for the steps webhook. Rotate it if it leaks.
- The deployed code was tested offline. Push on a real iPhone is the part exercised only in live use.
Likely viva questions
Why can't the app just read the Clock app's alarms?
iOS does not expose Clock alarms to third-party apps. There is no API to read, edit or observe them, so the app schedules and rings its own alarms.
Why use a Shortcut for steps instead of HealthKit directly?
HealthKit is native-only (Swift/Objective-C). A web app cannot call it. Shortcuts can read Health data and make an HTTP request, which gives the server a step count without writing a native app.
How is the shifted time hidden from the user?
It is computed and stored only in the Worker's database. The state API selects only the intended time and status, so the browser never receives the shifted time. A unit test asserts the response contains no fire time.
Why a Normal distribution with σ = 15·x?
It treats earlier and later symmetrically, gives small shifts for small x, and has unbounded tails as the brief asked. Scaling σ linearly makes x a clean "dial" for how much chaos you earn.
What does the 3-minute grace do?
It subtracts the first 3 minutes of ringing from x, so a normal human reaction time causes no shift. Only time beyond that counts.
What happens if I never respond to the alarm?
After 30 minutes it auto-stops, x is capped at 27 (σ = 405 min), and a new 24h clock starts from when it began ringing.
How does the alarm ring if the app is closed?
A Cloudflare cron runs every minute, starts any due alarm and sends Web Push notifications every ~30 seconds until you open the app and press Stop or Snooze.
What would you improve next?
A native iOS app with AlarmKit and HealthKit would give true ringing and live steps; a signed Shortcut link for one-tap setup; and per-device auth instead of a shared token.