chore: initial scaffold

Vite + Vue 3 + DaisyUI SPA (project picker + Slack avatar member grid) with a
TypeScript/Node (Hono) backend proxying Napta + Slack, SQLite avatar cache via
node:sqlite, and docker-compose for Coolify deploy.
This commit is contained in:
Julien Calixte
2026-06-26 15:26:20 +01:00
commit 528251f176
40 changed files with 2410 additions and 0 deletions

47
backend/src/db.ts Normal file
View File

@@ -0,0 +1,47 @@
import { DatabaseSync } from "node:sqlite"
import { mkdirSync } from "node:fs"
import { dirname } from "node:path"
const dbPath = process.env.DB_PATH || "data/app.db"
mkdirSync(dirname(dbPath), { recursive: true })
export const db = new DatabaseSync(dbPath)
// Cache of email -> Slack avatar URL, so we don't hit Slack on every request.
// image_url is nullable: a NULL row means "looked up, no Slack match" (also
// worth caching, to avoid re-querying people who aren't in Slack).
db.exec(`
CREATE TABLE IF NOT EXISTS slack_avatar_cache (
email TEXT PRIMARY KEY,
image_url TEXT,
cached_at INTEGER NOT NULL
)
`)
const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000
interface CacheRow {
image_url: string | null
cached_at: number
}
// Returns the cached URL (which may be null = known-no-match), or undefined
// when there is no fresh cache entry and the caller should query Slack.
export function getCachedAvatar(email: string): string | null | undefined {
const row = db
.prepare("SELECT image_url, cached_at FROM slack_avatar_cache WHERE email = ?")
.get(email.toLowerCase()) as CacheRow | undefined
if (!row) return undefined
if (Date.now() - row.cached_at > CACHE_TTL_MS) return undefined
return row.image_url
}
export function setCachedAvatar(email: string, imageUrl: string | null): void {
db.prepare(
`INSERT INTO slack_avatar_cache (email, image_url, cached_at)
VALUES (?, ?, ?)
ON CONFLICT(email) DO UPDATE SET
image_url = excluded.image_url,
cached_at = excluded.cached_at`,
).run(email.toLowerCase(), imageUrl, Date.now())
}