TreeMap
A free, open-source disk-space visualizer. Squarified treemap, duplicate hunting, disk-full forecasting, and cleanup that only ever moves files to the Trash.
Daily habits, to-dos, ratings and a mini journal.
Translation pending
ProductivityTypeScript · JavaScriptGitHub @Bailey67777OnlineOwner 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
Loading the full README. The saved excerpt is shown below.
A personal daily habit tracker and journal, built as an installable web app (PWA).
One user, two devices, no login. Next.js + React + Tailwind, synced through a Firebase Realtime Database.Read the full README ↗A personal daily habit tracker and journal, built as an installable web app (PWA). One user, two devices, no login. Next.js + React + Tailwind, synced through a Firebase Realtime Database.
What's in it (five tabs: Today · Study · Health · Calendar · Settings)
Old Plan and Countdowns data is kept in the database but no longer shown.
Works offline for viewing; edits made offline are pushed when you reconnect (as long as the app stays open).
You need Node.js 20 or newer (node --version).
npm install
npm run dev
Open http://localhost:3000. Without Firebase configured the app runs in local-only mode (data is saved in the browser on that device). That's fine for trying it out.
Production build check:
npm run build && npm start
Go to https://console.firebase.google.com → Add project (any name, e.g. locked-in).
You can turn Google Analytics off.
In the left menu: Build → Realtime Database → Create database.
Pick a location close to you (e.g. europe-west1), choose Start in locked mode.
Open the Rules tab and replace everything with:
{
"rules": {
"spaces": {
"$key": {
".read": true,
".write": true
}
}
}
}
Click Publish. This allows reading/writing only under a specific key, and nobody can list the keys, so your data is only reachable by someone who knows both the database URL and your private key.
Copy the database URL shown at the top of the Data tab. It looks like
https://locked-in-xxxxx-default-rtdb.europe-west1.firebasedatabase.app.
Make a private key for your data path, e.g. run in a terminal:
openssl rand -hex 16
Create a file called .env.local in the project folder (copy .env.example) and fill in:
NEXT_PUBLIC_FIREBASE_DATABASE_URL=https://...firebasedatabase.app
NEXT_PUBLIC_DATA_KEY=<the random hex from step 5>
Restart npm run dev. The Settings screen should now say Synced with the cloud.
No auth is used on purpose. The deployed URL plus the random key is the whole privacy boundary. Don't share the Vercel URL, and keep
.env.localout of git (it already is).
Push the project to a private GitHub repository:
git add -A && git commit -m "Locked In"
Then create a private repo on GitHub and push (git remote add origin … && git push -u origin main).
Go to https://vercel.com → Add New → Project → import that repo. Framework is detected as Next.js; leave the defaults.
Before clicking Deploy, open Environment Variables and add:
| Name | Value |
|---|---|
NEXT_PUBLIC_FIREBASE_DATABASE_URL | your database URL |
NEXT_PUBLIC_DATA_KEY | the same random key as .env.local |
CLAUDE_API_SECRET | the server-only secret from .env.local (for the nightly task) |
(NEXT_PUBLIC_FIREBASE_API_KEY and NEXT_PUBLIC_FIREBASE_PROJECT_ID are optional and not needed.)
Click Deploy. You get a URL like https://locked-in-xxxx.vercel.app.
Later changes: just git push and Vercel redeploys. If you ever change an environment variable,
redeploy from the Vercel dashboard (Deployments → ⋯ → Redeploy) so the new value is baked in.
iPhone (Safari)
Android (Chrome)
Laptop
Bookmark the URL, or in Chrome/Edge click the install icon in the address bar to get a standalone window.
Drop files here (then commit + push so Vercel picks them up):
| File | Shows as |
|---|---|
public/photos/autumn/ (also winter/, spring/, summer/) | Today's banner in that season: any number of photos, any names; a different one each day |
public/photos/hero.jpg | the banner when that season's folder is empty |
public/photos/profile.jpg | round avatar next to the greeting (square) |
Until a file exists a wave placeholder is shown. Keep them under ~1 MB for a quick load on mobile.
Per-day photos are added inside the app (open any day → Photo of the day). They're resized in the browser (a 96px thumbnail for the calendar, a 720px version for the day view) and stored in Firebase, so they show on both devices.
Submitting a day never punishes you: it locks the ticks and logs the percentage. If the day is 80% or more with
every ⭐ must-do ticked, the reward video plays: public/media/day-80-100.mp4 (.mov, .m4v, .webm also work;
GitHub's website caps uploads at 25 MB). Close it whenever you like. If it can't load, it plays next time the app opens.
Web push can't reach an iPhone unless the site is installed from Safari, so there are three routes (Settings → Reminders):
.ics of daily repeating alarms and add it to the phone's Calendar.The journal is plain text, like everything else in the database, so anyone with the site URL and data key can read it. It is never sent to Claude: the API routes copy an allow-list of fields and the journal isn't on it.
Entries written back when the journal had a passphrase are stored encrypted (journalEnc). The first time the app
opens on a device that remembers the old key, it converts them to plain text and deletes the passphrase data in one
write. On a device that doesn't, the Journal card asks for the old passphrase once. Encryption code lives in
src/lib/crypto.ts only for that conversion.
Each day has one To Do list (todos), written the night before from Settings → Write the next day
(src/components/DayPlanner.tsx): import the previous day's list, add habits or anything else, reorder, and pick an
edit for any item. A written day is stored with list: true; habit items carry habitId, and the day's habits are
always worked out from the list, so streaks, Health stats, reminders and the ⭐ must-do rule keep working. A day that is
never written starts with all your habits plus its to-dos. Older days (habits and to-dos kept apart) are shown as one
list and converted the first time they're changed.
Edits live in a bank (src/lib/edits.ts): pieces at edits/<id>/<n>, a label with a thumbnail at editBank/<id>.
Upload from the planner's 🎬 picker or Settings → Edit bank (30 MB max each). Edits on today's list are downloaded in
the background and kept on the device, so ticking plays them straight away. Vlogs and edits share the 700 MB budget.
A vlog is recorded in the app (src/components/VlogRecorder.tsx), cut into 384 KB pieces and uploaded to
vlogs/<date>/<recordedAt>/<n> (src/lib/vlog.ts); the day record only carries a small vlog label. Uploads run
from a queue in IndexedDB, so a vlog recorded offline (or interrupted) finishes later, and the last few stay cached
on the device so they play instantly. Like the journal and photos, vlogs have no passphrase: anyone with the site
URL and data key could fetch them. They are never sent to Claude (the API's allow-list doesn't include them).
Recording is deliberately small (about 2 MB a minute) because the free database holds 1 GB in total. Settings shows how much vlogs use, and recording stops at 700 MB until some are saved to the device and deleted.
Server-only routes under /api/claude/*, protected by Authorization: Bearer <CLAUDE_API_SECRET> (a
server-only environment variable; never NEXT_PUBLIC_). Missing or wrong secret → 401.
| Route | What |
|---|---|
GET /api/claude/day?date=YYYY-MM-DD[&format=markdown] | submitted?, habit/to-do counts, academic journal, the day's questions + answers + feedback, task memory |
GET /api/claude/range?from=…&to=…[&format=markdown] | the same for every day in the range (max 31) |
POST /api/claude/update | questions, feedback, gradeEstimates, studyHours, econ, memory (zod-validated; keyed by date/subject, so re-posting overwrites) |
Reads copy an explicit allow-list of fields; the personal journal is never returned in any form.
The full nightly prompt and setup steps are in docs/NIGHTLY_CLAUDE_TASK.md.
Stored at spaces/<DATA_KEY> in the Realtime Database (days + settings are also mirrored to localStorage):
{
"settings": { "name": "Hugo", "examDate": "2028-05-15", "habits": [ { "id": "…", "name": "…", "emoji": "🌅", "color": "#…", "time": "06:50", "sound": "chime", "keystone": true } ], … },
"journalCrypto": { … }, // only until old passphrase entries are converted, then deleted
"days": {
"2026-09-25": {
"list": true, // the day's To Do is one written list
"todos": [ { "id": "h_gym", "text": "Gym", "done": true, "habitId": "gym", "editId": "137czy06x7ks" }, { "id": "…", "text": "Oxford reading", "done": false } ],
"habits": [ { "id": "gym", "name": "Gym", "done": true } ], // worked out from the list
"ratings": { "day": 7, "health": 8, "happy": 6 },
"journal": "…", // personal journal (plain text)
"academic": { "econ": "…", "maths": "…", "physics": "…" }, // active recall, plaintext
"answers": { "maths": "…", "physics": "…", "econ": "…" }, // answers to that day's questions
"song": "…", "screenMinutes": 185, "thumb": "data:image/jpeg;base64,…",
"vlog": { "v": 1, "at": 1791745317939, "dur": 92, "bytes": 3412345, "chunks": 9, "mime": "video/mp4" },
"submitted": { "at": 1789286956869, "pct": 86, "tier": "t80", "keystoneMissed": false }
}
},
"study": {
"questions": { "2026-09-26": { "maths": { "topic": "…", "question": "…", "why": "…" }, … } },
"feedback": { "2026-09-25": { "maths": { "mark": "2/3", "comment": "…", "correctAnswer": "…" } } },
"grades": { "maths": { "2026-09-25": { "grade": 4.4, "reason": "…" } }, "further": {}, "physics": {}, "econ": {} },
"hours": { "2026-09-21": { "maths": 4.5, "physics": 3 } },
"econ": { "2026-09-26": { "concept": { "title": "…", "explanation": "…" }, "reading": [ … ] } },
"memory": { "text": "…" },
"sync": { "lastRead": …, "lastWrite": … }
},
"photos": { "2026-09-25": { "data": "data:image/jpeg;base64,…" } }, // 720px versions, loaded on demand
"vlogs": { "2026-09-25": { "1791745317939": [ "<base64 piece>", … ] } }, // video pieces, fetched only to play
"editBank": { "137czy06x7ks": { "name": "gym edit", "dur": 14, "bytes": 5123456, "chunks": 14, "mime": "video/mp4", "at": …, "usedAt": …, "thumb": "data:image/jpeg;base64,…" } },
"edits": { "137czy06x7ks": [ "<base64 piece>", … ] }
}
Today and future days always follow the current habit list (with ticks preserved by id); past days keep the snapshot they were saved with, so editing the habit list never rewrites history. Habits with a time are kept in time order automatically.
Sounds. Fifty small MP3s in public/sounds, built by node scripts/make-sounds.mjs (needs a Mac for say, plus
ffmpeg). Nothing is downloaded or sampled from anyone's recording: the teachers and other voices are the Mac's
built-in speech voices, every tune is a public-domain melody written out as notes in the script, and the goats,
whistles, kazoos and horns are synthesised there. Every clip is levelled to the same loudness when built and checked
again after decoding (src/lib/sounds.ts). With lucky dip on (the default) each tick plays a random one from a
shuffled bag of all fifty; turn it off in Settings to give each habit its own. To change a sound, edit its entry in
the script and run it again; it rewrites the files and src/lib/sound-list.ts. The 50 ids never change, because
saved habits point at them. Bump VERSION in the script when files change so phones drop their cached copies.
src/app/ layout, page, manifest, global styles, icons
src/components/ AppShell (tabs), TodayView, DayEditor, CalendarView, TrendsView, SettingsView, …
src/lib/ types, dates, model (normalise days/study, tiers, reward rule), season (palettes + banners), crypto (old journal conversion), vlog (video storage and upload queue), stats, sounds (+ generated sound-list), images, daily (econ cards), reminders, store (state, outbox), firebase
src/lib/server/ claude.ts: auth, allow-listed reads, zod-validated writes for the nightly task
src/app/api/ econ-news (headlines proxy), claude/day, claude/range, claude/update
public/media/ your submit-day videos
public/sounds/ the fifty habit sounds (regenerate with `node scripts/make-sounds.mjs`)
public/sw.js service worker (offline app shell)
public/icons/ PWA icons (regenerate with `node scripts/gen-icons.mjs`)
public/photos/ your photos