Compare commits
3
Commits
ce6dba17e6
...
df0d395138
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df0d395138 | ||
|
|
b09aff80b2 | ||
|
|
61d4c25ce6 |
@@ -30,11 +30,15 @@ for access and recovery details.
|
|||||||
Create a disposable fixture without connecting game hosts:
|
Create a disposable fixture without connecting game hosts:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
npx svelte-kit sync
|
||||||
fixture_parent=$(mktemp -d "${TMPDIR:-/tmp}/ec-ui.XXXXXX")
|
fixture_parent=$(mktemp -d "${TMPDIR:-/tmp}/ec-ui.XXXXXX")
|
||||||
npm run seed:fixture -- --db "$fixture_parent/preview/cherry.db" --watch
|
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
|
synthetic observations every 15 seconds. In a second terminal, load the printed
|
||||||
environment file and start the controller on localhost:
|
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
|
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.
|
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
|
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).
|
T5/T6 server validation use the [host acceptance checklist](../guides/native-hosts.md#development-and-validation).
|
||||||
|
|
||||||
|
|||||||
@@ -41,10 +41,15 @@ new controller. See [.env.example](../../.env.example) for the other settings.
|
|||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
Proxy the HTTPS address to controller port 3000. Open the dashboard and follow
|
Proxy the HTTPS address to controller port 3000. Enable gzip or Brotli compression
|
||||||
`/setup` to create the first administrator. Then remove `EC_SETUP_TOKEN` from
|
at the HTTPS reverse proxy for HTML (`text/html`) and JSON (`application/json`),
|
||||||
`.env` and run `docker compose up -d` to recreate the controller. Setup stays
|
including server-rendered pages, page data, and API responses. The build already
|
||||||
closed once an account exists.
|
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
|
Add other users under **Admin → Users**. The
|
||||||
[access guide](authentication.md) explains roles, proxy configuration, and
|
[access guide](authentication.md) explains roles, proxy configuration, and
|
||||||
|
|||||||
@@ -241,14 +241,14 @@
|
|||||||
<label for={`activity-days${suffix}`}
|
<label for={`activity-days${suffix}`}
|
||||||
>Date range<select
|
>Date range<select
|
||||||
id={`activity-days${suffix}`}
|
id={`activity-days${suffix}`}
|
||||||
value={filters.days ?? 0}
|
value={String(filters.days ?? 0)}
|
||||||
onchange={(event) => update({ days: event.currentTarget.value })}
|
onchange={(event) => update({ days: event.currentTarget.value })}
|
||||||
><option value="0">All retained history</option><option value="1"
|
><option value="0">All retained history</option><option value="1"
|
||||||
>Last 24 hours</option
|
>Last 24 hours</option
|
||||||
><option value="7">Last 7 days</option><option value="30">Last 30 days</option
|
><option value="7">Last 7 days</option><option value="30">Last 30 days</option
|
||||||
><option value="90">Last 90 days</option
|
><option value="90">Last 90 days</option
|
||||||
>{#if filters.days && ![1, 7, 30, 90].includes(filters.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
|
>{/if}</select
|
||||||
></label
|
></label
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -176,7 +176,7 @@
|
|||||||
<label for={`match-days${suffix}`}
|
<label for={`match-days${suffix}`}
|
||||||
>Date range<select
|
>Date range<select
|
||||||
id={`match-days${suffix}`}
|
id={`match-days${suffix}`}
|
||||||
value={filters.days}
|
value={String(filters.days)}
|
||||||
onchange={(event) => update({ days: event.currentTarget.value })}
|
onchange={(event) => update({ days: event.currentTarget.value })}
|
||||||
><option value="0">All time</option><option value="7">Last 7 days</option
|
><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
|
><option value="30">Last 30 days</option><option value="90">Last 90 days</option
|
||||||
|
|||||||
@@ -53,6 +53,27 @@ beforeEach(() => {
|
|||||||
});
|
});
|
||||||
afterEach(() => vi.useRealTimers());
|
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', () => {
|
it('sends only supported normalized filters and enforces scoped identities', () => {
|
||||||
const url = new URL(
|
const url = new URL(
|
||||||
'https://example.test/servers/town?tab=history&server=wrong&source=host&state=failed&offset=50&returnTo=%2F'
|
'https://example.test/servers/town?tab=history&server=wrong&source=host&state=failed&offset=50&returnTo=%2F'
|
||||||
|
|||||||
@@ -41,6 +41,27 @@ beforeEach(() => {
|
|||||||
vi.setSystemTime(new Date('2026-10-06T20:00:00Z'));
|
vi.setSystemTime(new Date('2026-10-06T20:00:00Z'));
|
||||||
});
|
});
|
||||||
afterEach(() => vi.useRealTimers());
|
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', () => {
|
it('renders one page heading, six columns, a combined identity, and compact dates', () => {
|
||||||
const body = render(MatchHistoryPage, {
|
const body = render(MatchHistoryPage, {
|
||||||
props: { data: { history: report(), user: null, setupRequired: false } }
|
props: { data: { history: report(), user: null, setupRequired: false } }
|
||||||
|
|||||||
Reference in New Issue
Block a user