You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

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's VITE_ALLOW_ACCESS_* defaults set so a plain cp .env.example .env on the client's side turns on only what they licensed,
  • no Jenkinsfile (client has their own CI), no memory/ (dev notes), no ref/ (internal PRD/screenshots), no scripts/.

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 by src/router/index.tsx via import.meta.glob("./overlay/*.tsx"). Export routes: RouteObject[]. scripts/new_client_overlay.sh scaffolds 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 by src/shared/components/layouts/AppLayout.tsx (every domain's *LayoutRoute imports AppLayout from there, never DefaultAppLayout directly, so this one file swaps the whole shell — sidebar, navbar, colors — everywhere at once). Default export, same props as DefaultAppLayout.
  • Theme — src/styles/overlay/*.css, loaded by src/main.tsx after index.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 main doesn'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.