Migrate API to Elysia and switch runtime to Bun
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
# Optional API override. Leave empty to use same-origin internal API routes.
|
# Optional API override. Leave empty to use same-origin internal API routes.
|
||||||
NEXT_PUBLIC_API_URL=
|
NEXT_PUBLIC_API_URL=
|
||||||
NPM_VERSION=latest
|
|
||||||
|
|
||||||
# Local docker host port (used by docker-compose.override.yml)
|
# Local docker host port (used by docker-compose.override.yml)
|
||||||
APP_PORT=3000
|
APP_PORT=3000
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -22,6 +22,7 @@ out/
|
|||||||
# Misc
|
# Misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.log
|
*.log
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
32
Dockerfile
32
Dockerfile
@@ -1,22 +1,16 @@
|
|||||||
FROM node:20-alpine AS base
|
FROM oven/bun:1.3.5-alpine AS deps
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ARG NPM_VERSION=latest
|
COPY package.json bun.lock ./
|
||||||
RUN npm install -g "npm@${NPM_VERSION}"
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
FROM base AS deps
|
FROM deps AS builder
|
||||||
COPY package.json package-lock.json ./
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
FROM base AS builder
|
|
||||||
ARG NEXT_PUBLIC_API_URL=
|
ARG NEXT_PUBLIC_API_URL=
|
||||||
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN mkdir -p public && npm run build
|
RUN mkdir -p public && bun run build
|
||||||
|
|
||||||
FROM base AS runner
|
FROM oven/bun:1.3.5-alpine AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
@@ -24,19 +18,13 @@ ARG NEXT_PUBLIC_API_URL=
|
|||||||
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
RUN apk add --no-cache su-exec
|
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
|
||||||
RUN adduser --system --uid 1001 nextjs
|
|
||||||
|
|
||||||
COPY --from=builder /app/public ./public
|
COPY --from=builder /app/public ./public
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
COPY --from=builder /app/.next/standalone ./
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
COPY --from=builder /app/.next/static ./.next/static
|
||||||
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
RUN mkdir -p /app/data
|
||||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
ENV PORT=3000
|
ENV PORT=3000
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
CMD ["bun", "server.js"]
|
||||||
CMD ["node", "server.js"]
|
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -5,18 +5,20 @@ Turbopack-first rebuild of a fiscal.ai-style terminal with OpenClaw integration.
|
|||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
- Next.js 16 App Router
|
- Next.js 16 App Router
|
||||||
|
- Bun runtime/tooling
|
||||||
|
- Elysia route layer mounted in Next Route Handlers
|
||||||
- Turbopack for `dev` and `build`
|
- Turbopack for `dev` and `build`
|
||||||
- Better Auth (email/password, magic link, admin, organization plugins)
|
- Better Auth (email/password, magic link, admin, organization plugins)
|
||||||
- PostgreSQL adapter for Better Auth
|
- PostgreSQL adapter for Better Auth
|
||||||
- Internal API routes (`app/api/*`)
|
- Internal API routes via Elysia catch-all (`app/api/[[...slugs]]/route.ts`)
|
||||||
- Durable local task engine and JSON data store
|
- Durable local task engine and JSON data store
|
||||||
- OpenClaw/ZeroClaw analysis via OpenAI-compatible chat endpoint
|
- OpenClaw/ZeroClaw analysis via OpenAI-compatible chat endpoint
|
||||||
|
|
||||||
## Run locally
|
## Run locally
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
bun install
|
||||||
npm run dev
|
bun run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000).
|
Open [http://localhost:3000](http://localhost:3000).
|
||||||
@@ -27,8 +29,8 @@ Auth tables are migrated automatically on first authenticated request.
|
|||||||
## Production build
|
## Production build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
bun run build
|
||||||
npm run start
|
bun run start
|
||||||
```
|
```
|
||||||
|
|
||||||
## Docker deployment
|
## Docker deployment
|
||||||
@@ -43,7 +45,7 @@ The base Docker Compose now includes an internal PostgreSQL service (`postgres`)
|
|||||||
For Coolify/remote Docker Compose, only app container port `3000` is exposed internally (no fixed host port bind), avoiding host port collisions.
|
For Coolify/remote Docker Compose, only app container port `3000` is exposed internally (no fixed host port bind), avoiding host port collisions.
|
||||||
If you use an external Postgres instance, set `DATABASE_URL` explicitly.
|
If you use an external Postgres instance, set `DATABASE_URL` explicitly.
|
||||||
Runtime data persists in the `app_data` volume (`/app/data` in container).
|
Runtime data persists in the `app_data` volume (`/app/data` in container).
|
||||||
Docker builds install the npm version from `NPM_VERSION` (default `latest`).
|
Docker images use Bun (`oven/bun:1.3.5-alpine`) for build and runtime.
|
||||||
|
|
||||||
## Environment
|
## Environment
|
||||||
|
|
||||||
@@ -71,6 +73,8 @@ If OpenClaw is unset, the app uses local fallback analysis so task workflows sti
|
|||||||
|
|
||||||
## API surface
|
## API surface
|
||||||
|
|
||||||
|
All endpoints below are handled by Elysia in `app/api/[[...slugs]]/route.ts`.
|
||||||
|
|
||||||
- `GET|POST|PATCH|PUT|DELETE /api/auth/*` (Better Auth handler)
|
- `GET|POST|PATCH|PUT|DELETE /api/auth/*` (Better Auth handler)
|
||||||
- `GET /api/health`
|
- `GET /api/health`
|
||||||
- `GET /api/me`
|
- `GET /api/me`
|
||||||
|
|||||||
529
app/api/[[...slugs]]/route.ts
Normal file
529
app/api/[[...slugs]]/route.ts
Normal file
@@ -0,0 +1,529 @@
|
|||||||
|
import { Elysia } from 'elysia';
|
||||||
|
import type { Holding, TaskStatus, WatchlistItem } from '@/lib/types';
|
||||||
|
import { ensureAuthSchema } from '@/lib/auth';
|
||||||
|
import { requireAuthenticatedSession } from '@/lib/server/auth-session';
|
||||||
|
import { asErrorMessage, jsonError } from '@/lib/server/http';
|
||||||
|
import { buildPortfolioSummary, recalculateHolding } from '@/lib/server/portfolio';
|
||||||
|
import { getStoreSnapshot, withStore } from '@/lib/server/store';
|
||||||
|
import { enqueueTask, getTaskById, listRecentTasks } from '@/lib/server/tasks';
|
||||||
|
|
||||||
|
const ALLOWED_STATUSES: TaskStatus[] = ['queued', 'running', 'completed', 'failed'];
|
||||||
|
|
||||||
|
function nowIso() {
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function asRecord(value: unknown): Record<string, unknown> {
|
||||||
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return value as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function asPositiveNumber(value: unknown) {
|
||||||
|
const parsed = typeof value === 'number' ? value : Number(value);
|
||||||
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAuthRequest(request: Request) {
|
||||||
|
const auth = await ensureAuthSchema();
|
||||||
|
return auth.handler(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const app = new Elysia({ prefix: '/api' })
|
||||||
|
.get('/auth', ({ request }) => handleAuthRequest(request))
|
||||||
|
.post('/auth', ({ request }) => handleAuthRequest(request))
|
||||||
|
.patch('/auth', ({ request }) => handleAuthRequest(request))
|
||||||
|
.put('/auth', ({ request }) => handleAuthRequest(request))
|
||||||
|
.delete('/auth', ({ request }) => handleAuthRequest(request))
|
||||||
|
.get('/auth/*', ({ request }) => handleAuthRequest(request))
|
||||||
|
.post('/auth/*', ({ request }) => handleAuthRequest(request))
|
||||||
|
.patch('/auth/*', ({ request }) => handleAuthRequest(request))
|
||||||
|
.put('/auth/*', ({ request }) => handleAuthRequest(request))
|
||||||
|
.delete('/auth/*', ({ request }) => handleAuthRequest(request))
|
||||||
|
.options('/auth', ({ request }) => handleAuthRequest(request))
|
||||||
|
.options('/auth/*', ({ request }) => handleAuthRequest(request))
|
||||||
|
.get('/health', async () => {
|
||||||
|
const snapshot = await getStoreSnapshot();
|
||||||
|
const queue = snapshot.tasks.reduce<Record<string, number>>((acc, task) => {
|
||||||
|
acc[task.status] = (acc[task.status] ?? 0) + 1;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
status: 'ok',
|
||||||
|
version: '3.0.0',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
queue
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.get('/me', async () => {
|
||||||
|
const { session, response } = await requireAuthenticatedSession();
|
||||||
|
if (response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
user: {
|
||||||
|
id: session.user.id,
|
||||||
|
email: session.user.email,
|
||||||
|
name: session.user.name,
|
||||||
|
image: session.user.image
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.get('/watchlist', async () => {
|
||||||
|
const { session, response } = await requireAuthenticatedSession();
|
||||||
|
if (response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapshot = await getStoreSnapshot();
|
||||||
|
const items = snapshot.watchlist
|
||||||
|
.filter((item) => item.user_id === session.user.id)
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at));
|
||||||
|
|
||||||
|
return Response.json({ items });
|
||||||
|
})
|
||||||
|
.post('/watchlist', async ({ body }) => {
|
||||||
|
const { session, response } = await requireAuthenticatedSession();
|
||||||
|
if (response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = asRecord(body);
|
||||||
|
const ticker = typeof payload.ticker === 'string' ? payload.ticker.trim().toUpperCase() : '';
|
||||||
|
const companyName = typeof payload.companyName === 'string' ? payload.companyName.trim() : '';
|
||||||
|
const sector = typeof payload.sector === 'string' ? payload.sector.trim() : '';
|
||||||
|
|
||||||
|
if (!ticker) {
|
||||||
|
return jsonError('ticker is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!companyName) {
|
||||||
|
return jsonError('companyName is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let item: WatchlistItem | null = null;
|
||||||
|
|
||||||
|
await withStore((store) => {
|
||||||
|
const existingIndex = store.watchlist.findIndex((entry) => {
|
||||||
|
return entry.user_id === session.user.id && entry.ticker === ticker;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
const existing = store.watchlist[existingIndex];
|
||||||
|
const updated: WatchlistItem = {
|
||||||
|
...existing,
|
||||||
|
company_name: companyName,
|
||||||
|
sector: sector || null
|
||||||
|
};
|
||||||
|
|
||||||
|
store.watchlist[existingIndex] = updated;
|
||||||
|
item = updated;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
store.counters.watchlist += 1;
|
||||||
|
const created: WatchlistItem = {
|
||||||
|
id: store.counters.watchlist,
|
||||||
|
user_id: session.user.id,
|
||||||
|
ticker,
|
||||||
|
company_name: companyName,
|
||||||
|
sector: sector || null,
|
||||||
|
created_at: nowIso()
|
||||||
|
};
|
||||||
|
|
||||||
|
store.watchlist.unshift(created);
|
||||||
|
item = created;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Response.json({ item });
|
||||||
|
} catch (error) {
|
||||||
|
return jsonError(asErrorMessage(error, 'Failed to create watchlist item'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.delete('/watchlist/:id', async ({ params }) => {
|
||||||
|
const { session, response } = await requireAuthenticatedSession();
|
||||||
|
if (response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numericId = Number(params.id);
|
||||||
|
if (!Number.isInteger(numericId) || numericId <= 0) {
|
||||||
|
return jsonError('Invalid watchlist id', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
let removed = false;
|
||||||
|
|
||||||
|
await withStore((store) => {
|
||||||
|
const next = store.watchlist.filter((item) => !(item.id === numericId && item.user_id === session.user.id));
|
||||||
|
removed = next.length !== store.watchlist.length;
|
||||||
|
store.watchlist = next;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!removed) {
|
||||||
|
return jsonError('Watchlist item not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({ success: true });
|
||||||
|
})
|
||||||
|
.get('/portfolio/holdings', async () => {
|
||||||
|
const { session, response } = await requireAuthenticatedSession();
|
||||||
|
if (response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapshot = await getStoreSnapshot();
|
||||||
|
const holdings = snapshot.holdings
|
||||||
|
.filter((holding) => holding.user_id === session.user.id)
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => Number(b.market_value) - Number(a.market_value));
|
||||||
|
|
||||||
|
return Response.json({ holdings });
|
||||||
|
})
|
||||||
|
.post('/portfolio/holdings', async ({ body }) => {
|
||||||
|
const { session, response } = await requireAuthenticatedSession();
|
||||||
|
if (response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = asRecord(body);
|
||||||
|
const ticker = typeof payload.ticker === 'string' ? payload.ticker.trim().toUpperCase() : '';
|
||||||
|
const shares = asPositiveNumber(payload.shares);
|
||||||
|
const avgCost = asPositiveNumber(payload.avgCost);
|
||||||
|
|
||||||
|
if (!ticker) {
|
||||||
|
return jsonError('ticker is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shares === null) {
|
||||||
|
return jsonError('shares must be a positive number');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avgCost === null) {
|
||||||
|
return jsonError('avgCost must be a positive number');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const now = nowIso();
|
||||||
|
let holding: Holding | null = null;
|
||||||
|
|
||||||
|
await withStore((store) => {
|
||||||
|
const existingIndex = store.holdings.findIndex((entry) => {
|
||||||
|
return entry.user_id === session.user.id && entry.ticker === ticker;
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentPrice = asPositiveNumber(payload.currentPrice) ?? avgCost;
|
||||||
|
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
const existing = store.holdings[existingIndex];
|
||||||
|
const updated = recalculateHolding({
|
||||||
|
...existing,
|
||||||
|
ticker,
|
||||||
|
shares: shares.toFixed(6),
|
||||||
|
avg_cost: avgCost.toFixed(6),
|
||||||
|
current_price: currentPrice.toFixed(6),
|
||||||
|
updated_at: now,
|
||||||
|
last_price_at: now
|
||||||
|
});
|
||||||
|
|
||||||
|
store.holdings[existingIndex] = updated;
|
||||||
|
holding = updated;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
store.counters.holdings += 1;
|
||||||
|
const created = recalculateHolding({
|
||||||
|
id: store.counters.holdings,
|
||||||
|
user_id: session.user.id,
|
||||||
|
ticker,
|
||||||
|
shares: shares.toFixed(6),
|
||||||
|
avg_cost: avgCost.toFixed(6),
|
||||||
|
current_price: currentPrice.toFixed(6),
|
||||||
|
market_value: '0',
|
||||||
|
gain_loss: '0',
|
||||||
|
gain_loss_pct: '0',
|
||||||
|
last_price_at: now,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now
|
||||||
|
});
|
||||||
|
|
||||||
|
store.holdings.unshift(created);
|
||||||
|
holding = created;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Response.json({ holding });
|
||||||
|
} catch (error) {
|
||||||
|
return jsonError(asErrorMessage(error, 'Failed to save holding'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.patch('/portfolio/holdings/:id', async ({ params, body }) => {
|
||||||
|
const { session, response } = await requireAuthenticatedSession();
|
||||||
|
if (response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numericId = Number(params.id);
|
||||||
|
if (!Number.isInteger(numericId) || numericId <= 0) {
|
||||||
|
return jsonError('Invalid holding id');
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = asRecord(body);
|
||||||
|
let found = false;
|
||||||
|
let updated: Holding | null = null;
|
||||||
|
|
||||||
|
await withStore((store) => {
|
||||||
|
const index = store.holdings.findIndex((entry) => {
|
||||||
|
return entry.id === numericId && entry.user_id === session.user.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (index < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
found = true;
|
||||||
|
const existing = store.holdings[index];
|
||||||
|
|
||||||
|
const shares = asPositiveNumber(payload.shares) ?? Number(existing.shares);
|
||||||
|
const avgCost = asPositiveNumber(payload.avgCost) ?? Number(existing.avg_cost);
|
||||||
|
const currentPrice = asPositiveNumber(payload.currentPrice) ?? Number(existing.current_price ?? existing.avg_cost);
|
||||||
|
|
||||||
|
const next = recalculateHolding({
|
||||||
|
...existing,
|
||||||
|
shares: shares.toFixed(6),
|
||||||
|
avg_cost: avgCost.toFixed(6),
|
||||||
|
current_price: currentPrice.toFixed(6),
|
||||||
|
updated_at: nowIso(),
|
||||||
|
last_price_at: nowIso()
|
||||||
|
});
|
||||||
|
|
||||||
|
store.holdings[index] = next;
|
||||||
|
updated = next;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
return jsonError('Holding not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({ holding: updated });
|
||||||
|
})
|
||||||
|
.delete('/portfolio/holdings/:id', async ({ params }) => {
|
||||||
|
const { session, response } = await requireAuthenticatedSession();
|
||||||
|
if (response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numericId = Number(params.id);
|
||||||
|
if (!Number.isInteger(numericId) || numericId <= 0) {
|
||||||
|
return jsonError('Invalid holding id');
|
||||||
|
}
|
||||||
|
|
||||||
|
let removed = false;
|
||||||
|
|
||||||
|
await withStore((store) => {
|
||||||
|
const next = store.holdings.filter((holding) => {
|
||||||
|
return !(holding.id === numericId && holding.user_id === session.user.id);
|
||||||
|
});
|
||||||
|
removed = next.length !== store.holdings.length;
|
||||||
|
store.holdings = next;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!removed) {
|
||||||
|
return jsonError('Holding not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({ success: true });
|
||||||
|
})
|
||||||
|
.get('/portfolio/summary', async () => {
|
||||||
|
const { session, response } = await requireAuthenticatedSession();
|
||||||
|
if (response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapshot = await getStoreSnapshot();
|
||||||
|
const summary = buildPortfolioSummary(
|
||||||
|
snapshot.holdings.filter((holding) => holding.user_id === session.user.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
return Response.json({ summary });
|
||||||
|
})
|
||||||
|
.post('/portfolio/refresh-prices', async () => {
|
||||||
|
const { session, response } = await requireAuthenticatedSession();
|
||||||
|
if (response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const task = await enqueueTask({
|
||||||
|
userId: session.user.id,
|
||||||
|
taskType: 'refresh_prices',
|
||||||
|
payload: {},
|
||||||
|
priority: 80
|
||||||
|
});
|
||||||
|
|
||||||
|
return Response.json({ task });
|
||||||
|
} catch (error) {
|
||||||
|
return jsonError(asErrorMessage(error, 'Failed to queue refresh task'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.post('/portfolio/insights/generate', async () => {
|
||||||
|
const { session, response } = await requireAuthenticatedSession();
|
||||||
|
if (response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const task = await enqueueTask({
|
||||||
|
userId: session.user.id,
|
||||||
|
taskType: 'portfolio_insights',
|
||||||
|
payload: {},
|
||||||
|
priority: 70
|
||||||
|
});
|
||||||
|
|
||||||
|
return Response.json({ task });
|
||||||
|
} catch (error) {
|
||||||
|
return jsonError(asErrorMessage(error, 'Failed to queue insights task'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get('/portfolio/insights/latest', async () => {
|
||||||
|
const { session, response } = await requireAuthenticatedSession();
|
||||||
|
if (response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapshot = await getStoreSnapshot();
|
||||||
|
const insight = snapshot.insights
|
||||||
|
.filter((entry) => entry.user_id === session.user.id)
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at))[0] ?? null;
|
||||||
|
|
||||||
|
return Response.json({ insight });
|
||||||
|
})
|
||||||
|
.get('/filings', async ({ request }) => {
|
||||||
|
const { response } = await requireAuthenticatedSession();
|
||||||
|
if (response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const tickerFilter = url.searchParams.get('ticker')?.trim().toUpperCase();
|
||||||
|
const limitValue = Number(url.searchParams.get('limit') ?? 50);
|
||||||
|
const limit = Number.isFinite(limitValue)
|
||||||
|
? Math.min(Math.max(Math.trunc(limitValue), 1), 250)
|
||||||
|
: 50;
|
||||||
|
|
||||||
|
const snapshot = await getStoreSnapshot();
|
||||||
|
const filtered = tickerFilter
|
||||||
|
? snapshot.filings.filter((filing) => filing.ticker === tickerFilter)
|
||||||
|
: snapshot.filings;
|
||||||
|
|
||||||
|
const filings = filtered
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => Date.parse(b.filing_date) - Date.parse(a.filing_date))
|
||||||
|
.slice(0, limit);
|
||||||
|
|
||||||
|
return Response.json({ filings });
|
||||||
|
})
|
||||||
|
.post('/filings/sync', async ({ body }) => {
|
||||||
|
const { session, response } = await requireAuthenticatedSession();
|
||||||
|
if (response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = asRecord(body);
|
||||||
|
const ticker = typeof payload.ticker === 'string' ? payload.ticker.trim().toUpperCase() : '';
|
||||||
|
|
||||||
|
if (!ticker) {
|
||||||
|
return jsonError('ticker is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const limit = typeof payload.limit === 'number' ? payload.limit : Number(payload.limit);
|
||||||
|
const task = await enqueueTask({
|
||||||
|
userId: session.user.id,
|
||||||
|
taskType: 'sync_filings',
|
||||||
|
payload: {
|
||||||
|
ticker,
|
||||||
|
limit: Number.isFinite(limit) ? limit : 20
|
||||||
|
},
|
||||||
|
priority: 90
|
||||||
|
});
|
||||||
|
|
||||||
|
return Response.json({ task });
|
||||||
|
} catch (error) {
|
||||||
|
return jsonError(asErrorMessage(error, 'Failed to queue filings sync task'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.post('/filings/:accessionNumber/analyze', async ({ params }) => {
|
||||||
|
const { session, response } = await requireAuthenticatedSession();
|
||||||
|
if (response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessionNumber = params.accessionNumber?.trim() ?? '';
|
||||||
|
if (accessionNumber.length < 4) {
|
||||||
|
return jsonError('Invalid accession number');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const task = await enqueueTask({
|
||||||
|
userId: session.user.id,
|
||||||
|
taskType: 'analyze_filing',
|
||||||
|
payload: { accessionNumber },
|
||||||
|
priority: 65
|
||||||
|
});
|
||||||
|
|
||||||
|
return Response.json({ task });
|
||||||
|
} catch (error) {
|
||||||
|
return jsonError(asErrorMessage(error, 'Failed to queue filing analysis task'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get('/tasks', async ({ request }) => {
|
||||||
|
const { session, response } = await requireAuthenticatedSession();
|
||||||
|
if (response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const limitValue = Number(url.searchParams.get('limit') ?? 20);
|
||||||
|
const limit = Number.isFinite(limitValue)
|
||||||
|
? Math.min(Math.max(Math.trunc(limitValue), 1), 200)
|
||||||
|
: 20;
|
||||||
|
|
||||||
|
const rawStatuses = url.searchParams.getAll('status');
|
||||||
|
const statuses = rawStatuses.filter((status): status is TaskStatus => {
|
||||||
|
return ALLOWED_STATUSES.includes(status as TaskStatus);
|
||||||
|
});
|
||||||
|
|
||||||
|
const tasks = await listRecentTasks(
|
||||||
|
session.user.id,
|
||||||
|
limit,
|
||||||
|
statuses.length > 0 ? statuses : undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
return Response.json({ tasks });
|
||||||
|
})
|
||||||
|
.get('/tasks/:taskId', async ({ params }) => {
|
||||||
|
const { session, response } = await requireAuthenticatedSession();
|
||||||
|
if (response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const task = await getTaskById(params.taskId, session.user.id);
|
||||||
|
if (!task) {
|
||||||
|
return jsonError('Task not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({ task });
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GET = app.fetch;
|
||||||
|
export const POST = app.fetch;
|
||||||
|
export const PATCH = app.fetch;
|
||||||
|
export const PUT = app.fetch;
|
||||||
|
export const DELETE = app.fetch;
|
||||||
|
export const OPTIONS = app.fetch;
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { toNextJsHandler } from 'better-auth/next-js';
|
|
||||||
import { ensureAuthSchema } from '@/lib/auth';
|
|
||||||
|
|
||||||
const authHandler = toNextJsHandler(async (request: Request) => {
|
|
||||||
const auth = await ensureAuthSchema();
|
|
||||||
return auth.handler(request);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { GET, POST, PATCH, PUT, DELETE } = authHandler;
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { asErrorMessage, jsonError } from '@/lib/server/http';
|
|
||||||
import { requireAuthenticatedSession } from '@/lib/server/auth-session';
|
|
||||||
import { enqueueTask } from '@/lib/server/tasks';
|
|
||||||
|
|
||||||
type Context = {
|
|
||||||
params: Promise<{ accessionNumber: string }>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function POST(_request: Request, context: Context) {
|
|
||||||
const { session, response } = await requireAuthenticatedSession();
|
|
||||||
if (response) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { accessionNumber } = await context.params;
|
|
||||||
|
|
||||||
if (!accessionNumber || accessionNumber.trim().length < 4) {
|
|
||||||
return jsonError('Invalid accession number');
|
|
||||||
}
|
|
||||||
|
|
||||||
const task = await enqueueTask({
|
|
||||||
userId: session.user.id,
|
|
||||||
taskType: 'analyze_filing',
|
|
||||||
payload: { accessionNumber: accessionNumber.trim() },
|
|
||||||
priority: 65
|
|
||||||
});
|
|
||||||
|
|
||||||
return Response.json({ task });
|
|
||||||
} catch (error) {
|
|
||||||
return jsonError(asErrorMessage(error, 'Failed to queue filing analysis task'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { requireAuthenticatedSession } from '@/lib/server/auth-session';
|
|
||||||
import { getStoreSnapshot } from '@/lib/server/store';
|
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
|
||||||
const { response } = await requireAuthenticatedSession();
|
|
||||||
if (response) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = new URL(request.url);
|
|
||||||
const tickerFilter = url.searchParams.get('ticker')?.trim().toUpperCase();
|
|
||||||
const limitValue = Number(url.searchParams.get('limit') ?? 50);
|
|
||||||
const limit = Number.isFinite(limitValue)
|
|
||||||
? Math.min(Math.max(Math.trunc(limitValue), 1), 250)
|
|
||||||
: 50;
|
|
||||||
|
|
||||||
const snapshot = await getStoreSnapshot();
|
|
||||||
const filtered = tickerFilter
|
|
||||||
? snapshot.filings.filter((filing) => filing.ticker === tickerFilter)
|
|
||||||
: snapshot.filings;
|
|
||||||
|
|
||||||
const filings = filtered
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) => Date.parse(b.filing_date) - Date.parse(a.filing_date))
|
|
||||||
.slice(0, limit);
|
|
||||||
|
|
||||||
return Response.json({ filings });
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { asErrorMessage, jsonError } from '@/lib/server/http';
|
|
||||||
import { requireAuthenticatedSession } from '@/lib/server/auth-session';
|
|
||||||
import { enqueueTask } from '@/lib/server/tasks';
|
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
|
||||||
const { session, response } = await requireAuthenticatedSession();
|
|
||||||
if (response) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const payload = await request.json() as {
|
|
||||||
ticker?: string;
|
|
||||||
limit?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!payload.ticker || payload.ticker.trim().length < 1) {
|
|
||||||
return jsonError('ticker is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
const task = await enqueueTask({
|
|
||||||
userId: session.user.id,
|
|
||||||
taskType: 'sync_filings',
|
|
||||||
payload: {
|
|
||||||
ticker: payload.ticker.trim().toUpperCase(),
|
|
||||||
limit: payload.limit ?? 20
|
|
||||||
},
|
|
||||||
priority: 90
|
|
||||||
});
|
|
||||||
|
|
||||||
return Response.json({ task });
|
|
||||||
} catch (error) {
|
|
||||||
return jsonError(asErrorMessage(error, 'Failed to queue filings sync task'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { getStoreSnapshot } from '@/lib/server/store';
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
const snapshot = await getStoreSnapshot();
|
|
||||||
const queue = snapshot.tasks.reduce<Record<string, number>>((acc, task) => {
|
|
||||||
acc[task.status] = (acc[task.status] ?? 0) + 1;
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return Response.json({
|
|
||||||
status: 'ok',
|
|
||||||
version: '3.0.0',
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
queue
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { requireAuthenticatedSession } from '@/lib/server/auth-session';
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
const { session, response } = await requireAuthenticatedSession();
|
|
||||||
if (response) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response.json({
|
|
||||||
user: {
|
|
||||||
id: session.user.id,
|
|
||||||
email: session.user.email,
|
|
||||||
name: session.user.name,
|
|
||||||
image: session.user.image
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import { jsonError } from '@/lib/server/http';
|
|
||||||
import { requireAuthenticatedSession } from '@/lib/server/auth-session';
|
|
||||||
import { recalculateHolding } from '@/lib/server/portfolio';
|
|
||||||
import { withStore } from '@/lib/server/store';
|
|
||||||
|
|
||||||
type Context = {
|
|
||||||
params: Promise<{ id: string }>;
|
|
||||||
};
|
|
||||||
|
|
||||||
function nowIso() {
|
|
||||||
return new Date().toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function asPositiveNumber(value: unknown) {
|
|
||||||
const parsed = typeof value === 'number' ? value : Number(value);
|
|
||||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function PATCH(request: Request, context: Context) {
|
|
||||||
const { session, response } = await requireAuthenticatedSession();
|
|
||||||
if (response) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = session.user.id;
|
|
||||||
const { id } = await context.params;
|
|
||||||
const numericId = Number(id);
|
|
||||||
|
|
||||||
if (!Number.isInteger(numericId) || numericId <= 0) {
|
|
||||||
return jsonError('Invalid holding id');
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = await request.json() as {
|
|
||||||
shares?: number;
|
|
||||||
avgCost?: number;
|
|
||||||
currentPrice?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
let found = false;
|
|
||||||
let updated: unknown = null;
|
|
||||||
|
|
||||||
await withStore((store) => {
|
|
||||||
const index = store.holdings.findIndex((entry) => entry.id === numericId && entry.user_id === userId);
|
|
||||||
if (index < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
found = true;
|
|
||||||
const existing = store.holdings[index];
|
|
||||||
|
|
||||||
const shares = asPositiveNumber(payload.shares) ?? Number(existing.shares);
|
|
||||||
const avgCost = asPositiveNumber(payload.avgCost) ?? Number(existing.avg_cost);
|
|
||||||
const currentPrice = asPositiveNumber(payload.currentPrice) ?? Number(existing.current_price ?? existing.avg_cost);
|
|
||||||
|
|
||||||
const next = recalculateHolding({
|
|
||||||
...existing,
|
|
||||||
shares: shares.toFixed(6),
|
|
||||||
avg_cost: avgCost.toFixed(6),
|
|
||||||
current_price: currentPrice.toFixed(6),
|
|
||||||
updated_at: nowIso(),
|
|
||||||
last_price_at: nowIso()
|
|
||||||
});
|
|
||||||
|
|
||||||
store.holdings[index] = next;
|
|
||||||
updated = next;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
return jsonError('Holding not found', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response.json({ holding: updated });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function DELETE(_request: Request, context: Context) {
|
|
||||||
const { session, response } = await requireAuthenticatedSession();
|
|
||||||
if (response) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = session.user.id;
|
|
||||||
const { id } = await context.params;
|
|
||||||
const numericId = Number(id);
|
|
||||||
|
|
||||||
if (!Number.isInteger(numericId) || numericId <= 0) {
|
|
||||||
return jsonError('Invalid holding id');
|
|
||||||
}
|
|
||||||
|
|
||||||
let removed = false;
|
|
||||||
|
|
||||||
await withStore((store) => {
|
|
||||||
const next = store.holdings.filter((holding) => !(holding.id === numericId && holding.user_id === userId));
|
|
||||||
removed = next.length !== store.holdings.length;
|
|
||||||
store.holdings = next;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!removed) {
|
|
||||||
return jsonError('Holding not found', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response.json({ success: true });
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import type { Holding } from '@/lib/types';
|
|
||||||
import { asErrorMessage, jsonError } from '@/lib/server/http';
|
|
||||||
import { requireAuthenticatedSession } from '@/lib/server/auth-session';
|
|
||||||
import { recalculateHolding } from '@/lib/server/portfolio';
|
|
||||||
import { getStoreSnapshot, withStore } from '@/lib/server/store';
|
|
||||||
|
|
||||||
function nowIso() {
|
|
||||||
return new Date().toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function asPositiveNumber(value: unknown) {
|
|
||||||
const parsed = typeof value === 'number' ? value : Number(value);
|
|
||||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
const { session, response } = await requireAuthenticatedSession();
|
|
||||||
if (response) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = session.user.id;
|
|
||||||
const snapshot = await getStoreSnapshot();
|
|
||||||
const holdings = snapshot.holdings
|
|
||||||
.filter((holding) => holding.user_id === userId)
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) => Number(b.market_value) - Number(a.market_value));
|
|
||||||
|
|
||||||
return Response.json({ holdings });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
|
||||||
const { session, response } = await requireAuthenticatedSession();
|
|
||||||
if (response) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = session.user.id;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const payload = await request.json() as {
|
|
||||||
ticker?: string;
|
|
||||||
shares?: number;
|
|
||||||
avgCost?: number;
|
|
||||||
currentPrice?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!payload.ticker || payload.ticker.trim().length < 1) {
|
|
||||||
return jsonError('ticker is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
const shares = asPositiveNumber(payload.shares);
|
|
||||||
const avgCost = asPositiveNumber(payload.avgCost);
|
|
||||||
|
|
||||||
if (shares === null) {
|
|
||||||
return jsonError('shares must be a positive number');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (avgCost === null) {
|
|
||||||
return jsonError('avgCost must be a positive number');
|
|
||||||
}
|
|
||||||
|
|
||||||
const ticker = payload.ticker.trim().toUpperCase();
|
|
||||||
const now = nowIso();
|
|
||||||
let holding: Holding | null = null;
|
|
||||||
|
|
||||||
await withStore((store) => {
|
|
||||||
const existingIndex = store.holdings.findIndex((entry) => entry.user_id === userId && entry.ticker === ticker);
|
|
||||||
const currentPrice = asPositiveNumber(payload.currentPrice) ?? avgCost;
|
|
||||||
|
|
||||||
if (existingIndex >= 0) {
|
|
||||||
const existing = store.holdings[existingIndex];
|
|
||||||
const updated = recalculateHolding({
|
|
||||||
...existing,
|
|
||||||
ticker,
|
|
||||||
shares: shares.toFixed(6),
|
|
||||||
avg_cost: avgCost.toFixed(6),
|
|
||||||
current_price: currentPrice.toFixed(6),
|
|
||||||
updated_at: now,
|
|
||||||
last_price_at: now
|
|
||||||
});
|
|
||||||
|
|
||||||
store.holdings[existingIndex] = updated;
|
|
||||||
holding = updated;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
store.counters.holdings += 1;
|
|
||||||
const created = recalculateHolding({
|
|
||||||
id: store.counters.holdings,
|
|
||||||
user_id: userId,
|
|
||||||
ticker,
|
|
||||||
shares: shares.toFixed(6),
|
|
||||||
avg_cost: avgCost.toFixed(6),
|
|
||||||
current_price: currentPrice.toFixed(6),
|
|
||||||
market_value: '0',
|
|
||||||
gain_loss: '0',
|
|
||||||
gain_loss_pct: '0',
|
|
||||||
last_price_at: now,
|
|
||||||
created_at: now,
|
|
||||||
updated_at: now
|
|
||||||
});
|
|
||||||
|
|
||||||
store.holdings.unshift(created);
|
|
||||||
holding = created;
|
|
||||||
});
|
|
||||||
|
|
||||||
return Response.json({ holding });
|
|
||||||
} catch (error) {
|
|
||||||
return jsonError(asErrorMessage(error, 'Failed to save holding'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { asErrorMessage, jsonError } from '@/lib/server/http';
|
|
||||||
import { requireAuthenticatedSession } from '@/lib/server/auth-session';
|
|
||||||
import { enqueueTask } from '@/lib/server/tasks';
|
|
||||||
|
|
||||||
export async function POST() {
|
|
||||||
const { session, response } = await requireAuthenticatedSession();
|
|
||||||
if (response) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const task = await enqueueTask({
|
|
||||||
userId: session.user.id,
|
|
||||||
taskType: 'portfolio_insights',
|
|
||||||
payload: {},
|
|
||||||
priority: 70
|
|
||||||
});
|
|
||||||
|
|
||||||
return Response.json({ task });
|
|
||||||
} catch (error) {
|
|
||||||
return jsonError(asErrorMessage(error, 'Failed to queue insights task'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { requireAuthenticatedSession } from '@/lib/server/auth-session';
|
|
||||||
import { getStoreSnapshot } from '@/lib/server/store';
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
const { session, response } = await requireAuthenticatedSession();
|
|
||||||
if (response) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = session.user.id;
|
|
||||||
const snapshot = await getStoreSnapshot();
|
|
||||||
const insight = snapshot.insights
|
|
||||||
.filter((entry) => entry.user_id === userId)
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at))[0] ?? null;
|
|
||||||
|
|
||||||
return Response.json({ insight });
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { asErrorMessage, jsonError } from '@/lib/server/http';
|
|
||||||
import { requireAuthenticatedSession } from '@/lib/server/auth-session';
|
|
||||||
import { enqueueTask } from '@/lib/server/tasks';
|
|
||||||
|
|
||||||
export async function POST() {
|
|
||||||
const { session, response } = await requireAuthenticatedSession();
|
|
||||||
if (response) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const task = await enqueueTask({
|
|
||||||
userId: session.user.id,
|
|
||||||
taskType: 'refresh_prices',
|
|
||||||
payload: {},
|
|
||||||
priority: 80
|
|
||||||
});
|
|
||||||
|
|
||||||
return Response.json({ task });
|
|
||||||
} catch (error) {
|
|
||||||
return jsonError(asErrorMessage(error, 'Failed to queue refresh task'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { buildPortfolioSummary } from '@/lib/server/portfolio';
|
|
||||||
import { requireAuthenticatedSession } from '@/lib/server/auth-session';
|
|
||||||
import { getStoreSnapshot } from '@/lib/server/store';
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
const { session, response } = await requireAuthenticatedSession();
|
|
||||||
if (response) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = session.user.id;
|
|
||||||
const snapshot = await getStoreSnapshot();
|
|
||||||
const summary = buildPortfolioSummary(snapshot.holdings.filter((holding) => holding.user_id === userId));
|
|
||||||
return Response.json({ summary });
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { jsonError } from '@/lib/server/http';
|
|
||||||
import { requireAuthenticatedSession } from '@/lib/server/auth-session';
|
|
||||||
import { getTaskById } from '@/lib/server/tasks';
|
|
||||||
|
|
||||||
type Context = {
|
|
||||||
params: Promise<{ taskId: string }>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function GET(_request: Request, context: Context) {
|
|
||||||
const { session, response } = await requireAuthenticatedSession();
|
|
||||||
if (response) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { taskId } = await context.params;
|
|
||||||
const task = await getTaskById(taskId, session.user.id);
|
|
||||||
|
|
||||||
if (!task) {
|
|
||||||
return jsonError('Task not found', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response.json({ task });
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import type { TaskStatus } from '@/lib/types';
|
|
||||||
import { requireAuthenticatedSession } from '@/lib/server/auth-session';
|
|
||||||
import { listRecentTasks } from '@/lib/server/tasks';
|
|
||||||
|
|
||||||
const ALLOWED_STATUSES: TaskStatus[] = ['queued', 'running', 'completed', 'failed'];
|
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
|
||||||
const { session, response } = await requireAuthenticatedSession();
|
|
||||||
if (response) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = new URL(request.url);
|
|
||||||
const limitValue = Number(url.searchParams.get('limit') ?? 20);
|
|
||||||
const limit = Number.isFinite(limitValue)
|
|
||||||
? Math.min(Math.max(Math.trunc(limitValue), 1), 200)
|
|
||||||
: 20;
|
|
||||||
|
|
||||||
const rawStatuses = url.searchParams.getAll('status');
|
|
||||||
const statuses = rawStatuses.filter((status): status is TaskStatus => {
|
|
||||||
return ALLOWED_STATUSES.includes(status as TaskStatus);
|
|
||||||
});
|
|
||||||
|
|
||||||
const tasks = await listRecentTasks(session.user.id, limit, statuses.length > 0 ? statuses : undefined);
|
|
||||||
return Response.json({ tasks });
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { jsonError } from '@/lib/server/http';
|
|
||||||
import { requireAuthenticatedSession } from '@/lib/server/auth-session';
|
|
||||||
import { withStore } from '@/lib/server/store';
|
|
||||||
|
|
||||||
type Context = {
|
|
||||||
params: Promise<{ id: string }>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function DELETE(_request: Request, context: Context) {
|
|
||||||
const { session, response } = await requireAuthenticatedSession();
|
|
||||||
if (response) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = session.user.id;
|
|
||||||
const { id } = await context.params;
|
|
||||||
const numericId = Number(id);
|
|
||||||
|
|
||||||
if (!Number.isInteger(numericId) || numericId <= 0) {
|
|
||||||
return jsonError('Invalid watchlist id', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
let removed = false;
|
|
||||||
|
|
||||||
await withStore((store) => {
|
|
||||||
const next = store.watchlist.filter((item) => !(item.id === numericId && item.user_id === userId));
|
|
||||||
removed = next.length !== store.watchlist.length;
|
|
||||||
store.watchlist = next;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!removed) {
|
|
||||||
return jsonError('Watchlist item not found', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response.json({ success: true });
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import type { WatchlistItem } from '@/lib/types';
|
|
||||||
import { asErrorMessage, jsonError } from '@/lib/server/http';
|
|
||||||
import { requireAuthenticatedSession } from '@/lib/server/auth-session';
|
|
||||||
import { getStoreSnapshot, withStore } from '@/lib/server/store';
|
|
||||||
|
|
||||||
function nowIso() {
|
|
||||||
return new Date().toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
const { session, response } = await requireAuthenticatedSession();
|
|
||||||
if (response) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = session.user.id;
|
|
||||||
const snapshot = await getStoreSnapshot();
|
|
||||||
const items = snapshot.watchlist
|
|
||||||
.filter((item) => item.user_id === userId)
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at));
|
|
||||||
|
|
||||||
return Response.json({ items });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
|
||||||
const { session, response } = await requireAuthenticatedSession();
|
|
||||||
if (response) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = session.user.id;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const payload = await request.json() as {
|
|
||||||
ticker?: string;
|
|
||||||
companyName?: string;
|
|
||||||
sector?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!payload.ticker || payload.ticker.trim().length < 1) {
|
|
||||||
return jsonError('ticker is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!payload.companyName || payload.companyName.trim().length < 1) {
|
|
||||||
return jsonError('companyName is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
let item: WatchlistItem | null = null;
|
|
||||||
|
|
||||||
await withStore((store) => {
|
|
||||||
const ticker = payload.ticker!.trim().toUpperCase();
|
|
||||||
const existingIndex = store.watchlist.findIndex((entry) => entry.user_id === userId && entry.ticker === ticker);
|
|
||||||
|
|
||||||
if (existingIndex >= 0) {
|
|
||||||
const existing = store.watchlist[existingIndex];
|
|
||||||
const updated: WatchlistItem = {
|
|
||||||
...existing,
|
|
||||||
company_name: payload.companyName!.trim(),
|
|
||||||
sector: payload.sector?.trim() || null
|
|
||||||
};
|
|
||||||
|
|
||||||
store.watchlist[existingIndex] = updated;
|
|
||||||
item = updated;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
store.counters.watchlist += 1;
|
|
||||||
const created: WatchlistItem = {
|
|
||||||
id: store.counters.watchlist,
|
|
||||||
user_id: userId,
|
|
||||||
ticker,
|
|
||||||
company_name: payload.companyName!.trim(),
|
|
||||||
sector: payload.sector?.trim() || null,
|
|
||||||
created_at: nowIso()
|
|
||||||
};
|
|
||||||
|
|
||||||
store.watchlist.unshift(created);
|
|
||||||
item = created;
|
|
||||||
});
|
|
||||||
|
|
||||||
return Response.json({ item });
|
|
||||||
} catch (error) {
|
|
||||||
return jsonError(asErrorMessage(error, 'Failed to create watchlist item'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
423
bun.lock
Normal file
423
bun.lock
Normal file
@@ -0,0 +1,423 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 0,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "fiscal-frontend",
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.2.1",
|
||||||
|
"better-auth": "^1.4.19",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"elysia": "latest",
|
||||||
|
"lucide-react": "^0.575.0",
|
||||||
|
"next": "^16.1.6",
|
||||||
|
"pg": "^8.18.0",
|
||||||
|
"react": "^19.2.4",
|
||||||
|
"react-dom": "^19.2.4",
|
||||||
|
"recharts": "^3.7.0",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.3.0",
|
||||||
|
"@types/pg": "^8.16.0",
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"autoprefixer": "^10.4.24",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"tailwindcss": "^4.2.1",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||||
|
|
||||||
|
"@better-auth/core": ["@better-auth/core@1.4.19", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.3.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.8", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-uADLHG1jc5BnEJi7f6ijUN5DmPPRSj++7m/G19z3UqA3MVCo4Y4t1MMa4IIxLCqGDFv22drdfxescgW+HnIowA=="],
|
||||||
|
|
||||||
|
"@better-auth/telemetry": ["@better-auth/telemetry@1.4.19", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.19" } }, "sha512-ApGNS7olCTtDpKF8Ow3Z+jvFAirOj7c4RyFUpu8axklh3mH57ndpfUAUjhgA8UVoaaH/mnm/Tl884BlqiewLyw=="],
|
||||||
|
|
||||||
|
"@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="],
|
||||||
|
|
||||||
|
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="],
|
||||||
|
|
||||||
|
"@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="],
|
||||||
|
|
||||||
|
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||||
|
|
||||||
|
"@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
|
||||||
|
|
||||||
|
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
||||||
|
|
||||||
|
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
|
||||||
|
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
|
||||||
|
|
||||||
|
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
|
||||||
|
|
||||||
|
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
|
||||||
|
|
||||||
|
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
|
||||||
|
|
||||||
|
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
|
||||||
|
|
||||||
|
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
|
||||||
|
|
||||||
|
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
|
||||||
|
|
||||||
|
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
|
||||||
|
|
||||||
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
|
|
||||||
|
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||||
|
|
||||||
|
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||||
|
|
||||||
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@next/env": ["@next/env@16.1.6", "", {}, "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ=="],
|
||||||
|
|
||||||
|
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw=="],
|
||||||
|
|
||||||
|
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.1.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ=="],
|
||||||
|
|
||||||
|
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw=="],
|
||||||
|
|
||||||
|
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ=="],
|
||||||
|
|
||||||
|
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ=="],
|
||||||
|
|
||||||
|
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg=="],
|
||||||
|
|
||||||
|
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.1.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw=="],
|
||||||
|
|
||||||
|
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A=="],
|
||||||
|
|
||||||
|
"@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="],
|
||||||
|
|
||||||
|
"@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
|
||||||
|
|
||||||
|
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" } }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="],
|
||||||
|
|
||||||
|
"@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="],
|
||||||
|
|
||||||
|
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
|
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
|
||||||
|
|
||||||
|
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
||||||
|
|
||||||
|
"@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.1", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "postcss": "^8.5.6", "tailwindcss": "4.2.1" } }, "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw=="],
|
||||||
|
|
||||||
|
"@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
|
||||||
|
|
||||||
|
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||||
|
|
||||||
|
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
|
||||||
|
|
||||||
|
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
|
||||||
|
|
||||||
|
"@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
|
||||||
|
|
||||||
|
"@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
|
||||||
|
|
||||||
|
"@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="],
|
||||||
|
|
||||||
|
"@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
|
||||||
|
|
||||||
|
"@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="],
|
||||||
|
|
||||||
|
"@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
|
||||||
|
|
||||||
|
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
|
||||||
|
|
||||||
|
"@types/pg": ["@types/pg@8.16.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ=="],
|
||||||
|
|
||||||
|
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
||||||
|
|
||||||
|
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
|
|
||||||
|
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
|
||||||
|
|
||||||
|
"autoprefixer": ["autoprefixer@10.4.24", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001766", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": "bin/autoprefixer" }, "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw=="],
|
||||||
|
|
||||||
|
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": "dist/cli.cjs" }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="],
|
||||||
|
|
||||||
|
"better-auth": ["better-auth@1.4.19", "", { "dependencies": { "@better-auth/core": "1.4.19", "@better-auth/telemetry": "1.4.19", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "prisma", "solid-js", "svelte", "vitest", "vue"] }, "sha512-3RlZJcA0+NH25wYD85vpIGwW9oSTuEmLIaGbT8zg41w/Pa2hVWHKedjoUHHJtnzkBXzDb+CShkLnSw7IThDdqQ=="],
|
||||||
|
|
||||||
|
"better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="],
|
||||||
|
|
||||||
|
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": "cli.js" }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
||||||
|
|
||||||
|
"caniuse-lite": ["caniuse-lite@1.0.30001770", "", {}, "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw=="],
|
||||||
|
|
||||||
|
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
||||||
|
|
||||||
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
|
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||||
|
|
||||||
|
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
|
||||||
|
|
||||||
|
"d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
|
||||||
|
|
||||||
|
"d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
|
||||||
|
|
||||||
|
"d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="],
|
||||||
|
|
||||||
|
"d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
|
||||||
|
|
||||||
|
"d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
|
||||||
|
|
||||||
|
"d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
|
||||||
|
|
||||||
|
"d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
|
||||||
|
|
||||||
|
"d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
|
||||||
|
|
||||||
|
"d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
|
||||||
|
|
||||||
|
"d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
|
||||||
|
|
||||||
|
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
|
||||||
|
|
||||||
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
|
||||||
|
|
||||||
|
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||||
|
|
||||||
|
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
|
"electron-to-chromium": ["electron-to-chromium@1.5.302", "", {}, "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg=="],
|
||||||
|
|
||||||
|
"elysia": ["elysia@1.4.25", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-liKjavH99Gpzrv9cDil6uYWmPuqESfPFV1FIaFSd3iNqo3y7e29sN43VxFIK8tWWnyi6eDAmi2SZk8hNAMQMyg=="],
|
||||||
|
|
||||||
|
"enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="],
|
||||||
|
|
||||||
|
"es-toolkit": ["es-toolkit@1.44.0", "", {}, "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg=="],
|
||||||
|
|
||||||
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
|
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
|
||||||
|
|
||||||
|
"exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="],
|
||||||
|
|
||||||
|
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
||||||
|
|
||||||
|
"file-type": ["file-type@21.3.0", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA=="],
|
||||||
|
|
||||||
|
"fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
|
||||||
|
|
||||||
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
|
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||||
|
|
||||||
|
"immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="],
|
||||||
|
|
||||||
|
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||||
|
|
||||||
|
"jiti": ["jiti@2.6.1", "", { "bin": "lib/jiti-cli.mjs" }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||||
|
|
||||||
|
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
|
||||||
|
|
||||||
|
"kysely": ["kysely@0.28.11", "", {}, "sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg=="],
|
||||||
|
|
||||||
|
"lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
|
||||||
|
|
||||||
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
|
||||||
|
|
||||||
|
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
|
||||||
|
|
||||||
|
"lucide-react": ["lucide-react@0.575.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg=="],
|
||||||
|
|
||||||
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|
||||||
|
"memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
|
||||||
|
|
||||||
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
"nanostores": ["nanostores@1.1.0", "", {}, "sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA=="],
|
||||||
|
|
||||||
|
"next": ["next@16.1.6", "", { "dependencies": { "@next/env": "16.1.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.6", "@next/swc-darwin-x64": "16.1.6", "@next/swc-linux-arm64-gnu": "16.1.6", "@next/swc-linux-arm64-musl": "16.1.6", "@next/swc-linux-x64-gnu": "16.1.6", "@next/swc-linux-x64-musl": "16.1.6", "@next/swc-win32-arm64-msvc": "16.1.6", "@next/swc-win32-x64-msvc": "16.1.6", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": "dist/bin/next" }, "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw=="],
|
||||||
|
|
||||||
|
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
||||||
|
|
||||||
|
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
||||||
|
|
||||||
|
"pg": ["pg@8.18.0", "", { "dependencies": { "pg-connection-string": "^2.11.0", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ=="],
|
||||||
|
|
||||||
|
"pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="],
|
||||||
|
|
||||||
|
"pg-connection-string": ["pg-connection-string@2.11.0", "", {}, "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ=="],
|
||||||
|
|
||||||
|
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
|
||||||
|
|
||||||
|
"pg-pool": ["pg-pool@3.11.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w=="],
|
||||||
|
|
||||||
|
"pg-protocol": ["pg-protocol@1.11.0", "", {}, "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g=="],
|
||||||
|
|
||||||
|
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
|
||||||
|
|
||||||
|
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||||
|
|
||||||
|
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||||
|
|
||||||
|
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
||||||
|
|
||||||
|
"postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
|
||||||
|
|
||||||
|
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
|
||||||
|
|
||||||
|
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
||||||
|
|
||||||
|
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
|
||||||
|
|
||||||
|
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
|
||||||
|
|
||||||
|
"react-is": ["react-is@19.2.4", "", {}, "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA=="],
|
||||||
|
|
||||||
|
"react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" } }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="],
|
||||||
|
|
||||||
|
"recharts": ["recharts@3.7.0", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew=="],
|
||||||
|
|
||||||
|
"redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="],
|
||||||
|
|
||||||
|
"redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="],
|
||||||
|
|
||||||
|
"reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="],
|
||||||
|
|
||||||
|
"rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
|
||||||
|
|
||||||
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
|
"semver": ["semver@7.7.4", "", { "bin": "bin/semver.js" }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||||
|
|
||||||
|
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
|
||||||
|
|
||||||
|
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
|
||||||
|
|
||||||
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||||
|
|
||||||
|
"strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="],
|
||||||
|
|
||||||
|
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
|
||||||
|
|
||||||
|
"tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
|
||||||
|
|
||||||
|
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||||
|
|
||||||
|
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||||
|
|
||||||
|
"token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
|
||||||
|
|
||||||
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||||
|
|
||||||
|
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||||
|
|
||||||
|
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||||
|
|
||||||
|
"victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
|
||||||
|
|
||||||
|
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||||
|
|
||||||
|
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||||
|
|
||||||
|
"@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="],
|
||||||
|
|
||||||
|
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
args:
|
args:
|
||||||
NPM_VERSION: ${NPM_VERSION:-latest}
|
|
||||||
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-}
|
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
env_file:
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
mkdir -p /app/data
|
|
||||||
chown -R nextjs:nodejs /app/data
|
|
||||||
|
|
||||||
exec su-exec nextjs "$@"
|
|
||||||
2606
package-lock.json
generated
2606
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -2,17 +2,19 @@
|
|||||||
"name": "fiscal-frontend",
|
"name": "fiscal-frontend",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"packageManager": "bun@1.3.5",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "bun --bun next dev --turbopack",
|
||||||
"build": "next build --turbopack",
|
"build": "bun --bun next build --turbopack",
|
||||||
"start": "next start",
|
"start": "bun --bun next start",
|
||||||
"lint": "next lint"
|
"lint": "bun --bun tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/postcss": "^4.2.1",
|
"@tailwindcss/postcss": "^4.2.1",
|
||||||
"better-auth": "^1.4.19",
|
"better-auth": "^1.4.19",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"elysia": "latest",
|
||||||
"lucide-react": "^0.575.0",
|
"lucide-react": "^0.575.0",
|
||||||
"next": "^16.1.6",
|
"next": "^16.1.6",
|
||||||
"pg": "^8.18.0",
|
"pg": "^8.18.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user