A separate app from the desktop console, talking to the same platform with the same client. Not a breakpoint on the old one: the two answer different questions. The console answers "what is the state of the book?" across a wide grid; a phone answers "what needs me, and what happened to this one?" in a column you scroll with a thumb. The old console at 390px showed why. Tables scrolled sideways, nine queues stacked above the content ate the first screenful, and the action you came to perform sat below the fold. So every row of every table is a CARD — customer, stage, when it is due, who is holding it, and one line about what is happening. Everything else is one tap away. The queues live in a drawer. The action a lead is waiting on is pinned to the bottom of the screen, where a thumb already is. Shared with the console, because a divergence would be two apps disagreeing about the same lead: the whole api/ layer, ActivityForm, and Timeline — whose audit-row merging (one submission writes three rows) is hard-won and must not be reimplemented twice. Written fresh: the shell, the three screens, and the stylesheet. The colour rule is unchanged and is the product in four colours: blue the AI holds it, amber a person, teal the customer, red risk and nothing else. A PWA, so Add to Home Screen gives a full-screen app; the layout pads for the notch and the home indicator. Verified at 402x874: login, overview, drawer, a queue, and a lead, with no horizontal overflow on any of them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
54 lines
2.3 KiB
JavaScript
54 lines
2.3 KiB
JavaScript
/**
|
|
* Runtime configuration, written by the server as `config.js` when it places a
|
|
* build — never baked in.
|
|
*
|
|
* One build is promoted between environments unchanged, so an API URL compiled
|
|
* at build time would be the *source* environment's URL everywhere it lands.
|
|
*
|
|
* SAME_ORIGIN exists because an empty string is falsy and could never survive
|
|
* the `||` chain, which is exactly what "use the current origin" has to do.
|
|
*/
|
|
export function resolveConfigValue(key, fallback = '') {
|
|
const runtime = (window.__RUNTIME_CONFIG__ || {})[key]
|
|
// Gated on DEV: a production build with no config.js must fail loudly rather
|
|
// than quietly calling whichever backend happened to build it.
|
|
const devFallback = import.meta.env.DEV ? (import.meta.env[key] ?? '') : ''
|
|
const value = runtime || devFallback || fallback
|
|
return value === 'SAME_ORIGIN' ? window.location.origin.replace(/\/$/, '') : value
|
|
}
|
|
|
|
export function requireConfigValue(key) {
|
|
const value = resolveConfigValue(key)
|
|
if (!value) {
|
|
throw new Error(
|
|
`Missing runtime config "${key}". The server writes config.js when it ` +
|
|
`places the build; for local dev set ${key} in .env.`,
|
|
)
|
|
}
|
|
return value
|
|
}
|
|
|
|
/**
|
|
* The mount path this app is served under, read from the `<base href>` the
|
|
* server writes into index.html. Drives the router basename, so one build
|
|
* serves any mount path without a rebuild.
|
|
*
|
|
* `import.meta.env.BASE_URL` is NOT usable here — with Vite's relative base it
|
|
* resolves to "./".
|
|
*
|
|
* It reads the TAG, not `document.baseURI`. With no <base> in the document —
|
|
* which is every `npm run dev` session, where the placeholder is still an HTML
|
|
* comment — baseURI falls back to the current page URL, so the basename became
|
|
* whatever deep path happened to be loaded. Open /lead/16911 directly and every
|
|
* link then rendered under it: /lead/16911/lead/11411, and again on the next
|
|
* click. Unmounted means mounted at the root.
|
|
*/
|
|
export function basePath() {
|
|
const tag = document.querySelector('base[href]')
|
|
if (!tag) return '/'
|
|
const path = new URL(tag.href, window.location.origin).pathname
|
|
// A relative href ("./") resolves against the current URL and would reintroduce
|
|
// exactly the same drift. Only an absolute mount path is a mount path.
|
|
return tag.getAttribute('href').startsWith('/') ? path : '/'
|
|
}
|