route diagnostics SAS expiry warnings to notification bell

Instead of inline banners on the settings page, expired or near-expiry
SAS URLs now fire a persistent warning/error notification to the bell icon.
The notification includes a direct "View" link to /settings#diagnostics.

NotificationContext: added optional href field to Notification; addNotification
signature extended with href parameter for both keyed and unkeyed paths.

Navbar: notifications with href render a "View" link that marks the
notification as read and navigates to the target anchor.

Settings: removed inline SAS warning banners; addNotification called on
config load when expiry <= 7 days (warning) or expired (error); diagnostics
section gets id="diagnostics" anchor for deep-linking.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-02-24 21:34:56 +01:00
parent 6aafaa5ff8
commit 8b17b82b78
3 changed files with 36 additions and 31 deletions
@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
import { ProtectedRoute } from "../../components/ProtectedRoute";
import { useTenant } from "../../contexts/TenantContext";
import { useAuth } from "../../contexts/AuthContext";
import { useNotifications } from "../../contexts/NotificationContext";
import { API_BASE_URL } from "@/lib/config";
interface TenantConfiguration {
@@ -65,6 +66,7 @@ export default function SettingsPage() {
const router = useRouter();
const { tenantId } = useTenant();
const { getAccessToken, user, logout } = useAuth();
const { addNotification } = useNotifications();
const [config, setConfig] = useState<TenantConfiguration | null>(null);
const [admins, setAdmins] = useState<TenantAdmin[]>([]);
const [loading, setLoading] = useState(true);
@@ -115,7 +117,6 @@ export default function SettingsPage() {
const [diagnosticsBlobSasUrl, setDiagnosticsBlobSasUrl] = useState("");
const [diagnosticsUploadMode, setDiagnosticsUploadMode] = useState("Off");
const [diagnosticsSasExpiry, setDiagnosticsSasExpiry] = useState<Date | null>(null);
const [diagnosticsSasWarning, setDiagnosticsSasWarning] = useState<string | null>(null);
// Fetch configuration
useEffect(() => {
@@ -167,7 +168,7 @@ export default function SettingsPage() {
setDiagnosticsBlobSasUrl(sasUrl);
setDiagnosticsUploadMode(data.diagnosticsUploadMode ?? "Off");
// Parse SAS expiry and set warning/error notifications
// Parse SAS expiry and fire notification to bell if needed
if (sasUrl) {
const expiry = parseSasExpiry(sasUrl);
setDiagnosticsSasExpiry(expiry);
@@ -175,11 +176,21 @@ export default function SettingsPage() {
const now = new Date();
const daysRemaining = Math.ceil((expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
if (daysRemaining <= 0) {
setDiagnosticsSasWarning("error");
addNotification(
'error',
'Diagnostics SAS URL Expired',
`The Diagnostics SAS URL expired on ${expiry.toLocaleDateString()}. Diagnostics upload is non-functional.`,
'diagnostics-sas-expiry',
'/settings#diagnostics'
);
} else if (daysRemaining <= 7) {
setDiagnosticsSasWarning("warning");
} else {
setDiagnosticsSasWarning(null);
addNotification(
'warning',
'Diagnostics SAS URL Expiring Soon',
`The Diagnostics SAS URL expires on ${expiry.toLocaleDateString()} (${daysRemaining} day${daysRemaining === 1 ? '' : 's'} remaining). Please update it soon.`,
'diagnostics-sas-expiry',
'/settings#diagnostics'
);
}
}
}
@@ -656,27 +667,6 @@ export default function SettingsPage() {
</div>
)}
{/* Diagnostics SAS URL expiry notifications */}
{diagnosticsSasWarning === "error" && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center space-x-3">
<svg className="w-5 h-5 text-red-600 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="text-red-800">
<strong>Diagnostics SAS URL has expired.</strong> Diagnostics upload is non-functional. Please update the SAS URL in the Diagnostics Package section.
</span>
</div>
)}
{diagnosticsSasWarning === "warning" && diagnosticsSasExpiry && (
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 flex items-center space-x-3">
<svg className="w-5 h-5 text-amber-600 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
</svg>
<span className="text-amber-900">
<strong>Diagnostics SAS URL expires on {diagnosticsSasExpiry.toLocaleDateString()}</strong> — please update it soon to avoid interruption.
</span>
</div>
)}
{/* Autopilot Device Validation */}
<div className="bg-white rounded-lg shadow">
@@ -1277,7 +1267,7 @@ export default function SettingsPage() {
</div>
{/* Diagnostics Package */}
<div className="bg-white rounded-lg shadow">
<div id="diagnostics" className="bg-white rounded-lg shadow">
<div className="p-6 border-b border-gray-200 bg-gradient-to-r from-amber-50 to-orange-50">
<div className="flex items-center space-x-2">
<svg className="w-6 h-6 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -261,7 +261,18 @@ export default function Navbar() {
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900">{notification.title}</p>
<p className="text-xs text-gray-600 mt-0.5">{notification.message}</p>
<p className="text-[10px] text-gray-400 mt-1">{formatTime(notification.timestamp)}</p>
<div className="flex items-center gap-3 mt-1">
<p className="text-[10px] text-gray-400">{formatTime(notification.timestamp)}</p>
{notification.href && (
<Link
href={notification.href}
onClick={(e) => { e.stopPropagation(); markAsRead(notification.id); }}
className="text-[10px] text-blue-600 hover:text-blue-800 font-medium underline"
>
View
</Link>
)}
</div>
</div>
</div>
<button onClick={(e) => { e.stopPropagation(); removeNotification(notification.id); }} className="ml-2 text-gray-300 hover:text-gray-500">
@@ -12,12 +12,13 @@ export interface Notification {
timestamp: Date;
read: boolean;
key?: string; // Optional unique key for deduplication
href?: string; // Optional navigation link shown as "View" action
}
interface NotificationContextType {
notifications: Notification[];
unreadCount: number;
addNotification: (type: NotificationType, title: string, message: string, key?: string) => void;
addNotification: (type: NotificationType, title: string, message: string, key?: string, href?: string) => void;
markAsRead: (id: string) => void;
markAllAsRead: () => void;
removeNotification: (id: string) => void;
@@ -29,7 +30,7 @@ const NotificationContext = createContext<NotificationContextType | undefined>(u
export function NotificationProvider({ children }: { children: React.ReactNode }) {
const [notifications, setNotifications] = useState<Notification[]>([]);
const addNotification = useCallback((type: NotificationType, title: string, message: string, key?: string) => {
const addNotification = useCallback((type: NotificationType, title: string, message: string, key?: string, href?: string) => {
// Check if a notification with this key already exists
if (key) {
setNotifications(prev => {
@@ -41,6 +42,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
const updated = {
...existing,
message,
href,
timestamp: new Date(),
read: false, // Mark as unread to get user's attention again
};
@@ -59,6 +61,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
timestamp: new Date(),
read: false,
key,
href,
};
// Auto-remove success/info notifications after 10 seconds
@@ -80,6 +83,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
message,
timestamp: new Date(),
read: false,
href,
};
setNotifications(prev => [notification, ...prev]);