From 16fa9bb05180e6b77133620d0a552d9600e25cfe Mon Sep 17 00:00:00 2001 From: Tero Halla-aho Date: Wed, 17 Dec 2025 14:02:17 +0200 Subject: [PATCH 1/3] Run prisma migrate deploy during deploy --- deploy/README.md | 1 + deploy/deploy.sh | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/deploy/README.md b/deploy/README.md index 31da2fe..1750032 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -24,3 +24,4 @@ Deploy commands Notes - Ensure `deploy/.last-image` exists (run `deploy/build.sh` first). - `AUTH_SECRET`/`DATABASE_URL` should be in your env or loaded via `scripts/load-secrets.sh`. +- `deploy/deploy.sh` now runs `prisma migrate deploy` automatically when `DATABASE_URL` is set (recommended for test/staging/prod). diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 1317dd2..1f7f36a 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -71,6 +71,13 @@ APP_VERSION="${APP_VERSION:-$(echo \"$IMAGE\" | awk -F: '{print $NF}')}" export K8S_NAMESPACE APP_HOST API_HOST NEXT_PUBLIC_SITE_URL NEXT_PUBLIC_API_BASE APP_ENV CLUSTER_ISSUER INGRESS_CLASS APP_REPLICAS K8S_IMAGE APP_VERSION +if [[ -n "${DATABASE_URL:-}" ]]; then + echo "Running Prisma migrations for APP_ENV=$APP_ENV" + npx prisma migrate deploy +else + echo "DATABASE_URL not set; skipping Prisma migrations" >&2 +fi + TMP_MANIFEST=$(mktemp) envsubst < k8s/app.yaml > "$TMP_MANIFEST" From 675c0c0924fb78b3fced89f6bbdac24e540f26fc Mon Sep 17 00:00:00 2001 From: Tero Halla-aho Date: Thu, 18 Dec 2025 12:45:30 +0200 Subject: [PATCH 2/3] deploy: run migrations using in-cluster DATABASE_URL --- deploy/README.md | 2 +- deploy/deploy.sh | 31 +++++++++++++++++++++++++------ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/deploy/README.md b/deploy/README.md index 1750032..76722af 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -24,4 +24,4 @@ Deploy commands Notes - Ensure `deploy/.last-image` exists (run `deploy/build.sh` first). - `AUTH_SECRET`/`DATABASE_URL` should be in your env or loaded via `scripts/load-secrets.sh`. -- `deploy/deploy.sh` now runs `prisma migrate deploy` automatically when `DATABASE_URL` is set (recommended for test/staging/prod). +- `deploy/deploy.sh` runs `prisma migrate deploy` automatically when `DATABASE_URL` is set; if it isn't, it will try to read `DATABASE_URL` from the in-cluster `lomavuokraus-web-secrets` in the target namespace (recommended for test/staging/prod). diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 1f7f36a..4b19c27 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -71,12 +71,31 @@ APP_VERSION="${APP_VERSION:-$(echo \"$IMAGE\" | awk -F: '{print $NF}')}" export K8S_NAMESPACE APP_HOST API_HOST NEXT_PUBLIC_SITE_URL NEXT_PUBLIC_API_BASE APP_ENV CLUSTER_ISSUER INGRESS_CLASS APP_REPLICAS K8S_IMAGE APP_VERSION -if [[ -n "${DATABASE_URL:-}" ]]; then - echo "Running Prisma migrations for APP_ENV=$APP_ENV" - npx prisma migrate deploy -else - echo "DATABASE_URL not set; skipping Prisma migrations" >&2 -fi +maybe_run_prisma_migrations() { + local db_url="${DATABASE_URL:-}" + if [[ -z "$db_url" ]]; then + # If DATABASE_URL isn't available locally, try to reuse the in-cluster secret. + # This prevents "works in cluster but deploy skipped migrations" drift. + if command -v kubectl >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then + if kubectl -n "$K8S_NAMESPACE" get secret lomavuokraus-web-secrets >/dev/null 2>&1; then + db_url="$( + kubectl -n "$K8S_NAMESPACE" get secret lomavuokraus-web-secrets -o json \ + | jq -r '.data.DATABASE_URL // empty' \ + | base64 -d 2>/dev/null || true + )" + fi + fi + fi + + if [[ -n "$db_url" ]]; then + echo "Running Prisma migrations for APP_ENV=$APP_ENV (namespace=$K8S_NAMESPACE)" + DATABASE_URL="$db_url" npx prisma migrate deploy + else + echo "DATABASE_URL not set and lomavuokraus-web-secrets/DATABASE_URL not found; skipping Prisma migrations" >&2 + fi +} + +maybe_run_prisma_migrations TMP_MANIFEST=$(mktemp) envsubst < k8s/app.yaml > "$TMP_MANIFEST" From fd177ca5fa34151a469d89f8a94c853c9b1bf4ac Mon Sep 17 00:00:00 2001 From: Tero Halla-aho Date: Thu, 18 Dec 2025 13:02:22 +0200 Subject: [PATCH 3/3] ui: group admin links under dropdown --- PROGRESS.md | 1 + app/components/NavBar.tsx | 89 +++++++++++++++++++++++++++++++-------- app/globals.css | 35 +++++++++++++++ lib/i18n.ts | 3 ++ 4 files changed, 110 insertions(+), 18 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index f8d47f2..f842433 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -87,3 +87,4 @@ - Footer now includes a minimal cookie usage statement (essential cookies only; site requires acceptance). - Forgejo deployment scaffolding added: Docker Compose + runner config guidance and Apache vhost for git.halla-aho.net, plus CI workflow placeholder under `.forgejo/workflows/`. - Amenities: added separate EV charging flags (on-site vs nearby) plus wheelchair accessibility, including browse filters and admin approvals view badges. +- Navbar: combined admin actions (approvals/users/monitoring) under a single “Admin” dropdown menu. diff --git a/app/components/NavBar.tsx b/app/components/NavBar.tsx index 438b679..aea646d 100644 --- a/app/components/NavBar.tsx +++ b/app/components/NavBar.tsx @@ -2,7 +2,7 @@ import Link from 'next/link'; import Image from 'next/image'; -import { useEffect, useState, type SVGProps } from 'react'; +import { useEffect, useRef, useState, type SVGProps } from 'react'; import { useI18n } from './I18nProvider'; import logo from '../../img/logo.png'; @@ -91,6 +91,13 @@ function Icon({ name }: { name: string }) { ); + case 'admin': + return ( + + + + + ); default: return null; } @@ -100,6 +107,8 @@ export default function NavBar() { const { t, locale, setLocale } = useI18n(); const [user, setUser] = useState(null); const [pendingCount, setPendingCount] = useState(0); + const [adminMenuOpen, setAdminMenuOpen] = useState(false); + const adminMenuRef = useRef(null); async function loadUser() { try { @@ -116,6 +125,10 @@ export default function NavBar() { loadUser(); }, []); + useEffect(() => { + if (!user) setAdminMenuOpen(false); + }, [user]); + useEffect(() => { const role = user?.role; const canSeeApprovals = role === 'ADMIN' || role === 'LISTING_MODERATOR' || role === 'USER_MODERATOR'; @@ -144,6 +157,26 @@ export default function NavBar() { const isListingMod = user?.role === 'LISTING_MODERATOR'; const isUserMod = user?.role === 'USER_MODERATOR'; const showApprovals = Boolean(user && (isAdmin || isListingMod || isUserMod)); + const showAdminMenu = Boolean(user && (showApprovals || isAdmin)); + + useEffect(() => { + if (!adminMenuOpen) return; + const onMouseDown = (e: MouseEvent) => { + const target = e.target as Node | null; + if (target && adminMenuRef.current && !adminMenuRef.current.contains(target)) { + setAdminMenuOpen(false); + } + }; + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') setAdminMenuOpen(false); + }; + document.addEventListener('mousedown', onMouseDown); + document.addEventListener('keydown', onKeyDown); + return () => { + document.removeEventListener('mousedown', onMouseDown); + document.removeEventListener('keydown', onKeyDown); + }; + }, [adminMenuOpen]); return (
@@ -171,27 +204,47 @@ export default function NavBar() { {t('navNewListing')} - {showApprovals ? ( - <> - - {t('navApprovals')} - {pendingCount > 0 ? ( - + {showAdminMenu ? ( +
+ + {adminMenuOpen ? ( +
+ {showApprovals ? ( + setAdminMenuOpen(false)}> + {t('navApprovals')} + {pendingCount > 0 ? ( + + {t('approvalsBadge', { count: pendingCount })} + + ) : null} + + ) : null} + {isAdmin ? ( + setAdminMenuOpen(false)}> + {t('navUsers')} + + ) : null} + {isAdmin ? ( + setAdminMenuOpen(false)}> + {t('navMonitoring')} + + ) : null} +
) : null} - {isAdmin ? ( - - {t('navMonitoring')} - - ) : null} - +
) : null}