Frontend:
- apps/web (Vite+TS, TanStack Router/Query, shadcn/ui @cfdm/ui base-nova)
- 10 страниц в routes/_auth/, Recharts через shadcn Chart, lucide-react
- формы на RHF + Zod (FormSheet/FormField)
- удалены Tabler, Chart.js, react-router-dom
Backend (параллельный трек):
- apps/api (Fastify 5 + Drizzle + better-sqlite3)
- packages/db: Drizzle-схема и repositories по сущностям
- packages/shared: Zod-контракты
- роуты с валидацией и единым форматом ошибок { error: { code, message } }
- sync/backup — заглушки 501 (billmanager-адаптеры переносятся отдельно)
- legacy Express оставлен как runtime по умолчанию (RUNTIME=express)
Infra:
- Dockerfile multi-stage под pnpm workspaces
- .dockerignore и docker-compose обновлены под monorepo
Rules:
- удалены нерелевантные правила (rust, cloudflare, server/frontend-conventions)
- project-structure.mdc и AGENTS.md переписаны под monorepo
- frontend-shadcn.mdc, shadcn-ui-production.mdc, sqlite.mdc обновлены
Co-authored-by: Cursor <[email protected]>
84 lines
2.4 KiB
TypeScript
84 lines
2.4 KiB
TypeScript
import Fastify from 'fastify'
|
|
import cors from '@fastify/cors'
|
|
import sensible from '@fastify/sensible'
|
|
import staticPlugin from '@fastify/static'
|
|
import { existsSync } from 'node:fs'
|
|
import { dirname, join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
import { getDb } from '@cfdm/db'
|
|
|
|
import { dataRoutes } from './routes/data.js'
|
|
import { vpsRoutes } from './routes/vps.js'
|
|
import { providersRoutes } from './routes/providers.js'
|
|
import { providerAccountsRoutes } from './routes/provider-accounts.js'
|
|
import { paymentsRoutes } from './routes/payments.js'
|
|
import { balanceLedgerRoutes } from './routes/balance-ledger.js'
|
|
import { settingsRoutes } from './routes/settings.js'
|
|
import { syncRoutes } from './routes/sync.js'
|
|
import { projectsRoutes } from './routes/projects.js'
|
|
import { backupRoutes } from './routes/backup.js'
|
|
import { ratesProxyRoutes } from './routes/rates-proxy.js'
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
|
|
export interface BuildAppOptions {
|
|
dbPath?: string
|
|
staticDir?: string
|
|
}
|
|
|
|
export async function buildApp(opts: BuildAppOptions = {}) {
|
|
if (opts.dbPath) process.env.DB_PATH = opts.dbPath
|
|
getDb()
|
|
|
|
const app = Fastify({
|
|
logger: process.env.NODE_ENV !== 'production',
|
|
})
|
|
|
|
await app.register(cors, { origin: true })
|
|
await app.register(sensible)
|
|
|
|
await app.register(dataRoutes)
|
|
await app.register(vpsRoutes)
|
|
await app.register(providersRoutes)
|
|
await app.register(providerAccountsRoutes)
|
|
await app.register(paymentsRoutes)
|
|
await app.register(balanceLedgerRoutes)
|
|
await app.register(settingsRoutes)
|
|
await app.register(syncRoutes)
|
|
await app.register(projectsRoutes)
|
|
await app.register(backupRoutes)
|
|
await app.register(ratesProxyRoutes)
|
|
|
|
const staticDir = opts.staticDir ?? join(__dirname, '..', '..', 'web', 'dist')
|
|
if (existsSync(staticDir)) {
|
|
await app.register(staticPlugin, {
|
|
root: staticDir,
|
|
prefix: '/',
|
|
wildcard: false,
|
|
})
|
|
app.setNotFoundHandler((req, reply) => {
|
|
if (req.url.startsWith('/api')) {
|
|
reply.code(404).send({ error: { code: 'NOT_FOUND', message: 'Not found' } })
|
|
return
|
|
}
|
|
reply.sendFile('index.html')
|
|
})
|
|
}
|
|
|
|
return app
|
|
}
|
|
|
|
async function start() {
|
|
const port = Number(process.env.PORT ?? 3001)
|
|
const app = await buildApp()
|
|
try {
|
|
await app.listen({ port, host: '0.0.0.0' })
|
|
} catch (err) {
|
|
app.log.error(err)
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
void start()
|