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.
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 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.檢視完整 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