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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ DEFAULT_DATASET=uoregon
# Set one database path or use automatic discovery at data/*/netflow.sqlite.
# LOCAL_SQLITE_PATH=/absolute/path/to/netflow.sqlite
# DATABASE_PATH=/absolute/path/to/netflow.sqlite
# Or discover */netflow.sqlite under another directory instead of data/.
# LOCAL_DATA_DIR=/absolute/path/to/data
6 changes: 5 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ jobs:

- run: cp datasets.json.example datasets.json

- uses: actions/setup-node@v6
with:
node-version-file: .node-version

- uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json
Expand All @@ -49,7 +53,7 @@ jobs:
run: bun run --cwd apps/web typecheck && bun run --cwd infra typecheck

- name: Test
run: bun run test:web
run: bun run test:web && bun run test:infra

- name: Build
run: bun run build:web
Expand Down
2 changes: 1 addition & 1 deletion .node-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
24.4.1
24.18.1
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ ATLANTIS turns NetFlow captures and CSV imports into queryable aggregate databas
- `tools/netflow-db`: Rust pipeline (`atlantis-netflow-db` crate) for ingestion, aggregation, verification, and analysis-window exports. Native `nfcapd` ingestion uses the pinned `nfdump` fork in `vendor/nfdump`.
- `apps/web`: Svelte 5/SvelteKit 3 dashboard and API routes. It reads local SQLite during development and Cloudflare D1 in deployment.
- `apps/landing`: Astro marketing and SEO site.
- `infra`: Alchemy v2 stacks (Effect programs) that deploy the dashboard. `cloudflare.ts` deploys the worker and its D1 database; `shared.ts` holds names and paths common to all stacks.
- `infra`: Alchemy v2 stacks (Effect programs) that deploy the dashboard. `cloudflare.ts` deploys the worker and its D1 database; `self-hosted.ts` runs the SQLite dashboard as a Docker container on a self-hosted machine that users reach through SSH port forwarding; `shared.ts` holds names and paths common to all stacks.
- `vendor/*`: Third-party analysis submodules. Treat these as read-only; build repo-local binaries through the scripts in `vendor/scripts/`.
- `data/`, `.env`, and `datasets.json`: Machine-local inputs and generated databases. Keep paths and dataset contents out of commits.
- `docs/user`: User setup and operation documentation.
Expand Down
2 changes: 1 addition & 1 deletion apps/landing/.node-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
22.16.0
24.18.1
58 changes: 58 additions & 0 deletions apps/web/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# syntax=docker/dockerfile:1

ARG BUN_VERSION=1.3.11
ARG NODE_VERSION=24.18.1

FROM oven/bun:${BUN_VERSION}-slim AS bun

FROM node:${NODE_VERSION}-bookworm-slim AS workspace

ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install --yes --no-install-recommends g++ make python3 \
&& rm -rf /var/lib/apt/lists/*

COPY --from=bun /usr/local/bin/bun /usr/local/bin/bun

WORKDIR /app
COPY package.json bun.lock ./
COPY patches patches
COPY apps/web/package.json apps/web/package.json
COPY apps/landing/package.json apps/landing/package.json
COPY infra/package.json infra/package.json

FROM workspace AS build

RUN bun install --frozen-lockfile --ignore-scripts --filter @atlantis/web

COPY apps/web apps/web

RUN cd apps/web && ATLANTIS_DB_DRIVER=sqlite bun run build

FROM workspace AS production-deps

RUN bun install --frozen-lockfile --ignore-scripts --production --filter @atlantis/web \
&& npm run build-release --prefix node_modules/better-sqlite3 \
&& rm -rf node_modules/better-sqlite3/build/Release/obj node_modules/better-sqlite3/deps \
&& node -e "new (require('better-sqlite3'))(':memory:').prepare('select 1').get()"

FROM node:${NODE_VERSION}-bookworm-slim AS runtime

ARG RUNTIME_UID=1000
ARG RUNTIME_GID=1000

ENV NODE_ENV=production \
HOST=0.0.0.0 \
PORT=3000 \
LOCAL_DATA_DIR=/data

WORKDIR /app
COPY --from=production-deps /app/node_modules node_modules
COPY --from=build /app/apps/web/package.json package.json
COPY --from=build /app/apps/web/build build

RUN mkdir -p /data

USER ${RUNTIME_UID}:${RUNTIME_GID}
EXPOSE 3000
CMD ["node", "build"]
18 changes: 18 additions & 0 deletions apps/web/Dockerfile.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
*
!package.json
!bun.lock
!patches
!infra/package.json
!apps/landing/package.json
!apps/web
apps/web/node_modules
apps/web/build
apps/web/dist
apps/web/.svelte-kit
apps/web/test-results
apps/web/playwright-report
apps/web/tests
apps/web/Dockerfile
apps/web/Dockerfile.dockerignore
**/.env
**/.env.*
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@eslint/js": "^9.18.0",
"@internationalized/date": "^3.12.0",
"@playwright/test": "1.52.0",
"@sveltejs/adapter-node": "6.0.0-next.12",
"@sveltejs/kit": "3.0.0-next.27",
"@sveltejs/load-config": "^0.2.3",
"@sveltejs/vite-plugin-svelte": "^7.3.1",
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export const variables = defineEnvVars({
description: 'Fallback for LOCAL_SQLITE_PATH',
schema: optional
},
LOCAL_DATA_DIR: {
description:
'Directory scanned for <dataset>/netflow.sqlite products; replaces the data/ and ../../data defaults',
schema: optional
},
DEFAULT_DATASET: {
description: 'Dataset ID selected when a request does not name one',
schema: optional
Expand Down
6 changes: 4 additions & 2 deletions apps/web/src/lib/server/db/local-files.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DATABASE_PATH, LOCAL_SQLITE_PATH } from '$app/env/private';
import { DATABASE_PATH, LOCAL_DATA_DIR, LOCAL_SQLITE_PATH } from '$app/env/private';

async function resolvePath(value: string): Promise<string> {
if (value === ':memory:') {
Expand All @@ -17,7 +17,9 @@ export async function discoverLocalSqlitePaths(): Promise<string[]> {

const fs = await import('node:fs/promises');
const path = await import('node:path');
const roots = [path.resolve(process.cwd(), 'data'), path.resolve(process.cwd(), '../../data')];
const roots = LOCAL_DATA_DIR
? [await resolvePath(LOCAL_DATA_DIR)]
: [path.resolve(process.cwd(), 'data'), path.resolve(process.cwd(), '../../data')];
const dbPaths = new Set<string>();

for (const root of roots) {
Expand Down
32 changes: 32 additions & 0 deletions apps/web/tests/lib/server/datasets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,38 @@ describe('dataset server helpers', () => {
await expect(datasets.getDatasetConfig('legacy')).rejects.toThrow(/Unknown dataset 'legacy'/);
});

it('skips pre-locality databases found under LOCAL_DATA_DIR', async () => {
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'datasets-local-data-dir-'));
const dataDir = path.join(workspace, 'mounted-data');
const legacyDir = path.join(dataDir, 'legacy');
const currentDir = path.join(dataDir, 'current');
fs.mkdirSync(legacyDir, { recursive: true });
fs.mkdirSync(currentDir, { recursive: true });
seedPreLocalityDatasetDb(
path.join(legacyDir, 'netflow.sqlite'),
'legacy',
'Legacy Only',
'router-legacy'
);
seedDatasetDb(path.join(currentDir, 'netflow.sqlite'), 'current', 'Current', 'router-current');
process.chdir(os.tmpdir());
vi.stubEnv('LOCAL_DATA_DIR', dataDir);

const datasets = await loadDatasetsModule();

await expect(datasets.listDatasetSummaries()).resolves.toEqual([
{
datasetId: 'current',
label: 'Current',
defaultStartDate: '2025-03-01',
discoveryMode: 'static',
hasLocality: false,
isDefault: true
}
]);
await expect(datasets.getDatasetConfig('legacy')).rejects.toThrow(/Unknown dataset 'legacy'/);
});

it('refreshes local dataset discovery after files move', async () => {
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'datasets-refresh-'));
const alphaDir = path.join(workspace, 'data', 'alpha');
Expand Down
42 changes: 42 additions & 0 deletions apps/web/tests/lib/server/db/local-files.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { discoverLocalSqlitePaths } from '../../../../src/lib/server/db/local-files';

describe('local SQLite discovery', () => {
let dataDir: string;

beforeEach(() => {
dataDir = mkdtempSync(join(tmpdir(), 'atlantis-local-files-'));
for (const dataset of ['beta', 'alpha']) {
mkdirSync(join(dataDir, dataset));
writeFileSync(join(dataDir, dataset, 'netflow.sqlite'), '');
}
mkdirSync(join(dataDir, 'backups'));
writeFileSync(join(dataDir, 'backups', 'old.sqlite'), '');
});

afterEach(() => {
vi.unstubAllEnvs();
rmSync(dataDir, { recursive: true, force: true });
});

it('scans LOCAL_DATA_DIR for dataset products', async () => {
vi.stubEnv('LOCAL_DATA_DIR', dataDir);

await expect(discoverLocalSqlitePaths()).resolves.toEqual([
join(dataDir, 'alpha', 'netflow.sqlite'),
join(dataDir, 'beta', 'netflow.sqlite')
]);
});

it('prefers LOCAL_SQLITE_PATH over LOCAL_DATA_DIR', async () => {
vi.stubEnv('LOCAL_DATA_DIR', dataDir);
vi.stubEnv('LOCAL_SQLITE_PATH', join(dataDir, 'beta', 'netflow.sqlite'));

await expect(discoverLocalSqlitePaths()).resolves.toEqual([
join(dataDir, 'beta', 'netflow.sqlite')
]);
});
});
3 changes: 3 additions & 0 deletions apps/web/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fileURLToPath } from 'node:url';
import adapterNode from '@sveltejs/adapter-node';
import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/kit/vite';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
Expand Down Expand Up @@ -56,13 +57,15 @@ function databaseDriver(driver: DatabaseDriver): Plugin {

export default defineConfig(({ command, mode, isPreview = false }) => {
const driver = resolveDatabaseDriver(command, mode, isPreview);
const nodeBuild = command === 'build' && driver === 'sqlite';

return {
plugins: [
tailwindcss(),
databaseDriver(driver),
sveltekit({
preprocess: vitePreprocess(),
adapter: nodeBuild ? adapterNode() : undefined,
env: {
dir: '../..'
}
Expand Down
38 changes: 38 additions & 0 deletions bun.lock

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

Loading
Loading