TreeMap
무료 오픈소스 디스크 사용량 시각화 도구로, 사각형 트리맵·중복 파일 탐색·디스크 가득 예측·정리 시 파일을 휴지통으로만 이동한다
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Lists, tasks and recipes for your home. Multi-tenant: every home is isolated, and a
person belongs to as many of them as they have been invited into — the flat and the
summer house — reading one at a time and switching between them from the header.README 전문 보기 ↗Lists, tasks and recipes for your home. Multi-tenant: every home is isolated, and a person belongs to as many of them as they have been invited into — the flat and the summer house — reading one at a time and switching between them from the header.
Next.js 15 (App Router) · Prisma · PostgreSQL · Tailwind · Web Push
Lists — Create any number of lists (shopping, to-do, packing). Add, tick off, remove and clear items. Rename or delete a whole list. Drag items into the order you shop in, and when you type something that was ticked off earlier it is offered back rather than duplicated — so next week's list is mostly rebuilt from last week's. Ticked-off items fold away into a "Completed" section, and the dashboard counts what is still open rather than everything a list has ever held.
Tasks — A task happens once or over and over, and the repeat picker is where you say which. A one-off is finished when you mark it done: it moves to a folded-away "Done" section at the bottom of the page, where it can be brought back if the wrong card was pressed. A recurring task is given an interval in days, and marking it done reschedules it that many days out. Either can be handed to one member of the home, and either can be edited into the other kind later. Overdue and due-today tasks are highlighted on the dashboard, and a daily job sends a push notification for anything due — a one-off that has been done is out of all of that for good.
Recipes — Title, description, ingredients and instructions (one per line). Paste an
Instagram, YouTube, TikTok, Vimeo or Facebook link and the video is embedded on the recipe page.
Links from any other host are shown as a plain "open in new tab" link rather than embedded.
Import from a link takes a recipe page or a reel: the text is pulled off the page — its
schema.org markup, or a reel's own description, or failing both the page's visible words —
and then read into a recipe by Claude, which deduplicates the lines, separates each amount
from its unit and its ingredient, and throws away the hashtags and the "follow for more".
The result opens in the ordinary create form to check over before saving. This needs
ANTHROPIC_API_KEY; without it the importer says so and offers the form instead.
Send to Nemlig fills the household's basket at nemlig.com from a list, after showing
which product it would pick for each line — see Deploying below for switching it on.
Add to list puts every ingredient onto whichever of the home's lists you pick: anything
already on it is wanted one more time rather than written twice, anything ticked off comes
back, and each line then says which recipe asked for it — two recipes wanting onions name
both. Ticking an item off drops the note, because the question it answers is about the shop
still to do.
Pictures — The home has one of its own, shown beside its name and across the top of the dashboard, and so can each list, task and recipe. Taking a photo on a phone and adding it works: it is shrunk in the browser before any of it is uploaded. See below.
A picture can be added to the home itself and to anything a home creates. What you pick is never what is stored:
Content-Type the request claims — and refuses anything past the limits in
src/lib/photo-file.ts. The browser doing the work first is a
convenience; this is the part that holds when the browser is not the caller.The bytes live in Postgres, in a Photo row belonging to a home. A household's collection is
measured in megabytes, and keeping it in the database means no second service to hold
credentials for and nothing to keep in step when a row is deleted. Pictures are served from
/api/photos/<id> behind the session, so there are no public URLs; an id belonging to another
home answers 404, exactly as an id that never existed does.
Uploading happens when the picture is chosen rather than when the form is saved, so it appears straight away and the form itself carries only an id. Abandon the form and that picture belongs to nothing — the next upload from the same home clears any such stray older than an hour.
The worst bug this app could have is one household seeing another's data, and the way
that happens is a forgotten where: { homeId } in a query someone wrote in a hurry.
So pages do not write that clause. They read through a client bound to one home:
const lists = await homeDb(user.homeId).list.findMany({ orderBy: { createdAt: "desc" } });
src/lib/home-db.ts carries the home into every query against a
home-scoped model — lists, tasks, recipes, invites and memberships — and stamps it onto
anything created. A query that forgets the home returns nothing instead of somebody
else's rows, and a record cannot be filed under the wrong home. Models that belong to
nobody in particular, such as push subscriptions, pass through untouched.
A user is not one of them. Somebody belongs to several homes, so their row carries no
home to scope by, and a household's roster is its HomeMember rows with the person read
through each. Asking homeDb for a user is refused outright rather than answered with
every account on the installation.
A lint rule keeps pages and components on that path. Reaching for prisma.list in a
page is an error with a message saying why; prisma directly is for the places where
crossing homes is the point, such as the nightly reminder job and the super admin's
system view.
This sits alongside the permission checks in src/lib/access.ts
rather than replacing them: those decide whether someone may act, this removes the
chance to ask the wrong question.
Admin → System (super admin only) answers "is this installation healthy?": whether the database is reachable and how fast it responds, when reminders last went out, the last few runs with their counts, content totals, and the slowest database calls of the last day.
Settings → Reminders (every home admin) answers the same question for one household: how many people have notifications switched on, when a reminder last went out, and what is overdue. Nothing there reveals another home.
The point of both is the failure that hides: if the reminder schedule stops firing, no reminders looks exactly like nothing being due. Every run writes a row, so a gap becomes visible.
/api/healthcurl https://<your-app>/api/health
Answers {"status":"ok"} and HTTP 200, or 503 when the database is unreachable — enough for an
uptime monitor, and nothing an outsider can learn from. status is degraded when reminders
have not run in REMINDER_STALE_AFTER_HOURS (26) or the last run failed; that stays HTTP 200,
because the app is still serving.
Send the cron secret for the detail behind the verdict:
curl -H "Authorization: Bearer $CRON_SECRET" https://<your-app>/api/health
Database calls over SLOW_QUERY_MS (400 by default) are recorded and shown on the System page.
Only slow ones are kept — logging every query would make the metrics table the busiest in the
database — and the daily reminder run prunes anything older than seven days.
Failed requests are not stored. src/instrumentation.ts writes each one to the platform log
as a single JSON line with the path, route and stack, so log search can filter on the fields.
Errors arrive in bursts and are unbounded in size; a table of them would need its own paging and
pruning to be worth reading.
The app runs on one household clock, set by TIME_ZONE in src/lib/time.ts
(currently Europe/Copenhagen). Tasks come due at 09:00 on that clock, and "Due today" means
today's calendar date there.
This is deliberately not the server's own timezone, which is UTC on Vercel and something else on
a developer's laptop. Anything that decides or displays a date goes through src/lib/time.ts
rather than using new Date(...) arithmetic directly. If the household moves, change that one
constant.
A person belongs to any number of homes. Your homes (/homes) lists them, and where
there is more than one the home's name in the header becomes the way between them; what is
on screen is one household at a time.
Running a home is a property of the membership, not of the person, so somebody can be the admin of the flat and an ordinary member of the summer house. A home is run from its own Settings, behind the home's picture and name in the header, so what is offered follows the home on screen rather than the person. The same menu holds Profile — your name, your password, your picture and your notifications — and the way to your other homes.
The Admin tab is the super admin's alone, because what is behind it is the installation rather than a household: System and Homes.
| Role | Can do |
|---|---|
| Super admin | Everything; creates homes, and reads any home without joining it |
| Admin (of one home) | Manages that home: settings, members, roles, invitations |
| User (of one home) | Creates and edits lists, tasks and recipes in that home |
Everybody in a home shares its lists, tasks and recipes. No one can reach a home they are not in — requests for its records return 404.
Removing somebody from a home removes the membership and nothing else: their account stands, so do their other homes, and what they wrote stays with the household. Deleting a home removes its contents and empties it of members, who keep their accounts.
There is no mail server. An admin creates an invitation for a specific email address and the app
generates a one-time code. The admin passes that code on however they like (text, chat, in
person). The invitee goes to /accept-invite and must supply both the exact invited email
and the code. Somebody new gets an account; somebody who already has one joins the home
with it, giving their existing password rather than choosing a new one. Codes are stored
hashed, expire after 14 days, and are displayed only once — create a new invitation if one
is lost.
npm install
Copy .env.example to .env and fill it in:
DATABASE_URL — any Postgres instance.AUTH_SECRET — generate with node -e "console.log(require('crypto').randomBytes(32).toString('hex'))".NEXT_PUBLIC_VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY — generate with npx web-push generate-vapid-keys.CRON_SECRET — any random string; the reminder endpoint requires it.QSTASH_TOKEN / QSTASH_CURRENT_SIGNING_KEY / QSTASH_NEXT_SIGNING_KEY — optional, from
the Upstash console's QStash page. With them, a cook-mode timer also rings as a push
notification on a locked phone; without them it counts and buzzes in the page only.SUPER_ADMIN_EMAIL / SUPER_ADMIN_PASSWORD — used once, to seed the first account.npm run db:push
npm run db:seed
npm run dev
Log in at /login with the super admin credentials, create a home under Admin → Homes,
switch into it, and invite the rest of the household. Inviting somebody who already has an
account here adds them to this home as well; they keep the ones they were in.
npm test
Two kinds of test run from that command:
tests/unit) — pure logic with no database: the video-embed allowlist, due-date
wording, invite codes and the role rules.tests/integration) — the real server actions and the reminder endpoint,
driven against a real Postgres database, with only Next.js's per-request APIs stubbed. These
cover logging in, throttling, accepting an invite, lists, tasks, recipes,
administration, and that one home can never reach another home's data.Run one group on its own with npm run test:unit or npm run test:integration, and watch them
with npm run test:watch.
Browser tests are separate, because they need the app built and served:
npm run e2e
e2e) — Playwright drives a real Chromium against a production build of the
app: signing in and out, issuing an invite and redeeming the code, building a list and ticking
items off, completing a task of either kind, embedding a recipe video, every administration screen,
and one home being unable to open another home's pages. npm run e2e:ui opens the interactive
runner; after a failure npm run e2e:report shows the trace, screenshot and DOM snapshot.npm run verify runs the whole gate in the order the pre-push hook does: lint, types, the
schema check, the vitest suites, then the browser tests.
The tests need Postgres running:
docker start homehub-pg
They use separate databases from development, named after DATABASE_URL with a suffix —
homehub_test for vitest and homehub_e2e for Playwright — each created and migrated
automatically on first run. Override with TEST_DATABASE_URL or E2E_DATABASE_URL.
Neither of those is ever run against. Both are templates: each worker gets a
CREATE DATABASE … TEMPLATE copy of one, named homehub_w<key>_test (or _e2e), so the files
can run at the same time without truncating each other's tables. Copying takes about a tenth of a
second against nearly two for spawning the migration CLI again, and it cannot produce a database
the migrations have not been applied to. The copies are swept away afterwards, and again at the
start of the next run, since a run that is killed never reaches its own teardown — a vitest copy
whose process is still alive is spared, so a test:watch left open and a full npm test do not
drop each other's databases.
Both suites truncate tables between tests, so each starts from a known state. Two guards make it
impossible for that to reach real data: the database name must carry the right suffix — a
worker's copy included, which is why the key goes before it — and it is checked again
immediately before the first delete. tests/unit/test-db-url.test.ts holds those rules to
account without opening a connection. Your development data is never touched.
The plumbing behind all of that — making the template, migrating it, copying it, sweeping up and
truncating — is scripts/test-db.mjs, shared by both suites. It was the same code twice, and the
two halves that have to agree are not within a suite but across them: one writes a copy's name
and the other has to recognise it again.
A second worktree needs its own databases and its own browser-test port. The databases are the obvious half; the port is the half that wastes an afternoon.
# in the second checkout's .env
TEST_DATABASE_URL=postgresql://…/homehub_myfeature_test
E2E_DATABASE_URL=postgresql://…/homehub_myfeature_e2e
E2E_PORT=3180
Playwright serves the built app on E2E_PORT (3100 by default) and reuses a server it finds
already listening there. Two checkouts on the same port therefore share one server built from
one of them, and whichever suite finishes first tears it down under the other. It does not look
like a collision: it looks like ERR_CONNECTION_REFUSED, tests failing in a different place
each run, and a summary reporting a handful of passes out of a hundred. Every one of those is
worth chasing, and none of them is real.
Migrations are the other thing to watch. Both checkouts migrate the same shared databases unless they are pointed apart, so a branch carrying a migration the other does not have will break the other's suite — usually as a null-constraint violation in a model neither branch was touching.
Pushing to main is what triggers a Vercel deploy, so the tests gate the push:
npm install points git at .githooks, where a pre-push hook
checks the change. Lint, types and the unit tests always run — seconds, and they need
nothing running. The integration and browser suites run only when something they could
exercise has changed, so editing a README costs about ten seconds rather than three
minutes and a full build; anything unrecognised runs everything. If a check fails,
nothing is pushed and nothing deploys. In a genuine emergency, git push --no-verify
skips it..github/workflows/test.yml runs the same checks on GitHub against a throwaway
Postgres, on every push and pull request. Lint, types, the schema check and the vitest suites
run in one job; the browser suite runs in another, alongside rather than behind it, since the
two share nothing but two minutes of setup. Both report into a third job named
Lint, types and tests, which is the single check a deploy gate waits for — see below. A
failing browser test uploads its Playwright report as a build artifact.npm run build runs the unit tests before next build, so a broken
build fails on Vercel even if the first two were bypassed. (Integration tests are left out
here: the build has no test database, and it must never touch the production one.)The build runs prisma migrate deploy, so a migration is applied by the deploy that ships it.
A bad one therefore presents as a failed build, on a commit that is already on main, with no
rollback path — the one failure in this project that cannot be fixed by reverting.
npm run db:check is the cheap half of that problem. It replays the migrations into a
throwaway _shadow database and compares the result against prisma/schema.prisma, so a
schema edited without a migration to match is caught in about two seconds. It runs in the
pre-push hook and in CI, and it is worth running by hand after any schema change.
The integration suite catches some of this already, but only where a test happens to touch the model: the generated client asks for a column the database has not got, and the query errors. A field on a model no test exercises reaches the deploy. This asks the question directly.
What it does not check is a migration that is valid against an empty database and fails
against a full one — a NOT NULL column added to a populated table, a unique index over values
that are already duplicated. Both suites migrate from empty. Until that gap is closed, a
migration of that shape is worth applying to a copy of production by hand before it is merged.
main deploysvercel.json disables automatic deployments for every branch except main:
"git": { "deploymentEnabled": { "**": false, "main": true } }
Preview deployments are deliberately off. They shared the production database — the
build runs prisma migrate deploy, so a branch adding a migration applied it to
production before anyone reviewed it, and the preview app itself read and wrote real
data. Rather than give previews their own database, there are no previews.
What replaces them: the browser tests, which drive a production build of the app against a throwaway database on every push, locally and in CI. That is a better check than clicking through a preview, and it cannot touch anything real.
If you ever want a one-off preview, npx vercel still deploys from your machine on
demand. This setting only turns off the automatic ones.
By default Vercel builds and releases as soon as main moves, without waiting for the workflow
above. Two independent ways to close that gap — either is enough, and they combine well:
Vercel Deployment Checks hold a finished production build back from your production domain
until the checks pass. First make sure automatic aliasing is on under
Project Settings → Environments → Production. Then open
Project Settings → Build and Deployment → Deployment Checks, choose Add Checks, pick
GitHub as the provider, and select the Lint, types and tests check (GitHub identifies
checks by job name, so renaming that job in test.yml means re-selecting it here). That job runs
nothing itself: it is green exactly when the checks and browser jobs beside it are, which is what
lets the work be split in two without the gate having to be re-selected or widened to a list. The build
still runs; it just is not released to users until the tests go green. Force Promote on the
deployment page overrides this when you need it.
A GitHub ruleset on main requiring the same check keeps failing code off the branch in the
first place, so no production build is ever created from it. Set it under
Repository Settings → Rules → Rulesets, requiring a pull request and the status check.
In Supabase create a project, then open Project Settings → Database → Connection string. Two different strings are needed:
6543) → DATABASE_URL. Append ?pgbouncer=true&connection_limit=1,
which Prisma requires when talking to a transaction pooler.5432) → DIRECT_URL. Migrations need a real session and cannot run
through the pooler.node -e "console.log('AUTH_SECRET=' + require('crypto').randomBytes(32).toString('hex'))"
node -e "console.log('CRON_SECRET=' + require('crypto').randomBytes(24).toString('hex'))"
npx web-push generate-vapid-keys
Import the GitHub repo, then add every variable from .env.example under Settings → Environment
Variables. The build runs prisma migrate deploy, so the schema is created on the first deploy —
there is no separate migration step.
An open list updates on other phones the moment somebody ticks something off, through Supabase Realtime. Without this it still updates, every three seconds. In the same Supabase project:
SUPABASE_URL, the publishable
(anon) key into SUPABASE_PUBLISHABLE_KEY, and the secret (service role) key into
SUPABASE_SECRET_KEY.SUPABASE_JWT_SECRET.
The app signs each phone's Realtime token with it (HS256); if the project has moved to
the new signing keys, keep the legacy secret un-revoked, or import a shared secret
(HS256) as a signing key and use that.Could not create the Realtime home channel policy, run the CREATE POLICY from
prisma/migrations/20260930120000_realtime_home_channels/migration.sql in the SQL editor.To check it works, open the same list on two phones and tick something on one: the other
should move within a second. In the browser's network tab, /api/realtime/token should
answer "enabled": true and a websocket to <ref>.supabase.co/realtime/v1 should stay
open.
A list can be sent to the household's basket at nemlig.com: every open line is searched, a product is proposed for each (the one chosen last time, where there was one), the household checks and swaps them, and only then is the basket filled. It never checks out or pays — that is still done on nemlig.com.
Each home connects its own Nemlig login, on Settings → Nemlig (admins only). Nemlig has no OAuth, so the password itself is kept, encrypted with AES-256-GCM under one key that lives only in Vercel:
node -e "console.log('NEMLIG_SECRET_KEY=' + require('crypto').randomBytes(32).toString('hex'))"
Add it to Vercel and redeploy; the Nemlig card appears in Settings. Without the key the feature is not offered at all. Changing the key makes every stored login unreadable — each home then has to connect again.
It uses Nemlig's own, unpublished web API (worked out by
nemlig-shopper), so it can break when
Nemlig changes its site. The review says so plainly ("Nemlig answered in a way this app
does not understand"), and the runtime logs carry nemlig_* lines with the detail. The
client is src/lib/nemlig.ts.
Once deployed, run the seed once from your machine against the production database:
DATABASE_URL="<direct-url>" DIRECT_URL="<direct-url>" SUPER_ADMIN_EMAIL="[email protected]" SUPER_ADMIN_PASSWORD="<a strong password>" npm run db:seed
Log in, create a home under Admin → Homes, switch into it, and invite the household.
vercel.json registers a daily cron at 06:00 UTC that calls /api/cron/reminders. Vercel sends
its own Authorization: Bearer $CRON_SECRET header, so setting CRON_SECRET is all that is needed.
Hobby-plan projects are limited to one cron run per day, which this schedule fits.
The job notifies about everything due by the end of that day rather than by the minute it runs, so the exact hour is not load-bearing — it only has to land in the morning.
Push notifications require HTTPS, which Vercel provides. On iOS the site must be added to the home screen before Safari will deliver notifications.
Browse the database:
npm run db:studio
Run every check exactly as the pre-push hook does:
npm run verify
Build as Vercel will:
npm run build