diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c3e7d43 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +# Build output and local caches +.next +.cache + +# Dependencies +node_modules + +# Local runtime data and environment +.env +.env.local +.env.*.local +data + +# Editor/system files +.DS_Store +*.log + +# Git +.git +.gitignore diff --git a/.env.example b/.env.example index 7250be5..9edf9eb 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ # Optional API override. Leave empty to use same-origin internal API routes. NEXT_PUBLIC_API_URL= +APP_PORT=3000 # OpenClaw / ZeroClaw (OpenAI-compatible) OPENCLAW_BASE_URL=http://localhost:4000 diff --git a/.gitignore b/.gitignore index 54bc099..ef532e4 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,4 @@ out/ *.sqlite # Local app runtime state -frontend/data/*.json +data/*.json diff --git a/frontend/Dockerfile b/Dockerfile similarity index 69% rename from frontend/Dockerfile rename to Dockerfile index 97b00e5..3619ae8 100644 --- a/frontend/Dockerfile +++ b/Dockerfile @@ -3,12 +3,13 @@ FROM node:20-alpine AS base WORKDIR /app FROM base AS deps -COPY package.json ./ -RUN npm install +COPY package.json package-lock.json ./ +RUN npm ci FROM base AS builder ARG NEXT_PUBLIC_API_URL= ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} +ENV NEXT_TELEMETRY_DISABLED=1 COPY --from=deps /app/node_modules ./node_modules COPY . . RUN mkdir -p public && npm run build @@ -19,18 +20,21 @@ WORKDIR /app ENV NODE_ENV=production ARG NEXT_PUBLIC_API_URL= ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} +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 --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static - -USER nextjs +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh EXPOSE 3000 ENV PORT=3000 +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] CMD ["node", "server.js"] diff --git a/README.md b/README.md index 1c2967a..367b5cb 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,6 @@ Turbopack-first rebuild of a fiscal.ai-style terminal with OpenClaw integration. ## Run locally ```bash -cd frontend npm install npm run dev ``` @@ -23,14 +22,23 @@ Open [http://localhost:3000](http://localhost:3000). ## Production build ```bash -cd frontend npm run build npm run start ``` +## Docker deployment + +```bash +cp .env.example .env +docker compose up --build -d +``` + +Default app URL: `http://localhost:3000` (override with `APP_PORT` in `.env`). +Runtime data persists in the `app_data` volume (`/app/data` in container). + ## Environment -Use root `.env` or `frontend/.env.local`: +Use root `.env` or root `.env.local`: ```env # leave blank for same-origin API diff --git a/frontend/app/api/filings/[accessionNumber]/analyze/route.ts b/app/api/filings/[accessionNumber]/analyze/route.ts similarity index 100% rename from frontend/app/api/filings/[accessionNumber]/analyze/route.ts rename to app/api/filings/[accessionNumber]/analyze/route.ts diff --git a/frontend/app/api/filings/route.ts b/app/api/filings/route.ts similarity index 100% rename from frontend/app/api/filings/route.ts rename to app/api/filings/route.ts diff --git a/frontend/app/api/filings/sync/route.ts b/app/api/filings/sync/route.ts similarity index 100% rename from frontend/app/api/filings/sync/route.ts rename to app/api/filings/sync/route.ts diff --git a/frontend/app/api/health/route.ts b/app/api/health/route.ts similarity index 100% rename from frontend/app/api/health/route.ts rename to app/api/health/route.ts diff --git a/frontend/app/api/me/route.ts b/app/api/me/route.ts similarity index 100% rename from frontend/app/api/me/route.ts rename to app/api/me/route.ts diff --git a/frontend/app/api/portfolio/holdings/[id]/route.ts b/app/api/portfolio/holdings/[id]/route.ts similarity index 100% rename from frontend/app/api/portfolio/holdings/[id]/route.ts rename to app/api/portfolio/holdings/[id]/route.ts diff --git a/frontend/app/api/portfolio/holdings/route.ts b/app/api/portfolio/holdings/route.ts similarity index 100% rename from frontend/app/api/portfolio/holdings/route.ts rename to app/api/portfolio/holdings/route.ts diff --git a/frontend/app/api/portfolio/insights/generate/route.ts b/app/api/portfolio/insights/generate/route.ts similarity index 100% rename from frontend/app/api/portfolio/insights/generate/route.ts rename to app/api/portfolio/insights/generate/route.ts diff --git a/frontend/app/api/portfolio/insights/latest/route.ts b/app/api/portfolio/insights/latest/route.ts similarity index 100% rename from frontend/app/api/portfolio/insights/latest/route.ts rename to app/api/portfolio/insights/latest/route.ts diff --git a/frontend/app/api/portfolio/refresh-prices/route.ts b/app/api/portfolio/refresh-prices/route.ts similarity index 100% rename from frontend/app/api/portfolio/refresh-prices/route.ts rename to app/api/portfolio/refresh-prices/route.ts diff --git a/frontend/app/api/portfolio/summary/route.ts b/app/api/portfolio/summary/route.ts similarity index 100% rename from frontend/app/api/portfolio/summary/route.ts rename to app/api/portfolio/summary/route.ts diff --git a/frontend/app/api/tasks/[taskId]/route.ts b/app/api/tasks/[taskId]/route.ts similarity index 100% rename from frontend/app/api/tasks/[taskId]/route.ts rename to app/api/tasks/[taskId]/route.ts diff --git a/frontend/app/api/tasks/route.ts b/app/api/tasks/route.ts similarity index 100% rename from frontend/app/api/tasks/route.ts rename to app/api/tasks/route.ts diff --git a/frontend/app/api/watchlist/[id]/route.ts b/app/api/watchlist/[id]/route.ts similarity index 100% rename from frontend/app/api/watchlist/[id]/route.ts rename to app/api/watchlist/[id]/route.ts diff --git a/frontend/app/api/watchlist/route.ts b/app/api/watchlist/route.ts similarity index 100% rename from frontend/app/api/watchlist/route.ts rename to app/api/watchlist/route.ts diff --git a/frontend/app/auth/signin/page.tsx b/app/auth/signin/page.tsx similarity index 100% rename from frontend/app/auth/signin/page.tsx rename to app/auth/signin/page.tsx diff --git a/frontend/app/auth/signup/page.tsx b/app/auth/signup/page.tsx similarity index 100% rename from frontend/app/auth/signup/page.tsx rename to app/auth/signup/page.tsx diff --git a/frontend/app/filings/page.tsx b/app/filings/page.tsx similarity index 100% rename from frontend/app/filings/page.tsx rename to app/filings/page.tsx diff --git a/frontend/app/globals.css b/app/globals.css similarity index 100% rename from frontend/app/globals.css rename to app/globals.css diff --git a/frontend/app/layout.tsx b/app/layout.tsx similarity index 100% rename from frontend/app/layout.tsx rename to app/layout.tsx diff --git a/frontend/app/page.tsx b/app/page.tsx similarity index 100% rename from frontend/app/page.tsx rename to app/page.tsx diff --git a/frontend/app/portfolio/page.tsx b/app/portfolio/page.tsx similarity index 100% rename from frontend/app/portfolio/page.tsx rename to app/portfolio/page.tsx diff --git a/frontend/app/watchlist/page.tsx b/app/watchlist/page.tsx similarity index 100% rename from frontend/app/watchlist/page.tsx rename to app/watchlist/page.tsx diff --git a/frontend/components/auth/auth-shell.tsx b/components/auth/auth-shell.tsx similarity index 100% rename from frontend/components/auth/auth-shell.tsx rename to components/auth/auth-shell.tsx diff --git a/frontend/components/dashboard/metric-card.tsx b/components/dashboard/metric-card.tsx similarity index 100% rename from frontend/components/dashboard/metric-card.tsx rename to components/dashboard/metric-card.tsx diff --git a/frontend/components/dashboard/task-feed.tsx b/components/dashboard/task-feed.tsx similarity index 100% rename from frontend/components/dashboard/task-feed.tsx rename to components/dashboard/task-feed.tsx diff --git a/frontend/components/shell/app-shell.tsx b/components/shell/app-shell.tsx similarity index 100% rename from frontend/components/shell/app-shell.tsx rename to components/shell/app-shell.tsx diff --git a/frontend/components/ui/button.tsx b/components/ui/button.tsx similarity index 100% rename from frontend/components/ui/button.tsx rename to components/ui/button.tsx diff --git a/frontend/components/ui/input.tsx b/components/ui/input.tsx similarity index 100% rename from frontend/components/ui/input.tsx rename to components/ui/input.tsx diff --git a/frontend/components/ui/panel.tsx b/components/ui/panel.tsx similarity index 100% rename from frontend/components/ui/panel.tsx rename to components/ui/panel.tsx diff --git a/frontend/components/ui/status-pill.tsx b/components/ui/status-pill.tsx similarity index 100% rename from frontend/components/ui/status-pill.tsx rename to components/ui/status-pill.tsx diff --git a/docker-compose.yml b/docker-compose.yml index adc9202..19065ea 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: app: build: - context: ./frontend + context: . dockerfile: Dockerfile args: NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-} @@ -17,5 +17,15 @@ services: OPENCLAW_API_KEY: ${OPENCLAW_API_KEY:-} OPENCLAW_MODEL: ${OPENCLAW_MODEL:-zeroclaw} SEC_USER_AGENT: ${SEC_USER_AGENT:-Fiscal Clone } + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/api/health || exit 1"] + interval: 30s + timeout: 10s + retries: 3 ports: - - '3000:3000' + - '${APP_PORT:-3000}:3000' + volumes: + - app_data:/app/data + +volumes: + app_data: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..0560d5b --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/sh +set -e + +mkdir -p /app/data +chown -R nextjs:nodejs /app/data + +exec su-exec nextjs "$@" diff --git a/frontend/hooks/use-auth-guard.ts b/hooks/use-auth-guard.ts similarity index 100% rename from frontend/hooks/use-auth-guard.ts rename to hooks/use-auth-guard.ts diff --git a/frontend/hooks/use-task-poller.ts b/hooks/use-task-poller.ts similarity index 100% rename from frontend/hooks/use-task-poller.ts rename to hooks/use-task-poller.ts diff --git a/frontend/lib/api.ts b/lib/api.ts similarity index 100% rename from frontend/lib/api.ts rename to lib/api.ts diff --git a/frontend/lib/format.ts b/lib/format.ts similarity index 100% rename from frontend/lib/format.ts rename to lib/format.ts diff --git a/frontend/lib/runtime-url.ts b/lib/runtime-url.ts similarity index 100% rename from frontend/lib/runtime-url.ts rename to lib/runtime-url.ts diff --git a/frontend/lib/server/http.ts b/lib/server/http.ts similarity index 100% rename from frontend/lib/server/http.ts rename to lib/server/http.ts diff --git a/frontend/lib/server/openclaw.ts b/lib/server/openclaw.ts similarity index 100% rename from frontend/lib/server/openclaw.ts rename to lib/server/openclaw.ts diff --git a/frontend/lib/server/portfolio.ts b/lib/server/portfolio.ts similarity index 100% rename from frontend/lib/server/portfolio.ts rename to lib/server/portfolio.ts diff --git a/frontend/lib/server/prices.ts b/lib/server/prices.ts similarity index 100% rename from frontend/lib/server/prices.ts rename to lib/server/prices.ts diff --git a/frontend/lib/server/sec.ts b/lib/server/sec.ts similarity index 100% rename from frontend/lib/server/sec.ts rename to lib/server/sec.ts diff --git a/frontend/lib/server/store.ts b/lib/server/store.ts similarity index 91% rename from frontend/lib/server/store.ts rename to lib/server/store.ts index d19e809..2dc3fad 100644 --- a/frontend/lib/server/store.ts +++ b/lib/server/store.ts @@ -1,4 +1,4 @@ -import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'; import path from 'node:path'; import type { Filing, Holding, PortfolioInsight, Task, WatchlistItem } from '@/lib/types'; @@ -74,7 +74,9 @@ async function readStore(): Promise { } async function writeStore(store: DataStore) { - await writeFile(STORE_PATH, JSON.stringify(store, null, 2), 'utf8'); + const tempPath = `${STORE_PATH}.tmp`; + await writeFile(tempPath, JSON.stringify(store, null, 2), 'utf8'); + await rename(tempPath, STORE_PATH); } function cloneStore(store: DataStore): DataStore { diff --git a/frontend/lib/server/tasks.ts b/lib/server/tasks.ts similarity index 100% rename from frontend/lib/server/tasks.ts rename to lib/server/tasks.ts diff --git a/frontend/lib/types.ts b/lib/types.ts similarity index 100% rename from frontend/lib/types.ts rename to lib/types.ts diff --git a/frontend/lib/utils.ts b/lib/utils.ts similarity index 100% rename from frontend/lib/utils.ts rename to lib/utils.ts diff --git a/frontend/next-env.d.ts b/next-env.d.ts similarity index 100% rename from frontend/next-env.d.ts rename to next-env.d.ts diff --git a/frontend/next.config.js b/next.config.js similarity index 100% rename from frontend/next.config.js rename to next.config.js diff --git a/frontend/package-lock.json b/package-lock.json similarity index 100% rename from frontend/package-lock.json rename to package-lock.json diff --git a/frontend/package.json b/package.json similarity index 100% rename from frontend/package.json rename to package.json diff --git a/frontend/postcss.config.js b/postcss.config.js similarity index 100% rename from frontend/postcss.config.js rename to postcss.config.js diff --git a/frontend/tailwind.config.js b/tailwind.config.js similarity index 100% rename from frontend/tailwind.config.js rename to tailwind.config.js diff --git a/frontend/tsconfig.json b/tsconfig.json similarity index 100% rename from frontend/tsconfig.json rename to tsconfig.json