diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml new file mode 100644 index 0000000..1241ab7 --- /dev/null +++ b/.forgejo/workflows/deploy.yml @@ -0,0 +1,42 @@ +name: Deploy release to production + +# Pushing a release tag (vX.Y.Z) deploys that tag to the portal at +# pms.pelagiamarine.com. Runs on the pms1 host runner (label: host), +# which executes as shad0w with direct access to the pm2-managed app. + +on: + push: + tags: + - "v*" + +jobs: + deploy: + runs-on: host + steps: + - name: Deploy tag to ~/pms and restart ppms + run: | + set -euo pipefail + export NVM_DIR="$HOME/.nvm" + . "$NVM_DIR/nvm.sh" + + TAG="${GITHUB_REF_NAME}" + echo "=== Deploying $TAG ===" + + cd "$HOME/pms" + git fetch origin --tags --force + git checkout -f "refs/tags/$TAG" + + cd App + pnpm install --frozen-lockfile + pnpm build # includes prisma generate + pnpm db:migrate:deploy + + pm2 restart ppms --update-env + echo "=== Deployed $TAG ===" + + - name: Verify portal responds + run: | + sleep 5 + code=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/login) + echo "Portal /login returned HTTP $code" + test "$code" = "200" diff --git a/.gitignore b/.gitignore index 341c6be..bd4dceb 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,11 @@ App/coverage/ .codex/ .antigravity/ +# Claude issue watcher (real token + logs stay local) +automation/watcher.config.json +automation/logs/ +automation/.watcher.lock + # OS .DS_Store Thumbs.db diff --git a/App/.env.example b/App/.env.example index 4d7757b..3001b4b 100644 --- a/App/.env.example +++ b/App/.env.example @@ -48,3 +48,9 @@ EMAIL_FROM_NAME="Pelagia Portal" # Development default (localhost:3002) is used if this is unset. # Start the service with: cd GstService && npm run dev GST_SERVICE_URL=http://localhost:3003 + +# ── Forgejo issue reporting (Report Issue button) ───────────── +# Token needs write:issue scope on the repo below. +FORGEJO_URL=https://git.pelagiamarine.com +FORGEJO_REPO=shad0w/pelagia-portal +FORGEJO_TOKEN= diff --git a/App/components/layout/header.tsx b/App/components/layout/header.tsx index 0189acc..c70d7f8 100644 --- a/App/components/layout/header.tsx +++ b/App/components/layout/header.tsx @@ -5,6 +5,7 @@ import { LogOut } from "lucide-react"; import type { Role } from "@prisma/client"; import { CartIcon } from "./cart-icon"; import { NotificationBell } from "./notification-bell"; +import { ReportIssueButton } from "./report-issue-button"; const ROLE_LABELS: Record = { TECHNICAL: "Technical", @@ -39,6 +40,7 @@ export function Header({ user, initialUnreadCount, initialNotifications }: Heade
{CART_ROLES.includes(user.role) && } + { + const session = await auth(); + if (!session?.user) return { error: "Unauthorized" }; + + const parsed = reportSchema.safeParse(Object.fromEntries(formData)); + if (!parsed.success) return { error: parsed.error.errors[0].message }; + const { title, description, priority, page } = parsed.data; + + const body = [ + "### Raised by", + `${session.user.name} (${session.user.email}, ${session.user.role}) — via portal Report Issue button`, + "", + "### Description", + description, + "", + "### Priority", + priority, + "", + "### Context", + `- Page: \`${page || "unknown"}\``, + `- Reported at: ${new Date().toISOString()}`, + ].join("\n"); + + try { + const issue = await createForgejoIssue({ + title: `[Issue]: ${title}`, + body, + labels: ["portal", "claude-queue"], + }); + return { ok: true, issueNumber: issue.number, issueUrl: issue.html_url }; + } catch (err) { + console.error("reportIssue failed:", err); + return { error: "Could not file the issue. Please try again or contact the administrator." }; + } +} diff --git a/App/components/layout/report-issue-button.tsx b/App/components/layout/report-issue-button.tsx new file mode 100644 index 0000000..bf61793 --- /dev/null +++ b/App/components/layout/report-issue-button.tsx @@ -0,0 +1,130 @@ +"use client"; + +import { useState, useTransition } from "react"; +import { usePathname } from "next/navigation"; +import { Bug } from "lucide-react"; +import { AdminDialog } from "@/components/ui/admin-dialog"; +import { reportIssue } from "./report-issue-actions"; + +const PRIORITIES = [ + "P0 — Critical (broken / blocking)", + "P1 — High", + "P2 — Medium", + "P3 — Low", +] as const; + +export function ReportIssueButton() { + const pathname = usePathname(); + const [open, setOpen] = useState(false); + const [pending, startTransition] = useTransition(); + const [error, setError] = useState(null); + const [filedIssue, setFiledIssue] = useState<{ number: number; url: string } | null>(null); + + function close() { + setOpen(false); + setError(null); + setFiledIssue(null); + } + + function onSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + const formData = new FormData(e.currentTarget); + formData.set("page", pathname); + startTransition(async () => { + const result = await reportIssue(formData); + if ("error" in result) { + setError(result.error); + } else { + setFiledIssue({ number: result.issueNumber, url: result.issueUrl }); + } + }); + } + + return ( + <> + + + + {filedIssue ? ( +
+

+ Thanks — issue #{filedIssue.number} has been + filed and queued for an automated fix. You'll see the change in an upcoming + release. +

+ +
+ ) : ( +
+
+ + +
+
+ +