mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 20:53:06 +00:00
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:
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user