diff --git a/src/app/api/v1/alerts/route.ts b/src/app/api/v1/alerts/route.ts
index f01c90f3..dcdccf1e 100644
--- a/src/app/api/v1/alerts/route.ts
+++ b/src/app/api/v1/alerts/route.ts
@@ -5,11 +5,13 @@ import { listAlertsForPort } from '@/lib/services/alerts.service';
type AlertStatus = 'open' | 'dismissed' | 'resolved';
-// Tier-4 (authz-auditor): alerts include permission_denied + audit-adjacent
-// signals. Gated on admin.view_audit_log - same permission the audit log
-// page uses.
+// The alert feed is entirely operational/deal signals (stale interest, hot lead
+// silent, EOI unsigned, signer overdue, reservation needs agreement, berth
+// stalled, duplicate/unscanned expense) — there are no audit/security alert
+// rules. Gated on interests.view so the operational roles that act on these
+// (sales, director, viewer) see them; external residential partners don't.
export const GET = withAuth(
- withPermission('admin', 'view_audit_log', async (req: NextRequest, ctx) => {
+ withPermission('interests', 'view', async (req: NextRequest, ctx) => {
const url = new URL(req.url);
const status = (url.searchParams.get('status') ?? 'open') as AlertStatus;
diff --git a/src/components/inbox/inbox-page-shell.tsx b/src/components/inbox/inbox-page-shell.tsx
index 47a94afd..e9c2c548 100644
--- a/src/components/inbox/inbox-page-shell.tsx
+++ b/src/components/inbox/inbox-page-shell.tsx
@@ -8,6 +8,7 @@ import { PageHeader } from '@/components/shared/page-header';
import { AlertsPageShell } from '@/components/alerts/alerts-page-shell';
import { ReminderList } from '@/components/reminders/reminder-list';
import { useAlertCount } from '@/components/alerts/use-alerts';
+import { usePermissions } from '@/hooks/use-permissions';
/**
* Merged "Inbox" surface - replaces the previously-separate /alerts and
@@ -29,6 +30,11 @@ export function InboxPageShell() {
const [alertsOpen, setAlertsOpen] = useState(true);
const [remindersOpen, setRemindersOpen] = useState(true);
const { data: alertCount } = useAlertCount();
+ // The deal-alert feed (stale interests, overdue signers, …) is gated on
+ // interests.view — operational roles see it; external residential partners
+ // don't. Hide the whole section rather than letting its query 403.
+ const { can } = usePermissions();
+ const canSeeAlerts = can('interests', 'view');
// localStorage hydration on mount - canonical "read from external
// store" pattern. setState in effect is intentional.
@@ -95,20 +101,22 @@ export function InboxPageShell() {
) : null}
-
- }
- label="Alerts"
- count={activeAlerts}
- open={alertsOpen}
- onToggle={toggleAlerts}
- />
- {alertsOpen ? (
-
- ) : null}
-
+ {canSeeAlerts ? (
+
+ }
+ label="Alerts"
+ count={activeAlerts}
+ open={alertsOpen}
+ onToggle={toggleAlerts}
+ />
+ {alertsOpen ? (
+
+ ) : null}
+
+ ) : null}
);
}
diff --git a/src/hooks/use-onboarding-status.ts b/src/hooks/use-onboarding-status.ts
index 82d056ce..59c31c5c 100644
--- a/src/hooks/use-onboarding-status.ts
+++ b/src/hooks/use-onboarding-status.ts
@@ -27,8 +27,11 @@ export interface OnboardingStatusPayload {
* and the admin checklist summary. Cached for 60s so all three surfaces
* share a single fetch on first paint.
*
- * Pass `enabled=false` to skip the network call (e.g. when the current
- * user isn't a super_admin and the surface won't render anyway).
+ * Defaults to OFF: the endpoint is admin-only (admin.manage_settings), so
+ * callers must opt in with `enabled: true` once they've confirmed the user is
+ * a super_admin. This prevents a transient 403 (e.g. a stale `isSuperAdmin`
+ * during permission hydration) from firing the privileged request for
+ * non-admins.
*/
export function useOnboardingStatus(opts: { enabled?: boolean } = {}) {
return useQuery({
@@ -38,7 +41,7 @@ export function useOnboardingStatus(opts: { enabled?: boolean } = {}) {
(r) => r.data,
),
staleTime: 60_000,
- enabled: opts.enabled ?? true,
+ enabled: opts.enabled === true,
retry: false,
});
}