diff --git a/Dockerfile b/Dockerfile index 75a0708..5f2316a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,18 +2,25 @@ FROM denoland/deno:latest AS builder WORKDIR /app -# Copy project files -COPY ./api /app/api -COPY ./tasks/vite.ts /app/tasks/vite.ts +# Copy lock COPY ./deno.json /app/deno.json COPY ./deno.lock /app/deno.lock -COPY ./web /app/web -# Cache dependencies -RUN deno cache --allow-scripts --lock=deno.lock api/server.ts tasks/vite.ts +# Install dependencies +RUN deno install # Build frontend (dist/web) and compile backend with static files -RUN deno task prod +COPY ./tasks/vite.ts /app/tasks/vite.ts +COPY ./web /app/web +RUN deno cache --allow-scripts --lock=deno.lock tasks/vite.ts web/index.tsx +ENV BASE_URL="/" +RUN deno task prod:vite + +# Build API +COPY ./api /app/api +COPY ./db /app/db +RUN deno cache --allow-scripts --lock=deno.lock api/server.ts +RUN deno task prod:api # Stage 2: Final image FROM debian:bookworm-slim @@ -21,6 +28,7 @@ WORKDIR /app # Copy compiled executable and Deno cache COPY --from=builder /app/dist/api /app/server +COPY --from=builder /app/db/functions /app/db/functions # Expose port from .env.prod (3021) EXPOSE 3021 diff --git a/README.md b/README.md index 0b30a8e..3c39b7d 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,27 @@ deno task docker:prod deno task docker:logs ``` +### Docker Compose + +Use the compose stack when you want ClickHouse and the app started together. It +has inline defaults, so no env file is required. + +```bash +docker compose up --build +``` + +Override any value from your shell or a local `.env` file. Example: + +```bash +PORT=8877 CLICKHOUSE_PASSWORD=strong-password docker compose up --build +``` + +The stack starts: + +- `clickhouse`: database server on ports `8123` and `9000` +- `clickhouse-init`: one-shot schema creation for the `logs` table +- `app`: compiled application on port `3021` + ### Available Tasks ```bash diff --git a/api/clickhouse-client.ts b/api/clickhouse-client.ts index 0a70e4f..c716543 100644 --- a/api/clickhouse-client.ts +++ b/api/clickhouse-client.ts @@ -1,4 +1,7 @@ -import { createClient } from '@clickhouse/client' +import defer * as chclient from '@clickhouse/client' +import defer * as local from './lib/clickhouse-local.ts' +import { isLocal } from './lib/env.ts' + import { CLICKHOUSE_HOST, CLICKHOUSE_PASSWORD, @@ -15,7 +18,7 @@ import { UNION, } from '@01edu/api/validator' -const LogSchema = OBJ({ +export const LogSchema = OBJ({ timestamp: NUM('The timestamp of the log event'), trace_id: NUM('A float64 representation of the trace ID'), span_id: optional(NUM('A float64 representation of the span ID')), @@ -41,7 +44,7 @@ export const LogSchemaOutput = OBJ({ service_instance_id: optional(STR('Service instance ID')), }, 'A log event') -const LogsInputSchema = UNION( +export const LogsInputSchema = UNION( LogSchema, ARR(LogSchema, 'An array of log events'), ) @@ -49,18 +52,22 @@ const LogsInputSchema = UNION( type Log = Asserted type LogsInput = Asserted -const client = createClient({ - url: CLICKHOUSE_HOST, - username: CLICKHOUSE_USER, - password: CLICKHOUSE_PASSWORD, - compression: { - request: true, - response: true, - }, - clickhouse_settings: { - date_time_input_format: 'best_effort', - }, -}) +export const client: ReturnType = isLocal + ? local.createLocalClient(CLICKHOUSE_HOST) as unknown as ReturnType< + typeof chclient.createClient + > + : chclient.createClient({ + url: CLICKHOUSE_HOST, + username: CLICKHOUSE_USER, + password: CLICKHOUSE_PASSWORD, + compression: { + request: true, + response: true, + }, + clickhouse_settings: { + date_time_input_format: 'best_effort', + }, + }) const numberToHex128 = (() => { const alphabet = new TextEncoder().encode('0123456789abcdef') @@ -79,10 +86,7 @@ const numberToHex128 = (() => { } })() -async function insertLogs( - service_name: string, - data: LogsInput, -) { +export async function insertLogs(service_name: string, data: LogsInput) { const logsToInsert = Array.isArray(data) ? data : [data] if (logsToInsert.length === 0) return respond.NoContent() @@ -204,7 +208,7 @@ function inferParamType(key: string, value: string): string { return 'String' } -async function getLogs(dep: string, data: FetchTablesParams) { +export async function getLogs(dep: string, data: FetchTablesParams) { const { query, params } = buildLogsQuery(dep, data) try { const rs = await client.query({ @@ -244,4 +248,39 @@ async function getLogs(dep: string, data: FetchTablesParams) { // } // } -export { client, getLogs, insertLogs, LogSchema, LogsInputSchema } +export const initLogTable = async () => { + await client.ping() + await client.command({ + query: ` + CREATE TABLE IF NOT EXISTS logs ( + id UUID DEFAULT generateUUIDv4(), + -- Flattened resource fields + service_name LowCardinality(String), + service_version LowCardinality(String), + service_instance_id String, + + timestamp DateTime64(3, 'UTC') DEFAULT now64(3, 'UTC'), + observed_timestamp DateTime64(3, 'UTC') DEFAULT now64(3, 'UTC'), + trace_id FixedString(16), + span_id FixedString(16), + severity_number UInt8, + -- derived column, computed by DB from severity_number + severity_text LowCardinality(String) MATERIALIZED CASE + WHEN severity_number > 4 AND severity_number <= 8 THEN 'DEBUG' + WHEN severity_number > 8 AND severity_number <= 12 THEN 'INFO' + WHEN severity_number > 12 AND severity_number <= 16 THEN 'WARN' + WHEN severity_number > 20 AND severity_number <= 24 THEN 'FATAL' + ELSE 'ERROR' + END, + -- Often empty, but kept for OTEL spec compliance + body Nullable(String), + attributes JSON, + event_name LowCardinality(String) + ) + ENGINE = MergeTree + PARTITION BY toYYYYMMDD(timestamp) + ORDER BY (service_name, timestamp, trace_id) + SETTINGS index_granularity = 8192, min_bytes_for_wide_part = 0; + `, + }) +} diff --git a/api/lib/clickhouse-local.ts b/api/lib/clickhouse-local.ts new file mode 100644 index 0000000..be64994 --- /dev/null +++ b/api/lib/clickhouse-local.ts @@ -0,0 +1,43 @@ +import defer * as chdb from 'chdb' + +export function createLocalClient(path:string) { + const session = new chdb.Session(path) + session.query(`SET date_time_input_format = 'best_effort'`) + + return { + insert: ( + { table, values }: { table: string; values: unknown[]; format?: string }, + ) => { + const data = values.map((v) => JSON.stringify(v)).join('\n') + session.query(`INSERT INTO ${table} FORMAT JSONEachRow\n${data}`) + return { executed: true, query_id: '' } + }, + query: ( + { query, query_params }: { + query: string + query_params?: Record + format?: string + }, + ) => { + let q = query + if (query_params) { + for (const [key, value] of Object.entries(query_params)) { + const str = typeof value === 'string' + ? `'${value.replace(/'/g, "\\'")}'` + : String(value) + q = q.replace(new RegExp(`\\{${key}:\\w+\\}`, 'g'), str) + } + } + const result = session.query(q, 'JSON') + return { json: () => JSON.parse(result) } + }, + ping: () => { + session.query('SELECT 1') + return { success: true } + }, + command: ({ query }: { query: string }) => { + session.query(query) + return { query_id: '' } + }, + } +} diff --git a/api/lib/env.ts b/api/lib/env.ts index 12eff48..1b76eed 100644 --- a/api/lib/env.ts +++ b/api/lib/env.ts @@ -1,19 +1,20 @@ -import { ENV } from '@01edu/api/env' +import { APP_ENV, ENV } from '@01edu/api/env' +export { APP_ENV } export const PORT = Number(ENV('PORT', '2119')) export const PICTURE_DIR = ENV('PICTURE_DIR', './.picture') -export const GOOGLE_CLIENT_ID = ENV('GOOGLE_CLIENT_ID') -export const CLIENT_SECRET = ENV('CLIENT_SECRET') -export const REDIRECT_URI = ENV('REDIRECT_URI') +export const GOOGLE_CLIENT_ID = ENV('GOOGLE_CLIENT_ID', '') +export const CLIENT_SECRET = ENV('CLIENT_SECRET', '') +export const REDIRECT_URI = ENV('REDIRECT_URI', `http://localhost:${PORT}`) export const ORIGIN = new URL(REDIRECT_URI).origin export const SECRET = ENV( 'SECRET', 'iUokBru8WPSMAuMspijlt7F-Cnpqyg84F36b1G681h0', ) -export const CLICKHOUSE_HOST = ENV('CLICKHOUSE_HOST') -export const CLICKHOUSE_USER = ENV('CLICKHOUSE_USER') -export const CLICKHOUSE_PASSWORD = ENV('CLICKHOUSE_PASSWORD') +export const CLICKHOUSE_HOST = ENV('CLICKHOUSE_HOST','./db/chdb') +export const CLICKHOUSE_USER = ENV('CLICKHOUSE_USER','default') +export const CLICKHOUSE_PASSWORD = ENV('CLICKHOUSE_PASSWORD','') // Optional interval (ms) for refreshing external SQL database schemas // Defaults to 24 hours @@ -21,8 +22,11 @@ export const DB_SCHEMA_REFRESH_MS = Number( ENV('DB_SCHEMA_REFRESH_MS', `${24 * 60 * 60 * 1000}`), ) -export const STORE_URL = ENV('STORE_URL') -export const STORE_SECRET = ENV('STORE_SECRET') +export const STORE_URL = ENV('STORE_URL', '') +export const STORE_SECRET = ENV('STORE_SECRET', '') +const LOCAL_ENV = ENV('LOCAL_ENV', '') +export const isLocal = LOCAL_ENV === 'yes' || LOCAL_ENV === '1' || + LOCAL_ENV === 'true' export const GEMINI_API_KEY = ENV('GEMINI_API_KEY') diff --git a/api/lib/functions.ts b/api/lib/functions.ts index 6b7be0b..4fb002f 100644 --- a/api/lib/functions.ts +++ b/api/lib/functions.ts @@ -1,5 +1,5 @@ import { batch } from '/api/lib/json_store.ts' -import { join } from '@std/path' +import { join, toFileUrl } from '@std/path' import { ensureDir } from '@std/fs' import { log } from '/api/lib/logger.ts' @@ -36,13 +36,14 @@ export type LoadedFunction = { // Map const functionsMap = new Map() -let watcher: Deno.FsWatcher | null = null -const functionsDir = './db/functions' +const functionsDir = join(import.meta.dirname!, '../../db/functions') +const functionsDirUrl = toFileUrl( + functionsDir.endsWith('/') ? functionsDir : `${functionsDir}/`, +) export async function init() { await ensureDir(functionsDir) await loadAll() - startWatcher() } async function loadAll() { @@ -56,18 +57,15 @@ async function loadAll() { async function reloadProjectFunctions(slug: string) { const projectDir = join(functionsDir, slug) + const projectDirUrl = new URL(`${slug}/`, functionsDirUrl) const loaded: LoadedFunction[] = [] try { await batch(5, Deno.readDir(projectDir), async (entry) => { if (entry.isFile && entry.name.endsWith('.js')) { - const mainFile = join(projectDir, entry.name) - // Build a fresh import URL to bust cache - const importUrl = `file://${await Deno.realPath( - mainFile, - )}?t=${Date.now()}` + const mainFileUrl = new URL(entry.name, projectDirUrl) try { - const module = await import(importUrl) + const module = await import(`${mainFileUrl.href}?t=${Date.now()}`) // We expect a default export or specific named exports const fns = module.default if (fns && typeof fns === 'object') { @@ -106,40 +104,12 @@ async function reloadProjectFunctions(slug: string) { } } -function startWatcher() { - if (watcher) return - log.info('starting-function-watcher', { dir: functionsDir }) - watcher = Deno.watchFs(functionsDir, { recursive: true }) // Process events - ;(async () => { - for await (const event of watcher!) { - if (!['modify', 'create', 'remove', 'rename'].includes(event.kind)) { - continue - } - for (const path of event.paths) { - if (!path.endsWith('.js')) continue - const parts = path.split('/') - const fileName = parts.pop() - const slug = parts.pop() - if (!fileName || !slug) continue - await reloadProjectFunctions(slug) - } - } - })() -} - export function getProjectFunctions( slug: string, ): LoadedFunction[] | undefined { return functionsMap.get(slug) } -export function stopWatcher() { - if (watcher) { - watcher.close() - watcher = null - } -} - export async function applyReadTransformers( data: T, projectId: string, diff --git a/api/lib/functions_test.ts b/api/lib/functions_test.ts index 8914422..86d8556 100644 --- a/api/lib/functions_test.ts +++ b/api/lib/functions_test.ts @@ -19,9 +19,6 @@ Deno.test('Functions Module - Pipeline & Config', async () => { await ensureDir(projectDir) - // Initialize module - await functions.init() - // Define test row type type TestRow = { id: number @@ -48,8 +45,8 @@ Deno.test('Functions Module - Pipeline & Config', async () => { await Deno.writeTextFile(file1, code1) await Deno.writeTextFile(file2, code2) - // Give watcher time - await new Promise((r) => setTimeout(r, 1000)) + // Initialize module after files are written + await functions.init() // 2. Verify loading and sorting const loaded = functions.getProjectFunctions(testSlug) @@ -105,5 +102,4 @@ Deno.test('Functions Module - Pipeline & Config', async () => { // Skipped } await new Promise((r) => setTimeout(r, 500)) - functions.stopWatcher() }) diff --git a/api/lib/google-oauth.ts b/api/lib/google-oauth.ts index 40f43e8..1866e71 100644 --- a/api/lib/google-oauth.ts +++ b/api/lib/google-oauth.ts @@ -10,7 +10,7 @@ export const GOOGLE_OAUTH_CONFIG = { class StateManager { private store = new Map() - private cleanupInterval: number + private cleanupInterval: NodeJS.Timeout constructor(private maxAge = 10 * 60 * 1000) { // 10 minutes this.cleanupInterval = setInterval(() => this.cleanup(), 60000) diff --git a/api/lib/local_ipc.ts b/api/lib/local_ipc.ts new file mode 100644 index 0000000..42523a7 --- /dev/null +++ b/api/lib/local_ipc.ts @@ -0,0 +1,159 @@ +import { ensureDir } from '@std/fs/ensure-dir' +import { TextLineStream } from '@std/streams/text-line-stream' +import { PORT } from '/api/lib/env.ts' +import { DeploymentsCollection, ProjectsCollection } from '/api/schema.ts' +import { refreshOneSchema } from '/api/sql.ts' + +export const defaultSocketPath = Deno.build.os === 'windows' + ? '\\\\.\\pipe\\01-devtools' + : `${Deno.env.get('XDG_RUNTIME_DIR') || '/tmp'}/01-devtools/01-devtools.sock` + +const encoder = new TextEncoder() + +async function removeSocket(path: string) { + if (Deno.build.os === 'windows') return + try { + await Deno.remove(path) + } catch (error) { + if (!(error instanceof Deno.errors.NotFound)) throw error + } +} + +async function sendCommand(socketPath: string, command: string) { + try { + const conn = await Deno.connect({ transport: 'unix', path: socketPath }) + await conn.write(encoder.encode(`${command}\n`)) + const reader = conn.readable + .pipeThrough(new TextDecoderStream()) + .pipeThrough(new TextLineStream()) + .getReader() + const { value } = await reader.read() + reader.releaseLock() + conn.close() + return value ? JSON.parse(value) : null + } catch { + return null + } +} + +type JSONPrimitive = string | number | boolean | null +type JSONValue = JSONPrimitive | JSONObject | JSONArray +interface JSONObject { + [member: string]: JSONValue +} +interface JSONArray extends Array {} + +const commands: Record< + string, + (arg: string) => Promise | JSONObject +> = { + info: () => ({ pid: Deno.pid, port: PORT }), + + register: async (arg: string): Promise => { + try { + const { + projectId, + name, + url, + logsEnabled, + databaseEnabled, + sqlEndpoint, + sqlToken, + } = JSON.parse(arg) + + if (!projectId || !url) { + return { + error: 'Usage: register/{"projectId":"...","url":"..."...}', + } + } + + // Create or update project + const projectName = name || projectId + const existingProject = ProjectsCollection.get(projectId) + if (!existingProject) { + await ProjectsCollection.insert({ + slug: projectId, + name: projectName, + teamId: 'local', + isPublic: true, + repositoryUrl: null, + }) + } + + // Create or update deployment + let dep = DeploymentsCollection.get(url) + if (dep) { + await DeploymentsCollection.update(url, { + projectId, + logsEnabled: logsEnabled ?? true, + databaseEnabled: databaseEnabled ?? false, + sqlEndpoint: sqlEndpoint || undefined, + sqlToken: sqlToken || 'local', + tokenSalt: crypto.randomUUID(), + }) + } else { + dep = await DeploymentsCollection.insert({ + projectId, + url, + logsEnabled: logsEnabled ?? true, + databaseEnabled: databaseEnabled ?? false, + sqlEndpoint: sqlEndpoint || undefined, + sqlToken: sqlToken || 'local', + tokenSalt: crypto.randomUUID(), + }) + } + refreshOneSchema(dep) + return { pid: Deno.pid, port: PORT } + } catch (err) { + console.error(err) + return { error: (err as Error)?.message || String(err) } + } + }, + + _: () => ({ error: 'Command not found' }), +} + +async function handleConn(conn: Deno.Conn) { + const reader = conn.readable + .pipeThrough(new TextDecoderStream()) + .pipeThrough(new TextLineStream()) + .getReader() + const { value = '' } = await reader.read() + reader.releaseLock() + const [name] = value.split('/', 1) + const arg = value.slice((name?.length || 0) + 1) || '' + const cmd = commands[name] || commands._ + await conn.write(encoder.encode(JSON.stringify(await cmd(arg)) + '\n')) + conn.close() +} + +async function acceptLoop(listener: Deno.Listener) { + try { + for await (const conn of listener) void handleConn(conn) + } catch (error) { + if (!(error instanceof Deno.errors.BadResource)) throw error + } +} + +export async function startRegistryServer(socketPath = defaultSocketPath) { + const existing = await sendCommand(socketPath, 'info') + if (existing) { + console.info( + `devtools already started here pid=${existing.pid} port=${existing.port}`, + ) + Deno.exit(0) + } + + if (Deno.build.os !== 'windows') { + await ensureDir(socketPath.slice(0, socketPath.lastIndexOf('/'))) + } + await removeSocket(socketPath) + const listener = Deno.listen({ transport: 'unix', path: socketPath }) + void acceptLoop(listener) + return { + close: () => { + listener.close() + return removeSocket(socketPath) + }, + } +} diff --git a/api/lib/local_ipc_test.ts b/api/lib/local_ipc_test.ts new file mode 100644 index 0000000..be01c71 --- /dev/null +++ b/api/lib/local_ipc_test.ts @@ -0,0 +1,66 @@ +import { assert, assertEquals } from '@std/assert' +import { TextLineStream } from '@std/streams/text-line-stream' +import { startRegistryServer } from './local_ipc.ts' + +const encoder = new TextEncoder() +const getEndpoint = async () => { + const endpoint = Deno.build.os === 'windows' + ? `\\\\.\\pipe\\devtools-test-${crypto.randomUUID()}` + : `${await Deno.makeTempDir()}/devtools.sock` + const server = await startRegistryServer(endpoint) + assert(server) + + async function sendCommand(command: string) { + const conn = await Deno.connect({ transport: 'unix', path: endpoint }) + await conn.write(encoder.encode(`${command}\n`)) + const reader = conn.readable + .pipeThrough(new TextDecoderStream()) + .pipeThrough(new TextLineStream()) + .getReader() + const { value } = await reader.read() + reader.releaseLock() + conn.close() + return JSON.parse(value!) + } + return { + sendCommand, + async [Symbol.asyncDispose]() { + await server?.close() + if (Deno.build.os !== 'windows') { + await Deno.remove(endpoint.slice(0, endpoint.lastIndexOf('/')), { + recursive: true, + }) + } + }, + } +} + +Deno.test('local ipc server returns current pid and port', async () => { + await using endpoint = await getEndpoint() + const res = await endpoint.sendCommand('info') + assertEquals(res.pid, Deno.pid) + assertEquals(typeof res.port, 'number') +}) + +Deno.test('register returns pid and port on success', async () => { + await using endpoint = await getEndpoint() + const res = await endpoint.sendCommand( + `register/${ + JSON.stringify({ + projectId: 'test-project', + name: 'Test Project', + url: 'localhost:9999', + logsEnabled: true, + databaseEnabled: false, + }) + }`, + ) + assertEquals(res.pid, Deno.pid) + assertEquals(typeof res.port, 'number') +}) + +Deno.test('register returns error for invalid payload', async () => { + await using endpoint = await getEndpoint() + const res = await endpoint.sendCommand(`register/${JSON.stringify({})}`) + assertEquals(typeof res.error, 'string') +}) diff --git a/api/lib/router.md b/api/lib/router.md deleted file mode 100644 index 83e0731..0000000 --- a/api/lib/router.md +++ /dev/null @@ -1,152 +0,0 @@ -# TypeScript Router - -A type-safe HTTP router with built-in validation and API documentation -generation. - -## Features - -- 🔒 Type-safe request/response handling -- ✅ Built-in input/output validation -- 📝 Automatic API documentation generation -- 🎯 Support for all standard HTTP methods -- 🔄 Async request handling -- 🍪 Cookie and session management -- ⚡ High-precision request timing - -## Basic Usage - -```typescript -import { NUM, OBJ, router, STR } from '@your-package/router' -// Define your routes -const api = router({ - 'GET/users': { - fn: (input, ctx) => ({ id: input.id, name: 'John' }), - description: 'Get user by ID', - input: OBJ({ id: NUM('User ID') }), - output: OBJ({ - id: NUM('User ID'), - name: STR('User name'), - }), - }, -}) -// Use with your server -server.handle('/api/', api) -``` - -## Validation - -The router includes a powerful validation system: - -```typescript -// Define a schema -const UserSchema = OBJ({ - name: STR("User's full name"), - age: NUM("User's age"), - tags: ARR(STR('Tag name'), 'User tags'), - settings: optional( - OBJ({ - theme: STR('UI theme'), - notifications: BOOL('Notification preferences'), - }), - ), -}) -// Use in route definition -const api = router({ - 'POST/users': { - fn: async (input) => { - // input is fully typed and validated - return { id: 1, ...input } - }, - input: UserSchema, - output: OBJ({ - id: NUM('User ID'), - ...UserSchema.properties, - }), - }, -}) -``` - -## API Documentation - -The router automatically generates interactive HTML documentation for your API: - -```typescript -import { generateApiDocs } from '@your-package/router' -const docs = generateApiDocs({ - 'POST/users': { - fn: async (input) => { - // input is fully typed and validated - return { id: 1, ...input } - }, - input: UserSchema, - output: OBJ({ - id: NUM('User ID'), - ...UserSchema.properties, - }), - }, -}) -server.handle('/docs', docs) -``` - -The documentation includes: - -- Method and path information -- Request/response schemas -- Interactive examples -- Search and filtering -- Dark/light mode support - -## Error Handling - -The router provides built-in error handling: - -```typescript -// Validation errors return 400 -// Example response: -{ -error: "Validation Error", -failures: [ -{ -path: ["age"], -type: "number", -value: "not a number" -} -] -} -// Not found returns 404 -{ -error: "Not Found" -} -// Method not allowed returns 405 -{ -error: "Method Not Allowed" -} -``` - -## Context and Sessions - -Each request handler receives a context object: - -```typescript -interface Context { - readonly session: Record // Cookie data - readonly trace: number // Request trace ID - readonly span?: number // Request timing -} -const api = router({ - 'GET/profile': { - fn: (input, ctx) => { - // Access session data - const userId = ctx.session.userId - // Use timing information - const requestTime = ctx.span - ctx.trace - return { userId, requestTime } - }, - input: OBJ({}), - output: OBJ({ - userId: STR(), - requestTime: NUM(), - }), - }, -}) -``` diff --git a/api/routes.ts b/api/routes.ts index 16328c2..8f5d5b0 100644 --- a/api/routes.ts +++ b/api/routes.ts @@ -12,7 +12,7 @@ import { TeamDetailDef, User, UserDef, -} from './schema.ts' +} from '/api/schema.ts' import { ARR, BOOL, @@ -40,6 +40,7 @@ import { SQLQueryError, updateTableData, } from '/api/sql.ts' +import { isLocal } from '/api/lib/env.ts' import { get, getOne } from './lmdb-store.ts' import { log } from '/api/lib/logger.ts' import { analyzeQueryWithAI } from '/api/fix-query.ts' @@ -70,15 +71,25 @@ const MetricSchema = OBJ({ }, 'SQLite sqlite3_stmt_status counters'), }) -const withUserSession = async ({ cookies }: RequestContext) => { - const session = await decodeSession(cookies.session) - if (!session) { - log.warn('auth-missing-session') - throw new respond.UnauthorizedError({ message: 'Missing user session' }) +const localUser = { + id: 'local', // this id is for local env, it will ignore permissions + email: 'local@admin.dev', + fullName: 'Local Dev', + picture: '', + isAdmin: true, +} as const + +const withUserSession = isLocal + ? () => localUser + : async ({ cookies }: RequestContext) => { + const session = await decodeSession(cookies.session) + if (!session) { + log.warn('auth-missing-session') + throw new respond.UnauthorizedError({ message: 'Missing user session' }) + } + const admin = AdminsCollection.get(session.id) + return { ...session, isAdmin: !!admin } } - const admin = AdminsCollection.get(session.id) - return { ...session, isAdmin: !!admin } -} const withAdminSession = async (ctx: RequestContext) => { const session = await withUserSession(ctx) @@ -241,6 +252,8 @@ const defs = { 'GET/api/teams': route({ authorize: withUserSession, fn: async () => { + if (isLocal) return [{ id: 'local', name: 'Local', members: [] }] + const groups = await get<{ id: string; name: string }[]>( 'google/group', { @@ -271,6 +284,7 @@ const defs = { 'GET/api/team': route({ authorize: withUserSession, fn: async (_ctx, { id }) => { + if (isLocal) return { id: 'local', name: 'Local', members: [] } const group = await getOne<{ name: string }>('google/group', id) if (!group) throw new respond.NotFoundError({ message: 'Team not found' }) @@ -406,10 +420,7 @@ const defs = { const token = await encryptMessage( JSON.stringify({ url: deployment.url, tokenSalt }), ) - return { - ...deployment, - token, - } + return { ...deployment, token } }, input: OBJ({ url: STR('Deployment URL') }), output: deploymentOutput, @@ -420,10 +431,7 @@ const defs = { fn: async (_ctx, input) => { const tokenSalt = performance.now().toString() const { tokenSalt: _, ...deployment } = await DeploymentsCollection - .insert({ - ...input, - tokenSalt, - }) + .insert({ ...input, tokenSalt }) log.info('deployment-created', { url: deployment.url, projectId: deployment.projectId, @@ -431,10 +439,7 @@ const defs = { const token = await encryptMessage( JSON.stringify({ url: deployment.url, tokenSalt }), ) - return { - ...deployment, - token, - } + return { ...deployment, token } }, input: DeploymentDef, output: deploymentOutput, @@ -449,10 +454,7 @@ const defs = { const token = await encryptMessage( JSON.stringify({ url: deployment.url, tokenSalt }), ) - return { - ...deployment, - token, - } + return { ...deployment, token } }, input: DeploymentDef, output: deploymentOutput, diff --git a/api/server.ts b/api/server.ts index 596cef5..e406c2e 100644 --- a/api/server.ts +++ b/api/server.ts @@ -1,20 +1,21 @@ import { serveDir } from '@std/http/file-server' -import { APP_ENV } from '@01edu/api/env' import { server } from '@01edu/api/server' import { routeHandler } from '/api/routes.ts' -import { PORT } from './lib/env.ts' +import { APP_ENV, isLocal, PORT } from '/api/lib/env.ts' import { init } from '/api/lib/functions.ts' -import { startSchemaRefreshLoop } from './sql.ts' +import { initLogTable } from '/api/clickhouse-client.ts' +import { startRegistryServer } from '/api/lib/local_ipc.ts' +import { startSchemaRefreshLoop } from '/api/sql.ts' import { log } from '/api/lib/logger.ts' +await initLogTable() await init() startSchemaRefreshLoop() +isLocal && (await startRegistryServer()) const fetch = server({ log, routeHandler }) export default { - fetch(req: Request) { - return fetch(req, new URL(req.url)) - }, + fetch: (req: Request) => fetch(req, new URL(req.url)), } if (APP_ENV === 'prod') { diff --git a/deno.json b/deno.json index 09b72af..c693224 100644 --- a/deno.json +++ b/deno.json @@ -3,9 +3,8 @@ "tasks": { "all": "deno task check && deno task lint && deno task test --parallel", "check": "deno check", - "dev": { "dependencies": ["dev:clickhouse", "dev:api", "dev:vite"] }, + "dev": { "dependencies": ["dev:api", "dev:vite"] }, "dev:api": "deno serve --port 3021 -A --env-file=.env.dev api/server.ts", - "dev:clickhouse": "deno run -A --env-file=.env.dev tasks/clickhouse.ts", "dev:env": "deno run -A tasks/env.ts", "dev:vite": "deno run -A --env-file=.env.dev tasks/vite.ts", "dev:with-seed": "deno task seed && deno task dev", @@ -21,12 +20,13 @@ "fmt": "deno fmt", "lint": "deno lint", "prod": "deno task prod:vite && deno task prod:api", - "prod:api": "deno compile -A --no-check --output dist/api --target x86_64-unknown-linux-gnu --include dist/web api/server.ts --env=prod", - "prod:clickhouse": "APP_ENV=prod deno run -A --env-file tasks/clickhouse.ts", + "prod:api:bundle": "mkdir -p dist && deno bundle api/server.ts -o dist/server.bundle.js", + "prod:api": "deno task prod:api:bundle && deno compile -c deno.json -A --no-check --output dist/api --target x86_64-unknown-linux-gnu --include dist/web --include db/functions dist/server.bundle.js --env=prod", "prod:start": "deno task clickhouse:prod && dist/api", "prod:vite": "BASE_URL=/ APP_ENV=prod deno run -A tasks/vite.ts", "review": "deno run -A https://gistcdn.githack.com/kigiri/7658b4af30bb5eaca3e4cad1fcac7b0c/raw/review.js", "seed": "deno run -A --env-file=.env.dev tasks/seed.ts", + "setup": "deno install --allow-scripts=npm:chdb --entrypoint api/server.ts", "test": "deno test --env-file=.env.test -A --unstable-worker-options --no-check" }, "imports": { @@ -42,19 +42,21 @@ "@std/crypto": "jsr:@std/crypto@^1.1.0", "@std/encoding": "jsr:@std/encoding@^1.0.10", "@std/fmt": "jsr:@std/fmt@^1.0.10", - "@std/fs": "jsr:@std/fs@^1.0.23", - "@std/http": "jsr:@std/http@^1.1.0", - "@std/path": "jsr:@std/path@^1.1.4", - "@std/testing": "jsr:@std/testing@^1.0.18", - "vite": "npm:vite@^8.0.13", + "@std/fs": "jsr:@std/fs@^1.0.24", + "@std/http": "jsr:@std/http@^1.1.1", + "@std/path": "jsr:@std/path@^1.1.5", + "@std/streams": "jsr:@std/streams@^1.1.1", + "@std/testing": "jsr:@std/testing@^1.0.19", + "chdb": "npm:chdb@^2.0.1", + "vite": "npm:vite@^8.0.16", "preact": "npm:preact@^10.29.2", "@preact/preset-vite": "npm:@preact/preset-vite@^2.10.5", - "@preact/signals": "npm:@preact/signals@^2.9.0", - "@clickhouse/client": "npm:@clickhouse/client@^1.18.5", + "@preact/signals": "npm:@preact/signals@^2.9.1", + "@clickhouse/client": "npm:@clickhouse/client@^1.19.0", "@tailwindcss/vite": "npm:@tailwindcss/vite@^4.3.0", "tailwindcss": "npm:tailwindcss@^4.3.0", - "daisyui": "npm:daisyui@^5.5.19", - "lucide-preact": "npm:lucide-preact@^1.16.0", + "daisyui": "npm:daisyui@^5.5.20", + "lucide-preact": "npm:lucide-preact@^1.17.0", "@deno/gfm": "jsr:@deno/gfm@0.12.0" }, "fmt": { @@ -71,6 +73,7 @@ }, "nodeModulesDir": "auto", "compilerOptions": { + "module": "esnext", "jsx": "react-jsx", "jsxImportSource": "preact", "lib": [ @@ -82,5 +85,9 @@ "DOM.AsyncIterable", "webworker" ] + }, + "allowScripts": { + "allow": ["npm:chdb@2.0.1", "npm:esbuild@0.28.0"], + "deny": [] } } diff --git a/deno.lock b/deno.lock index 3d15a9b..9ccc29f 100644 --- a/deno.lock +++ b/deno.lock @@ -11,39 +11,43 @@ "jsr:@denosaurs/emoji@~0.3.1": "0.3.1", "jsr:@denosaurs/plug@1": "1.1.0", "jsr:@std/assert@^1.0.19": "1.0.19", - "jsr:@std/cli@^1.0.29": "1.0.29", + "jsr:@std/async@^1.4.0": "1.4.0", + "jsr:@std/bytes@^1.0.6": "1.0.6", + "jsr:@std/cli@^1.0.30": "1.0.30", "jsr:@std/crypto@^1.1.0": "1.1.0", - "jsr:@std/data-structures@^1.0.11": "1.0.11", + "jsr:@std/data-structures@^1.1.0": "1.1.0", "jsr:@std/encoding@1": "1.0.10", "jsr:@std/encoding@^1.0.10": "1.0.10", "jsr:@std/fmt@1": "1.0.10", "jsr:@std/fmt@^1.0.10": "1.0.10", "jsr:@std/fmt@^1.0.9": "1.0.10", - "jsr:@std/fs@1": "1.0.23", - "jsr:@std/fs@^1.0.23": "1.0.23", - "jsr:@std/html@^1.0.6": "1.0.6", - "jsr:@std/http@^1.0.25": "1.1.0", - "jsr:@std/http@^1.1.0": "1.1.0", - "jsr:@std/internal@^1.0.12": "1.0.13", - "jsr:@std/internal@^1.0.13": "1.0.13", + "jsr:@std/fs@1": "1.0.24", + "jsr:@std/fs@^1.0.24": "1.0.24", + "jsr:@std/html@^1.0.7": "1.0.7", + "jsr:@std/http@^1.0.25": "1.1.1", + "jsr:@std/http@^1.1.1": "1.1.1", + "jsr:@std/internal@^1.0.12": "1.0.14", + "jsr:@std/internal@^1.0.14": "1.0.14", "jsr:@std/media-types@^1.1.0": "1.1.0", "jsr:@std/net@^1.0.6": "1.0.6", - "jsr:@std/path@1": "1.1.4", + "jsr:@std/path@1": "1.1.5", "jsr:@std/path@1.0": "1.0.9", - "jsr:@std/path@^1.1.4": "1.1.4", - "jsr:@std/streams@^1.1.0": "1.1.0", - "jsr:@std/testing@^1.0.18": "1.0.18", - "npm:@01edu/signal-router@~0.2.3": "0.2.3_@preact+signals@2.9.0__preact@10.29.2_preact@10.29.2", - "npm:@clickhouse/client@^1.18.5": "1.18.5", - "npm:@deno/vite-plugin@^2.0.2": "2.0.2_vite@8.0.13", - "npm:@preact/preset-vite@^2.10.5": "2.10.5_vite@8.0.13_preact@10.29.2", - "npm:@preact/signals@^2.9.0": "2.9.0_preact@10.29.2", - "npm:@tailwindcss/vite@^4.3.0": "4.3.0_vite@8.0.13", - "npm:daisyui@^5.5.19": "5.5.19", + "jsr:@std/path@^1.1.5": "1.1.5", + "jsr:@std/streams@^1.1.1": "1.1.1", + "jsr:@std/testing@^1.0.19": "1.0.19", + "npm:@01edu/signal-router@~0.2.3": "0.2.3_@preact+signals@2.9.1__preact@10.29.2_preact@10.29.2", + "npm:@clickhouse/client@^1.19.0": "1.19.0", + "npm:@deno/vite-plugin@^2.0.2": "2.0.2_vite@8.0.16", + "npm:@preact/preset-vite@^2.10.5": "2.10.5_@babel+core@7.29.7_vite@8.0.16_preact@10.29.2", + "npm:@preact/signals@^2.9.0": "2.9.1_preact@10.29.2", + "npm:@preact/signals@^2.9.1": "2.9.1_preact@10.29.2", + "npm:@tailwindcss/vite@^4.3.0": "4.3.0_vite@8.0.16", + "npm:chdb@^2.0.1": "2.0.1", + "npm:daisyui@^5.5.20": "5.5.20", "npm:github-slugger@2": "2.0.0", "npm:he@^1.2.0": "1.2.0", - "npm:katex@0.16": "0.16.46", - "npm:lucide-preact@^1.16.0": "1.16.0_preact@10.29.2", + "npm:katex@0.16": "0.16.47", + "npm:lucide-preact@^1.17.0": "1.17.0_preact@10.29.2", "npm:marked-alert@^2.1.2": "2.1.2_marked@17.0.6", "npm:marked-footnote@^1.4.0": "1.4.0_marked@17.0.6", "npm:marked-gfm-heading-id@^4.1.3": "4.1.4_marked@17.0.6", @@ -52,8 +56,8 @@ "npm:prismjs@^1.30.0": "1.30.0", "npm:sanitize-html@^2.17.0": "2.17.4", "npm:tailwindcss@^4.3.0": "4.3.0", - "npm:vite@^8.0.13": "8.0.13", - "npm:vite@^8.0.3": "8.0.13" + "npm:vite@^8.0.16": "8.0.16", + "npm:vite@^8.0.3": "8.0.16" }, "jsr": { "@01edu/api@0.2.7": { @@ -69,7 +73,7 @@ "integrity": "b5cd8e30259735734c2345312f335ada0ba4dfe28fc03e33f3ae478b7c810173", "dependencies": [ "jsr:@01edu/types", - "npm:@preact/signals" + "npm:@preact/signals@^2.9.0" ] }, "@01edu/api-proxy@0.2.1": { @@ -127,14 +131,20 @@ "jsr:@std/internal@^1.0.12" ] }, - "@std/cli@1.0.29": { - "integrity": "fa4ef29130baa834d8a13b7d138240c3a2fcfba740bfb7afa646a360a15ec84f" + "@std/async@1.4.0": { + "integrity": "4d70b008634f571cff9b554090d628c76141c32613aae0ff283fd5fa23d0c379" + }, + "@std/bytes@1.0.6": { + "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a" + }, + "@std/cli@1.0.30": { + "integrity": "769446536522d0417d7127ebcabcafac1ab0ce6766d0eb3fca1d36326fe98d13" }, "@std/crypto@1.1.0": { "integrity": "b8d6d0a6377a32b213af2661ed7bf1062d94feac0c57def5526a8e74a95c3ec8" }, - "@std/data-structures@1.0.11": { - "integrity": "53b98ed7efa61f107dfc14244bd2ec5557f7f7ee0bbaef6d449d7937facacb89" + "@std/data-structures@1.1.0": { + "integrity": "c35ae4ad5d8e41a38573c2fe3e19b18ea2505f63cfea201edcb8720aca1f7f58" }, "@std/encoding@1.0.10": { "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" @@ -142,32 +152,32 @@ "@std/fmt@1.0.10": { "integrity": "90dfba288802ac6de82fb31d0917eb9e4450b9925b954d5e51fc29ac07419db5" }, - "@std/fs@1.0.23": { - "integrity": "3ecbae4ce4fee03b180fa710caff36bb5adb66631c46a6460aaad49515565a37", + "@std/fs@1.0.24": { + "integrity": "f3061b45b81673a2bece689da041df32d174be064c89eb6397fb5718d3fb7877", "dependencies": [ - "jsr:@std/internal@^1.0.12", - "jsr:@std/path@^1.1.4" + "jsr:@std/internal@^1.0.14", + "jsr:@std/path@^1.1.5" ] }, - "@std/html@1.0.6": { - "integrity": "eaf759c8141e0733ca30eb49e4c08d8e6ca442b85c4d51f9894a56f502993e08" + "@std/html@1.0.7": { + "integrity": "175c818905a6e75743c69c251395e273d82698e58cfe7dd76268d70e28b8d8fe" }, - "@std/http@1.1.0": { - "integrity": "265cd9a589fea924c5bb0bbed8bebb4bb2fa19129f760bd014e78dbd7a365a51", + "@std/http@1.1.1": { + "integrity": "e343a9a80aea07c716b91be5c79df764144430ad2dd7c9121ed7443f08dc74f7", "dependencies": [ "jsr:@std/cli", "jsr:@std/encoding@^1.0.10", "jsr:@std/fmt@^1.0.10", - "jsr:@std/fs@^1.0.23", + "jsr:@std/fs@^1.0.24", "jsr:@std/html", "jsr:@std/media-types", "jsr:@std/net", - "jsr:@std/path@^1.1.4", + "jsr:@std/path@^1.1.5", "jsr:@std/streams" ] }, - "@std/internal@1.0.13": { - "integrity": "2f9546691d4ac2d32859c82dff284aaeac980ddeca38430d07941e7e288725c0" + "@std/internal@1.0.14": { + "integrity": "291516b3d4c35024d6ffbc0a9df5bf4c64116e05b50012cf846710152d2ffdf7" }, "@std/media-types@1.1.0": { "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" @@ -178,47 +188,51 @@ "@std/path@1.0.9": { "integrity": "260a49f11edd3db93dd38350bf9cd1b4d1366afa98e81b86167b4e3dd750129e" }, - "@std/path@1.1.4": { - "integrity": "1d2d43f39efb1b42f0b1882a25486647cb851481862dc7313390b2bb044314b5", + "@std/path@1.1.5": { + "integrity": "ccea00982ea28c36becaf6e62f855406c76a8c32d462f66f415bbb7d83a271bc", "dependencies": [ - "jsr:@std/internal@^1.0.12" + "jsr:@std/internal@^1.0.14" ] }, - "@std/streams@1.1.0": { - "integrity": "2f7024d841f343fd478afe0c958a3f0f068ef2a0d2bcc954f550f97ac1fa22e3" + "@std/streams@1.1.1": { + "integrity": "92556d350e537e9dce527a6d08f6f15be3ff65e656079dea69d15252187c7613", + "dependencies": [ + "jsr:@std/bytes" + ] }, - "@std/testing@1.0.18": { - "integrity": "d3152f57b11666bf6358d0e127c7e3488e91178b0c2d8fbf0793e1c53cd13cb1", + "@std/testing@1.0.19": { + "integrity": "f4236172365b216728dc3cc8b5e80a9f4c33083d1e4ede7613d5b25b4014898e", "dependencies": [ "jsr:@std/assert", + "jsr:@std/async", "jsr:@std/data-structures", - "jsr:@std/fs@^1.0.23", - "jsr:@std/internal@^1.0.13", - "jsr:@std/path@^1.1.4" + "jsr:@std/fs@^1.0.24", + "jsr:@std/internal@^1.0.14", + "jsr:@std/path@^1.1.5" ] } }, "npm": { - "@01edu/signal-router@0.2.3_@preact+signals@2.9.0__preact@10.29.2_preact@10.29.2": { + "@01edu/signal-router@0.2.3_@preact+signals@2.9.1__preact@10.29.2_preact@10.29.2": { "integrity": "sha512-Eg2ORuigaA8i3vM+Lr4k0P7+A53vZ3mKb2wsVnbBslkItaebEFwPUDxXiz2zDCJYHd06wLdvd64r/VRQQw6XFw==", "dependencies": [ "@preact/signals", "preact" ] }, - "@babel/code-frame@7.29.0": { - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "@babel/code-frame@7.29.7": { + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dependencies": [ "@babel/helper-validator-identifier", "js-tokens", "picocolors" ] }, - "@babel/compat-data@7.29.3": { - "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==" + "@babel/compat-data@7.29.7": { + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==" }, - "@babel/core@7.29.0": { - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "@babel/core@7.29.7": { + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dependencies": [ "@babel/code-frame", "@babel/generator", @@ -237,8 +251,8 @@ "semver" ] }, - "@babel/generator@7.29.1": { - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "@babel/generator@7.29.7": { + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dependencies": [ "@babel/parser", "@babel/types", @@ -247,14 +261,14 @@ "jsesc" ] }, - "@babel/helper-annotate-as-pure@7.27.3": { - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "@babel/helper-annotate-as-pure@7.29.7": { + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", "dependencies": [ "@babel/types" ] }, - "@babel/helper-compilation-targets@7.28.6": { - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "@babel/helper-compilation-targets@7.29.7": { + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dependencies": [ "@babel/compat-data", "@babel/helper-validator-option", @@ -263,18 +277,18 @@ "semver" ] }, - "@babel/helper-globals@7.28.0": { - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==" + "@babel/helper-globals@7.29.7": { + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==" }, - "@babel/helper-module-imports@7.28.6": { - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "@babel/helper-module-imports@7.29.7": { + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dependencies": [ "@babel/traverse", "@babel/types" ] }, - "@babel/helper-module-transforms@7.28.6_@babel+core@7.29.0": { - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "@babel/helper-module-transforms@7.29.7_@babel+core@7.29.7": { + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dependencies": [ "@babel/core", "@babel/helper-module-imports", @@ -282,48 +296,48 @@ "@babel/traverse" ] }, - "@babel/helper-plugin-utils@7.28.6": { - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==" + "@babel/helper-plugin-utils@7.29.7": { + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==" }, - "@babel/helper-string-parser@7.27.1": { - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==" + "@babel/helper-string-parser@7.29.7": { + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==" }, - "@babel/helper-validator-identifier@7.28.5": { - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==" + "@babel/helper-validator-identifier@7.29.7": { + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==" }, - "@babel/helper-validator-option@7.27.1": { - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==" + "@babel/helper-validator-option@7.29.7": { + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==" }, - "@babel/helpers@7.29.2": { - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "@babel/helpers@7.29.7": { + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dependencies": [ "@babel/template", "@babel/types" ] }, - "@babel/parser@7.29.3": { - "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "@babel/parser@7.29.7": { + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dependencies": [ "@babel/types" ], "bin": true }, - "@babel/plugin-syntax-jsx@7.28.6_@babel+core@7.29.0": { - "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "@babel/plugin-syntax-jsx@7.29.7_@babel+core@7.29.7": { + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", "dependencies": [ "@babel/core", "@babel/helper-plugin-utils" ] }, - "@babel/plugin-transform-react-jsx-development@7.27.1_@babel+core@7.29.0": { - "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "@babel/plugin-transform-react-jsx-development@7.29.7_@babel+core@7.29.7": { + "integrity": "sha512-Xfy3UVMF04+ypnFbkhvfqtmvwfe92qwQdbGZVonhE+6v35GzlofmOnA1szaZqzb9xYWr0nl1e5EMmzi0DNON1g==", "dependencies": [ "@babel/core", "@babel/plugin-transform-react-jsx" ] }, - "@babel/plugin-transform-react-jsx@7.28.6_@babel+core@7.29.0": { - "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", + "@babel/plugin-transform-react-jsx@7.29.7_@babel+core@7.29.7": { + "integrity": "sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A==", "dependencies": [ "@babel/core", "@babel/helper-annotate-as-pure", @@ -333,16 +347,16 @@ "@babel/types" ] }, - "@babel/template@7.28.6": { - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "@babel/template@7.29.7": { + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dependencies": [ "@babel/code-frame", "@babel/parser", "@babel/types" ] }, - "@babel/traverse@7.29.0": { - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "@babel/traverse@7.29.7": { + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dependencies": [ "@babel/code-frame", "@babel/generator", @@ -353,23 +367,23 @@ "debug" ] }, - "@babel/types@7.29.0": { - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "@babel/types@7.29.7": { + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dependencies": [ "@babel/helper-string-parser", "@babel/helper-validator-identifier" ] }, - "@clickhouse/client-common@1.18.5": { - "integrity": "sha512-g9LwcS1dvkatKDsIjT1PwUHldsiYzwdKAB0nXfd9APLd+t4PrNJa+my+dXcqJdmcWyhWjKLP/2/ztBwgxp+sbQ==" + "@clickhouse/client-common@1.19.0": { + "integrity": "sha512-HHQw7MUt1aFquSlnoQhlRO8e7zhRw7rJZFNSYiHP2wxzEVeoI0cX8kKoTp+xlVySvFi8s0mAC3bi8D031xmIQg==" }, - "@clickhouse/client@1.18.5": { - "integrity": "sha512-4FfoyMkFWhsdNMuXsoEL6l3c12svA63BBJBtDo9SrxRZ14RdmN6jLr/rF3f84BK8cFoxETZCSeKlsbk6NNYebw==", + "@clickhouse/client@1.19.0": { + "integrity": "sha512-R/35tIFZjwRyqtTN0cnlvd45zU+YREuQ/cnfi6c+KGGkVSCF+1cl8mZ9kkxshqHx2U4YjcmPVElJaRn2bEqJ4g==", "dependencies": [ "@clickhouse/client-common" ] }, - "@deno/vite-plugin@2.0.2_vite@8.0.13": { + "@deno/vite-plugin@2.0.2_vite@8.0.16": { "integrity": "sha512-bzuKApn9Jr2x1jSrbuJEJzy++8LUwjFVOAopAbepcE3RgYzdcPEWd36PSp7P5dNMQlNnQlgtm3MeNbcKZ/Eh/Q==", "dependencies": [ "@deno/loader@npm:@jsr/deno__loader@0.5.0", @@ -445,12 +459,12 @@ ], "tarball": "https://npm.jsr.io/~/11/@jsr/std__jsonc/1.0.2.tgz" }, - "@jsr/std__streams@1.1.0": { - "integrity": "sha512-0yP/bIRAgcpdIg1o/XSYnVmCs3a7rtfKfhPPiKo7GFEtvF2qngWKJ80u8xN+D8o3otsrf/Ta90XiNj5WZJfFgg==", + "@jsr/std__streams@1.1.1": { + "integrity": "sha512-V9auR/i6gJz6SR1+h5wc4EN42IC5+ObBjXu/h7Buef7UOE0GtX4vRSx+3MJywEiBcJqQC+lCnf+wBAodvBL1SA==", "dependencies": [ "@jsr/std__bytes" ], - "tarball": "https://npm.jsr.io/~/11/@jsr/std__streams/1.1.0.tgz" + "tarball": "https://npm.jsr.io/~/11/@jsr/std__streams/1.1.1.tgz" }, "@napi-rs/wasm-runtime@1.1.4_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0": { "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", @@ -460,17 +474,17 @@ "@tybys/wasm-util" ] }, - "@oxc-project/types@0.130.0": { - "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==" + "@oxc-project/types@0.133.0": { + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==" }, - "@preact/preset-vite@2.10.5_vite@8.0.13_preact@10.29.2": { + "@preact/preset-vite@2.10.5_@babel+core@7.29.7_vite@8.0.16_preact@10.29.2": { "integrity": "sha512-p0vJpxiVO7KWWazWny3LUZ+saXyZKWv6Ju0bYMWNJRp2YveufRPgSUB1C4MTqGJfz07EehMgfN+AJNwQy+w6Iw==", "dependencies": [ "@babel/core", "@babel/plugin-transform-react-jsx", "@babel/plugin-transform-react-jsx-development", "@prefresh/vite", - "@rollup/pluginutils@5.3.0", + "@rollup/pluginutils@5.4.0", "babel-plugin-transform-hook-names", "debug", "magic-string", @@ -483,8 +497,8 @@ "@preact/signals-core@1.14.2": { "integrity": "sha512-RZHdBj9ZF4n40Rp4jS052EHHjBWf96P9oNdXPfhQTovCuWY9iQn3Gq+gOTJSgBO9A/JBuPfMOWsSX/lIU9Pc/A==" }, - "@preact/signals@2.9.0_preact@10.29.2": { - "integrity": "sha512-hYrY0KyUqkDgOl1qba/JGn6y81pXnurn21PMaxfcMwdncdZ3M/oVdmpTvEnsGjh48dIwDVc7bjWHqIsngSjYug==", + "@preact/signals@2.9.1_preact@10.29.2": { + "integrity": "sha512-xVqN8mJjbSN5IB/8Ubmd9NN+Ew6zJswoRxrjZbH3YsgkMshFeO6d8zxEFpHRTq9GJZx7cnPs2CnCpFqtGXGNsw==", "dependencies": [ "@preact/signals-core", "preact" @@ -502,7 +516,7 @@ "@prefresh/utils@1.2.1": { "integrity": "sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw==" }, - "@prefresh/vite@2.4.12_preact@10.29.2_vite@8.0.13": { + "@prefresh/vite@2.4.12_preact@10.29.2_vite@8.0.16": { "integrity": "sha512-FY1fzXpUjiuosznMV0YM7XAOPZjB5FIdWS0W24+XnlxYkt9hNAwwsiKYn+cuTEoMtD/ZVazS5QVssBr9YhpCQA==", "dependencies": [ "@babel/core", @@ -514,68 +528,68 @@ "vite" ] }, - "@rolldown/binding-android-arm64@1.0.1": { - "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==", + "@rolldown/binding-android-arm64@1.0.3": { + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", "os": ["android"], "cpu": ["arm64"] }, - "@rolldown/binding-darwin-arm64@1.0.1": { - "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==", + "@rolldown/binding-darwin-arm64@1.0.3": { + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", "os": ["darwin"], "cpu": ["arm64"] }, - "@rolldown/binding-darwin-x64@1.0.1": { - "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==", + "@rolldown/binding-darwin-x64@1.0.3": { + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", "os": ["darwin"], "cpu": ["x64"] }, - "@rolldown/binding-freebsd-x64@1.0.1": { - "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==", + "@rolldown/binding-freebsd-x64@1.0.3": { + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", "os": ["freebsd"], "cpu": ["x64"] }, - "@rolldown/binding-linux-arm-gnueabihf@1.0.1": { - "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==", + "@rolldown/binding-linux-arm-gnueabihf@1.0.3": { + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", "os": ["linux"], "cpu": ["arm"] }, - "@rolldown/binding-linux-arm64-gnu@1.0.1": { - "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==", + "@rolldown/binding-linux-arm64-gnu@1.0.3": { + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", "os": ["linux"], "cpu": ["arm64"] }, - "@rolldown/binding-linux-arm64-musl@1.0.1": { - "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==", + "@rolldown/binding-linux-arm64-musl@1.0.3": { + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", "os": ["linux"], "cpu": ["arm64"] }, - "@rolldown/binding-linux-ppc64-gnu@1.0.1": { - "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==", + "@rolldown/binding-linux-ppc64-gnu@1.0.3": { + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", "os": ["linux"], "cpu": ["ppc64"] }, - "@rolldown/binding-linux-s390x-gnu@1.0.1": { - "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==", + "@rolldown/binding-linux-s390x-gnu@1.0.3": { + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", "os": ["linux"], "cpu": ["s390x"] }, - "@rolldown/binding-linux-x64-gnu@1.0.1": { - "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==", + "@rolldown/binding-linux-x64-gnu@1.0.3": { + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", "os": ["linux"], "cpu": ["x64"] }, - "@rolldown/binding-linux-x64-musl@1.0.1": { - "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==", + "@rolldown/binding-linux-x64-musl@1.0.3": { + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", "os": ["linux"], "cpu": ["x64"] }, - "@rolldown/binding-openharmony-arm64@1.0.1": { - "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==", + "@rolldown/binding-openharmony-arm64@1.0.3": { + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", "os": ["openharmony"], "cpu": ["arm64"] }, - "@rolldown/binding-wasm32-wasi@1.0.1": { - "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==", + "@rolldown/binding-wasm32-wasi@1.0.3": { + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", "dependencies": [ "@emnapi/core", "@emnapi/runtime", @@ -583,13 +597,13 @@ ], "cpu": ["wasm32"] }, - "@rolldown/binding-win32-arm64-msvc@1.0.1": { - "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==", + "@rolldown/binding-win32-arm64-msvc@1.0.3": { + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", "os": ["win32"], "cpu": ["arm64"] }, - "@rolldown/binding-win32-x64-msvc@1.0.1": { - "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==", + "@rolldown/binding-win32-x64-msvc@1.0.3": { + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", "os": ["win32"], "cpu": ["x64"] }, @@ -603,8 +617,8 @@ "picomatch@2.3.2" ] }, - "@rollup/pluginutils@5.3.0": { - "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "@rollup/pluginutils@5.4.0": { + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", "dependencies": [ "@types/estree", "estree-walker", @@ -699,7 +713,7 @@ "@tailwindcss/oxide-win32-x64-msvc" ] }, - "@tailwindcss/vite@4.3.0_vite@8.0.13": { + "@tailwindcss/vite@4.3.0_vite@8.0.16": { "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==", "dependencies": [ "@tailwindcss/node", @@ -717,14 +731,14 @@ "@types/estree@1.0.9": { "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==" }, - "babel-plugin-transform-hook-names@1.0.2_@babel+core@7.29.0": { + "babel-plugin-transform-hook-names@1.0.2_@babel+core@7.29.7": { "integrity": "sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==", "dependencies": [ "@babel/core" ] }, - "baseline-browser-mapping@2.10.29": { - "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "baseline-browser-mapping@2.10.33": { + "integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==", "bin": true }, "boolbase@1.0.0": { @@ -741,8 +755,16 @@ ], "bin": true }, - "caniuse-lite@1.0.30001792": { - "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==" + "caniuse-lite@1.0.30001793": { + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==" + }, + "chdb@2.0.1": { + "integrity": "sha512-m4pL7h310enExpF0amPN56f6HS0RpubZKoT4TOPIBX2QdMqNWLv96kVSr0kUrgydRNOpMSyQJqZHLVDlwSOVjw==", + "dependencies": [ + "node-addon-api", + "node-gyp-build" + ], + "scripts": true }, "commander@8.3.0": { "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==" @@ -763,11 +785,11 @@ "css-what@6.2.2": { "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==" }, - "daisyui@5.5.19": { - "integrity": "sha512-pbFAkl1VCEh/MPCeclKL61I/MqRIFFhNU7yiXoDDRapXN4/qNCoMxeCCswyxEEhqL5eiTTfwHvucFtOE71C9sA==" + "daisyui@5.5.20": { + "integrity": "sha512-HemJcjl0Gk9rQ8BcgofN6p+EURrqftQG9wK1Hkxs98i49xe68+QxpNvry+PyxwkIUgrbMpNmZ5ZWjmtffAjfhQ==" }, - "dayjs@1.11.20": { - "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==" + "dayjs@1.11.21": { + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==" }, "debug@4.4.3": { "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", @@ -806,11 +828,11 @@ "domhandler" ] }, - "electron-to-chromium@1.5.355": { - "integrity": "sha512-LUPZhKzZPYSPme1jEYohpkA+ybYCJztr1quAdBd7E7h3+VOBVcKkwwtBJu41nrjawrRzfb8mtMfzWozoaK0ZIQ==" + "electron-to-chromium@1.5.366": { + "integrity": "sha512-OlRuhb688YTCzzU3gXPLn6nGyd+F+53INE1qaKKlu6kETErE8FYsyDh0XqXEU+uBRn0MpCzz2vfNwORhkap8qg==" }, - "enhanced-resolve@5.21.3": { - "integrity": "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q==", + "enhanced-resolve@5.22.1": { + "integrity": "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==", "dependencies": [ "graceful-fs", "tapable" @@ -885,8 +907,8 @@ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "bin": true }, - "katex@0.16.46": { - "integrity": "sha512-WHy4Coo+bGZyH7NwJKHkS04YFsFcarWbAEOAC3EMndzdN6VSZqklLLIgfxzyaW9jDoeGYJX9SWbJPKpecox0Uw==", + "katex@0.16.47": { + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", "dependencies": [ "commander" ], @@ -981,8 +1003,8 @@ "yallist" ] }, - "lucide-preact@1.16.0_preact@10.29.2": { - "integrity": "sha512-Sa1XSig6iWCFhdrqS+vT1EFJH4Yos2Q7H/EMawql1K+D9qjNRzjMutPySak0iMYto6PneHYAWoxnDBBlryjngw==", + "lucide-preact@1.17.0_preact@10.29.2": { + "integrity": "sha512-QoHqgMY9WJanT+zd8x5Hq9quOqY/MHt7oTpuPAZzCkfUhfxsTJ+L0CFnUaBBh+H54OySAElm2+UQ1JwQ2CDPmg==", "dependencies": [ "preact" ] @@ -1023,6 +1045,13 @@ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "bin": true }, + "node-addon-api@6.1.0": { + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" + }, + "node-gyp-build@4.8.4": { + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "bin": true + }, "node-html-parser@6.1.13": { "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", "dependencies": [ @@ -1030,8 +1059,8 @@ "he" ] }, - "node-releases@2.0.44": { - "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==" + "node-releases@2.0.47": { + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==" }, "nth-check@2.1.1": { "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", @@ -1051,8 +1080,8 @@ "picomatch@4.0.4": { "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==" }, - "postcss@8.5.14": { - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "postcss@8.5.15": { + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dependencies": [ "nanoid", "picocolors", @@ -1065,8 +1094,8 @@ "prismjs@1.30.0": { "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==" }, - "rolldown@1.0.1": { - "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==", + "rolldown@1.0.3": { + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", "dependencies": [ "@oxc-project/types", "@rolldown/pluginutils" @@ -1127,8 +1156,8 @@ "tapable@2.3.3": { "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==" }, - "tinyglobby@0.2.16": { - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "tinyglobby@0.2.17": { + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dependencies": [ "fdir", "picomatch@4.0.4" @@ -1146,7 +1175,7 @@ ], "bin": true }, - "vite-prerender-plugin@0.5.13_vite@8.0.13": { + "vite-prerender-plugin@0.5.13_vite@8.0.16": { "integrity": "sha512-IKSpYkzDBsKAxa05naRbj7GvNVMSdww/Z/E89oO3xndz+gWnOBOKOAbEXv7qDhktY/j3vHgJmoV1pPzqU2tx9g==", "dependencies": [ "kolorist", @@ -1158,8 +1187,8 @@ "vite" ] }, - "vite@8.0.13": { - "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==", + "vite@8.0.16": { + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dependencies": [ "lightningcss", "picomatch@4.0.4", @@ -1193,21 +1222,23 @@ "jsr:@std/crypto@^1.1.0", "jsr:@std/encoding@^1.0.10", "jsr:@std/fmt@^1.0.10", - "jsr:@std/fs@^1.0.23", - "jsr:@std/http@^1.1.0", - "jsr:@std/path@^1.1.4", - "jsr:@std/testing@^1.0.18", + "jsr:@std/fs@^1.0.24", + "jsr:@std/http@^1.1.1", + "jsr:@std/path@^1.1.5", + "jsr:@std/streams@^1.1.1", + "jsr:@std/testing@^1.0.19", "npm:@01edu/signal-router@~0.2.3", - "npm:@clickhouse/client@^1.18.5", + "npm:@clickhouse/client@^1.19.0", "npm:@deno/vite-plugin@^2.0.2", "npm:@preact/preset-vite@^2.10.5", - "npm:@preact/signals@^2.9.0", + "npm:@preact/signals@^2.9.1", "npm:@tailwindcss/vite@^4.3.0", - "npm:daisyui@^5.5.19", - "npm:lucide-preact@^1.16.0", + "npm:chdb@^2.0.1", + "npm:daisyui@^5.5.20", + "npm:lucide-preact@^1.17.0", "npm:preact@^10.29.2", "npm:tailwindcss@^4.3.0", - "npm:vite@^8.0.13" + "npm:vite@^8.0.16" ] } } diff --git a/tasks/clickhouse.ts b/tasks/clickhouse.ts deleted file mode 100644 index d147dde..0000000 --- a/tasks/clickhouse.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { client } from '/api/clickhouse-client.ts' - -if (import.meta.main) { - try { - await client.ping() - - await client.command({ - query: ` - CREATE TABLE IF NOT EXISTS logs ( - id UUID DEFAULT generateUUIDv4(), - -- Flattened resource fields - service_name LowCardinality(String), - service_version LowCardinality(String), - service_instance_id String, - - timestamp DateTime64(3, 'UTC') DEFAULT now64(3, 'UTC'), - observed_timestamp DateTime64(3, 'UTC') DEFAULT now64(3, 'UTC'), - trace_id FixedString(16), - span_id FixedString(16), - severity_number UInt8, - -- derived column, computed by DB from severity_number - severity_text LowCardinality(String) MATERIALIZED CASE - WHEN severity_number > 4 AND severity_number <= 8 THEN 'DEBUG' - WHEN severity_number > 8 AND severity_number <= 12 THEN 'INFO' - WHEN severity_number > 12 AND severity_number <= 16 THEN 'WARN' - WHEN severity_number > 20 AND severity_number <= 24 THEN 'FATAL' - ELSE 'ERROR' - END, - -- Often empty, but kept for OTEL spec compliance - body Nullable(String), - attributes JSON, - event_name LowCardinality(String) - ) - ENGINE = MergeTree - PARTITION BY toYYYYMMDD(timestamp) - ORDER BY (service_name, timestamp, trace_id) - SETTINGS index_granularity = 8192, min_bytes_for_wide_part = 0; - `, - }) - - console.log('logs table is ready') - } catch (error) { - console.error('Error creating ClickHouse table:', { error }) - Deno.exit(1) - } -} diff --git a/tasks/vite.ts b/tasks/vite.ts index 0fb51f7..b7ed947 100644 --- a/tasks/vite.ts +++ b/tasks/vite.ts @@ -1,11 +1,10 @@ // tasks/vite.js import { join } from 'node:path' -import { AliasOptions, build, createServer } from 'vite' +import { build, createServer } from 'vite' import { apiProxy } from '@01edu/api-proxy' import deno from '@deno/vite-plugin' import preact from '@preact/preset-vite' import tailwindcss from '@tailwindcss/vite' -import { APP_ENV } from '@01edu/api/env' const plugins = [ preact({ jsxImportSource: 'preact' }), @@ -13,27 +12,27 @@ const plugins = [ deno(), ] -const alias: AliasOptions = [ +const BASE_URL = Deno.env.get('BASE_URL') || '/' +const preactRuntimeAlias = [ { - find: 'npm:@preact/signals@^2.5.1', - replacement: '@preact/signals', + find: /^npm:preact(?:@[^/]+)?\/jsx-runtime$/, + replacement: 'preact/jsx-runtime', }, { - find: 'npm:preact@^10.27.2', - replacement: 'preact', + find: /^npm:preact(?:@[^/]+)?\/jsx-dev-runtime$/, + replacement: 'preact/jsx-dev-runtime', }, ] + // Production build -if (APP_ENV === 'prod') { +if (Deno.env.get('APP_ENV') === 'prod') { await build({ configFile: false, root: join(import.meta.dirname!, '../web'), plugins, - resolve: { alias }, - build: { - outDir: '../dist/web', - emptyOutDir: true, - }, + base: BASE_URL, + resolve: { alias: preactRuntimeAlias }, + build: { outDir: '../dist/web', emptyOutDir: true }, }) Deno.exit(0) } @@ -43,12 +42,10 @@ const PORT = Number(Deno.env.get('PORT')) || 2119 const server = await createServer({ configFile: false, root: join(import.meta.dirname!, '../web'), + base: BASE_URL, plugins: [...plugins, apiProxy({ port: PORT, prefix: '/api/' })], - resolve: { alias }, - server: { - port: 7737, - host: true, - }, + resolve: { alias: preactRuntimeAlias }, + server: { port: 7737, host: true }, }) await server.listen() server.printUrls() diff --git a/web/components/BrandIcons.tsx b/web/components/BrandIcons.tsx deleted file mode 100644 index f8c410b..0000000 --- a/web/components/BrandIcons.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type { SVGAttributes } from 'preact' -// Copy path / name from: https://simpleicons.org/ - -type SVGProps = SVGAttributes -const Icon = (title: string, path: string) => (props: SVGProps) => ( - - {title} - - -) - -export const GitHub = Icon( - 'GibHub', - 'M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12', -) diff --git a/web/components/Layout.tsx b/web/components/Layout.tsx index 32f2307..169c86f 100644 --- a/web/components/Layout.tsx +++ b/web/components/Layout.tsx @@ -1,8 +1,6 @@ -import { JSX } from 'preact' +import { ComponentChildren } from 'preact' -export const PageLayout = ( - { children }: { children: JSX.Element | JSX.Element[] }, -) => ( +export const PageLayout = ({ children }: { children: ComponentChildren }) => (
{children} @@ -13,7 +11,7 @@ export const PageLayout = ( export const PageHeader = ( { children, class: className }: { class?: string - children: JSX.Element | JSX.Element[] + children: ComponentChildren }, ) => (
) -export const PageContent = ( - { children }: { children: JSX.Element | JSX.Element[] }, -) => ( +export const PageContent = ({ children }: { children: ComponentChildren }) => (
{children}
) diff --git a/web/components/SideBar.tsx b/web/components/SideBar.tsx index ba853c7..2e85035 100644 --- a/web/components/SideBar.tsx +++ b/web/components/SideBar.tsx @@ -5,8 +5,8 @@ import { LucideIcon, Settings, } from 'lucide-preact' -import { user } from '../lib/session.ts' import { A, url } from '@01edu/signal-router' +import { sidebarItems } from '../lib/shared.tsx' export type SidebarItem = { label: string @@ -15,10 +15,10 @@ export type SidebarItem = { } export function Sidebar( - { sidebarItems, sbi, title }: { - sidebarItems: Record + { sbi, title, isAdmin }: { sbi?: string title?: string + isAdmin?: boolean }, ) { const sb = url.params.sb @@ -70,7 +70,7 @@ export function Sidebar( params={{ sbi: 'settings' }} replace class={`rounded p-2 w-full flex items-center gap-2 ${ - user.data?.isAdmin + isAdmin ? 'settings' === sbi ? 'bg-primary text-primary-content' : '' : 'opacity-50 pointer-events-none' }`} diff --git a/web/components/forms.tsx b/web/components/forms.tsx index 6e0fc47..c73be30 100644 --- a/web/components/forms.tsx +++ b/web/components/forms.tsx @@ -1,4 +1,8 @@ -import { JSX } from 'preact' +import type { + ButtonHTMLAttributes, + ComponentChildren, + InputHTMLAttributes, +} from 'preact' import { useId } from 'preact/hooks' import { A, LinkProps } from '@01edu/signal-router' @@ -7,7 +11,7 @@ export const Card = ( { children, title, description }: { title: string description?: string - children: JSX.Element | JSX.Element[] + children: ComponentChildren }, ) => (
@@ -27,7 +31,7 @@ export const Card = ( export const Input = ( { label, name, note, ...props }: & { label: string; name: string; note?: string } - & JSX.InputHTMLAttributes, + & InputHTMLAttributes, ) => { const id = useId() return ( @@ -52,7 +56,7 @@ export const Button = ( & { variant?: 'primary' | 'secondary' | 'danger' } - & Omit, 'class' | 'style'> + & Omit, 'class' | 'style'> & Partial, ) => { const baseClasses = @@ -94,7 +98,7 @@ export const Switch = ( label: string note?: string // checked: boolean - } & JSX.InputHTMLAttributes, + } & InputHTMLAttributes, ) => { const id = useId() return ( diff --git a/web/index.tsx b/web/index.tsx index eee60ee..db5b716 100644 --- a/web/index.tsx +++ b/web/index.tsx @@ -7,31 +7,25 @@ import { user } from './lib/session.ts' import { url } from '@01edu/signal-router' import { ProjectPage } from './pages/ProjectPage.tsx' -const renderPage = () => { - if (user.pending) return - if (!user.data) { - return - } - if (url.path.startsWith('/projects/')) { - return - } +const Router = () => { + if (user.pending) return null + if (!user.data) return + if (url.path.startsWith('/projects/')) return return } -const App = () => { - return ( -
-
- -
-
-
-
-
- {renderPage()} -
+const App = () => ( +
+
+
- ) -} +
+
+
+
+ +
+
+) const root = document.getElementById('app') if (!root) throw new Error('Unable to find root element #app') diff --git a/web/layout.tsx b/web/layout.tsx index 741a20c..f6fd8d6 100644 --- a/web/layout.tsx +++ b/web/layout.tsx @@ -1,7 +1,6 @@ import { effect, signal } from '@preact/signals' import { A, url } from '@01edu/signal-router' import { Code, LogOut, Moon, Sun } from 'lucide-preact' -import { GitHub } from './components/BrandIcons.tsx' import { user } from './lib/session.ts' const $theme = signal(localStorage.theme || 'dark') @@ -16,7 +15,7 @@ const toggleTheme = () => { } const UserInfo = () => { - if (!user.data) return null + if (!user.data || user.data.id === 'local') return null return (
@@ -58,25 +57,16 @@ export const SwitchTheme = () => ( export const Header = () => (
) } diff --git a/web/pages/ProjectsPage.tsx b/web/pages/ProjectsPage.tsx index 9bc3628..12312b0 100644 --- a/web/pages/ProjectsPage.tsx +++ b/web/pages/ProjectsPage.tsx @@ -24,7 +24,6 @@ type Team = ApiOutput['GET/api/team'] const teams = api['GET/api/teams'].signal() teams.fetch() - const projects = api['GET/api/projects'].signal() projects.fetch()