chore: scaffold Nuxt 4 app with Postgres, tooling and health route
Nuxt 4 (app/ structure) + TypeScript, ESLint, Vitest + @nuxt/test-utils, Drizzle + pg, a /api/health route with a DB ping, and a dev docker-compose (app + Postgres). Verified: pnpm test, lint, and build pass; the built server answers /api/health.
This commit is contained in:
15
.env.example
Normal file
15
.env.example
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# App
|
||||||
|
NUXT_SESSION_SECRET=change-me
|
||||||
|
|
||||||
|
# Database (Postgres) — Coolify-managed in prod, docker-compose in dev
|
||||||
|
DATABASE_URL=postgres://andon:andon@localhost:5432/andon
|
||||||
|
|
||||||
|
# Google OAuth (Task 9) — restricted to the theodo.com hosted domain
|
||||||
|
GOOGLE_CLIENT_ID=
|
||||||
|
GOOGLE_CLIENT_SECRET=
|
||||||
|
GOOGLE_REDIRECT_URI=
|
||||||
|
|
||||||
|
# Web Push / VAPID (Task 10)
|
||||||
|
VAPID_PUBLIC_KEY=
|
||||||
|
VAPID_PRIVATE_KEY=
|
||||||
|
VAPID_SUBJECT=mailto:owner@theodo.com
|
||||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Nuxt dev/build outputs
|
||||||
|
.output
|
||||||
|
.data
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Node dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.fleet
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
13
Dockerfile.dev
Normal file
13
Dockerfile.dev
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
FROM node:24-alpine
|
||||||
|
RUN corepack enable
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies first for better layer caching.
|
||||||
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ENV HOST=0.0.0.0
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["pnpm", "dev"]
|
||||||
4
app/app.vue
Normal file
4
app/app.vue
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<template>
|
||||||
|
<NuxtRouteAnnouncer />
|
||||||
|
<NuxtPage />
|
||||||
|
</template>
|
||||||
6
app/pages/dashboard.vue
Normal file
6
app/pages/dashboard.vue
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<main>
|
||||||
|
<h1>Andon — weak point dashboard</h1>
|
||||||
|
<p>The red-dot defect map lands in Task 7.</p>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
6
app/pages/index.vue
Normal file
6
app/pages/index.vue
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<main>
|
||||||
|
<h1>Andon — report a board problem</h1>
|
||||||
|
<p>The interactive board lands in Task 2.</p>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
35
docker-compose.yml
Normal file
35
docker-compose.yml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgres://andon:andon@db:5432/andon
|
||||||
|
HOST: 0.0.0.0
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- /app/node_modules
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: andon
|
||||||
|
POSTGRES_PASSWORD: andon
|
||||||
|
POSTGRES_DB: andon
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- andon_pgdata:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U andon"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
andon_pgdata:
|
||||||
10
drizzle.config.ts
Normal file
10
drizzle.config.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'drizzle-kit'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
dialect: 'postgresql',
|
||||||
|
schema: './server/db/schema.ts',
|
||||||
|
out: './server/db/migrations',
|
||||||
|
dbCredentials: {
|
||||||
|
url: process.env.DATABASE_URL ?? '',
|
||||||
|
},
|
||||||
|
})
|
||||||
4
eslint.config.mjs
Normal file
4
eslint.config.mjs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// Nuxt generates the base flat config during `nuxt prepare`.
|
||||||
|
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||||
|
|
||||||
|
export default withNuxt()
|
||||||
6
nuxt.config.ts
Normal file
6
nuxt.config.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
compatibilityDate: '2025-07-15',
|
||||||
|
devtools: { enabled: true },
|
||||||
|
modules: ['@nuxt/eslint', '@nuxt/test-utils/module'],
|
||||||
|
})
|
||||||
36
package.json
Normal file
36
package.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "andon",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "nuxt build",
|
||||||
|
"dev": "nuxt dev",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"preview": "nuxt preview",
|
||||||
|
"postinstall": "nuxt prepare",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"typecheck": "nuxt typecheck",
|
||||||
|
"db:generate": "drizzle-kit generate",
|
||||||
|
"db:migrate": "tsx server/db/migrate.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"drizzle-orm": "^0.45.2",
|
||||||
|
"nuxt": "^4.4.6",
|
||||||
|
"pg": "^8.21.0",
|
||||||
|
"vue": "^3.5.34",
|
||||||
|
"vue-router": "^5.0.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nuxt/eslint": "^1.15.2",
|
||||||
|
"@nuxt/test-utils": "^4.0.3",
|
||||||
|
"@types/pg": "^8.20.0",
|
||||||
|
"@vue/test-utils": "^2.4.10",
|
||||||
|
"drizzle-kit": "^0.31.10",
|
||||||
|
"eslint": "^10.4.0",
|
||||||
|
"happy-dom": "^20.9.0",
|
||||||
|
"tsx": "^4.22.3",
|
||||||
|
"vitest": "^4.1.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
10031
pnpm-lock.yaml
generated
Normal file
10031
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
User-Agent: *
|
||||||
|
Disallow:
|
||||||
5
server/api/health.get.ts
Normal file
5
server/api/health.get.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { checkDb } from '../db/client'
|
||||||
|
|
||||||
|
export default defineEventHandler(async () => {
|
||||||
|
return { ok: true, db: await checkDb() }
|
||||||
|
})
|
||||||
21
server/db/client.ts
Normal file
21
server/db/client.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import pg from 'pg'
|
||||||
|
|
||||||
|
let pool: pg.Pool | undefined
|
||||||
|
|
||||||
|
/** Lazily-created shared Postgres connection pool. */
|
||||||
|
export function getPool(): pg.Pool {
|
||||||
|
if (!pool) {
|
||||||
|
pool = new pg.Pool({ connectionString: process.env.DATABASE_URL })
|
||||||
|
}
|
||||||
|
return pool
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Liveness probe for the database, used by /api/health. */
|
||||||
|
export async function checkDb(): Promise<'up' | 'down'> {
|
||||||
|
try {
|
||||||
|
await getPool().query('select 1')
|
||||||
|
return 'up'
|
||||||
|
} catch {
|
||||||
|
return 'down'
|
||||||
|
}
|
||||||
|
}
|
||||||
2
server/db/schema.ts
Normal file
2
server/db/schema.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Drizzle (Postgres) schema — tables are defined in Task 3.
|
||||||
|
export {}
|
||||||
7
tests/smoke.spec.ts
Normal file
7
tests/smoke.spec.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
|
||||||
|
describe('smoke', () => {
|
||||||
|
it('runs the test suite', () => {
|
||||||
|
expect(true).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.server.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.shared.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.node.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
9
vitest.config.ts
Normal file
9
vitest.config.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { defineVitestConfig } from '@nuxt/test-utils/config'
|
||||||
|
|
||||||
|
export default defineVitestConfig({
|
||||||
|
test: {
|
||||||
|
// Default to a plain node environment; component/integration specs opt into
|
||||||
|
// the Nuxt runtime per-file with `// @vitest-environment nuxt`.
|
||||||
|
environment: 'node',
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user