Install ReUI (MCP, skill, @reui registry, Tailwind v4, shadcn base-nova) and move the shared UI onto it: Button, Tag, Check, Input/Textarea, Segmented, Alert, Panel, Dialog, Tooltip, Menu and Select (Popover), Toast (sonner), the Table frame (DataGrid), the Dashboard KPI strip, the shnt Ops palette, Geist and the type scale. Design only: behaviour, texts and roles are unchanged. Tests that asserted the old DOM were updated; the tests now format numbers in English wherever they run. docs/reui-migration-plan.md records the plan and its decisions. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
115 lines
4.9 KiB
TypeScript
115 lines
4.9 KiB
TypeScript
import { readFileSync } from 'node:fs'
|
|
import { defineConfig, type Plugin } from 'vite'
|
|
import react from '@vitejs/plugin-react'
|
|
import tailwindcss from '@tailwindcss/vite'
|
|
import { STATIC_ROUTES } from './src/app/static-routes.js'
|
|
|
|
/*
|
|
One folder with its own `index.html` for each route of the console.
|
|
|
|
The server serves `www/` with static files and `UseDefaultFiles()`
|
|
(DnsWebService.cs:1960): `/settings/logging/` resolves to
|
|
`/settings/logging/index.html`, and `/settings/logging` gets a 301 to the
|
|
trailing-slash version. With the file in place the URL is real (no `#/`) and F5
|
|
brings you back where you were, **without touching a line of C#**.
|
|
|
|
The asset paths are corrected to the depth of each copy (`../` or `../../`). It
|
|
is not cosmetic: `base` is relative on purpose. The server honours
|
|
`X-Forwarded-Prefix` by mounting a `PathBase`, and with an absolute base the
|
|
console breaks behind a prefixed proxy, so the only way for
|
|
`/dns/settings/logging/` to find `/dns/assets/…` is to count the hops.
|
|
|
|
And each copy carries its route in a `<meta>`, which is what lets the application
|
|
know its own root without knowing the prefix: the root is its `pathname` minus
|
|
those segments.
|
|
*/
|
|
function staticRoutes(): Plugin {
|
|
return {
|
|
name: 'static-routes',
|
|
enforce: 'post',
|
|
generateBundle(_options, bundle) {
|
|
const index = bundle['index.html']
|
|
if (index == null || index.type !== 'asset') return
|
|
|
|
for (const route of STATIC_ROUTES) {
|
|
const hops = '../'.repeat(route.split('/').length)
|
|
const html = String(index.source)
|
|
.replace(/(href|src)="\.\//g, `$1="${hops}`)
|
|
.replace('<head>', `<head>\n <meta name="route" content="${route}" />`)
|
|
|
|
this.emitFile({ type: 'asset', fileName: `${route}/index.html`, source: html })
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
/*
|
|
The console's own version, from package.json at build time.
|
|
|
|
It cannot be discovered at runtime: the server's CSP is `default-src 'self'`
|
|
with no `connect-src`, so the browser cannot reach GitHub to ask what the latest
|
|
release is. Verified against a running server. Whoever installs it is told by the
|
|
installer, and here it is only reported.
|
|
*/
|
|
const version = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version
|
|
|
|
export default defineConfig(({ mode }) => ({
|
|
define: { __CONSOLE_VERSION__: JSON.stringify(version) },
|
|
/*
|
|
The development server, for working only: `vite` does not serve the API, so
|
|
without this the console starts and cannot talk to anyone.
|
|
|
|
It forwards `/api` to the harness's `dev` container (`dev/compose.yaml`, :5380).
|
|
What it buys is HMR: a change goes in without a reload and **the session is not
|
|
lost**, which is what made every tweak cost a login by hand.
|
|
|
|
It does not affect the build: `vite build` does not read `server`. The target
|
|
can be changed with `DNS=` to point at another instance of the harness.
|
|
*/
|
|
server: {
|
|
proxy: {
|
|
/*
|
|
The pattern starts with `.*` for a concrete reason: in development the
|
|
console does NOT ask for `/api/…`, it asks for `/dashboard/api/…`.
|
|
|
|
And it excludes `src/`, `@` and `node_modules/` for another: without that it
|
|
swallowed `/src/api/client.ts` (the module Vite itself serves) and
|
|
forwarded it to the DNS server, which answered with JSON. The browser
|
|
rejected the module by MIME type and the console did not start.
|
|
|
|
`app/base.ts` works out its root by taking off the `pathname` the segments
|
|
declared by `<meta name="route">`, and that meta **is injected by the plugin
|
|
at build time**: under `vite dev` it does not exist, so the root ends up
|
|
being the current path. With a bare `/api` proxy the session was never
|
|
restored and the console stayed on the login, while a hand-made
|
|
`fetch('/api/…')` worked, which is what made it confusing.
|
|
*/
|
|
/* The console served from the root, which is how dev works, asks for a
|
|
bare `/api/…`. This is that one. */
|
|
'/api/': {
|
|
target: process.env.DNS ?? 'http://127.0.0.1:5380',
|
|
changeOrigin: true,
|
|
},
|
|
/* And this one, for a deep route opened directly. */
|
|
'^/(?!src/|@|node_modules/).*/api/': {
|
|
target: process.env.DNS ?? 'http://127.0.0.1:5380',
|
|
changeOrigin: true,
|
|
rewrite: (path: string) => path.replace(/^.*\/api\//, '/api/'),
|
|
},
|
|
},
|
|
},
|
|
// The build output is what gets installed as the server's web root. The
|
|
// server honours X-Forwarded-Prefix by mounting a PathBase, so base has to be
|
|
// relative: with an absolute base the console works in Docker and breaks
|
|
// behind a prefixed proxy.
|
|
base: './',
|
|
build: {
|
|
outDir: mode === 'check' ? 'dist-check' : 'dist',
|
|
emptyOutDir: true,
|
|
// Every asset that has to ship lives in public/ and the build emits it
|
|
// again, so emptying the output directory is safe.
|
|
},
|
|
plugins: [react(), tailwindcss(), staticRoutes()],
|
|
resolve: { alias: { '@': new URL('./src', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1') } },
|
|
}))
|