← Home

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

LayerChoiceWhy
ClientInstallable 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 deliveryService 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
BackendCloudflare Worker (JS)Serverless, free tier, supports a cron every minute
StorageCloudflare D1 (SQLite)Tiny relational state: alarms, chaos state, push subscriptions
Health dataiOS Shortcuts automation, calling the Worker's /api/stepsHealthKit has no web API, so a native Shortcut reads steps and pushes them to us
AuthOne secret token (single user)Simplest thing that keeps strangers out
Testsnode:test, with node:sqlite standing in for D1Runs 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

1. Measure x. When you stop an alarm, 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.
x = max(0, min(stop − ringStart, 30 min) − 3 min)
2. Start the 24h clock. 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:
s ~ Normal(0, σ²), σ = 15 min × x
fire = max(intended + s, now + 1 min)
Generated with Box–Muller from the browser-grade crypto RNG. The 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

Honest limitations

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.