π CircaLog (https://circalog.vercel.app/)
A sleep tracking PWA built for humans whose bodies don't run on a 24-hour clock.
CircaLog is an open-source, offline-first Progressive Web App (PWA) designed specifically for people living with Non-24-Hour Sleep-Wake Disorder (Non-24) and other circadian rhythm disorders. Unlike every mainstream sleep app built for neurotypical sleep patterns, CircaLog tracks, visualizes, and understands the drift, the defining feature of your condition, not a flaw in your data.
Every mainstream sleep app assumes you sleep at night and wake in the morning. For the estimated 3 million people living with Non-24, this assumption makes those apps useless, or worse, actively misleading.
CircaLog was built from the ground up for people whose sleep cycle drifts continuously around the clock. It speaks your language:
Cycles, not calendar days. Actograms, not bedtime scores.
- One-tap start/stop sleep timer, or full manual time entry
- Required fields: sleep start time, wake time, quality rating (1β5)
- Optional fields: notes, dreams/nightmares, interruptions (nocturia, thirst, hunger, pain, other), medication taken (before/during/after sleep)
- Automatic nap detection (sessions under 3 hours = nap)
- Back-fill past entries going back weeks or months
- Correct night-anchor dating: sessions that cross midnight (e.g. bed at 23:10 May 31, asleep at 00:37 June 1) are displayed under the night they started, not the calendar date of sleep onset, because that is how patients and doctors think about sleep
- Actogram: the primary chart. A diagonal drift visualization showing your sleep band shifting over time, exactly as sleep researchers view Non-24 data. Time of day on the Y axis, cycle number and calendar date on the X axis.
- Time range toggle:
[ 1W ] [ 2W ] [ 1M ] [ 3M ] [ 6M ] [ 1Y ] [ All ] - Each entry displays both calendar date and cycle number simultaneously
- Free-running period estimation, displayed automatically after 14+ days of data using linear regression on sleep onset times
- Anomaly detection for significant cycle drift
- Flags for unusually short or fragmented sleep sessions
- Bedtime reminder notifications based on predicted sleep window
- Configurable lead time for reminders (default: 30β60 minutes)
- Import past sleep sessions from a CSV file exported from the CircaLog Daily Tracker spreadsheet (or any CSV matching the format)
- Full preview table before confirming: shows every parsed row with date, sleep start, wake time, duration, quality, session type, and parse status
- Duplicate detection: sessions already in the app are skipped automatically; re-importing is always safe
- Midnight crossover handled automatically: sessions where sleep start falls past midnight are correctly dated to the following calendar day
- Structured interruption mapping: free-text interruption notes are
mapped to typed objects (
bathroom,other) so they are queryable in future Insights views - Row-by-row progress indicator during import
- Warn-before-leave: navigating away mid-import shows a confirmation dialog; any sessions already processed are kept
- Export all sleep sessions as a JSON backup file (
circalog-backup-YYYY-MM-DD.json) - Restore from a JSON backup: preview session counts (new vs. already present) before confirming
- Merge mode: adds only new sessions, keeps existing data intact
- Replace mode: wipes all current sessions and restores the full backup
- Schema migration handler: backups from older app versions are automatically upgraded to the current format before restore, so data is always safe to use
- Full reports (PDF/CSV, weekly/monthly summaries, doctor report) in V2
- Dark mode by default, user-selectable light/dark toggle
- Clinical + cosmic aesthetic, clean, data-forward UI with subtle night sky elements
- Color palette: dark charcoal with purple/violet accents
- Bottom tab bar (Log / Chart / History / Insights) + hamburger side drawer for secondary features (Settings, Reports, Export, Import, About, Privacy, etc.)
- Accessible: high contrast, legible typography, mobile-first layout
- Proper bottom padding for Android and iOS system navigation bars
- Fully functional offline without login (IndexedDB local storage)
- Optional Google Sign-In to enable cloud sync and backup
- Local-first architecture: data lives on device, syncs to cloud when connected
- After OAuth sign-in, the app returns the user to the page they came from
| Layer | Technology |
|---|---|
| Framework | React 19 + Vite 8 |
| Language | TypeScript 6 |
| Styling | TailwindCSS 4 |
| Charts | Recharts 3 |
| Local Storage | IndexedDB via Dexie 4 |
| Cloud Database | Supabase (PostgreSQL) |
| Authentication | Google Sign-In (optional) |
| Hosting | Vercel |
| Serverless | Vercel Functions (V1) β Cloudflare Workers (V2+) |
| Updates | PWA Service Worker (silent auto-update) |
| URL | Content |
|---|---|
circalog.app |
Landing page / coming soon (V1) β marketing page (V2+) |
circalog.app/log |
The PWA app (permanent URL, never changes) |
circalog.app/log/import |
CSV import page |
circalog.app/log/export |
JSON backup export page |
circalog.app/log/restore |
JSON backup restore page |
- Sleep log with required + optional fields
- Actogram drift chart with time range toggle
- Nap auto-detection
- Dark/light mode with user toggle
- PWA manifest + service worker (offline support + auto-update)
- Local IndexedDB storage
- Optional Google Sign-In with cloud sync to Supabase
- CSV import from CircaLog Daily Tracker spreadsheet
- JSON backup export and restore with schema migration handler
- Coming soon landing page at root domain
- Continuous Vercel deployment from GitHub
- Push notifications + configurable bedtime reminders
- Free-running period calculation display
- Weekly + monthly PDF and CSV reports
- Doctor report (one-tap PDF with actogram included)
- Medication log linked to sleep patterns
- Android home screen widget (log + summary)
- Educational resources about Non-24
- Sleep debt tracker
- Multi-user / multi-tenant architecture
- Tab bar customization
- Full marketing landing page
- Public open-source release
- Potential integrations (Google Health Connect, etc.)
- Node.js 20+ (v24.17.0 used in development)
- npm 10+ (v11.17.0 used in development)
git clone https://github.com/sobhy0101/CircaLog.git
cd CircaLog
npm install
npm run devnpm run dev # Start dev server at http://localhost:5173
npm run build # TypeScript check + production bundle
npm run lint # Run ESLint
npm run format # Format src/**/*.{ts,tsx,css} with Prettier
npm run preview # Preview the production build locallyThese commands are only needed for V2+ cloud sync work. The app runs fully offline without them.
Install the CLI globally first (one-time per machine).
npm install -g supabaseThen authenticate and link the project:
supabase login # Opens a browser tab to authenticate
supabase init # Creates supabase/ config directory (run once per project clone)
supabase link # Prompts you to select the project interactivelysupabase init must run before supabase link. The supabase link command
will prompt you to select the CircaLog project from a list β no need to
pass the project ref manually.
Copy .env.example to .env.local and fill in your values:
cp .env.example .env.localVITE_SUPABASE_URL=your_supabase_project_url
VITE_SUPABASE_ANON_KEY=your_supabase_anon_key
VITE_SUPABASE_PUBLISHABLE_KEY=your_supabase_publishable_keyThese are decisions made during development that are not obvious from the code alone. Captured here so contributors and future maintainers don't rediscover them the hard way.
The sync status indicator is a tab shape anchored to the top edge of
the viewport (top-0, rounded-b-xl, border-x border-b), not a
floating pill. This was a deliberate design choice: pills are used
throughout the UI (quality picker, session type badges) and a pill-shaped
status indicator was visually indistinct. The tab shape reads as
infrastructure rather than content; it belongs to the chrome of the
app, not to any page.
Five states are implemented: synced (grey) / syncing (amber, rotating
icon) / pending (purple, pulsing red dot) / error (red) / offline
(neutral, cloud-off icon). The offline state is detected via
navigator.onLine (read on mount) and window online/offline events
(subscribed in a useEffect). It is distinct from pending: offline is
expected and not alarming; pending while online is not.
The navigator.onLine guard in syncService.ts (pushEntry and
flushQueue) prevents the "Syncingβ¦ β Synced" flicker that occurred
because flushQueue was running, failing silently, and re-queuing entries
into a briefly-empty queue. With the guard in place, no push is attempted
while offline, so the queue stays non-empty and the tab correctly shows
offline until connectivity is restored.
Toast notifications (sign-in success, sign-out, errors) are positioned at
bottom-20 (80px from the bottom) to clear the 64px tab bar with
breathing room. They use w-[90%] max-w-sm to prevent long display names
(e.g. "Welcome, Mahmoud Sobhy!") from wrapping to more than two lines on
narrow screens. The icon and message text are centered (justify-center)
with the dismiss button absolutely positioned at right-3 so it does not
disrupt the centering.
<input type="date"> renders in the browser's OS locale format. On
en-US devices this shows MM/DD/YYYY, which is ambiguous for users who
expect DD/MM/YYYY. A formatDisplayDate() helper in
ManualEntryForm.tsx renders the selected date as DD Mon YYYY
(e.g. 07 Jun 2026) below each date input. This format is unambiguous
regardless of locale. The label only renders when the field has a value,
and each date input is wrapped in a <div className="flex-1"> with
items-start on the parent flex row so the adjacent time input does not
stretch to match the date+label height.
navigator.onLine === true means "not definitely offline"; it does not
guarantee the server is reachable. Captive portals, DNS failures, or a
down Supabase instance all return onLine: true while pushes fail. The
navigator.onLine guard in syncService.ts catches the obvious case
(airplane mode / no adapter); the try/catch in pushEntry catches
subtler failures and queues the entry for retry. Both layers are needed.
When GoogleSignInButton is rendered on a page other than the main log
(e.g. ImportPage), it accepts a returnPath prop. Before triggering
the OAuth redirect, signInWithGoogle() in useAuth.ts writes the path
to sessionStorage under the key circalog-auth-return-path. OAuth
requires a full-page reload, which destroys React state; sessionStorage
survives this reload. On the SIGNED_IN auth event, useAuth reads the
key, navigates to that path with replace: true, and immediately removes
the key. If no return path was stored, the user lands on /log as normal.
The CSV parser in src/utils/csvParser.ts handles sessions that cross
midnight using a two-step comparison:
- If
Sleep Starttime is earlier in the day thanBed Time, the sleep start date is the calendar day after theDatecolumn value. - If
Wake Timeis earlier in the day thanSleep Start, the wake date is the calendar day after the sleep start date.
Time comparison uses lexicographic string comparison on HH:MM strings,
which is correct for 24-hour time. The Date constructor interprets
YYYY-MM-DDTHH:MM:00 (no timezone offset) as local time, so all UTC
conversion reflects the user's actual local timezone at import time.
Free-text interruption values from the CSV are mapped to structured
Interruption[] objects rather than appended to the notes field. This
preserves queryability for future Insights features (e.g. "how often did
you wake to use the bathroom this week?"). The mapping strategy:
- Empty,
"N/A","none"(case-insensitive) βundefined - Text containing
pee,peed,bathroom,toilet, orloo(case-insensitive) β{ type: 'bathroom', note: originalText } - Anything else β
{ type: 'other', note: originalText }
The original text is always preserved in the note field so no
information is lost. See docs/SleepEntry-Field-Guide.md for the full
guide on what to enter in these fields for clean future imports.
CircaLog is open-source. Contributions are especially welcome from people living with Non-24, DSPD, or other circadian rhythm disorders, because the best people to build this tool are the ones who need it.
Please read CONTRIBUTING.md before submitting a pull request.
MIT License. See LICENSE for details.
Circa: from Latin, "approximately," "around." Log: a record, a diary, a history.
CircaLog: an approximate record of time, for people who live approximately outside of it.
Built with π for the sleepless, the drifting, and the uncounted.