Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ updates:
directory: /dashboard
schedule:
interval: monthly
ignore:
# Keep the compiler within typescript-eslint's supported API range.
- dependency-name: typescript
versions: ['>=6.1.0']
# Node types must match the Node 24 runtime in .nvmrc and CI.
- dependency-name: '@types/node'
versions: ['>=25.0.0']
groups:
react:
patterns: [react, react-dom, '@types/react*']
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/dashboard-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ jobs:
working-directory: dashboard
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: dashboard/.nvmrc
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ jobs:
working-directory: dashboard
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: dashboard/.nvmrc
Expand Down
8 changes: 6 additions & 2 deletions dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Interactive dashboard for exploring astronomical lens data.

The dashboard uses Node 24 LTS/npm 11, React 19, Material UI 9 and Vite 8.
The MinIO exporter uses Python 3.14, pandas 3 and PyArrow. TypeScript stays
on version 6 because the ESLint integration currently supports versions below
on the 6.0 patch line because the ESLint integration currently supports versions below
6.1; see [typescript-eslint compatibility](https://typescript-eslint.io/users/dependency-versions/).

## Quick Start
Expand Down Expand Up @@ -70,7 +70,11 @@ shards, WebP assets and omission of redundant source catalogs).
Pages artifact after a push to `main` or `streamlit`, or a manual run on either
branch. Both workflows use Ubuntu 24.04, Node 24, dependency caches and Actions
pinned to release commit hashes. Deployment permissions belong to the deploy
job. Dependabot checks npm dependencies and Actions monthly.
job. Both build checkouts disable credential persistence. Dependabot checks npm
dependencies and Actions monthly, allowing Node 24 type updates and TypeScript
6.0 patches. Newer Node types or TypeScript versions require a coordinated runtime
or ESLint migration; the ignore rules in `.github/dependabot.yml` must be reviewed
when that compatibility changes.

Install the reproducible exporter environment with:

Expand Down
18 changes: 1 addition & 17 deletions dashboard/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.7",
"globals": "^17.13.0",
"typescript": "^6.0.3",
"typescript": "~6.0.3",
"typescript-eslint": "^8.71.0",
"vite": "^8.3.2"
},
Expand Down
13 changes: 12 additions & 1 deletion dashboard/scripts/check-build.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import assert from 'node:assert/strict';
import { readFile, readdir, access } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
const directory = fileURLToPath(new URL('../dist/', import.meta.url));
const directory = process.argv[2] || fileURLToPath(new URL('../dist/', import.meta.url));
const html = await readFile(`${directory}/index.html`, 'utf8');
assert.match(html, /\/slcomp\/assets\//, 'Pages asset URLs must retain /slcomp/');
const index = JSON.parse(await readFile(`${directory}/data/catalog.json`));
Expand All @@ -10,6 +10,17 @@ assert.equal((await readdir(`${directory}/data/objects`)).filter(file => file.en
const dataFiles = await readdir(`${directory}/data`);
for (const source of ['database.json', 'consolidated_database.json', 'cutouts.json']) assert(!dataFiles.includes(source), `Redundant source ${source} must not ship`);
const manifest = JSON.parse(await readFile(`${directory}/footprints/manifest.json`));
const expectedLayerIds = [
'legacy', 'des', 'hsc', 'kids', 'rcslens', 'cs82', 'cfhtlens', 'sdss',
'delve', 'gama', 'ozdes', 'wigglez', '2slaq', '2df', '6df', 'lamost',
'ssrs', 'lcrs', 'vipers', 'deep2', 'zcosmos', 'cnoc', 'ages', 'mgc',
'2mrs', 'pscz', 'cfa', 'vvds',
];
assert.deepEqual(
manifest.layers.map(layer => layer.id).sort(),
expectedLayerIds.sort(),
'Footprint manifest must contain exactly the expected 28 layers',
);
for (const layer of manifest.layers) {
for (const key of ['image', 'border']) {
assert(layer[key].endsWith('.webp'));
Expand Down
59 changes: 59 additions & 0 deletions dashboard/tests/check-build.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, readFile, writeFile, mkdir, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { promisify } from 'node:util';
import { execFile } from 'node:child_process';
import { fileURLToPath } from 'node:url';

const run = promisify(execFile);
const checker = fileURLToPath(new URL('../scripts/check-build.mjs', import.meta.url));
const manifestSource = new URL('../public/footprints/manifest.json', import.meta.url);

async function artifact(t) {
const directory = await mkdtemp(join(tmpdir(), 'slcomp-build-check-'));
t.after(() => rm(directory, { recursive: true, force: true }));
for (const path of ['data/objects', 'footprints', 'assets']) {
await mkdir(join(directory, path), { recursive: true });
}
await writeFile(join(directory, 'index.html'), '<script src="/slcomp/assets/index.js"></script>');
await writeFile(join(directory, 'data/catalog.json'), JSON.stringify({ objects: [{ JNAME: 'J0001' }] }));
await Promise.all(Array.from({ length: 256 }, (_, index) =>
writeFile(join(directory, 'data/objects', `${index.toString(16).padStart(2, '0')}.json`), '{}')));
const manifest = JSON.parse(await readFile(manifestSource, 'utf8'));
await writeFile(join(directory, 'footprints/manifest.json'), JSON.stringify(manifest));
for (const layer of manifest.layers) {
for (const key of ['image', 'border']) await writeFile(join(directory, 'footprints', layer[key]), 'asset');
}
await writeFile(join(directory, 'slcomp.webp'), 'asset');
await writeFile(join(directory, 'assets/observatory-test.webp'), 'asset');
return { directory, manifest };
}

test('build validation accepts the complete footprint set regardless of order', async t => {
const { directory, manifest } = await artifact(t);
manifest.layers.reverse();
await writeFile(join(directory, 'footprints/manifest.json'), JSON.stringify(manifest));
await run(process.execPath, [checker, directory]);
});

test('build validation rejects missing, duplicate and unexpected footprint IDs', async t => {
const { directory, manifest } = await artifact(t);
const variants = [
manifest.layers.slice(1),
[...manifest.layers, manifest.layers[0]],
[{ ...manifest.layers[0], id: 'unexpected' }, ...manifest.layers.slice(1)],
];
for (const layers of variants) {
await writeFile(join(directory, 'footprints/manifest.json'), JSON.stringify({ ...manifest, layers }));
await assert.rejects(run(process.execPath, [checker, directory]), error =>
error.stderr.includes('Footprint manifest must contain exactly the expected 28 layers'));
}
});

test('build validation rejects a missing overlay file even with a complete manifest', async t => {
const { directory, manifest } = await artifact(t);
await rm(join(directory, 'footprints', manifest.layers[0].border));
await assert.rejects(run(process.execPath, [checker, directory]), error => error.stderr.includes('ENOENT'));
});