diff --git a/backend/src/main/resources/content/profile/inbox/architect.md b/backend/src/main/resources/content/profile/inbox/architect.md index 58e2949..d9a0728 100644 --- a/backend/src/main/resources/content/profile/inbox/architect.md +++ b/backend/src/main/resources/content/profile/inbox/architect.md @@ -12,6 +12,7 @@ How the Profile **Inbox** is built and how data flows. The feature spans three s | Concern | Service | Where | |---|---|---| | UI (view/compose/tabs) | `hiveops-profile` | `frontend/src/components/Profile/Inbox.svelte` | +| Sidebar unread badge + poll | `hiveops-profile` | `App.svelte` + `lib/stores.ts` (`unreadCount`, `refreshUnread()`); interval from `inboxPollSeconds`, an **auth**-owned pref — see [profile.notifications] | | Message CRUD + inbox/sent/read/delete | `hiveops-messaging` (8086, `hiveiq_messaging`) | `MessageController` → `MessageService` | | Recipient picker (`GET /auth/api/admin/users`) | `hiveops-auth` | `UserAdminController` | | Institution dropdown (`GET /api/v1/msp/institutions`) | `hiveops-mgmt` | `MspController` | diff --git a/backend/src/main/resources/content/profile/inbox/claude.md b/backend/src/main/resources/content/profile/inbox/claude.md index 097e6a1..2cd7c37 100644 --- a/backend/src/main/resources/content/profile/inbox/claude.md +++ b/backend/src/main/resources/content/profile/inbox/claude.md @@ -20,7 +20,7 @@ Terse agent reference. Owner service: **`hiveops-messaging`** (port 8086, DB `hi | GET | `/api/messaging/messages?page&size&sort` | paged inbox (Page) | | GET | `/api/messaging/messages/sent` | sent list (sender_user_id = you) | | GET | `/api/messaging/messages/unread` | unread list | -| GET | `/api/messaging/messages/unread/count` | `{unreadCount}`; cached per userId | +| GET | `/api/messaging/messages/unread/count` | `{unreadCount}`; cached per userId. Drives the sidebar Inbox badge (see below) | | POST | `/api/messaging/messages` | send; body `SendMessageRequest`; 201 | | PATCH | `/api/messaging/messages/{id}/read` | mark one read; 204 | | PATCH | `/api/messaging/messages/read-all` | mark all read; 204 | @@ -56,6 +56,15 @@ Terse agent reference. Owner service: **`hiveops-messaging`** (port 8086, DB `hi - unread count is `@Cacheable("unreadCount", key=userId)`, evicted on read/read-all/delete. - `loadRecipients()` and `loadInstitutions()` fail silently in the SPA (non-fatal catch). +## Sidebar unread badge (added 2026-07-16) +- Frontend-only feature over the existing count endpoint; **no backend change in messaging**. +- `lib/stores.ts` owns `unreadCount` + `refreshUnread()`. `refreshUnread()` swallows errors on purpose (it runs on a timer; a toast per failed poll is worse than a stale badge), so a broken count fails **silent**, not loud. +- `App.svelte` renders the badge on the Inbox nav item (capped `99+`) and arms the poll from `$userPreferences.inboxPollSeconds` — reactively, so a pref change re-arms without reload. `0` = off. That field is owned by **hiveops-auth**, not messaging (see [profile.notifications]). +- `Inbox.svelte` calls `refreshUnread()` after markRead / markAllRead / delete / auto-refresh so the badge clears immediately. +- Badge counts **unread only**, never total. Two read messages = no badge; that is correct, not a bug. +- Badge colour is `#ef4444`, deliberately NOT the `#2563eb` of Inbox's own `.tab-badge`: the nav sits on the sidebar gradient (`#081651` → `#1c49b8`) where blue-on-blue disappears. +- The nav badge re-calls the endpoint rather than reusing Inbox's local `messages.filter(m => !m.read).length`, which is page-local (size 50) and would undercount past page 0. **Do NOT "simplify" it to use the local tally.** + ## Do NOT - Do NOT create message endpoints/tables outside `hiveops-messaging`. - Do NOT hard-delete from `messages` to "remove" a user's message — use per-user soft-delete. diff --git a/backend/src/main/resources/content/profile/notifications/architect.md b/backend/src/main/resources/content/profile/notifications/architect.md index f8d5e4f..294e69c 100644 --- a/backend/src/main/resources/content/profile/notifications/architect.md +++ b/backend/src/main/resources/content/profile/notifications/architect.md @@ -50,6 +50,7 @@ hiveops-auth | `notifyOnStatusChange` | boolean | `false` | | `notificationQuietStart` | String `HH:MM` \| null | `null` | | `notificationQuietEnd` | String `HH:MM` \| null | `null` | +| `inboxPollSeconds` | int (0–3600) | `60` | The same document also carries display/appearance/security prefs (itemsPerPage, dateFormat, timeFormat, timezone, themeMode, tableDensity, sessionTimeoutMinutes) — one blob, one column, shared across all Profile tabs. @@ -64,7 +65,8 @@ The same document also carries display/appearance/security prefs (itemsPerPage, `updatePreferences` applies each incoming field only when non-null (`if (req.getX() != null) prefs.setX(...)`), so partial PUTs are safe — the Notifications tab sends only its own fields and leaves display/appearance/security untouched. **Consequence:** a `null` for a quiet-hours field is a no-op, so quiet hours cannot be cleared through this endpoint once set (see Internal tab). ## What is NOT here (by design / by gap) -- No producer to any notification/delivery topic; no consumer reads these flags. Delivery wiring (email/push, severity gating, quiet-hours enforcement) does not exist in the codebase yet — the screen is a stored-preference surface awaiting a consumer. +- No producer to any notification/delivery topic; no consumer reads the **email/push/severity/quiet-hours** flags. Delivery wiring (email/push, severity gating, quiet-hours enforcement) does not exist in the codebase yet — for those flags the screen is a stored-preference surface awaiting a consumer. +- **`inboxPollSeconds` (added 2026-07-16) is the one field here with a live consumer.** `hiveops-profile` `App.svelte` reads it reactively to arm the poll timer behind the Inbox unread badge — see [profile.inbox]. Still no Kafka: the consumer is the SPA itself, over the same synchronous prefs read. - No admin/cross-user editing path; identity is pinned to the JWT principal. Cross-links: [platform.data-architecture] · [platform.kafka] · [platform.service-ownership] diff --git a/backend/src/main/resources/content/profile/notifications/claude.md b/backend/src/main/resources/content/profile/notifications/claude.md index a522845..2420624 100644 --- a/backend/src/main/resources/content/profile/notifications/claude.md +++ b/backend/src/main/resources/content/profile/notifications/claude.md @@ -28,16 +28,19 @@ Terse agent reference. Act from this; do not infer beyond it. ## Storage (exact) - Table **`users`**, column **`preferences`** — `JSONB NOT NULL DEFAULT '{}'` (Flyway `V9__add_user_preferences.sql`). - Hibernate: `@JdbcTypeCode(SqlTypes.JSON)` on `User.preferences` → POJO `UserPreferences` (not an entity). -- Notification JSON keys: `emailNotificationsEnabled`, `pushNotificationsEnabled`, `notifyOnCritical`, `notifyOnHigh`, `notifyOnAssignment`, `notifyOnStatusChange`, `notificationQuietStart`, `notificationQuietEnd`. -- Defaults: email/push/critical/high = `true`; assignment/statusChange = `false`; quiet start/end = `null`. +- Notification JSON keys: `emailNotificationsEnabled`, `pushNotificationsEnabled`, `notifyOnCritical`, `notifyOnHigh`, `notifyOnAssignment`, `notifyOnStatusChange`, `notificationQuietStart`, `notificationQuietEnd`, `inboxPollSeconds`. +- Defaults: email/push/critical/high = `true`; assignment/statusChange = `false`; quiet start/end = `null`; `inboxPollSeconds` = `60`. +- `inboxPollSeconds` — how often the profile SPA re-fetches the Inbox unread count for the nav badge. `0` = polling off (badge refreshes on page load only). Added 2026-07-16; **no migration** (absent key reads as the POJO default, so existing rows are 60). ## Validation (PUT body → `UpdatePreferencesRequest`) - `notificationQuietStart` / `notificationQuietEnd`: regex `^([01]\d|2[0-3]):[0-5]\d$` (24h `HH:MM`) or omit. +- `inboxPollSeconds`: `@Min(0) @Max(3600)`. Out of range = HTTP 400. - (Non-notification, same doc) `sessionTimeoutMinutes` 5–480; `itemsPerPage` 1–500. ## Gotchas - Merge is null-skip: PUT `notificationQuietStart: null` does **not** clear it — old value persists. To clear, edit DB: `UPDATE users SET preferences = preferences - 'notificationQuietStart' - 'notificationQuietEnd' WHERE email=...`. -- Flags are **stored only** — no consumer anywhere sends email/push, gates on severity, or enforces quiet hours. Changing toggles has no delivery effect currently. +- The **email/push/severity/quiet-hours flags are stored only** — no consumer anywhere sends email/push, gates on severity, or enforces quiet hours. Changing those toggles has no delivery effect currently. +- **`inboxPollSeconds` is the exception: it has a real consumer.** `hiveops-profile` `App.svelte` reads it reactively to arm the unread-count poll timer. It is the only setting on this tab that does something today. Do not lump it in with the stored-only flags. - Empty/`{}` `preferences` column reads as all-defaults, not an error. - DB is on VM `10.10.10.188` (ProxyJump via services VM), not the services host. - CORS: `profile.bcos.cloud` must be in `hiveiq-auth` `CORS_ALLOWED_ORIGINS` (and mgmt). @@ -45,6 +48,6 @@ Terse agent reference. Act from this; do not infer beyond it. ## Do NOT - Do NOT look for a `hiveops-profile` backend, a `preferences` table, or a Kafka topic — none exist. - Do NOT claim admin/`MSP_ADMIN` can edit another user's notification prefs here. -- Do NOT tell a user these toggles change alert delivery — no pipeline consumes them yet. +- Do NOT tell a user the email/push/severity/quiet-hours toggles change alert delivery — no pipeline consumes them yet. (`inboxPollSeconds` DOES take effect.) - Do NOT send `null` to clear quiet hours and expect it to clear (server skips nulls). - Do NOT route these calls to incident/messaging/devices — the Notifications tab uses only `hiveops-auth`. diff --git a/backend/src/main/resources/content/profile/notifications/internal.md b/backend/src/main/resources/content/profile/notifications/internal.md index fcc0133..14df709 100644 --- a/backend/src/main/resources/content/profile/notifications/internal.md +++ b/backend/src/main/resources/content/profile/notifications/internal.md @@ -30,10 +30,12 @@ Ops/support view of the **Notifications** preference screen. This is a **self-se ## Common failure modes + fixes | Symptom | Likely cause | Fix | |---|---|---| -| PUT returns 400 on save | Quiet start/end not `HH:MM` 24h | Enter a valid time or clear (see caveat) | +| PUT returns 400 on save | Quiet start/end not `HH:MM` 24h, or `inboxPollSeconds` outside 0–3600 | Enter a valid time / interval | | Save 401 | Stale JWT | Re-login on `profile.bcos.cloud` | | Save 403 / CORS error | `profile.bcos.cloud` not in `CORS_ALLOWED_ORIGINS` | Add origin to `hiveiq-auth` env, restart | -| Toggles save, no alerts arrive | No delivery pipeline reads these flags | Product gap — set expectations, do not "fix" in this screen | +| Toggles save, no alerts arrive | No delivery pipeline reads the email/push/severity/quiet-hours flags | Product gap — set expectations, do not "fix" in this screen. (Does **not** apply to **Check for new messages**, which works.) | +| Inbox badge never updates | **Check for new messages** set to **Off** | By design: `Off` refreshes the badge on page load only. Pick an interval | +| Inbox badge stale but not Off | `GET /messaging/messages/unread/count` failing | The SPA swallows poll errors on purpose (no toast per failed poll). Check `hiveiq-messaging` health and the browser console | | Quiet hours won't clear | Service skips null fields on merge | Known bug — DB edit or backend fix needed | ## Direct DB inspection (hiveiq_auth, on database VM 10.10.10.188 via ProxyJump) diff --git a/backend/src/main/resources/content/profile/notifications/overview.md b/backend/src/main/resources/content/profile/notifications/overview.md index 22da519..b068f65 100644 --- a/backend/src/main/resources/content/profile/notifications/overview.md +++ b/backend/src/main/resources/content/profile/notifications/overview.md @@ -21,6 +21,14 @@ Decide which events are worth notifying you about: - **Incident assignments** — get notified when an incident is assigned to you. - **Status changes** — get notified when an incident's status is updated. +## 📬 Check for New Messages +Choose how often HiveIQ looks for new messages while you have it open. The unread count appears as a red badge on the **Inbox** item in the sidebar. + +1. Pick an interval: **Off**, every **30 seconds**, every **minute** (the default), every **5 minutes**, or every **15 minutes**. +2. Choosing **Off** means the badge only updates when you reload the page. + +> The badge counts **unread** messages only, not your total. Once you read a message the badge goes down straight away, and it disappears when nothing is unread. + ## 🕐 Set Quiet Hours 1. Set a **From** and **To** time to define a do-not-disturb window. 2. During that window, no notifications will be sent. diff --git a/backend/src/main/resources/content/profile/notifications/testing.md b/backend/src/main/resources/content/profile/notifications/testing.md index b7cb5ff..eb02554 100644 --- a/backend/src/main/resources/content/profile/notifications/testing.md +++ b/backend/src/main/resources/content/profile/notifications/testing.md @@ -23,13 +23,17 @@ No simulator devices are needed — this is a **personal, per-user settings page | # | Do this | ✅ Pass if | |---|---------|-----------| -| 1 | As `customer_a`, open **Profile → Notifications** | Page loads with three sections — **Channels**, **Alert severity**, **Quiet hours** — and **Save** is greyed out (nothing to save yet) | +| 1 | As `customer_a`, open **Profile → Notifications** | Page loads with four sections — **Channels**, **Alert severity**, **Messages**, **Quiet hours** — and **Save** is greyed out (nothing to save yet) | | 2 | Toggle **Email notifications** off | An **Unsaved changes** badge appears at the top, a **Discard** button appears, and **Save** becomes clickable | | 3 | Click **Discard** | The toggle jumps back to its saved state; the **Unsaved changes** badge and **Discard** button disappear and **Save** is greyed again | | 4 | Toggle **Incident assignments** on, then click **Save** | A green **"Notification settings saved"** toast shows; badge disappears and **Save** greys out again | | 5 | Reload the page (F5) | Your change stuck — **Incident assignments** is still on | | 6 | Under **Quiet hours**, set **From** and **To** times, then **Save**, then reload | Toast confirms save and both times are still shown after the reload | -| 7 | Log in as `msp_a`, open **Notifications** | You see the **same self-service screen** editing your own prefs — there is **no institution filter** and no way to pick or edit another user's notifications | -| 8 | Log in as `bcos_a` (admin), open **Notifications** | Same screen as everyone else — **no extra admin controls** to edit other users' notification settings (admins only manage their own here) | +| 7 | Under **Messages**, set **Check for new messages** to *Every 30 seconds*, **Save**, then reload | Toast confirms save and the dropdown still reads *Every 30 seconds* after the reload | +| 8 | Set it to **Off** | A line appears warning the Inbox badge will only update when you reload the page | +| 9 | Set it back to *Every minute* and **Save**. Have someone send you a message (or send one to yourself from **Inbox → Compose**) and wait about a minute **without** opening Inbox | A red count badge appears on the **Inbox** item in the sidebar | +| 10 | Open **Inbox** and read that message | The sidebar badge drops by one straight away (it does not wait for the next check), and disappears once nothing is unread | +| 11 | Log in as `msp_a`, open **Notifications** | You see the **same self-service screen** editing your own prefs — there is **no institution filter** and no way to pick or edit another user's notifications | +| 12 | Log in as `bcos_a` (admin), open **Notifications** | Same screen as everyone else — **no extra admin controls** to edit other users' notification settings (admins only manage their own here) | **Report a fail with:** the row #, which login you used, and what you actually saw.