diff --git a/.env.example b/.env.example index 8c6891a..3bb00fc 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ API_LISTEN_ADDR=0.0.0.0:8080 -ADMIN_UI_LISTEN_ADDR=0.0.0.0:8082 +BACKOFFICE_PORT=8082 API_BASE_URL=http://api:8080 INDEXER_LISTEN_ADDR=0.0.0.0:8081 INDEXER_SCAN_INTERVAL_SECONDS=5 diff --git a/.gitignore b/.gitignore index f12e09c..9055057 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ target/ .DS_Store tmp/ libraries/ +node_modules/ +.next/ diff --git a/PLAN.md b/PLAN.md index 655e7e3..380b16e 100644 --- a/PLAN.md +++ b/PLAN.md @@ -5,7 +5,7 @@ Construire un serveur ultra performant pour indexer et servir des bibliotheques - API REST (librairies, livres, metadonnees, recherche, streaming pages), - indexation incrementale, - recherche full-text, -- admin UI (Rust SSR) pour gerer librairies/jobs/tokens, + - backoffice web (Next.js) pour gerer librairies/jobs/tokens, - deploiement Docker Compose homelab. ## Decisions figees @@ -19,6 +19,7 @@ Construire un serveur ultra performant pour indexer et servir des bibliotheques - Rendu PDF: a la volee - CBR: extraction temporaire disque (`unrar-free`, commande `unrar`) + cleanup - Formats pages: `webp`, `jpeg`, `png` +- Backoffice UI: Next.js (remplace Rust SSR) --- @@ -134,12 +135,14 @@ Construire un serveur ultra performant pour indexer et servir des bibliotheques **DoD:** Service stable sous charge homelab. -### T16 - Admin UI Rust SSR +### T16 - Backoffice Next.js +- [x] Bootstrap app Next.js (`apps/backoffice`) - [x] Vue Libraries - [x] Vue Jobs - [x] Vue API Tokens (create/list/revoke) +- [x] Auth backoffice via token API -**DoD:** Admin complet utilisable sans SPA lourde. +**DoD:** Backoffice Next.js utilisable pour l'administration complete. ### T17 - Observabilite et hardening - [x] Logs structures `tracing` @@ -206,3 +209,5 @@ Construire un serveur ultra performant pour indexer et servir des bibliotheques - 2026-03-05: Lot 3 avancee: endpoint pages (`/books/:id/pages/:n`) actif avec cache LRU, ETag/Cache-Control, limite concurrence rendu et timeouts. - 2026-03-05: hardening API: readiness expose sans auth via `route_layer`, metriques simples `/metrics`, rate limiting lecture (120 req/s). - 2026-03-05: smoke + bench scripts corriges et verifies (`infra/smoke.sh`, `infra/bench.sh`). +- 2026-03-05: pivot backoffice valide: remplacement de l'admin UI Rust SSR par une app Next.js. +- 2026-03-05: backoffice Next.js implemente (`apps/backoffice`) avec branding neon base sur le logo, actions libraries/jobs/tokens, et integration Docker Compose. diff --git a/apps/backoffice/Dockerfile b/apps/backoffice/Dockerfile new file mode 100644 index 0000000..1b14390 --- /dev/null +++ b/apps/backoffice/Dockerfile @@ -0,0 +1,21 @@ +FROM node:22-alpine AS deps +WORKDIR /app +COPY apps/backoffice/package.json apps/backoffice/package-lock.json ./ +RUN npm ci + +FROM node:22-alpine AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY apps/backoffice/ ./ +RUN npm run build + +FROM node:22-alpine AS runner +WORKDIR /app +ENV NODE_ENV=production +ENV PORT=8082 +RUN apk add --no-cache wget +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static +COPY --from=builder /app/public ./public +EXPOSE 8082 +CMD ["node", "server.js"] diff --git a/apps/backoffice/app/globals.css b/apps/backoffice/app/globals.css new file mode 100644 index 0000000..ea45aca --- /dev/null +++ b/apps/backoffice/app/globals.css @@ -0,0 +1,279 @@ +:root { + color-scheme: light; + --background: hsl(36 33% 97%); + --foreground: hsl(222 33% 15%); + --card: hsl(0 0% 100%); + --line: hsl(32 18% 84%); + --line-strong: hsl(32 18% 76%); + --primary: hsl(198 78% 37%); + --primary-soft: hsl(198 52% 90%); + --cyan: hsl(192 85% 55%); + --pink: hsl(338 82% 62%); + --text-muted: hsl(220 13% 40%); + --shadow-1: 0 1px 2px 0 rgb(23 32 46 / 0.06); + --shadow-2: 0 12px 30px -12px rgb(23 32 46 / 0.22); +} + +.dark { + color-scheme: dark; + --background: hsl(222 35% 10%); + --foreground: hsl(38 20% 92%); + --card: hsl(221 31% 13%); + --line: hsl(219 18% 25%); + --line-strong: hsl(219 18% 33%); + --primary: hsl(194 76% 62%); + --primary-soft: hsl(210 34% 24%); + --cyan: hsl(192 85% 55%); + --pink: hsl(338 82% 62%); + --text-muted: hsl(218 17% 72%); + --shadow-1: 0 1px 2px 0 rgb(2 8 18 / 0.35); + --shadow-2: 0 12px 30px -12px rgb(2 8 18 / 0.55); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + color: var(--foreground); + font-family: "Avenir Next", "Segoe UI", "Noto Sans", sans-serif; + background: var(--background); + min-height: 100vh; +} + +body::before { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + background-image: + linear-gradient(112deg, hsl(198 78% 37% / 0.14) 0%, hsl(192 85% 55% / 0.11) 28%, transparent 56%), + linear-gradient(248deg, hsl(338 82% 62% / 0.11) 0%, transparent 46%), + repeating-linear-gradient(135deg, hsl(222 33% 15% / 0.035) 0 1px, transparent 1px 11px); + opacity: 1; +} + +main { + position: relative; + max-width: 1050px; + margin: 0 auto; + padding: 28px 22px 40px; +} + +h1, +h2, +h3 { + font-family: "Baskerville", "Times New Roman", serif; + letter-spacing: 0.01em; +} + +.top-nav { + position: sticky; + top: 0; + z-index: 3; + display: flex; + justify-content: space-between; + align-items: center; + gap: 18px; + padding: 14px 22px; + border-bottom: 1px solid hsl(198 78% 37% / 0.25); + background: hsl(36 33% 97% / 0.72); + backdrop-filter: blur(10px); + box-shadow: var(--shadow-1); +} + +.brand { + display: flex; + align-items: center; + gap: 10px; + text-decoration: none; + color: var(--foreground); +} + +.brand img { + border-radius: 8px; + box-shadow: 0 8px 24px -10px rgb(23 32 46 / 0.35); +} + +.brand-name { + background: linear-gradient(90deg, var(--primary), var(--cyan), var(--pink)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + font-size: 1.02rem; + font-weight: 800; + letter-spacing: 0.06em; +} + +.brand-subtitle { + margin-left: -4px; + text-transform: uppercase; + letter-spacing: 0.12em; + font-size: 0.68rem; + color: var(--text-muted); + font-weight: 700; +} + +.links { + display: flex; + gap: 14px; +} + +.links-wrap { + display: flex; + align-items: center; + gap: 10px; +} + +a { + color: var(--primary); + text-decoration: none; + font-weight: 700; +} + +.links a { + padding: 7px 10px; + border-radius: 8px; +} + +.links a:hover { + background: hsl(198 52% 90% / 0.65); + color: hsl(222 33% 15%); +} + +table { + width: 100%; + border-collapse: collapse; + background: hsl(0 0% 100% / 0.95); + border: 1px solid var(--line); + border-radius: 12px; + overflow: hidden; + box-shadow: var(--shadow-2); +} + +th, +td { + padding: 10px; + border-bottom: 1px solid hsl(32 18% 86%); + text-align: left; +} + +th { + color: hsl(222 33% 18%); + background: hsl(36 30% 95%); +} + +code, +pre { + font-family: "JetBrains Mono", "SF Mono", monospace; +} + +pre { + margin: 10px 0 0; + white-space: pre-wrap; + word-break: break-all; + background: hsl(36 24% 93%); + border: 1px solid var(--line); + border-radius: 8px; + padding: 10px; +} + +form.inline { + display: inline; +} + +input, +select, +button { + border-radius: 8px; + padding: 8px 10px; + margin: 4px 8px 4px 0; + border: 1px solid var(--line-strong); + background: white; + color: var(--foreground); +} + +button { + border: 1px solid hsl(198 78% 37% / 0.45); + background: linear-gradient(95deg, hsl(198 78% 37% / 0.12), hsl(192 85% 55% / 0.15)); + color: hsl(222 33% 16%); + cursor: pointer; + font-weight: 700; +} + +button:hover { + box-shadow: 0 6px 20px -10px hsl(198 78% 37% / 0.5); +} + +.theme-toggle { + min-width: 66px; +} + +.card { + background: var(--card); + border: 1px solid var(--line); + border-radius: 14px; + padding: 16px; + margin: 16px 0; + box-shadow: var(--shadow-2); +} + +.dark .top-nav { + background: hsl(222 35% 10% / 0.72); + border-bottom-color: hsl(194 76% 62% / 0.3); +} + +.dark .links a:hover { + background: hsl(210 34% 24% / 0.8); + color: hsl(38 20% 92%); +} + +.dark table { + background: hsl(221 31% 13% / 0.95); +} + +.dark th { + color: hsl(38 20% 92%); + background: hsl(221 24% 17%); +} + +.dark th, +.dark td { + border-bottom-color: hsl(219 18% 22%); +} + +.dark input, +.dark select, +.dark button { + background: hsl(221 31% 13%); + color: hsl(38 20% 92%); +} + +.dark pre { + background: hsl(221 24% 17%); +} + +@media (max-width: 720px) { + .top-nav { + flex-direction: column; + align-items: flex-start; + } + + .links-wrap { + width: 100%; + justify-content: space-between; + } + + .links { + flex-wrap: wrap; + } + + .brand span { + font-size: 14px; + } + + main { + padding: 18px 14px 30px; + } +} diff --git a/apps/backoffice/app/health/route.ts b/apps/backoffice/app/health/route.ts new file mode 100644 index 0000000..4069c33 --- /dev/null +++ b/apps/backoffice/app/health/route.ts @@ -0,0 +1,6 @@ +export async function GET() { + return new Response("ok", { + status: 200, + headers: { "content-type": "text/plain; charset=utf-8" } + }); +} diff --git a/apps/backoffice/app/jobs/page.tsx b/apps/backoffice/app/jobs/page.tsx new file mode 100644 index 0000000..f4a7fd6 --- /dev/null +++ b/apps/backoffice/app/jobs/page.tsx @@ -0,0 +1,41 @@ +import { listJobs } from "../../lib/api"; + +export const dynamic = "force-dynamic"; + +export default async function JobsPage() { + const jobs = await listJobs().catch(() => []); + + return ( + <> +

Index Jobs

+
+
+ + +
+
+ + + + + + + + + + + {jobs.map((job) => ( + + + + + + + ))} + +
IDTypeStatusCreated
+ {job.id} + {job.type}{job.status}{job.created_at}
+ + ); +} diff --git a/apps/backoffice/app/jobs/rebuild/route.ts b/apps/backoffice/app/jobs/rebuild/route.ts new file mode 100644 index 0000000..e4fa592 --- /dev/null +++ b/apps/backoffice/app/jobs/rebuild/route.ts @@ -0,0 +1,17 @@ +import { revalidatePath } from "next/cache"; +import { NextResponse } from "next/server"; +import { apiFetch } from "../../../lib/api"; + +export async function POST(req: Request) { + const form = await req.formData(); + const libraryId = String(form.get("library_id") || "").trim(); + const body = libraryId ? { library_id: libraryId } : {}; + + await apiFetch("/index/rebuild", { + method: "POST", + body: JSON.stringify(body) + }).catch(() => null); + + revalidatePath("/jobs"); + return NextResponse.redirect(new URL("/jobs", req.url)); +} diff --git a/apps/backoffice/app/layout.tsx b/apps/backoffice/app/layout.tsx new file mode 100644 index 0000000..52f43cc --- /dev/null +++ b/apps/backoffice/app/layout.tsx @@ -0,0 +1,40 @@ +import type { Metadata } from "next"; +import Image from "next/image"; +import Link from "next/link"; +import type { ReactNode } from "react"; +import "./globals.css"; +import { ThemeProvider } from "./theme-provider"; +import { ThemeToggle } from "./theme-toggle"; + +export const metadata: Metadata = { + title: "Stripstream Backoffice", + description: "Backoffice administration for Stripstream Librarian" +}; + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + + + +
{children}
+
+ + + ); +} diff --git a/apps/backoffice/app/page.tsx b/apps/backoffice/app/page.tsx new file mode 100644 index 0000000..b94c3f3 --- /dev/null +++ b/apps/backoffice/app/page.tsx @@ -0,0 +1,11 @@ +export default function DashboardPage() { + return ( + <> +

Stripstream Backoffice

+

Manage libraries, indexing jobs, and API tokens from a Next.js admin interface.

+
+

Use the navigation links above to access each admin section.

+
+ + ); +} diff --git a/apps/backoffice/app/theme-provider.tsx b/apps/backoffice/app/theme-provider.tsx new file mode 100644 index 0000000..ebb7579 --- /dev/null +++ b/apps/backoffice/app/theme-provider.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { ThemeProvider as NextThemesProvider } from "next-themes"; +import type { ReactNode } from "react"; + +export function ThemeProvider({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/apps/backoffice/app/theme-toggle.tsx b/apps/backoffice/app/theme-toggle.tsx new file mode 100644 index 0000000..c01a562 --- /dev/null +++ b/apps/backoffice/app/theme-toggle.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useTheme } from "next-themes"; + +export function ThemeToggle() { + const { theme, setTheme, resolvedTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + const activeTheme = theme === "system" ? resolvedTheme : theme; + const nextTheme = activeTheme === "dark" ? "light" : "dark"; + + return ( + + ); +} diff --git a/apps/backoffice/app/tokens/create/route.ts b/apps/backoffice/app/tokens/create/route.ts new file mode 100644 index 0000000..cb15bcf --- /dev/null +++ b/apps/backoffice/app/tokens/create/route.ts @@ -0,0 +1,27 @@ +import { revalidatePath } from "next/cache"; +import { NextResponse } from "next/server"; +import { apiFetch } from "../../../lib/api"; + +type CreatedToken = { token: string }; + +export async function POST(req: Request) { + const form = await req.formData(); + const name = String(form.get("name") || "").trim(); + const scope = String(form.get("scope") || "read").trim(); + + let created = ""; + if (name) { + const res = await apiFetch("/admin/tokens", { + method: "POST", + body: JSON.stringify({ name, scope }) + }).catch(() => null); + created = res?.token || ""; + } + + revalidatePath("/tokens"); + const url = new URL("/tokens", req.url); + if (created) { + url.searchParams.set("created", created); + } + return NextResponse.redirect(url); +} diff --git a/apps/backoffice/app/tokens/page.tsx b/apps/backoffice/app/tokens/page.tsx new file mode 100644 index 0000000..627ba37 --- /dev/null +++ b/apps/backoffice/app/tokens/page.tsx @@ -0,0 +1,66 @@ +import { listTokens } from "../../lib/api"; + +export const dynamic = "force-dynamic"; + +export default async function TokensPage({ + searchParams +}: { + searchParams: Promise<{ created?: string }>; +}) { + const params = await searchParams; + const tokens = await listTokens().catch(() => []); + + return ( + <> +

API Tokens

+ + {params.created ? ( +
+ Token created: +
{params.created}
+
+ ) : null} + +
+
+ + + +
+
+ + + + + + + + + + + + + {tokens.map((token) => ( + + + + + + + + ))} + +
NameScopePrefixRevokedActions
{token.name}{token.scope} + {token.prefix} + {token.revoked_at ? "yes" : "no"} +
+ + +
+
+ + ); +} diff --git a/apps/backoffice/app/tokens/revoke/route.ts b/apps/backoffice/app/tokens/revoke/route.ts new file mode 100644 index 0000000..d91cb31 --- /dev/null +++ b/apps/backoffice/app/tokens/revoke/route.ts @@ -0,0 +1,13 @@ +import { revalidatePath } from "next/cache"; +import { NextResponse } from "next/server"; +import { apiFetch } from "../../../lib/api"; + +export async function POST(req: Request) { + const form = await req.formData(); + const id = String(form.get("id") || "").trim(); + if (id) { + await apiFetch(`/admin/tokens/${id}`, { method: "DELETE" }).catch(() => null); + } + revalidatePath("/tokens"); + return NextResponse.redirect(new URL("/tokens", req.url)); +} diff --git a/apps/backoffice/lib/api.ts b/apps/backoffice/lib/api.ts new file mode 100644 index 0000000..a8ffe13 --- /dev/null +++ b/apps/backoffice/lib/api.ts @@ -0,0 +1,67 @@ +export type LibraryDto = { + id: string; + name: string; + root_path: string; + enabled: boolean; +}; + +export type IndexJobDto = { + id: string; + type: string; + status: string; + created_at: string; +}; + +export type TokenDto = { + id: string; + name: string; + scope: string; + prefix: string; + revoked_at: string | null; +}; + +function config() { + const baseUrl = process.env.API_BASE_URL || "http://api:8080"; + const token = process.env.API_BOOTSTRAP_TOKEN; + if (!token) { + throw new Error("API_BOOTSTRAP_TOKEN is required for backoffice"); + } + return { baseUrl: baseUrl.replace(/\/$/, ""), token }; +} + +export async function apiFetch(path: string, init?: RequestInit): Promise { + const { baseUrl, token } = config(); + const headers = new Headers(init?.headers || {}); + headers.set("Authorization", `Bearer ${token}`); + if (init?.body && !headers.has("Content-Type")) { + headers.set("Content-Type", "application/json"); + } + + const res = await fetch(`${baseUrl}${path}`, { + ...init, + headers, + cache: "no-store" + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`API ${path} failed (${res.status}): ${text}`); + } + + if (res.status === 204) { + return null as T; + } + return (await res.json()) as T; +} + +export async function listLibraries() { + return apiFetch("/libraries"); +} + +export async function listJobs() { + return apiFetch("/index/status"); +} + +export async function listTokens() { + return apiFetch("/admin/tokens"); +} diff --git a/apps/backoffice/next-env.d.ts b/apps/backoffice/next-env.d.ts new file mode 100644 index 0000000..9edff1c --- /dev/null +++ b/apps/backoffice/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +import "./.next/types/routes.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/backoffice/next.config.mjs b/apps/backoffice/next.config.mjs new file mode 100644 index 0000000..ed75d0a --- /dev/null +++ b/apps/backoffice/next.config.mjs @@ -0,0 +1,7 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: "standalone", + typedRoutes: true +}; + +export default nextConfig; diff --git a/apps/backoffice/package-lock.json b/apps/backoffice/package-lock.json new file mode 100644 index 0000000..9270510 --- /dev/null +++ b/apps/backoffice/package-lock.json @@ -0,0 +1,1047 @@ +{ + "name": "stripstream-backoffice", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "stripstream-backoffice", + "version": "0.1.0", + "dependencies": { + "next": "^16.1.6", + "next-themes": "^0.4.6", + "react": "19.0.0", + "react-dom": "19.0.0" + }, + "devDependencies": { + "@types/node": "22.13.14", + "@types/react": "19.0.12", + "@types/react-dom": "19.0.5", + "typescript": "5.8.2" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@next/env": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz", + "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz", + "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", + "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", + "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", + "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", + "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", + "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", + "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", + "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/node": { + "version": "22.13.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", + "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/react": { + "version": "19.0.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.12.tgz", + "integrity": "sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.0.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.5.tgz", + "integrity": "sha512-DJXtIUr9XyoKNEBKRk3QkdiFZ10M9VNZrO15AR331rWXd4niNj5Rox9SsRwiTgqDR6C9aURk+1QHO6S5chU+IA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001776", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001776.tgz", + "integrity": "sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", + "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", + "license": "MIT", + "dependencies": { + "@next/env": "16.1.6", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.8.3", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.1.6", + "@next/swc-darwin-x64": "16.1.6", + "@next/swc-linux-arm64-gnu": "16.1.6", + "@next/swc-linux-arm64-musl": "16.1.6", + "@next/swc-linux-x64-gnu": "16.1.6", + "@next/swc-linux-x64-musl": "16.1.6", + "@next/swc-win32-arm64-msvc": "16.1.6", + "@next/swc-win32-x64-msvc": "16.1.6", + "sharp": "^0.34.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.25.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/apps/backoffice/package.json b/apps/backoffice/package.json new file mode 100644 index 0000000..6151d69 --- /dev/null +++ b/apps/backoffice/package.json @@ -0,0 +1,22 @@ +{ + "name": "stripstream-backoffice", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev -p 8082", + "build": "next build", + "start": "next start -p 8082" + }, + "dependencies": { + "next": "^16.1.6", + "next-themes": "^0.4.6", + "react": "19.0.0", + "react-dom": "19.0.0" + }, + "devDependencies": { + "@types/node": "22.13.14", + "@types/react": "19.0.12", + "@types/react-dom": "19.0.5", + "typescript": "5.8.2" + } +} diff --git a/apps/backoffice/public/logo.png b/apps/backoffice/public/logo.png new file mode 100644 index 0000000..d1a478b Binary files /dev/null and b/apps/backoffice/public/logo.png differ diff --git a/apps/backoffice/tsconfig.json b/apps/backoffice/tsconfig.json new file mode 100644 index 0000000..e0c8e33 --- /dev/null +++ b/apps/backoffice/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": [ + "dom", + "dom.iterable", + "es2022" + ], + "allowJs": false, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ] + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index 0888e3e..63e3e5c 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -89,14 +89,17 @@ services: timeout: 5s retries: 5 - admin-ui: + backoffice: build: context: .. - dockerfile: apps/admin-ui/Dockerfile + dockerfile: apps/backoffice/Dockerfile env_file: - ../.env + environment: + - PORT=${BACKOFFICE_PORT:-8082} + - HOSTNAME=0.0.0.0 ports: - - "8082:8082" + - "${BACKOFFICE_PORT:-8082}:8082" depends_on: api: condition: service_healthy diff --git a/infra/smoke.sh b/infra/smoke.sh index f98c8fb..70f51a1 100755 --- a/infra/smoke.sh +++ b/infra/smoke.sh @@ -3,7 +3,7 @@ set -euo pipefail BASE_API="${BASE_API:-http://127.0.0.1:8080}" BASE_INDEXER="${BASE_INDEXER:-http://127.0.0.1:8081}" -BASE_ADMIN="${BASE_ADMIN:-http://127.0.0.1:8082}" +BASE_BACKOFFICE="${BASE_BACKOFFICE:-${BASE_ADMIN:-http://127.0.0.1:8082}}" TOKEN="${API_TOKEN:-stripstream-dev-bootstrap-token}" echo "[smoke] health checks" @@ -11,7 +11,7 @@ curl -fsS "$BASE_API/health" >/dev/null curl -fsS "$BASE_API/ready" >/dev/null curl -fsS "$BASE_INDEXER/health" >/dev/null curl -fsS "$BASE_INDEXER/ready" >/dev/null -curl -fsS "$BASE_ADMIN/health" >/dev/null +curl -fsS "$BASE_BACKOFFICE/health" >/dev/null echo "[smoke] list libraries" curl -fsS -H "Authorization: Bearer $TOKEN" "$BASE_API/libraries" >/dev/null