Compare commits

..

6 Commits

Author SHA1 Message Date
Carlos Escalante
20b4ad102d Wrap transaction_type in col() for notin_ filter
All checks were successful
Deploy to VPS / deploy (push) Successful in 12s
SQLModel enum columns need col() to expose SQLAlchemy operators like
notin_. Without it the agent tool raised at query build time and the
chat card flashed away.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:58:28 -06:00
Carlos Escalante
ec716e698f Exclude SALARY and DEPOSITO from agent recent-transactions tool
All checks were successful
Deploy to VPS / deploy (push) Successful in 13s
The 'last N transactions' answer was including salary deposits, which the
user reads as expense activity. Filter income types out at the query level.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:55:53 -06:00
Carlos Escalante
f556c392fb Pass OPENAI_API_KEY and AGENT_MODEL to prod from Gitea secrets
All checks were successful
Deploy to VPS / deploy (push) Successful in 13s
Backend was hitting OpenAI with no key (401) because the deploy workflow
never wrote OPENAI_API_KEY into .env.prod. Add it plus AGENT_MODEL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:49:55 -06:00
Carlos Escalante
aa4bb6512f Proxy /api/v1 and /api/auth from Hono to FastAPI in prod
All checks were successful
Deploy to VPS / deploy (push) Successful in 45s
In production the browser talks to the Hono server, which only proxied
/api/copilotkit/*. All other /api/* requests hit the SPA static fallback
and got index.html back. Forward /api/v1/* and /api/auth/* to BACKEND_URL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:43:34 -06:00
Carlos Escalante
6b3069eef4 Fix prod backend hostname collision on nginx-prod-network
All checks were successful
Deploy to VPS / deploy (push) Successful in 5s
Frontend joins both wealthysmart-network-prod and nginx-prod-network
(needed for nginx-proxy reverse proxy + TLS). Another container on
nginx-prod-network is named "backend" too (receipts-backend-prod),
so DNS resolved "backend" to a sibling app and the agent endpoint
returned 404. Pin the agent/backend URLs to the unique container
name wealthysmart-backend-prod.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:31:16 -06:00
Carlos Escalante
ead8fb8684 Fix prod frontend container: tsx on PATH and cap build heap
All checks were successful
Deploy to VPS / deploy (push) Successful in 5s
Runner stage was invoking `tsx server.ts` via sh, which doesn't
have node_modules/.bin on PATH, so the container crash-looped with
"tsx: not found" (502 at the edge). Use the absolute binary path
instead.

Also caps the Vite build's V8 heap to 1.5 GB so a future build on
the 4-core / 8 GB VPS can't OOM-kill neighbouring services.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:21:45 -06:00
5 changed files with 40 additions and 4 deletions

View File

@@ -23,6 +23,8 @@ jobs:
LETSENCRYPT_EMAIL=${{ secrets.LETSENCRYPT_EMAIL }}
VAPID_PRIVATE_KEY=${{ secrets.VAPID_PRIVATE_KEY }}
VAPID_PUBLIC_KEY=${{ secrets.VAPID_PUBLIC_KEY }}
OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}
AGENT_MODEL=${{ secrets.AGENT_MODEL }}
ENVEOF
sed -i 's/^[[:space:]]*//' .env.prod

View File

@@ -118,7 +118,11 @@ def get_recent_transactions(
) -> list[dict]:
"""Recent transactions, newest first. Use filters to narrow down. For
billing-cycle scoped totals prefer get_cycle_summary."""
q = select(Transaction)
q = select(Transaction).where(
col(Transaction.transaction_type).notin_(
[TransactionType.SALARY, TransactionType.DEPOSITO]
)
)
if source:
q = q.where(Transaction.source == TransactionSource(source))
if category_id is not None:

View File

@@ -57,8 +57,8 @@ services:
restart: unless-stopped
environment:
NODE_ENV: production
BACKEND_URL: http://backend:8000
AGENT_URL: http://backend:8000/api/v1/agent/agui
BACKEND_URL: http://wealthysmart-backend-prod:8000
AGENT_URL: http://wealthysmart-backend-prod:8000/api/v1/agent/agui
JWT_SECRET: ${SECRET_KEY}
COOKIE_DOMAIN: wealth.cescalante.dev
COOKIE_SECURE: "true"

View File

@@ -21,6 +21,8 @@ FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Cap node heap so a build on a small VPS can't OOM-kill neighbours.
ENV NODE_OPTIONS=--max-old-space-size=1536
RUN corepack enable && pnpm build
# Production: Hono serves dist/ + /api/copilotkit on port 3000
@@ -34,4 +36,4 @@ COPY server.ts package.json ./
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget -qO- http://127.0.0.1:3000/api/health || exit 1
CMD ["sh", "-c", "corepack enable && tsx server.ts"]
CMD ["./node_modules/.bin/tsx", "server.ts"]

View File

@@ -11,6 +11,8 @@ import { Hono } from "hono";
const AGENT_URL =
process.env.AGENT_URL ?? "http://backend:8000/api/v1/agent/agui";
const BACKEND_URL =
process.env.BACKEND_URL ?? "http://localhost:8001";
const isProd = process.env.NODE_ENV === "production";
const PORT = parseInt(process.env.PORT ?? (isProd ? "3000" : "3001"));
@@ -376,6 +378,32 @@ app.all("/api/copilotkit/*", async (c) => {
app.get("/api/health", (c) => c.json({ ok: true }));
// Proxy backend API calls (FastAPI). In dev these hit Vite's proxy directly,
// but in prod the browser talks to this Hono server, which must forward
// `/api/v1/*` and `/api/auth/*` to the FastAPI container — otherwise the SPA
// fallback below swallows them and returns index.html.
const proxyToBackend = async (c: import("hono").Context) => {
const url = new URL(c.req.url);
const target = `${BACKEND_URL}${url.pathname}${url.search}`;
const method = c.req.method;
const headers = new Headers(c.req.raw.headers);
headers.delete("host");
const init: RequestInit = {
method,
headers,
redirect: "manual",
};
if (method !== "GET" && method !== "HEAD") {
init.body = c.req.raw.body;
// @ts-expect-error undici requires duplex for streamed bodies
init.duplex = "half";
}
return fetch(target, init);
};
app.all("/api/v1/*", proxyToBackend);
app.all("/api/auth/*", proxyToBackend);
// In production, serve the Vite build output.
if (isProd) {
app.use("/*", serveStatic({ root: "./dist" }));