Compare commits

...
3 Commits
Author SHA1 Message Date
josh df0d395138 Document proxy compression for HTML and JSON. Refs #132
CI / checks (push) Successful in 4m52s
2026-10-09 20:05:47 -04:00
josh b09aff80b2 Document fixture setup and production startup (Refs #135) 2026-10-09 20:04:43 -04:00
josh 61d4c25ce6 Fix date-range filter selections (Refs #104) 2026-10-09 20:03:08 -04:00
6 changed files with 71 additions and 8 deletions
+17 -1
View File
@@ -30,11 +30,15 @@ for access and recovery details.
Create a disposable fixture without connecting game hosts:
```sh
npx svelte-kit sync
fixture_parent=$(mktemp -d "${TMPDIR:-/tmp}/ec-ui.XXXXXX")
npm run seed:fixture -- --db "$fixture_parent/preview/cherry.db" --watch
```
The command prints the generated paths. Leave that terminal open to refresh
The sync step generates the SvelteKit TypeScript configuration that the seeder
needs in a fresh checkout.
The seeder prints the generated paths. Leave that terminal open to refresh
synthetic observations every 15 seconds. In a second terminal, load the printed
environment file and start the controller on localhost:
@@ -120,6 +124,18 @@ and test commands compile those workers automatically. Restart `npm run dev`
after changing agent or worker code, or run `npm run agent:package` to refresh
only the downloadable agent assets while the development server is running.
To run the production build locally, run these commands from the repository root
with the controller environment configured as above:
```sh
npm run build
NODE_ENV=production node build
```
`NODE_ENV=production` makes the report and launch workers load from `build/reports`
and `build/launch`. Without it, they look under `.svelte-kit/`, where the production
build does not create worker bundles. The Docker image sets this variable already.
Automated tests simulate Docker and game hosts. Real Debian installation and
T5/T6 server validation use the [host acceptance checklist](../guides/native-hosts.md#development-and-validation).
+9 -4
View File
@@ -41,10 +41,15 @@ new controller. See [.env.example](../../.env.example) for the other settings.
docker compose up -d --build
```
Proxy the HTTPS address to controller port 3000. Open the dashboard and follow
`/setup` to create the first administrator. Then remove `EC_SETUP_TOKEN` from
`.env` and run `docker compose up -d` to recreate the controller. Setup stays
closed once an account exists.
Proxy the HTTPS address to controller port 3000. Enable gzip or Brotli compression
at the HTTPS reverse proxy for HTML (`text/html`) and JSON (`application/json`),
including server-rendered pages, page data, and API responses. The build already
precompresses static JavaScript and CSS assets; dynamically generated responses
need compression at the proxy.
Open the dashboard and follow `/setup` to create the first administrator. Then
remove `EC_SETUP_TOKEN` from `.env` and run `docker compose up -d` to recreate the
controller. Setup stays closed once an account exists.
Add other users under **Admin → Users**. The
[access guide](authentication.md) explains roles, proxy configuration, and
+2 -2
View File
@@ -241,14 +241,14 @@
<label for={`activity-days${suffix}`}
>Date range<select
id={`activity-days${suffix}`}
value={filters.days ?? 0}
value={String(filters.days ?? 0)}
onchange={(event) => update({ days: event.currentTarget.value })}
><option value="0">All retained history</option><option value="1"
>Last 24 hours</option
><option value="7">Last 7 days</option><option value="30">Last 30 days</option
><option value="90">Last 90 days</option
>{#if filters.days && ![1, 7, 30, 90].includes(filters.days)}<option
value={filters.days}>Last {filters.days} days</option
value={String(filters.days)}>Last {filters.days} days</option
>{/if}</select
></label
>
+1 -1
View File
@@ -176,7 +176,7 @@
<label for={`match-days${suffix}`}
>Date range<select
id={`match-days${suffix}`}
value={filters.days}
value={String(filters.days)}
onchange={(event) => update({ days: event.currentTarget.value })}
><option value="0">All time</option><option value="7">Last 7 days</option
><option value="30">Last 30 days</option><option value="90">Last 90 days</option
+21
View File
@@ -53,6 +53,27 @@ beforeEach(() => {
});
afterEach(() => vi.useRealTimers());
it.each([
['', '0', 'All retained history'],
['0', '0', 'All retained history'],
['1', '1', 'Last 24 hours'],
['7', '7', 'Last 7 days'],
['30', '30', 'Last 30 days'],
['90', '90', 'Last 90 days'],
['14', '14', 'Last 14 days']
])('selects the current date range for days=%s on desktop and mobile', (days, value, label) => {
scope(days ? `/activity?days=${days}` : '/activity');
const body = render(ActivityLog, { props: { initial: report() } }).body;
for (const suffix of ['', '-mobile']) {
const select = body.match(
new RegExp(`<select\\b[^>]*id="activity-days${suffix}"[^>]*>([\\s\\S]*?)</select>`)
)?.[1];
expect(select).toBeDefined();
expect(select).toContain(`<option value="${value}" selected="">${label}</option>`);
expect(select?.match(/\bselected=""/g)).toHaveLength(1);
}
});
it('sends only supported normalized filters and enforces scoped identities', () => {
const url = new URL(
'https://example.test/servers/town?tab=history&server=wrong&source=host&state=failed&offset=50&returnTo=%2F'
+21
View File
@@ -41,6 +41,27 @@ beforeEach(() => {
vi.setSystemTime(new Date('2026-10-06T20:00:00Z'));
});
afterEach(() => vi.useRealTimers());
it.each([
['', '0', 'All time'],
['0', '0', 'All time'],
['7', '7', 'Last 7 days'],
['30', '30', 'Last 30 days'],
['90', '90', 'Last 90 days']
])(
'selects the current date range for matchDays=%s on desktop and mobile',
(days, value, label) => {
scope(days ? `?matchDays=${days}` : '');
const body = render(MatchHistory, { props: { initial: report() } }).body;
for (const suffix of ['', '-mobile']) {
const select = body.match(
new RegExp(`<select\\b[^>]*id="match-days${suffix}"[^>]*>([\\s\\S]*?)</select>`)
)?.[1];
expect(select).toBeDefined();
expect(select).toContain(`<option value="${value}" selected="">${label}</option>`);
expect(select?.match(/\bselected=""/g)).toHaveLength(1);
}
}
);
it('renders one page heading, six columns, a combined identity, and compact dates', () => {
const body = render(MatchHistoryPage, {
props: { data: { history: report(), user: null, setupRequired: false } }