Fujimod
Try your camera firmware in the browser and build Fujimod SD boot files.
One app to replace the pile of apps a household uses: AI meal planning, a self-writing larder, the family agenda, shopping lists and jobs. Web-first prototype.
LifestyleTypeScript · CSSGitHub @tinytom21OnlineOwner 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
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗One app to replace the pile of apps: meal planning, the larder, the family agenda, shopping lists, tasks and reminders, and spending analysis. Web first, so it works on a laptop the day it exists.
The hosted site runs the real domain code in your browser — the same modules the server calls. Your week lives in that browser, and signing in keeps it in your account as well, so the same family turns up on a phone. It never holds an API key, because a key shipped to a web page is a key given away; the two AI buttons work there by asking a Supabase function that holds one, and stay dead until that function is deployed and you are signed in. Filling a Tesco basket stays on your own machine, where the supermarket session belongs.
| Path | What it is |
|---|---|
docs/ | The published site: landing page, build plan, Google runbook. |
docs/plan.html | The build plan — stack decisions, architecture, the larder design, phases, risks. Start here. |
docs/google-setup.html | Runbook for wiring Google Calendar access through Supabase auth. |
prototypes/meal-plan/ | The working spike. |
prototypes/meal-plan/src/app-state.ts | The whole app, with no idea where it is running. |
supabase/schema.sql | Households, membership, invites and state — and the RLS that is the security model. |
The hosted demo has no model behind it, so Replan with AI is disabled there. To use this for an actual week's shop, run it locally with a key:
cd "prototypes/meal-plan"
npm install
Set a key, then start it — in the same window, because a key is set per terminal:
$env:GEMINI_API_KEY = "your-key"
node web/server.ts
Command Prompt uses set GEMINI_API_KEY=your-key instead. ANTHROPIC_API_KEY
works just as well; if both are set Claude wins, and MEAL_PLAN_PROVIDER=gemini
forces the other.
These say node rather than npm run on purpose. Windows blocks npm.ps1
under the default execution policy, so npm fails in PowerShell with a security
error that has nothing to do with this project. node sidesteps it entirely;
npm.cmd run web also works if you prefer npm.
The local server keeps the household in prototypes/meal-plan/.family-state.json,
written after every change and read back at startup, so closing the terminal
costs nothing. It is the family's own data and is gitignored; FAMILY_STATE_FILE
moves it somewhere else.
Paste a real key, not the placeholder. $env:ANTHROPIC_API_KEY = "sk-ant-..."
sets the variable to the literal string sk-ant-..., and the assignment
succeeds even when the command after it fails — the app now refuses that rather
than sending it to Anthropic, but it is still a wasted minute.
Off by default, local only, and never in the published demo — which has no server and so has no path to a supermarket session at all.
Try it first in practice mode, which runs the whole flow against invented Tesco products and sends nothing anywhere:
$env:TESCO_BASKET = "practice"
node web/server.ts
The practice products are deliberately awkward — every ingredient comes back alongside a wrong-size version of itself, and anything with a specification like "5% fat" also comes back with a different one — so it shows whether the matching holds up, not just that the buttons work.
When you are happy, for real:
$env:TESCO_BASKET = "1"
node web/server.ts
The first run opens a real Chrome for you to sign in to Tesco yourself; no
password passes through this app. After that it is plain HTTP, and the session
persists at ~/.basketeer/session.json. That file is plain text and can act
on your grocery account — worth knowing before running this on a shared
machine.
In the Basket panel, Find all on Tesco searches every line. A 100% match — every word of the name, the exact size, and in stock — goes straight in, marked matched automatically so you can see which ones nobody looked at. Everything else is laid out with its options, the best one ticked where there is a sensible best, each with Tesco's shelf price, price per kilo or litre, any Clubcard offer, and a link to the product's page. Choices are remembered per ingredient and pack size, so week two is mostly one click. Anything can be changed.
When two products are both a 100% match — own brand and Finest at the same size, say — it takes the one Tesco lists first. When the best match is out of stock it chooses nothing, because remembering this week's substitute as your usual is a decision for a person.
Nothing here spends money. Filling sets quantities — pressing it twice leaves one week's shopping, not two — and checkout returns a URL for you to go and pay, having seen the real total and the real substitutions.
It uses basketeer, which is reverse-engineered and pre-release. Tesco publishes no API for this, so it will break when they change something; the provider is five methods so that break is an afternoon.
Roughly 2,300 tokens in and 7,000 out for one week's plan, so at one plan a week:
| Model | Per plan | Per year |
|---|---|---|
claude-sonnet-5-5 (default) | ~$0.07 | ~$4 |
claude-opus-5-5 | ~$0.15 | ~$8 |
claude-haiku-4-5 | ~$0.04 | ~$2 |
Sonnet 5.5 is the default because it is fast enough to finish a week inside
the hosted function's 150-second limit, and a meal plan is mostly careful
rule-following rather than deep reasoning. Model names use hyphens, not dots.
Set one locally with $env:CLAUDE_MODEL = "claude-opus-5-5", or for the
website with the PLAN_MODEL secret. A repair round costs
another call, so budget for two or three plans' worth on a bad week.
Gemini's free tier allows twenty requests a day, which is under three clicks of Replan once repair rounds are counted. It is enough to see the thing work and not enough to use, which is worth knowing before concluding the app is broken.
A static host cannot hold an API key: anyone who opens the page can read it and
spend it. So the key goes in a Supabase Edge Function —
supabase/functions/plan — and the
published app asks that instead of an SDK.
It is a carrier, not a second model API. The prompt, the schema, the repair
loop and the validation stay in the app, where they are tested; both hosts
build the request from the same module (src/ai/anthropic-wire.ts), so the
laptop and the phone ask Claude for exactly the same thing. What the function
adds is three refusals: it checks in the database, under your own token, that
you are in the household you claim; it claims a call against a daily allowance
before spending anything; and it allow-lists the model and the answer length
rather than trusting the browser for either.
To switch it on, once:
supabase login
supabase link --project-ref your-project-ref
supabase secrets set ANTHROPIC_API_KEY=sk-ant-your-real-key
supabase functions deploy plan
Then run the newer half of supabase/schema.sql — the
model_usage table and the two functions beneath it — in the SQL editor. The
whole file is re-runnable, so pasting all of it is fine.
PLAN_DAILY_CALLS sets the allowance (default 20 a day per household, and the
database refuses to be talked above 100 whatever the function asks for).
PLAN_MODEL pins a cheaper model for the hosted path alone. PLAN_ALLOWED_ORIGINS
is the CORS list, and already covers the published site and localhost:4321.
The two AI buttons come alive on the hosted site once you are signed in and the household is in your account — the allowance belongs to a household, so there has to be one. The local server ignores all of this and keeps using the key in its own environment.
The order that gets you a usable week:
/check-google.html
once first if you have not already.The week is taken from the real calendar — a household set up today is planned for tomorrow onward. The example family stays pinned to its fixture week in August, because its calendars and jobs are written against it.
You get the week, the jobs, the shopping list and the larder, all live — confirm what is in the cupboard and watch the shopping list change underneath you.
On a first visit you get an intro screen rather than somebody else's data:
how many of you, then names and ages, then allergies, likes and dislikes.
Allergies are matched against the tags the validator can actually enforce —
anything it can't check (kiwi, say) is kept as a strong dislike and it says
so, because an exclusion that looks like protection and isn't is worse than
none. There's a second door marked Show me an example family for looking
round.
Accounts are optional and come second. Set the household up, use it, and sign in later if you want it on another device or shared with someone else. Signing in uploads what's already in the browser; an invite code lets the other adult join and change everything. A person at the table and a user with a login are deliberately different things — a seven-year-old doesn't need an account to be allergic to peanuts, and requiring one is a good way to make sure the allergy never gets recorded.
Nothing is typed in twice. The family is entered once and then kept in
three places that agree with each other: the browser you're using, a file
beside the local server (.family-state.json, written after every change), and
— once you sign in — your account. After signing in there is no save button:
every change goes up a second after you stop making it, and the top bar says
Saved to your account when it has. Open the site on a phone, sign in, and the
family is already there; the intro screen offers I already have an account
for exactly that.
Two people editing at once is the normal case, so it's handled rather than prevented. On opening, the account's copy wins — a laptop that's been shut for a week doesn't get to undo the week. A change that loses a race isn't thrown away either: the database refuses a save built on a stale revision, so the losing copy is kept in that browser and offered back from the Account panel. Last-write-wins that nobody can see is how a Sunday evening disappears.
To switch accounts on you need a Supabase project: run
supabase/schema.sql in its SQL editor, then set
SUPABASE_URL and SUPABASE_ANON_KEY (repository secrets for the published
build, or a supabase.config.json at the top of this repo for local runs —
the local server serves the same details to the page, so signing in on
localhost reaches the same household as signing in on the real site instead
of quietly starting a second one). Without them the app says accounts are off
and keeps working in the browser. Both origins have to be on Supabase's
Redirect URLs list, which is why the runbook adds http://localhost:4321/**
as well as the published one. The anon key is public by design —
it names the project, not the person — so Row Level Security is what actually
protects a family's week, and the build refuses any key whose role isn't
anon.
Who lives here holds a profile per person: age bracket, allergies, likes and dislikes, whether they can cook, and optionally a Google account. Portion size and cooking ability both follow from the age bracket until you say otherwise, because asking a parent to type a portion multiplier for a toddler is how an app gets closed.
This week's table is the grid the family reviews on a Sunday: a tick per person per day for who is in for dinner, a dropdown for who is cooking, and a slider for how long they have. The calendars fill it in and every cell says where its value came from — read from a diary, assumed, or corrected by a human. Corrections are stored separately from the proposal, so re-reading a calendar can never quietly undo one.
That grid then drives everything downstream. Portions are the sum of who is actually in, so the Thursday one child is at a friend's house buys less food. The cooking limit belongs to the person cooking, not the evening. And a day where nobody is free to cook is a real answer — the planner is told to use leftovers or the freezer rather than just something faster.
Agreeing, revising and rating. A plan is a draft until somebody presses Agree this plan. Before that, anything you would like changed goes in the week's note box, and Revise the plan with this note changes only what the note asks for, keeping every other evening exactly as it was. Replan with AI starts again. Once a meal has been eaten it gets a 👍 or 👎, and the History panel lists anything from the past few weeks still waiting for a verdict.
Every agreed meal is logged, with its rating and its recipe, and each new plan is told what the log says. It avoids dishes the family disliked and anything had in the last fortnight, and brings a favourite back now and then, as the same recipe rather than a reinvention. A favourite rests three weeks after the first time, up to six for an old regular, and never more than two come back in one week. A planner that serves only what is already liked stops finding new things to like. Once there are enough ratings it also passes on patterns, such as "chicken liked 6 of 6, fish 1 of 4", as leanings to weigh rather than rules.
Connect calendar attaches a Google Calendar to one profile, matched by the
signed-in email address. Run /check-google.html once first — it sets up the
Supabase details this page then reuses.
Jobs are laid into the evenings the calendar actually leaves, around the cooking, one column each for the two adults. Three things make this more than a to-do list. Recurrence knows the difference between a bin day, which is fixed whatever you did last week, and clean sheets, whose fortnight restarts when you last managed it. Reminders can hang off the diary rather than a date, so "pack the swimming kit" fires the night before swimming and stays silent the week the lesson is off. And work that will not fit anywhere is said out loud instead of rolling quietly to tomorrow forever.
Type a brain-dump into the box — "bins tuesdays, ring the dentist before term starts, sheets every couple of weeks" — and Add with AI splits it into dated, assigned, correctly-anchored jobs. That button needs a model key; Add as one job does not.
The app keeps no prices of its own. Pack sizes are worth modelling because a 400 g tin is a 400 g tin wherever you shop; pack prices are not, and a total that is confidently wrong is worse than no total. So the list tells you what to put in the trolley, and the pack solver minimises what ends up in the bin rather than what it costs. The one place a price appears is the local Basket panel, and it is Tesco's own shelf price at the moment of searching: shown beside each product to help you choose, never added up, and dated once it is old.
Everything except the two AI buttons runs without an API key.
node --test "test/*.test.ts" # 207 tests, no API key needed
node scripts/doctor.mjs # why will the model not answer?
node src/run.ts demo # the same week, in the terminal
node src/run.ts guardrails # a deliberately broken plan, and what catches it
node src/run.ts live # one plan via the model, with token counts and cost
node scripts/build-static.mjs # the hosted demo, into docs/app
Requires Node 22.6+ — it runs the TypeScript directly, so there is no build step and no bundler.
The riskiest part of this product was never the AI writing a nice-looking menu. It is turning that menu into a shopping list that is correct — units reconciled across recipes, the larder subtracted, real pack sizes — and doing it without asking anyone to log what they ate.
src/domain/ units, catalogue, aggregation, larder, ← the part that must be right
people, the week's table, the agenda,
tasks and recurrence
src/validate.ts allergies, timings, portions, variety ← guard rails, not prompts
src/app-state.ts the routes and the state, host-agnostic ← one implementation
src/ai/ two providers behind one interface ← the swappable part
web/server.ts HTTP wrapper for local development ← ~20 lines of host
web/static/ browser wrapper for the hosted demo ← ~20 lines of host
web/public/ the rendering client ← computes nothing itself
The client asks window.__familyApi for state when the bundled app is present
and falls back to fetch("/api/...") when it is not. That seam is the only
difference between running locally and running on GitHub Pages, and it is
deliberately three lines long — two implementations of "what happens when you
untick Priya on Tuesday" is how a shopping list and a larder start disagreeing.
The web client does no arithmetic of its own; every figure on screen comes from the same modules a server would call. That is deliberate — two implementations of the same sum is how a shopping list and a larder start disagreeing.