From 2e0931343f44e0952f9f2287dee15d878e5d5092 Mon Sep 17 00:00:00 2001 From: tonym Date: Mon, 15 Jun 2026 22:03:47 -0500 Subject: [PATCH] =?UTF-8?q?v0.7.0=20=E2=80=94=20Suggestions=20module=20via?= =?UTF-8?q?=20@otm/account-panel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire the shared Gitea-backed suggestions panel: page, list/create + comments + upload API routes, sidebar link. Suggestions file as issues on bonna61/Moonbase; screenshot uploads persist via a mounted volume. Mirrors the FMB wiring. Secrets (NEXTAUTH_SECRET, GITEA_TOKEN) moved out of docker-compose.yml to an interpolated gitignored .env on the host, per CLAUDE.md (never commit the token). Co-Authored-By: Claude Opus 4.8 (1M context) --- .env.example | 6 ++ .npmrc | 1 + docker-compose.yml | 11 ++- next.config.mjs | 2 + package-lock.json | 83 +++---------------- package.json | 1 + src/app/(dashboard)/suggestions/page.tsx | 13 +++ .../suggestions/[number]/comments/route.ts | 9 ++ src/app/api/suggestions/route.ts | 9 ++ src/app/api/suggestions/upload/route.ts | 21 +++++ src/components/layout/sidebar.tsx | 3 +- src/lib/changelog.ts | 7 ++ src/lib/suggestions.ts | 20 +++++ src/lib/version.ts | 2 +- 14 files changed, 113 insertions(+), 75 deletions(-) create mode 100644 .npmrc create mode 100644 src/app/(dashboard)/suggestions/page.tsx create mode 100644 src/app/api/suggestions/[number]/comments/route.ts create mode 100644 src/app/api/suggestions/route.ts create mode 100644 src/app/api/suggestions/upload/route.ts create mode 100644 src/lib/suggestions.ts diff --git a/.env.example b/.env.example index c67422e..c7fb320 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,9 @@ DATABASE_URL="postgresql://tonym@localhost:5432/moonbase_dev" NEXTAUTH_SECRET="change-me-generate-with-openssl-rand-base64-32" NEXTAUTH_URL="http://localhost:3000" + +# Suggestions module — files feedback as issues on a Gitea repo. +# Leave blank to disable (the Suggestions page shows a "not set up" state). +GITEA_URL="http://192.168.0.5:3022" +GITEA_TOKEN="" +GITEA_REPO="bonna61/Moonbase" diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..d69d14f --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@otm:registry=http://192.168.0.5:3022/api/packages/tonym/npm/ diff --git a/docker-compose.yml b/docker-compose.yml index 0e0bd89..4a9a07f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,8 +21,17 @@ services: - "8053:3000" environment: DATABASE_URL: postgresql://moonbase:moonbase@postgres:5432/moonbase - NEXTAUTH_SECRET: 81Uds/ZnQNHZWMsHIIuqNVuBSzPqcVxdPyw1AewbjZA= + # Secrets are interpolated from a gitignored .env next to this file on the + # host (never committed — see CLAUDE.md). docker compose auto-loads ./.env. + NEXTAUTH_SECRET: ${NEXTAUTH_SECRET} NEXTAUTH_URL: http://192.168.0.5:8053 + # Suggestions module — files feedback as Gitea issues on bonna61/Moonbase. + GITEA_URL: http://192.168.0.5:3022 + GITEA_TOKEN: ${GITEA_TOKEN} + GITEA_REPO: bonna61/Moonbase + volumes: + # Persist suggestion screenshot uploads across rebuilds. + - /mnt/user/appdata/moonbase/uploads:/app/public/uploads networks: - internal diff --git a/next.config.mjs b/next.config.mjs index 68a8854..d060b43 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -4,6 +4,8 @@ const nextConfig = { remotePatterns: [{ protocol: "https", hostname: "**" }], }, reactStrictMode: true, + // Shared TSX package pulled from the Gitea npm registry — transpile its source. + transpilePackages: ["@otm/account-panel"], }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index fb2505c..3f8d8a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@hookform/resolvers": "^3.9.1", + "@otm/account-panel": "0.8.2", "@paralleldrive/cuid2": "^3.3.0", "@prisma/client": "^5.22.0", "@radix-ui/react-avatar": "^1.1.1", @@ -858,9 +859,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -877,9 +875,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -896,9 +891,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -915,9 +907,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1035,6 +1024,16 @@ "node": ">=12.4.0" } }, + "node_modules/@otm/account-panel": { + "version": "0.8.2", + "resolved": "http://192.168.0.5:3022/api/packages/tonym/npm/%40otm%2Faccount-panel/-/0.8.2/account-panel-0.8.2.tgz", + "integrity": "sha512-E7uJ3Xxm8/xz6gcUAS4TUI+eCjaVZLFs7CjF7RpwSLUn3DpIszGSE4BN0519BQOThpQ69p3m15h/b64HLMgHgw==", + "peerDependencies": { + "next": ">=14", + "next-auth": ">=4", + "react": ">=18" + } + }, "node_modules/@oxc-project/types": { "version": "0.133.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", @@ -2048,9 +2047,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2068,9 +2064,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2088,9 +2081,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2108,9 +2098,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2128,9 +2115,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2148,9 +2132,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2757,9 +2738,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2774,9 +2752,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2791,9 +2766,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2808,9 +2780,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2825,9 +2794,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2842,9 +2808,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2859,9 +2822,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2876,9 +2836,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2893,9 +2850,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2910,9 +2864,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6200,9 +6151,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6224,9 +6172,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6248,9 +6193,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6272,9 +6214,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/package.json b/package.json index 89b607f..f54c96b 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@hookform/resolvers": "^3.9.1", + "@otm/account-panel": "0.8.2", "@paralleldrive/cuid2": "^3.3.0", "@prisma/client": "^5.22.0", "@radix-ui/react-avatar": "^1.1.1", diff --git a/src/app/(dashboard)/suggestions/page.tsx b/src/app/(dashboard)/suggestions/page.tsx new file mode 100644 index 0000000..a8522fa --- /dev/null +++ b/src/app/(dashboard)/suggestions/page.tsx @@ -0,0 +1,13 @@ +import { Metadata } from "next"; +import { redirect } from "next/navigation"; +import { SuggestionsPanel } from "@otm/account-panel"; +import { getSession } from "@/lib/auth"; + +export const metadata: Metadata = { title: "Suggestions" }; +export const dynamic = "force-dynamic"; + +export default async function SuggestionsPage() { + const session = await getSession(); + if (!session) redirect("/login"); + return ; +} diff --git a/src/app/api/suggestions/[number]/comments/route.ts b/src/app/api/suggestions/[number]/comments/route.ts new file mode 100644 index 0000000..32d1553 --- /dev/null +++ b/src/app/api/suggestions/[number]/comments/route.ts @@ -0,0 +1,9 @@ +import { createSuggestionCommentsRoute } from "@otm/account-panel/server"; +import { giteaConfig, suggestionGuard } from "@/lib/suggestions"; + +export const dynamic = "force-dynamic"; + +export const { GET, POST } = createSuggestionCommentsRoute({ + gitea: giteaConfig(), + guard: suggestionGuard, +}); diff --git a/src/app/api/suggestions/route.ts b/src/app/api/suggestions/route.ts new file mode 100644 index 0000000..a0332ae --- /dev/null +++ b/src/app/api/suggestions/route.ts @@ -0,0 +1,9 @@ +import { createSuggestionsRoute } from "@otm/account-panel/server"; +import { giteaConfig, suggestionGuard } from "@/lib/suggestions"; + +export const dynamic = "force-dynamic"; + +export const { GET, POST } = createSuggestionsRoute({ + gitea: giteaConfig(), + guard: suggestionGuard, +}); diff --git a/src/app/api/suggestions/upload/route.ts b/src/app/api/suggestions/upload/route.ts new file mode 100644 index 0000000..4f46bde --- /dev/null +++ b/src/app/api/suggestions/upload/route.ts @@ -0,0 +1,21 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { createSuggestionUploadRoute } from "@otm/account-panel/server"; +import { suggestionGuard } from "@/lib/suggestions"; + +export const dynamic = "force-dynamic"; + +// Screenshots are saved under public/uploads/suggestions and referenced by +// absolute URL so Gitea's web UI can render them. In Docker this dir is mounted +// to a persistent volume (see docker-compose.yml) so images survive rebuilds. +export const { POST } = createSuggestionUploadRoute({ + guard: suggestionGuard, + save: async ({ name, bytes }) => { + const safe = `${Date.now()}-${name.replace(/[^a-zA-Z0-9._-]/g, "_")}`; + const dir = path.join(process.cwd(), "public", "uploads", "suggestions"); + await mkdir(dir, { recursive: true }); + await writeFile(path.join(dir, safe), Buffer.from(bytes)); + const base = (process.env.NEXT_PUBLIC_BASE_URL || process.env.NEXTAUTH_URL || "").replace(/\/+$/, ""); + return { url: base ? `${base}/uploads/suggestions/${safe}` : `/uploads/suggestions/${safe}` }; + }, +}); diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx index e2dff56..7c79c0f 100644 --- a/src/components/layout/sidebar.tsx +++ b/src/components/layout/sidebar.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; -import { Leaf, Home, Wrench, LayoutDashboard, Bell, HardDrive } from "lucide-react"; +import { Leaf, Home, Wrench, LayoutDashboard, Bell, HardDrive, Lightbulb } from "lucide-react"; import { cn } from "@/lib/utils"; const navItems = [ @@ -15,6 +15,7 @@ const navItems = [ ]; const settingsItems = [ + { label: "Suggestions", href: "/suggestions", icon: Lightbulb }, { label: "Backup & Restore", href: "/settings/backup", icon: HardDrive }, ]; diff --git a/src/lib/changelog.ts b/src/lib/changelog.ts index f8ee352..a17c654 100644 --- a/src/lib/changelog.ts +++ b/src/lib/changelog.ts @@ -8,6 +8,13 @@ export type ChangelogEntry = { }; export const CHANGELOG: ChangelogEntry[] = [ + { + version: "0.7.0", + date: "2026-06-15", + changes: [ + "New Suggestions page — share ideas, requests, or problems right from the app. You can attach screenshots, see what's still open vs. done, and reply in a thread. Everything you send goes straight to Tony.", + ], + }, { version: "0.6.0", date: "2026-06-15", diff --git a/src/lib/suggestions.ts b/src/lib/suggestions.ts new file mode 100644 index 0000000..6083880 --- /dev/null +++ b/src/lib/suggestions.ts @@ -0,0 +1,20 @@ +import type { NextRequest } from "next/server"; +import type { GiteaConfig } from "@otm/account-panel"; +import { getSession } from "./auth"; + +// Suggestions are filed as issues on the Gitea repo — no DB model. +// Returns null (→ routes answer 503, panel shows a tidy empty state) when any +// piece is missing, so the feature degrades gracefully if env isn't wired. +export function giteaConfig(): GiteaConfig | null { + const baseUrl = process.env.GITEA_URL; + const token = process.env.GITEA_TOKEN; + const repo = process.env.GITEA_REPO; + if (!baseUrl || !token || !repo) return null; + return { baseUrl, token, repo }; +} + +/** Only signed-in users may read/write suggestions. */ +export async function suggestionGuard(_req: NextRequest): Promise { + const session = await getSession(); + return !!session?.user; +} diff --git a/src/lib/version.ts b/src/lib/version.ts index 6cb9882..c0a374d 100644 --- a/src/lib/version.ts +++ b/src/lib/version.ts @@ -1,2 +1,2 @@ // Bump on every user-visible release. Changelog entries live in `src/lib/changelog.ts`. -export const APP_VERSION = "0.6.0"; +export const APP_VERSION = "0.7.0";