6.8 KiB
Branches in this repo
main — the dev workspace
Every feature, every client's code, all in one place — this is where you
work day to day. main is never pushed to a client's GitLab directly —
it's the source a client branch gets cut from.
client/<slug> — one per client, generated, not edited
Produced by:
scripts/export_client.sh <slug> <FLAG1,FLAG2,...>
Flags are the VITE_ALLOW_ACCESS_<FLAG> suffixes already used in
.env.example: EFFICIENCY, PFI, RELIABILITY, RISK_MATRIX, LCCA,
RBD, OH, IRCFA, AHM. Auth, the dashboard shell, admin, and ops-dock
have no flag and always ship.
Each export is main, with:
.env.example'sVITE_ALLOW_ACCESS_*defaults set so a plaincp .env.example .envon the client's side turns on only what they licensed,- no
Jenkinsfile(client has their own CI), nomemory/(dev notes), noref/(internal PRD/screenshots), noscripts/.
These branches are disposable — never hand-edit them. The script does
git checkout -B client/<slug> main, so re-running it regenerates the
branch from scratch every time. If a client needs a hotfix that shouldn't
wait for the next full export, make the fix on main first, then re-run
the export.
Pushing to the client's GitLab is a separate, explicit step (the script
prints the command, or pass --push <remote-url> to do it inline):
git push <client-remote> client/<slug>:main
What the flags do and don't do — read this before relying on them
Unlike the backend, this does not remove any code from the repo.
src/router/index.tsx statically imports every page under src/pages/
regardless of flags — they're still individually code-split via lazy()
(so a page nobody licensed isn't in anyone's initial JS payload), but the
source is still sitting in the export, reachable by anyone with repo access.
What a disabled flag does block, via src/router/FeatureGate.tsx: every
route under that domain — both the flat /xxx-app/* paths and the
/unit/:unitId/xxx/* paths, plus its /landing/* teaser page where one
exists — renders UnderConstructionPage instead. FeatureGate wraps the
layout route's element, not something inside it, so the real layout
component (and whatever data-fetching hooks it kicks off on mount) is
never mounted at all when disabled, and there's no <Outlet /> in the
tree for any child route to render into — direct URL navigation to a
sub-page hits the same placeholder as the domain's root. So a client
can't see a feature they weren't sold, even by guessing/typing the URL.
They just aren't prevented from finding it in the repo's source if they
have repo access at all — that's the part that would need the bigger job
below.
If you need the frontend repo itself to not contain another client's page
source at all (not just block them from seeing it rendered), that's a
bigger job: router/index.tsx's route tree would need to be filtered per
export the way config/urls.py is on the backend, and every page under
an unlicensed feature would need to come out of src/pages/ and
src/features/ too. Not done here — flag if you want it built.
Known gap: hardcoded branding
public/Logo_PLN.png, Logo_PLN.svg, rcfa-app/logo-tjb.png and similar
are referenced directly (by that literal filename) from ~16 files —
nav sidebars, the login page, PDF/Excel report generators. export_client.sh
prints a warning when these are still present in an export, but does not
attempt to fix it: swapping branding per client needs those references
replaced with a generic path (e.g. /branding/logo.png) on main first,
the same way config/settings_local.py only works on the backend because
main already has a hook pointed at it. Until that refactor happens, every
client export currently ships the same PLN/TJB branding and plant-specific
copy (see also src/pages/unit-3, and SchematicName-style hardcoding on
the backend) regardless of who it's actually for.
overlay/<slug> — one per client, permanent, hand-maintained
For whatever's unique to one client and genuinely not shared with any other. Created with:
scripts/new_client_overlay.sh <slug>
This is an orphan branch — no shared history with main, no shared
history with any other client's overlay. Nobody working on
overlay/client-b ever has a reason to check out overlay/client-a; the
two are as separate as if they were different repos.
An overlay may only ever add files, never edit one that also exists on
main. Three extension points exist, all the same shape — a path that
doesn't exist on main, discovered via import.meta.glob, which
resolves to an empty object when nothing matches, so each is always safe
to be absent:
- Routes —
src/router/overlay/<slug>.tsx, picked up bysrc/router/index.tsxviaimport.meta.glob("./overlay/*.tsx"). Exportroutes: RouteObject[].scripts/new_client_overlay.shscaffolds this one with an empty array — safe by construction, since an empty array adds nothing until you fill it in. - Layout/shell —
src/shared/components/layouts/overlay/AppLayout.tsx, picked up bysrc/shared/components/layouts/AppLayout.tsx(every domain's*LayoutRouteimportsAppLayoutfrom there, neverDefaultAppLayoutdirectly, so this one file swaps the whole shell — sidebar, navbar, colors — everywhere at once). Default export, same props asDefaultAppLayout. - Theme —
src/styles/overlay/*.css, loaded bysrc/main.tsxafterindex.css, so redefining:root/.dark's CSS custom properties (--primary,--background, etc.) there wins the cascade.
The layout and theme hooks are not scaffolded with a placeholder file
the way routes are, and that's deliberate: an empty routes array has no
effect, but the layout/theme hooks activate the instant a matching file
exists at all, regardless of content. Only add one once you have a real
replacement.
Also allowed, same "new path only" rule:
- New components/pages that an overlay route or layout references.
- New static assets under a path
maindoesn't already use (see the branding gap above for why an existing asset path isn't safe to override yet).
That constraint isn't arbitrary: client/<slug> is regenerated from
scratch on every export, so .env.example and every existing file are
rewritten every single time. An overlay branch has no commit in common
with whatever client/<slug> looks like this time, so if it touched a
file the export also touches, every future export would hit the same
merge conflict forever. Sticking to new, untouched paths sidesteps that
entirely — a merge that only adds files can't conflict with content it
never shared.
export_client.sh merges overlay/<slug> into the freshly-built
client/<slug> automatically, if that branch exists, right before the
final guard — so overlay content gets the same leak check as everything
else.