v0.7.0 — Suggestions module via @otm/account-panel
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) <noreply@anthropic.com>
This commit is contained in:
13
src/app/(dashboard)/suggestions/page.tsx
Normal file
13
src/app/(dashboard)/suggestions/page.tsx
Normal file
@@ -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 <SuggestionsPanel />;
|
||||
}
|
||||
9
src/app/api/suggestions/[number]/comments/route.ts
Normal file
9
src/app/api/suggestions/[number]/comments/route.ts
Normal file
@@ -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,
|
||||
});
|
||||
9
src/app/api/suggestions/route.ts
Normal file
9
src/app/api/suggestions/route.ts
Normal file
@@ -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,
|
||||
});
|
||||
21
src/app/api/suggestions/upload/route.ts
Normal file
21
src/app/api/suggestions/upload/route.ts
Normal file
@@ -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}` };
|
||||
},
|
||||
});
|
||||
@@ -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 },
|
||||
];
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
20
src/lib/suggestions.ts
Normal file
20
src/lib/suggestions.ts
Normal file
@@ -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<boolean> {
|
||||
const session = await getSession();
|
||||
return !!session?.user;
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user