diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..5d1470b Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/icon-192.png b/public/icon-192.png new file mode 100644 index 0000000..4f84bd3 Binary files /dev/null and b/public/icon-192.png differ diff --git a/public/icon-512.png b/public/icon-512.png new file mode 100644 index 0000000..1197b57 Binary files /dev/null and b/public/icon-512.png differ diff --git a/public/icon-maskable-512.png b/public/icon-maskable-512.png new file mode 100644 index 0000000..71ceb0f Binary files /dev/null and b/public/icon-maskable-512.png differ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index ca5ae48..faf717d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,4 +1,5 @@ import type { Metadata, Viewport } from "next"; +import { ChangelogButton } from "@/components/ChangelogButton"; import { CommandPalette } from "@/components/CommandPalette"; import { TopNav } from "@/components/TopNav"; import { getSessionUser } from "@/lib/auth/session"; @@ -20,6 +21,20 @@ export async function generateMetadata(): Promise { "Uebersicht ueber alle Vibe-Coding Projekte: Status, Notizen und Live-Daten aus GitHub, GitLab und Gitea.", applicationName: APP_NAME, robots: { index: false, follow: false }, + manifest: "/manifest.webmanifest", + // iOS liest das Manifest nicht; dort braucht es diese Angaben einzeln. + appleWebApp: { capable: true, title: APP_NAME, statusBarStyle: "black-translucent" }, + // Beide Eintraege ausdruecklich: sobald hier etwas steht, ersetzt es den + // Verweis, den Next aus app/icon.svg von selbst erzeugt. Ohne die erste + // Zeile haette die Seite gar kein Symbol mehr und der Browser fiele auf + // /favicon.ico zurueck – das es hier nicht gibt. + icons: { + icon: [ + { url: "/icon.svg", type: "image/svg+xml" }, + { url: "/icon-192.png", sizes: "192x192", type: "image/png" }, + ], + apple: "/apple-touch-icon.png", + }, }; } @@ -56,7 +71,7 @@ export default async function RootLayout({ diff --git a/src/app/manifest.ts b/src/app/manifest.ts new file mode 100644 index 0000000..f2fe772 --- /dev/null +++ b/src/app/manifest.ts @@ -0,0 +1,41 @@ +import type { MetadataRoute } from "next"; +import { APP_NAME, APP_TAGLINE } from "@/lib/config"; + +/** + * Web-App-Manifest: macht Nebula auf dem Handy installierbar. + * + * Als Funktion und nicht als Konstante, aus demselben Grund wie bei + * generateMetadata: nur so wird APP_NAME zur Laufzeit gelesen. Ein + * konstanter Export waere beim Build eingefroren und liesse sich im + * fertigen Docker-Image nicht mehr aendern. + */ +export const dynamic = "force-dynamic"; + +export default function manifest(): MetadataRoute.Manifest { + return { + name: `${APP_NAME} – ${APP_TAGLINE} Dashboard`, + short_name: APP_NAME, + description: + "Uebersicht ueber alle Vibe-Coding-Projekte: Status, Notizen und Live-Daten aus GitHub, GitLab und Gitea.", + // Ohne Adressleiste, aber mit den Systemleisten – die Anwendung braucht + // keinen Vollbildmodus, wohl aber die Uhrzeit und den Akkustand. + display: "standalone", + start_url: "/", + scope: "/", + background_color: "#05060f", + theme_color: "#05060f", + lang: "de", + orientation: "any", + icons: [ + { src: "/icon-192.png", sizes: "192x192", type: "image/png", purpose: "any" }, + { src: "/icon-512.png", sizes: "512x512", type: "image/png", purpose: "any" }, + // Getrennte Fassung mit mehr Rand: Android schneidet maskierbare + // Symbole je nach Geraet rund, eckig oder als Tropfen zu. + { src: "/icon-maskable-512.png", sizes: "512x512", type: "image/png", purpose: "maskable" }, + ], + shortcuts: [ + { name: "Dashboard", url: "/" }, + { name: "Mein Konto", url: "/account" }, + ], + }; +} diff --git a/src/middleware.ts b/src/middleware.ts index e4005e6..320773c 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -20,12 +20,21 @@ const SESSION_COOKIE = "nebula_session"; /** Ohne Anmeldung erreichbar. */ const PUBLIC_PATHS = ["/login", "/setup", "/register"]; const PUBLIC_API_PREFIXES = ["/api/auth/"]; -/** Der Docker-Healthcheck hat kein Cookie. */ -const PUBLIC_API_EXACT = ["/api/health"]; +/** + * Der Docker-Healthcheck hat kein Cookie. Manifest und Symbole braucht der + * Browser, bevor sich jemand angemeldet hat – wuerden sie zur Anmeldeseite + * umgeleitet, liesse sich die Anwendung nicht installieren und die + * Anmeldeseite haette kein Symbol. + */ +const PUBLIC_API_EXACT = ["/api/health", "/manifest.webmanifest"]; + +/** Bilder und aehnliches im Wurzelverzeichnis verraten nichts. */ +const PUBLIC_FILE = /\.(png|svg|ico|webmanifest|txt|webp)$/; function isPublicPath(pathname: string): boolean { if (PUBLIC_PATHS.includes(pathname)) return true; if (PUBLIC_API_EXACT.includes(pathname)) return true; + if (PUBLIC_FILE.test(pathname)) return true; return PUBLIC_API_PREFIXES.some((prefix) => pathname.startsWith(prefix)); }