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
+
+
+
+
+
+
+ | ID |
+ Type |
+ Status |
+ Created |
+
+
+
+ {jobs.map((job) => (
+
+
+ {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}
+
+
+
+
+
+
+
+
+ | Name |
+ Scope |
+ Prefix |
+ Revoked |
+ Actions |
+
+
+
+ {tokens.map((token) => (
+
+ | {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