diff --git a/README.md b/README.md index b55b590..c02a988 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![CI](https://github.com/rennerdo30/WebEncode/actions/workflows/ci.yml/badge.svg)](https://github.com/rennerdo30/WebEncode/actions/workflows/ci.yml) - [![Go Version](https://img.shields.io/badge/Go-1.24+-00ADD8?logo=go)](https://go.dev/) + [![Go Version](https://img.shields.io/badge/Go-1.25+-00ADD8?logo=go)](https://go.dev/) [![Next.js](https://img.shields.io/badge/Next.js-16-black?logo=next.js)](https://nextjs.org/) **Distributed, Plugin-Based Video Transcoding Platform** @@ -17,7 +17,7 @@ WebEncode is a high-performance, distributed media processing engine designed fo - **Hyper-Modular Architecture**: 5-Pillar Plugin System (Auth, Storage, Encoder, Live, Publisher) - **Distributed Workers**: Scalable FFmpeg execution engine powered by NATS JetStream -- **Modern UI**: Next.js 16 Dashboard with real-time monitoring +- **Modern UI**: Next.js 16 dashboard with real-time monitoring, dark and light themes, and five UI languages - **Live Streaming**: Zero-config RTMP ingest to HLS via MediaMTX integration - **Production Ready**: Docker, Kubernetes, OpenTelemetry, and structured logging - **Restreaming**: 1:N simulcasting to YouTube, Twitch, Kick, and Rumble with unified chat @@ -25,17 +25,19 @@ WebEncode is a high-performance, distributed media processing engine designed fo ## Technology Stack ### Backend -- **Language**: Go 1.24+ +- **Language**: Go 1.25+ - **Framework**: Chi/v5 (HTTP), gRPC (plugins) -- **Database**: PostgreSQL 16 (pgx/v5) +- **Database**: PostgreSQL 17 (pgx/v5, SQLC) - **Message Bus**: NATS JetStream 2.10+ - **Plugin System**: HashiCorp go-plugin ### Frontend -- **Framework**: Next.js 16 with React 19 -- **UI Components**: Shadcn/ui, Radix UI -- **Styling**: Tailwind CSS 4 +- **Framework**: Next.js 16 (App Router) with React 19 +- **UI Components**: shadcn/ui, Radix UI, Lucide icons +- **Styling**: Tailwind CSS 4 with themed CSS custom properties (dark + light) - **State Management**: TanStack React Query +- **Internationalisation**: next-intl (English, German, Spanish, French, Japanese) +- **Tests**: Vitest, Testing Library ### Infrastructure - **Containerization**: Docker, Docker Compose @@ -47,7 +49,7 @@ WebEncode is a high-performance, distributed media processing engine designed fo ### Prerequisites - [Docker](https://docs.docker.com/get-docker/) and Docker Compose -- [Go 1.24+](https://go.dev/dl/) (for local development) +- [Go 1.25+](https://go.dev/dl/) (for local development) - [Node.js 22+](https://nodejs.org/) (for UI development) ### Run with Docker @@ -75,6 +77,23 @@ make test make test-coverage ``` +### Develop the Dashboard + +The dashboard is a standalone Next.js app in `ui/` and talks to the kernel API. + +```bash +cd ui +npm install +npm run dev # http://localhost:3000 +npm run lint +npm run test:run +``` + +`/api/v1/*` is rewritten to the kernel; point `INTERNAL_API_URL` at it if it is +not listening on http://localhost:8090. The theme (dark by default, light +available from the header toggle) and the UI language are remembered per +browser; the language is also part of the URL, for example `/de/jobs`. + ## Project Structure ``` diff --git a/ui/README.md b/ui/README.md index e215bc4..3e3efeb 100644 --- a/ui/README.md +++ b/ui/README.md @@ -1,36 +1,52 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# WebEncode Dashboard -## Getting Started +The web UI for [WebEncode](../README.md): a Next.js 16 App Router app that +monitors jobs, workers, live streams and restreams against the kernel REST API. -First, run the development server: +## Getting Started ```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev +npm install +npm run dev # http://localhost:3000 ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +Requests to `/api/v1/*` are rewritten to the kernel (see `next.config.ts`). Set +`INTERNAL_API_URL` if the kernel is not listening on `http://localhost:8090`. -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +## Scripts -## Learn More +| Script | Purpose | +| --- | --- | +| `npm run dev` | Development server | +| `npm run build` | Production build (`output: "standalone"`) | +| `npm run start` | Serve a production build | +| `npm run lint` | ESLint (`eslint-config-next`) | +| `npm run test:run` | Vitest suite, single run | +| `npm run test:coverage` | Vitest with V8 coverage | -To learn more about Next.js, take a look at the following resources: +## Structure -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +``` +src/ +├── app/[locale]/ # Localised routes (dashboard, jobs, streams, ...) +├── components/ # App components; components/ui holds the shadcn primitives +├── i18n/ # next-intl routing and request config +├── lib/ # API client, providers, nav/theme/app metadata constants +└── app/globals.css # Theme tokens, component classes, utilities +messages/ # Translation bundles: en, de, es, fr, ja +``` -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +## Theming -## Deploy on Vercel +Colours are CSS custom properties in `src/app/globals.css`. Both a dark (default) +and a light theme are defined; the active one is the presence of the `dark` class +on ``, set before first paint by the init script in `src/lib/theme.ts` and +toggled from the header. Prefer the semantic tokens (`primary`, `brand`, +`success`, `warning`, `danger`, `info`, `muted`, `border`, ...) over fixed +palette utilities such as `text-violet-400`, which only work on one theme. -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +## Internationalisation -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +Text lives in `messages/.json` and is read with `next-intl`. Add new keys +to every bundle, and format dates and numbers with next-intl's `useFormatter` +rather than hardcoding a locale. diff --git a/ui/messages/de.json b/ui/messages/de.json index 2583c9b..21ea598 100644 --- a/ui/messages/de.json +++ b/ui/messages/de.json @@ -25,10 +25,17 @@ "success": "Erfolg", "live": "Live", "offline": "Offline", - "admin": "Admin" + "admin": "Admin", + "viewAll": "Alle anzeigen", + "language": "Sprache", + "openNavigation": "Navigation öffnen", + "mainNavigation": "Hauptnavigation", + "skipToContent": "Zum Hauptinhalt springen", + "switchToDark": "Zu dunklem Design wechseln", + "switchToLight": "Zu hellem Design wechseln" }, "dashboard": { - "welcome": "JWillkommen bei WebEncode", + "welcome": "Willkommen bei WebEncode", "activeJobs": "Aktive Aufträge", "workersOnline": "Worker Online", "completedJobs": "Abgeschlossene Aufträge", @@ -97,6 +104,20 @@ "stackTrace": "Stack-Trace", "timestamp": "Zeitstempel", "path": "Pfad", - "method": "Methode" + "method": "Methode", + "refresh": "Aktualisieren", + "filterBySource": "Nach Quelle filtern", + "allSources": "Alle Quellen", + "markResolved": "Als behoben markieren", + "copyDetails": "Fehlerdetails kopieren", + "noErrors": "Keine Fehler gefunden", + "systemHealthy": "System ist fehlerfrei", + "showStackTrace": "Stacktrace anzeigen" + }, + "footer": { + "label": "Fußzeile", + "copyright": "© {year} {name}. Open Source unter der MIT-Lizenz.", + "documentation": "Dokumentation", + "api": "API" } -} \ No newline at end of file +} diff --git a/ui/messages/en.json b/ui/messages/en.json index dd06536..2ed20cb 100644 --- a/ui/messages/en.json +++ b/ui/messages/en.json @@ -25,7 +25,14 @@ "success": "Success", "live": "Live", "offline": "Offline", - "admin": "Admin" + "admin": "Admin", + "viewAll": "View all", + "language": "Language", + "openNavigation": "Open navigation", + "mainNavigation": "Main navigation", + "skipToContent": "Skip to main content", + "switchToDark": "Switch to dark theme", + "switchToLight": "Switch to light theme" }, "dashboard": { "welcome": "Welcome to WebEncode", @@ -97,6 +104,20 @@ "stackTrace": "Stack Trace", "timestamp": "Timestamp", "path": "Path", - "method": "Method" + "method": "Method", + "refresh": "Refresh", + "filterBySource": "Filter by source", + "allSources": "All sources", + "markResolved": "Mark as resolved", + "copyDetails": "Copy error details", + "noErrors": "No errors detected", + "systemHealthy": "System is healthy", + "showStackTrace": "Show stack trace" + }, + "footer": { + "label": "Footer", + "copyright": "© {year} {name}. Open source under the MIT License.", + "documentation": "Documentation", + "api": "API" } -} \ No newline at end of file +} diff --git a/ui/messages/es.json b/ui/messages/es.json index f522e57..bb203af 100644 --- a/ui/messages/es.json +++ b/ui/messages/es.json @@ -25,10 +25,17 @@ "success": "Éxito", "live": "En vivo", "offline": "Desconectado", - "admin": "Administrador" + "admin": "Administrador", + "viewAll": "Ver todo", + "language": "Idioma", + "openNavigation": "Abrir navegación", + "mainNavigation": "Navegación principal", + "skipToContent": "Saltar al contenido principal", + "switchToDark": "Cambiar al tema oscuro", + "switchToLight": "Cambiar al tema claro" }, "dashboard": { - "welcome": "Bienvenido a WebEncode", + "welcome": "Bienvenido a WebEncode", "activeJobs": "Trabajos Activos", "workersOnline": "Trabajadores en Línea", "completedJobs": "Trabajos Completados", @@ -97,6 +104,20 @@ "stackTrace": "Rastro de pila", "timestamp": "Marca de tiempo", "path": "Ruta", - "method": "Método" + "method": "Método", + "refresh": "Actualizar", + "filterBySource": "Filtrar por origen", + "allSources": "Todos los orígenes", + "markResolved": "Marcar como resuelto", + "copyDetails": "Copiar detalles del error", + "noErrors": "No se detectaron errores", + "systemHealthy": "El sistema está en buen estado", + "showStackTrace": "Mostrar traza de pila" + }, + "footer": { + "label": "Pie de página", + "copyright": "© {year} {name}. Código abierto con licencia MIT.", + "documentation": "Documentación", + "api": "API" } -} \ No newline at end of file +} diff --git a/ui/messages/fr.json b/ui/messages/fr.json index b14e26b..1bbebb8 100644 --- a/ui/messages/fr.json +++ b/ui/messages/fr.json @@ -25,10 +25,17 @@ "success": "Succès", "live": "En direct", "offline": "Hors ligne", - "admin": "Admin" + "admin": "Admin", + "viewAll": "Tout afficher", + "language": "Langue", + "openNavigation": "Ouvrir la navigation", + "mainNavigation": "Navigation principale", + "skipToContent": "Aller au contenu principal", + "switchToDark": "Passer au thème sombre", + "switchToLight": "Passer au thème clair" }, "dashboard": { - "welcome": "Bienvenue sur WebEncode", + "welcome": "Bienvenue sur WebEncode", "activeJobs": "Tâches actives", "workersOnline": "Travailleurs en ligne", "completedJobs": "Tâches terminées", @@ -97,6 +104,20 @@ "stackTrace": "Trace de la pile", "timestamp": "Horodatage", "path": "Chemin", - "method": "Méthode" + "method": "Méthode", + "refresh": "Actualiser", + "filterBySource": "Filtrer par source", + "allSources": "Toutes les sources", + "markResolved": "Marquer comme résolu", + "copyDetails": "Copier les détails de l'erreur", + "noErrors": "Aucune erreur détectée", + "systemHealthy": "Le système est sain", + "showStackTrace": "Afficher la trace d'appels" + }, + "footer": { + "label": "Pied de page", + "copyright": "© {year} {name}. Open source sous licence MIT.", + "documentation": "Documentation", + "api": "API" } -} \ No newline at end of file +} diff --git a/ui/messages/ja.json b/ui/messages/ja.json index fce76bb..751a95e 100644 --- a/ui/messages/ja.json +++ b/ui/messages/ja.json @@ -25,10 +25,17 @@ "success": "成功", "live": "ライブ", "offline": "オフライン", - "admin": "管理者" + "admin": "管理者", + "viewAll": "すべて表示", + "language": "言語", + "openNavigation": "ナビゲーションを開く", + "mainNavigation": "メインナビゲーション", + "skipToContent": "メインコンテンツへスキップ", + "switchToDark": "ダークテーマに切り替え", + "switchToLight": "ライトテーマに切り替え" }, "dashboard": { - "welcome": "WebEncodeへようこそ", + "welcome": "WebEncodeへようこそ", "activeJobs": "アクティブなジョブ", "workersOnline": "オンラインのワーカー", "completedJobs": "完了したジョブ", @@ -97,6 +104,20 @@ "stackTrace": "スタックトレース", "timestamp": "タイムスタンプ", "path": "パス", - "method": "メソッド" + "method": "メソッド", + "refresh": "更新", + "filterBySource": "ソースで絞り込む", + "allSources": "すべてのソース", + "markResolved": "解決済みにする", + "copyDetails": "エラーの詳細をコピー", + "noErrors": "エラーは検出されていません", + "systemHealthy": "システムは正常です", + "showStackTrace": "スタックトレースを表示" + }, + "footer": { + "label": "フッター", + "copyright": "© {year} {name}. MIT ライセンスのオープンソース。", + "documentation": "ドキュメント", + "api": "API" } -} \ No newline at end of file +} diff --git a/ui/src/app/[locale]/errors/page.tsx b/ui/src/app/[locale]/errors/page.tsx index b9b4b9d..6ad1550 100644 --- a/ui/src/app/[locale]/errors/page.tsx +++ b/ui/src/app/[locale]/errors/page.tsx @@ -3,6 +3,31 @@ import { useCallback, useEffect, useState } from "react"; import { CheckCircle, RefreshCw, Copy } from "lucide-react"; import { format } from "date-fns"; +import { useTranslations } from "next-intl"; + +/** Timestamp format for log rows: sortable and locale-independent on purpose. */ +const LOG_TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm:ss"; + +/** + * Badge treatment per severity. Each level gets its own tint so `error` and + * `warning` stay distinguishable at a glance, and `fatal` reads as the loudest. + */ +const SEVERITY_STYLES: Record = { + fatal: "bg-danger text-danger-foreground border-danger", + critical: "bg-danger/20 text-danger border-danger/60", + error: "bg-danger/10 text-danger border-danger/30", + warning: "bg-warning/15 text-warning border-warning/40", +}; + +const SEVERITY_FALLBACK_STYLE = "badge-neutral"; + +/** Source components the API can filter on. */ +const SOURCE_FILTERS = ["kernel", "frontend", "worker"] as const; +const ALL_SOURCES = "all"; + +/** Shared affordance for the row actions that only appear on hover/focus. */ +const ROW_ACTION_CLASS = + "rounded-md p-2 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100 focus-ring"; type ErrorEvent = { id: string; @@ -16,14 +41,16 @@ type ErrorEvent = { }; export default function ErrorsPage() { + const t = useTranslations('errors'); + const commonT = useTranslations('common'); const [errors, setErrors] = useState([]); const [loading, setLoading] = useState(true); - const [filterSource, setFilterSource] = useState("all"); + const [filterSource, setFilterSource] = useState(ALL_SOURCES); const fetchErrors = useCallback(async () => { setLoading(true); try { - const url = filterSource !== "all" + const url = filterSource !== ALL_SOURCES ? `/api/v1/errors?source=${filterSource}` : `/api/v1/errors`; @@ -54,61 +81,68 @@ export default function ErrorsPage() { } }; - const getSeverityColor = (severity: string) => { - switch (severity) { - case "fatal": return "bg-red-900/50 text-red-200 border-red-700"; - case "critical": return "bg-red-500/20 text-red-300 border-red-500/50"; - case "error": return "bg-orange-500/20 text-orange-300 border-orange-500/50"; - case "warning": return "bg-yellow-500/20 text-yellow-300 border-yellow-500/50"; - default: return "bg-slate-800 text-slate-300"; - } - }; + const getSeverityColor = (severity: string) => + SEVERITY_STYLES[severity] ?? SEVERITY_FALLBACK_STYLE; const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text); }; return ( -
-
+
+
-

- System Errors -

-

Global error tracking and alerts

+

{t('title')}

+

{t('subtitle')}

-
+
- {errors.length === 0 && !loading ? ( + {loading && errors.length === 0 ? ( +
+
+
+
+
+ ) : errors.length === 0 ? (
- -

No errors detected

-

System is healthy

+
) : ( errors.map((error) => (
@@ -116,12 +150,12 @@ export default function ErrorsPage() { {error.severity} - + {error.source_component} - - {format(new Date(error.created_at), "yyyy-MM-dd HH:mm:ss")} - +

{error.message}

@@ -129,29 +163,33 @@ export default function ErrorsPage() {
{!error.resolved && ( )}
{error.stack_trace && (
- - Show Stack Trace + + {t('showStackTrace')} -
+                                    
                                         {error.stack_trace}
                                     
diff --git a/ui/src/app/[locale]/jobs/[id]/page.tsx b/ui/src/app/[locale]/jobs/[id]/page.tsx index 0dfd485..b2c6429 100644 --- a/ui/src/app/[locale]/jobs/[id]/page.tsx +++ b/ui/src/app/[locale]/jobs/[id]/page.tsx @@ -281,7 +281,7 @@ export default function JobDetailsPage() {

)} {job.error_message && ( -
+

{job.error_message}

@@ -428,19 +428,19 @@ export default function JobDetailsPage() { -
+
{logs && logs.length > 0 ? ( logs.map((log) => ( -
- {new Date(log.created_at).toLocaleTimeString()} - +
+ {new Date(log.created_at).toLocaleTimeString()} + {log.level.toUpperCase()} {log.message}
)) ) : ( - No logs available... + No logs available... )}
@@ -611,14 +611,14 @@ function PipelineNode({ task, label, small, pending, waiting }: { const baseStyle = small ? 'px-3 py-1.5 text-xs' : 'px-5 py-2.5 text-sm'; if (pending) { return ( -
+
{label}
); } if (waiting) { return ( -
+
⏳ {label}
); @@ -631,10 +631,10 @@ function PipelineNode({ task, label, small, pending, waiting }: { } const statusStyles: Record = { - pending: "border-zinc-500/30 bg-zinc-900/50 text-zinc-400", - assigned: "border-blue-500/50 bg-blue-500/10 text-blue-400 shadow-blue-500/10 shadow-md", - completed: "border-emerald-500/50 bg-emerald-500/10 text-emerald-400", - failed: "border-red-500/50 bg-red-500/15 text-red-400", + pending: "border-border bg-card/50 text-muted-foreground", + assigned: "border-info/50 bg-info/10 text-info shadow-info/10 shadow-md", + completed: "border-success/50 bg-success/10 text-success", + failed: "border-danger/50 bg-danger/15 text-danger", }; const StatusIcon = { @@ -661,13 +661,13 @@ function PipelineNode({ task, label, small, pending, waiting }: { function StatusBadge({ status }: { status: string }) { const styles: Record = { - queued: "bg-yellow-500/10 text-yellow-600 border-yellow-500/30", - processing: "bg-blue-500/10 text-blue-500 border-blue-500/30", - stitching: "bg-purple-500/10 text-purple-500 border-purple-500/30", - uploading: "bg-cyan-500/10 text-cyan-500 border-cyan-500/30", - completed: "bg-emerald-500/10 text-emerald-500 border-emerald-500/30", - failed: "bg-red-500/15 text-red-500 border-red-500/40 font-medium", - cancelled: "bg-zinc-500/10 text-zinc-400 border-zinc-500/30", + queued: "bg-warning/10 text-warning border-warning/30", + processing: "bg-info/10 text-info border-info/30", + stitching: "bg-primary/10 text-brand border-primary/30", + uploading: "bg-info/10 text-info border-info/30", + completed: "bg-success/10 text-success border-success/30", + failed: "bg-danger/15 text-danger border-danger/40 font-medium", + cancelled: "bg-muted/50 text-muted-foreground border-border", }; return ( @@ -709,9 +709,9 @@ function TaskRow({ task }: { task: Task }) { const statusIcons: Record = { pending: , - assigned: , - completed: , - failed: , + assigned: , + completed: , + failed: , }; const typeLabels: Record = { @@ -787,7 +787,7 @@ function TaskRow({ task }: { task: Task }) {
{showJson ? ( -
+
{rawTaskResultText}
) : ( @@ -821,7 +821,7 @@ function TaskRow({ task }: { task: Task }) {
{probeResult.Streams.map((stream) => (
- + {stream.CodecType} {stream.CodecName.toUpperCase()} @@ -836,11 +836,11 @@ function TaskRow({ task }: { task: Task }) {
) : taskResultText && ( // Default raw JSON view for others -
+
{taskResultText}
)} @@ -865,7 +865,7 @@ function CopyButton({ text, className }: { text: string; className?: string }) { onClick={onCopy} title="Copy to clipboard" > - {copied ? : } + {copied ? : } ); } diff --git a/ui/src/app/[locale]/jobs/new/page.tsx b/ui/src/app/[locale]/jobs/new/page.tsx index d3bf91d..a3fe535 100644 --- a/ui/src/app/[locale]/jobs/new/page.tsx +++ b/ui/src/app/[locale]/jobs/new/page.tsx @@ -152,7 +152,7 @@ export default function NewJobPage() { {sourceUrl && (
- + Selected: {sourceUrl}
@@ -221,7 +221,7 @@ export default function NewJobPage() {
{createMutation.isError && ( -
+
Failed to create job. Please try again.
)} @@ -270,7 +270,7 @@ function StepIndicator({ label, step, current, complete }: {
{complete ? : step} diff --git a/ui/src/app/[locale]/jobs/page.tsx b/ui/src/app/[locale]/jobs/page.tsx index 3226f81..8631ab2 100644 --- a/ui/src/app/[locale]/jobs/page.tsx +++ b/ui/src/app/[locale]/jobs/page.tsx @@ -57,7 +57,7 @@ export default function JobsPage() {

- @@ -98,7 +98,7 @@ export default function JobsPage() {
- + {t('title')} @@ -147,7 +147,7 @@ export default function JobsPage() {
- + {commonT('loading')}
@@ -204,7 +204,7 @@ export default function JobsPage() {

{!searchTerm && ( - @@ -223,11 +223,11 @@ export default function JobsPage() { function StatusBadge({ status }: { status: string }) { const config: Record = { - queued: { bg: "bg-amber-500/10", text: "text-amber-400", dot: "bg-amber-400" }, - processing: { bg: "bg-blue-500/10", text: "text-blue-400", dot: "bg-blue-400" }, - stitching: { bg: "bg-indigo-500/10", text: "text-indigo-400", dot: "bg-indigo-400" }, - completed: { bg: "bg-emerald-500/10", text: "text-emerald-400", dot: "bg-emerald-400" }, - failed: { bg: "bg-red-500/10", text: "text-red-400", dot: "bg-red-400" }, + queued: { bg: "bg-warning/10", text: "text-warning", dot: "bg-warning" }, + processing: { bg: "bg-info/10", text: "text-info", dot: "bg-info" }, + stitching: { bg: "bg-primary/10", text: "text-brand", dot: "bg-primary" }, + completed: { bg: "bg-success/10", text: "text-success", dot: "bg-success" }, + failed: { bg: "bg-danger/10", text: "text-danger", dot: "bg-danger" }, cancelled: { bg: "bg-muted", text: "text-muted-foreground", dot: "bg-muted-foreground" }, }; @@ -243,7 +243,7 @@ function StatusBadge({ status }: { status: string }) { function JobProgress({ status, progress }: { status: string; progress: number }) { if (status === 'completed') { - return 100%; + return 100%; } if (status === 'failed' || status === 'cancelled') { return ; @@ -270,10 +270,10 @@ interface StatusStatCardProps { function StatusStatCard({ label, count, icon: Icon, color, pulse }: StatusStatCardProps) { const colors = { - blue: "text-blue-400 bg-blue-500/10 border-blue-500/20", - amber: "text-amber-400 bg-amber-500/10 border-amber-500/20", - emerald: "text-emerald-400 bg-emerald-500/10 border-emerald-500/20", - red: "text-red-400 bg-red-500/10 border-red-500/20", + blue: "text-info bg-info/10 border-info/20", + amber: "text-warning bg-warning/10 border-warning/20", + emerald: "text-success bg-success/10 border-success/20", + red: "text-danger bg-danger/10 border-danger/20", }; return ( @@ -282,8 +282,8 @@ function StatusStatCard({ label, count, icon: Icon, color, pulse }: StatusStatCa {pulse && ( - - + + )}
diff --git a/ui/src/app/[locale]/layout.tsx b/ui/src/app/[locale]/layout.tsx index c17fc1b..201c451 100644 --- a/ui/src/app/[locale]/layout.tsx +++ b/ui/src/app/[locale]/layout.tsx @@ -4,15 +4,21 @@ import "@/app/globals.css"; import { Providers } from "@/lib/providers"; import { Link } from "@/i18n/routing"; import Image from "next/image"; -import { Menu, User } from "lucide-react"; +import { User } from "lucide-react"; import { NotificationsDropdown } from "@/components/notifications-dropdown"; import { Sidebar } from "@/components/sidebar"; +import { MobileNav } from "@/components/mobile-nav"; +import { ThemeToggle } from "@/components/theme-toggle"; +import { LocaleSwitcher } from "@/components/locale-switcher"; import { GlobalErrorTracker } from "@/components/global-error-tracker"; import { Toaster } from "sonner"; import { NextIntlClientProvider } from 'next-intl'; -import { getMessages, setRequestLocale } from 'next-intl/server'; +import { getMessages, getTranslations, setRequestLocale } from 'next-intl/server'; import { locales, type Locale } from '@/i18n/config'; import { notFound } from 'next/navigation'; +import { APP_NAME, API_DOCS_URL, DOCS_URL, REPO_URL } from "@/lib/app-meta"; +import { SIDEBAR_OFFSET_CLASS } from "@/lib/nav"; +import { THEME_INIT_SCRIPT } from "@/lib/theme"; const inter = Inter({ subsets: ["latin"], @@ -26,6 +32,15 @@ export const metadata: Metadata = { authors: [{ name: "WebEncode Team" }], }; +/** Anchor for the "skip to content" link. */ +const MAIN_CONTENT_ID = "main-content"; + +/** Page gutters and the ceiling that keeps content readable on wide screens. */ +const SHELL_WIDTH_CLASS = "w-full max-w-content-max mx-auto"; +const SHELL_PADDING_CLASS = "p-4 sm:p-6 lg:p-8"; + +const FOOTER_LINK_CLASS = "rounded-sm transition-colors hover:text-foreground focus-ring"; + export function generateStaticParams() { return locales.map((locale) => ({ locale })); } @@ -47,79 +62,119 @@ export default async function LocaleLayout({ setRequestLocale(locale); const messages = await getMessages(); + const t = await getTranslations({ locale, namespace: 'common' }); + const tFooter = await getTranslations({ locale, namespace: 'footer' }); + const year = new Intl.NumberFormat(locale, { useGrouping: false }).format(new Date().getFullYear()); return ( - + // The theme class is rewritten before paint by THEME_INIT_SCRIPT. + + +