feat(web): green welcome accent + session-dismissable dev banner with telemetry

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-08-22 19:17:53 +02:00
parent ede616444a
commit 4aa1f07fbb
3 changed files with 115 additions and 57 deletions
@@ -0,0 +1,105 @@
"use client";
import { useSyncExternalStore } from "react";
import { DOCS_URL } from "@/utils/config";
import { trackEvent } from "@/lib/appInsights";
// Session-scoped dismissal: sessionStorage is per-tab, so the banner stays hidden
// across reloads in this tab (service-desk monitor use case) but reappears in every
// new tab — deliberately never a permanent opt-out. Module-level store in the
// themeStore shape: a same-tab sessionStorage write fires no "storage" event, and
// useSyncExternalStore keeps the statically prerendered markup hydration-safe.
const DismissKey = "devBannerDismissed";
const listeners = new Set<() => void>();
const getSnapshot = (): boolean => sessionStorage.getItem(DismissKey) === "1";
const getServerSnapshot = (): boolean => false;
function subscribe(onStoreChange: () => void): () => void {
listeners.add(onStoreChange);
return () => listeners.delete(onStoreChange);
}
function dismiss(): void {
sessionStorage.setItem(DismissKey, "1");
for (const listener of [...listeners]) listener();
}
const linkClassName =
"underline font-medium hover:text-green-600 dark:hover:text-blue-200";
export function ActivelyDevelopedBanner() {
const dismissed = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
if (dismissed) return null;
const trackLink = (link: string) => trackEvent("dev_banner_link_clicked", { link });
return (
<div className="mb-4 bg-blue-50 border border-blue-300 rounded-lg px-4 py-3 flex items-start gap-3 dark:bg-blue-950/30 dark:border-blue-700/50">
<svg className="w-4 h-4 text-blue-500 mt-0.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
</svg>
<p className="text-sm text-blue-800 dark:text-blue-300">
<span className="font-semibold">Actively developed.</span>{" "}
Autopilot Monitor recognizes a wide range of deployment scenarios and improves
continuously — your reports directly shape it.{" "}
If something looks off, check the{" "}
<a
href={`${DOCS_URL}/changelog/platform-changelog`}
target="_blank"
rel="noopener noreferrer"
className={linkClassName}
onClick={() => trackLink("platform_changelog")}
>
Platform Changelog
</a>{" "}
or{" "}
<a
href={`${DOCS_URL}/troubleshooting/service-announcements`}
target="_blank"
rel="noopener noreferrer"
className={linkClassName}
onClick={() => trackLink("service_announcements")}
>
Service Announcements
</a>
.{" "}
Feedback or bug report?{" "}
<a
href="https://github.com/okieselbach/AutopilotMonitor/issues"
target="_blank"
rel="noopener noreferrer"
className={linkClassName}
onClick={() => trackLink("github_issues")}
>
Open a GitHub issue
</a>
{" "}or message me on{" "}
<a
href="https://www.linkedin.com/in/oliver-kieselbach/"
target="_blank"
rel="noopener noreferrer"
className={linkClassName}
onClick={() => trackLink("linkedin")}
>
LinkedIn
</a>
.
</p>
<button
type="button"
onClick={() => {
trackEvent("dev_banner_dismissed");
dismiss();
}}
aria-label="Hide for this browser tab"
title="Hide for this browser tab"
className="ml-auto shrink-0 -mr-1 -mt-1 p-1 rounded text-blue-300 hover:text-blue-600 hover:bg-blue-100 transition-colors dark:text-blue-700 dark:hover:text-blue-300 dark:hover:bg-blue-900/40"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
);
}
@@ -6,12 +6,12 @@ export function WelcomeMessage() {
return (
<div className="bg-white shadow rounded-lg overflow-hidden">
{/* Top accent bar */}
<div className="h-1 bg-gradient-to-r from-blue-500 via-indigo-500 to-purple-500" />
<div className="h-1 bg-gradient-to-r from-green-500 via-emerald-500 to-teal-500" />
<div className="p-6 sm:p-8">
{/* Header */}
<div className="flex items-start gap-4 mb-6">
<div className="shrink-0 w-10 h-10 rounded-xl bg-blue-100 flex items-center justify-center">
<svg className="w-5 h-5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<div className="shrink-0 w-10 h-10 rounded-xl bg-green-100 flex items-center justify-center">
<svg className="w-5 h-5 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
@@ -49,10 +49,10 @@ export function WelcomeMessage() {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<a
href={DOCS_URL} target="_blank" rel="noopener noreferrer"
className="group flex items-center gap-3 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 hover:border-blue-300 hover:bg-blue-50 transition-colors"
className="group flex items-center gap-3 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 hover:border-green-300 hover:bg-green-50 transition-colors"
>
<span className="shrink-0 w-8 h-8 rounded-lg bg-white border border-gray-200 group-hover:border-blue-200 flex items-center justify-center shadow-sm">
<svg className="w-4 h-4 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<span className="shrink-0 w-8 h-8 rounded-lg bg-white border border-gray-200 group-hover:border-green-200 flex items-center justify-center shadow-sm">
<svg className="w-4 h-4 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
</span>
@@ -60,7 +60,7 @@ export function WelcomeMessage() {
<p className="text-sm font-medium text-gray-900">Documentation</p>
<p className="text-xs text-gray-500">Setup and configuration guides</p>
</div>
<svg className="w-4 h-4 text-gray-300 group-hover:text-blue-400 ml-auto shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<svg className="w-4 h-4 text-gray-300 group-hover:text-green-400 ml-auto shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</a>
@@ -12,6 +12,7 @@ import { useAuth } from "../../contexts/AuthContext";
import { useNotifications } from "../../contexts/NotificationContext";
import { StatsCard } from "./components/StatsCards";
import { WelcomeMessage } from "./components/WelcomeMessage";
import { ActivelyDevelopedBanner } from "./components/ActivelyDevelopedBanner";
import { SessionTable } from "./components/SessionTable";
import { TenantFilterBar } from "./components/TenantFilterBar";
import { DeleteConfirmModal, BlockConfirmModal } from "./components/ConfirmationModals";
@@ -29,7 +30,6 @@ import { api } from "@/lib/api";
import { authenticatedFetch } from "@/lib/authenticatedFetch";
import { formatDuration } from "@/lib/formatting";
import { hasTenantReadScope } from "@/lib/tenantScope";
import { DOCS_URL } from "@/utils/config";
import { TableSkeleton } from "@/components/skeletons/TableSkeleton";
export default function Home() {
@@ -345,55 +345,8 @@ function HomeContent() {
{/* Main content */}
<main className={mainClassName}>
<div className="px-4 sm:px-0">
{/* Feedback & bug report banner */}
<div className="mb-4 bg-blue-50 border border-blue-300 rounded-lg px-4 py-3 flex items-start gap-3 dark:bg-blue-950/30 dark:border-blue-700/50">
<svg className="w-4 h-4 text-blue-500 mt-0.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
</svg>
<p className="text-sm text-blue-800 dark:text-blue-300">
<span className="font-semibold">Actively developed.</span>{" "}
Autopilot Monitor recognizes a wide range of deployment scenarios and improves
continuously — your reports directly shape it.{" "}
If something looks off, check the{" "}
<a
href={`${DOCS_URL}/changelog/platform-changelog`}
target="_blank"
rel="noopener noreferrer"
className="underline font-medium hover:text-green-600 dark:hover:text-blue-200"
>
Platform Changelog
</a>{" "}
or{" "}
<a
href={`${DOCS_URL}/troubleshooting/service-announcements`}
target="_blank"
rel="noopener noreferrer"
className="underline font-medium hover:text-green-600 dark:hover:text-blue-200"
>
Service Announcements
</a>
.{" "}
Feedback or bug report?{" "}
<a
href="https://github.com/okieselbach/AutopilotMonitor/issues"
target="_blank"
rel="noopener noreferrer"
className="underline font-medium hover:text-green-600 dark:hover:text-blue-200"
>
Open a GitHub issue
</a>
{" "}or message me on{" "}
<a
href="https://www.linkedin.com/in/oliver-kieselbach/"
target="_blank"
rel="noopener noreferrer"
className="underline font-medium hover:text-green-600 dark:hover:text-blue-200"
>
LinkedIn
</a>
.
</p>
</div>
{/* Feedback & bug report banner (session-dismissable, telemetry-instrumented) */}
<ActivelyDevelopedBanner />
{serialValidationEnabled === false && (
<div className="mb-6 bg-red-600 border-2 border-red-700 rounded-xl p-5 shadow-lg">