Javis OS
Javis OS는 기업가를 돕는 에이전시 AI로, 업무를 지원하고 함께 성장합니다
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Shared parts inventory, sales, and profit reporting for a Vancouver auto
recycler. Four partners, one shelf, four phones.
The problem it solves: when a customer messages one partner asking "do you
have a front bumper for a 2016 Civic", that partner can now answer in
seconds — and cannot promise a part another partner sold an hour ago.
At supabase.com. The free tier is enough to start.
Fill in, from Project Settings → API and → Database:README 전문 보기 ↗Shared parts inventory, sales, and profit reporting for a Vancouver auto recycler. Four partners, one shelf, four phones.
The problem it solves: when a customer messages one partner asking "do you have a front bumper for a 2016 Civic", that partner can now answer in seconds — and cannot promise a part another partner sold an hour ago.
| Framework | Next.js 16 (App Router, TypeScript, Turbopack) |
| Database, auth, storage, realtime | Supabase (Postgres 15+) |
| Styling | Tailwind CSS v4, Radix primitives |
| Install | PWA — home screen, no app store |
| Deploy | Vercel |
| Currency | CAD, stored as integer cents everywhere |
| Time | Stored UTC, displayed America/Vancouver |
At supabase.com. The free tier is enough to start.
cp .env.example .env.local
Fill in, from Project Settings → API and → Database:
| Variable | Where |
|---|---|
NEXT_PUBLIC_SUPABASE_URL | Settings → API → Project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Settings → API → publishable / anon key |
SUPABASE_DB_URL | Settings → Database → Connection string (URI) |
SUPABASE_DB_URL carries the database password, so URL-encode any
special characters in it — @ becomes %40. It is used only by the
scripts below, never by the app.
SUPABASE_SERVICE_ROLE_KEY is optional and currently unused: migrating,
seeding, and creating accounts all run over the database connection.
npm run db:migrate
Applies every file in supabase/migrations/ in order, each in its own
transaction, and records what it ran in app_migrations.schema_migrations
so re-running is safe. npm run db:status shows what is applied.
npm run seed:catalog # catalog only -- this is what production wants
npm run seed # catalog + a demo yard to look around
The catalog is reference data: vehicles cannot generate a parts list
without it. The demo yard is demo data, tagged [demo] on every row
and removable with npm run seed:clear-demo.
Fill the SEED_* variables in .env.local with the four partners (one as
owner), then:
npm run seed:users # create them
npm run team # list who exists
Everyone should change their password after their first sign-in. Accounts can equally be added from the Supabase dashboard under Authentication → Users; the profile row and role are created automatically by a trigger either way.
npm run verify # typecheck, unit tests, migration harness
npm run verify:live # the same guarantees against the real project
verify:live is worth running after any schema change. It exercises the
guarantees as the real roles through the real auth trigger, inside a
transaction that is always rolled back, and then again over HTTP through
PostgREST with a real JWT.
npm run dev
| Command | What it does |
|---|---|
npm run dev | Development server |
npm run build | Production build |
npm run verify | Typecheck + unit tests + migration harness (offline) |
npm run verify:live | Guarantee check + API smoke test against the real project |
npm run typecheck | tsc --noEmit |
npm test | Unit and architectural tests |
npm run lint | ESLint |
npm run db:verify | Applies every migration to a throwaway Postgres and asserts the guarantees |
npm run db:migrate | Apply pending migrations to the live project |
npm run db:status | Which migrations have been applied |
npm run db:check | Guarantees, live, in a rolled-back transaction |
npm run db:smoke | Guarantees, live, over HTTP with a real JWT |
npm run db:inspect | Read-only look at extensions, roles, tables, buckets |
npm run seed | Catalog + demo yard |
npm run seed:catalog | Catalog only (production) |
npm run seed:clear-demo | Remove every demo row |
npm run seed:users | Create the partner accounts from .env.local |
npm run team | List the accounts that exist |
npm run icons:generate | Re-render the PWA icons from the app mark |
NEXT_PUBLIC_ENABLE_STOREFRONT=false
under Settings → Environment Variables.These are enforced in the database, not the interface. npm run db:verify
applies every migration to a real Postgres (via PGlite) and asserts each one.
sell_part() carries its own status guard:
update public.parts set status = 'sold'
where id = p_part_id and status in ('available', 'reserved')
If that affects zero rows, the caller gets a payload naming who won, when,
and for how much — "Already sold by Ahmad for $180.00, 4 minutes ago."
Never a silent failure. sales.part_id also carries a unique constraint, so
even a bug in the function cannot produce two sales. The status flip and the
sales row share one transaction.
staff account can never see costs or profitThree independent layers, because one is not enough:
REVOKE SELECT on the five cost columns of vehicles from
authenticated. No query — not even select * — can return them.vehicle_finance, a security-definer
view whose WHERE clause calls has_finance_access(). It returns zero
rows to staff.insufficient_privilege. dashboard_stats() omits the money keys
entirely rather than sending nulls.parts snapshots name, category, and icon_key at creation. Renaming a
catalog entry changes what future vehicles generate and nothing else.
Every currency column is bigint. parseMoneyToCents() reads the digits out
of the string rather than multiplying a float — Math.round(1.005 * 100) is
100, not 101, because the nearest double to 1.005 is 1.00499999999999989.
Covered by tests/money.test.ts.
Deleting a vehicle or part requires confirmation and writes to
activity_log before the row disappears. Deleting a vehicle that has
recorded sales is refused outright, not confirmed — it would erase that
revenue from every report.
A hold sets reserved_until 48 hours out. expire_reservations() returns
expired parts to the shelf and logs a notice against the partner who placed
the hold. It reads the holds before clearing them, because
UPDATE ... RETURNING hands back the new row where reserved_by is already
null. FOR UPDATE SKIP LOCKED keeps four phones sweeping at once from
piling up.
One RPC, one trigram index. parts.search_text denormalises the part name,
side, category, and its vehicle's year/make/model/trim/stock number, so
"civic mirror" matches across the join in a single pass and "bumpr"
still finds a front bumper cover. Every whitespace token must match, either
as a substring or by trigram word similarity.
The search screen queries Supabase directly from the browser rather than through a Next.js route, so results land while the partner is still mid-sentence. A request-id guard stops a slow early response overwriting a fresher one.
parts is in the supabase_realtime publication with
REPLICA IDENTITY FULL. A status change patches the matching row on the
other three phones — greys it out, flashes it, and says what happened. Your
own actions don't toast: the optimistic update already set that status, so
the equality check short-circuits.
Stated once, in 0006_reporting.sql, and used consistently: a sold part is
charged a share of its vehicle's total invested cost, proportional to its
asking price against the sum of asking prices of every non-scrapped part on
that vehicle. Where a vehicle has no asking prices at all, cost is split
evenly by part count. The monthly report says this on screen.
The monthly report shows net profit (cash basis) and gross profit on parts sold (allocated cost basis) side by side, each labelled with its basis. Cash basis alone makes any month with three cars bought in it look like a disaster; the allocated figure says whether the business is actually working. Neither is presented as "the" profit.
src/lib/data/public-shop.ts builds its own anonymous Supabase client and
touches only public_parts and search_public_parts — the only two objects
granted to anon. The view carries no cost, shelf, VIN, note, or internal
asking-price column, so there is nothing there to leak.
tests/isolation.test.ts fails if a future edit imports an authenticated
client into that layer or a /shop route.
Part slugs (2016-honda-civic-front-bumper-cover-v0147-1) are generated at
creation, so public URLs are correct from day one and never change.
Caches the app shell only: hashed build output, icons, and one static
/offline page. No HTML page is ever cached — every screen is a
signed-in view of shared data, and a cached page could show a stale shelf or
appear on a different account after a sign-out. Navigations always hit the
network and fall back to /offline.
| Add a car | Search & sell | Edit parts | Vehicle costs | Reports | Expenses | Edit / delete a car | Team | |
|---|---|---|---|---|---|---|---|---|
owner | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
partner | ✅ | ✅ | ✅ | — | — | — | — | — |
staff | ✅ | ✅ | ✅ | — | — | — | — | — |
Partners and staff run the yard: they book cars in, trim the parts list, price parts, and sell them. What a car cost, what it made, and whether it stays are the owner's. A partner adding a car simply gets no cost fields — and the RLS policy refuses a priced insert from them regardless, so the form is a convenience, not the control.
Every vehicle carries a plan, chosen when it is booked in.
Part it out — the default, and what most ICBC salvage is for. Saving the car generates the full parts list; you trim it down on the next screen and sell from the shelf.
Repair and sell whole — bought at auction or off Facebook Marketplace,
fixed, inspected, and sold as a car. No parts list is generated. Repair,
inspection, transport and anything else spent on it goes in under
Expenses, against that vehicle, because those costs arrive over weeks
rather than at the auction. What it sold for is recorded on the edit
screen, and vehicle_pnl() counts it as revenue next to parts and scrap.
The title field decides what is even possible. A car branded
non_repairable or write_off can never be road-legal again in BC, so it
is locked to the part-out path in the form and in the server action —
neither trusts the other.
The plumbing is live from day one; the shop is not.
NEXT_PUBLIC_ENABLE_STOREFRONT=true and redeploy./shop returns 404 until that flag flips. There is no cart — prices are
shown in full and buyers are asked to get in touch.
reserved status, so it cannot also be the brand colour.pdx1Every screen reads live data, so each page render makes several sequential round trips to Supabase. Where those two run matters more than anything else in the request.
Vercel places functions in iad1 (Virginia) by default. This project's
database answers a TCP connect from Vancouver in about 16 ms, which
puts it in US West. Left on the default, every query would cross the
continent and back — roughly 130 ms each, several times per page.
vercel.json therefore pins functions to pdx1 (Portland), alongside
the database.
If the Supabase project is ever moved, change that one line to match.
Vercel's region codes are in the deployment's X-Vercel-Id header: the
second segment is where the function actually ran.
curl -sI https://your-app.vercel.app/login | grep -i x-vercel-id
# x-vercel-id: pdx1::pdx1::...
# ^edge ^function -- these two should match your database
Hobby plans allow one region, which is all this needs.