What happened?
The Inbox page bills itself as personal. Its subtitle renders {{ totalActionItems }} action item(s) requiring attention (web/src/views/Inbox.vue:22), summing all seven panes (:860-862). But most of those panes fetch the entire organization.
Logged in as a reader with no assignments, no authored comments and no open reviews, that banner reads 38 action items requiring attention. Not one of the 38 has anything to do with them.
The panes do not agree with each other about what "mine" means. Two pass the viewer's email; four pass nothing; one filters on a field the API never returns.
| Pane |
Endpoint |
Scoped to viewer? |
Cite |
| Comments |
GET /comments/open |
No — org-wide. SQL filters only organization_id and status = 'open' |
Inbox.vue:1024, api.js:268, api_collab.go:1699-1701, db/postgres.go:791-796 |
| Reviews |
GET /reviews |
Attempted and broken. Org-wide fetch, then a client-side filter on requested_by or rev._assignments — a key GET /reviews never returns |
Inbox.vue:1033, :1036-1040, api_collab.go:241-254, db/postgres.go:275-294 |
| Tasks |
GET /tasks |
No — org-wide, minus private tasks |
Inbox.vue:1091, api.js:294, api_collab.go:2107-2124, :201-207 |
| Incidents |
GET /incidents?assignee=<me> |
Yes — server-side |
Inbox.vue:1149, api.js:403, api_incidents.go:83 |
| Changes |
GET /changes |
No — org-wide, no viewer scoping of any kind |
Inbox.vue:1140, api.js:321, api_collab.go:2527-2541 |
| Corrective actions |
GET /corrective-actions?assignee=<me> |
Yes — server-side |
Inbox.vue:1159, api.js:412, api_corrective.go:59 |
| Suggestions |
GET /suggestions?status=<filter> |
No — org-wide, minus suggestions on private tasks |
Inbox.vue:1169, api.js:303, api_suggestions.go:198-227 |
All seven fire from the same Promise.all in onMounted (Inbox.vue:1264-1295). Incidents and corrective actions pass currentUserEmail.value; the loaders immediately above and below them do not.
(A smaller bug in the same loader: the fetch is filtered to a single status — suggestionFilter defaults to 'open' at Inbox.vue:813 — while the computed that feeds the pane accepts open or in_review (:858). An in_review suggestion can therefore never render on the default view.)
A code comment promises the filtering that never happens. In handleInbox:
// All open comments on documents the user has reviewed
allComments, _ := s.db.AllOpenComments(ctx, orgID)
— internal/isms/api/api_collab.go:3442-3443
The storage layer is honest about what that call does:
// AllOpenComments returns all open comments across all documents.
func (d *DB) AllOpenComments(ctx context.Context, orgID int) ([]Comment, error) {
rows, err := d.pool.Query(ctx, `
SELECT …
FROM comments WHERE organization_id = $1 AND status = 'open'
ORDER BY created_at DESC
`, orgID)
— internal/isms/db/postgres.go:790-796
There is no author predicate, no review-participation join, no involvement condition of any kind. "documents the user has reviewed" is not implemented anywhere. The same function is called from three places: the REST GET /comments/open that the web page uses (api_collab.go:1699-1701), GET /inbox (:3443), and GET /inbox/dump (:3517), the last two backing isms inbox list and isms inbox dump (internal/isms/client/client.go:679, :688).
The only client-side narrowing on that pane is .filter(c => !c.parent_id) (Inbox.vue:1025) — that drops threaded replies, not other people's comments. It is easy to misread as an involvement filter.
What scoping does exist is about privacy, not involvement. taskViewer sets CanSeeAll from the role (api_collab.go:201-207) and its SQL clause is AND (t.private = false OR t.created_by = $n OR t.assignee_id = …) (db/tasks.go:75-81). It hides private tasks. It does not make the pane personal, and there is no equivalent for changes, comments, or suggestions.
The Reviews pane fails the opposite way. It filters to reviews where the viewer is the requester or an assignee — but db.Review (db/postgres.go:275-294) has no assignments field and handleListReviews returns the struct as-is (api_collab.go:241-254), so rev._assignments is always undefined and the second clause is dead. _assignments is populated only by Reviews.vue:1383-1388, which fetches per-review assignments client-side on a different page. Net effect: the Inbox never shows you a review you were asked to review — only ones you sent. That is the single item most likely to actually need your attention. The CLI inbox has the same blind spot by a different mechanism (handleInbox filters on RequestedBy == actor at api_collab.go:3412 and :3423, with no assignment lookup at all), so this is systemic across both human surfaces rather than a Vue bug. Notably, the endpoint built for agents gets it right: handleAgentPendingActions documents "For reviewer agents: reviews with pending assignments for them" and calls ListPendingAssignmentsForReviewer (api_collab.go:3621-3634). (Separately, GET /reviews defaults to limit = 50 (api_collab.go:261-262) before the client filter runs, so past 50 org-wide reviews the pane silently truncates. Latent; noted, not the main point.)
Steps to reproduce
Against $BASE/api/v1 on an org with a few users' worth of activity. Purely read-only.
1. Log in as a reader with zero involvement, and as three other users. Hit the endpoints the Inbox page hits, applying the same client-side filters the page applies.
reader (no assignments):
banner = 38 action items requiring attention | involving the viewer: 0
comments 6 (mine 0) · reviews 0 · tasks 14 (mine 0) · changes 4 (mine 0)
· incidents 0 · CAs 0 · suggestions 14 (mine 0)
contributor A:
banner = 39 | involving the viewer: 17
comments 6 (mine 0) · reviews 0 · tasks 14 (mine 8) · changes 4 (mine 2)
· incidents 0 · CAs 1 · suggestions 14 (mine 6)
contributor B:
banner = 38 | involving the viewer: 11
comments 6 (mine 0) · reviews 0 · tasks 14 (mine 2) · changes 4 (mine 1)
· incidents 0 · CAs 0 · suggestions 14 (mine 8)
manager:
banner = 39 | involving the viewer: 5
comments 6 (mine 0) · reviews 0 · tasks 14 (mine 4) · changes 4 (mine 0)
· incidents 1 · CAs 0 · suggestions 14 (mine 0)
The org-wide panes return the same sets to all four — 6 comments, 14 tasks, 4 changes, 14 suggestions — and they do so by construction, not by coincidence: AllOpenComments and PaginatedChangeRequests take no viewer argument at all, and the org has zero private tasks, which makes taskViewer and taskSuggestionHidden no-ops. Even the one privacy filter that exists produces no difference between a reader and an admin here.
(The Inbox route and its sidebar link are not role-gated — router.js:39-40, App.vue:144 — so a reader reaches this page normally.)
2. Confirm the Reviews pane misses assignments. Two users each hold two pending review assignments. Fetch GET /reviews as each and apply the page's filter:
assigned reviewer 1: org total 4 → rendered 0 ('_assignments' key present in response: false)
assigned reviewer 2: org total 4 → rendered 0 ('_assignments' key present in response: false)
3. Confirm the comments pane is org-wide by construction. GET /comments/open returns the same 7 rows to every account including the reader (6 after the page drops the one threaded reply at Inbox.vue:1025). handleAllOpenComments (api_collab.go:1699-1701) takes only orgID — there is no query parameter that would narrow it.
Expected vs. actual
Expected: a page called "Inbox" that says "N action items requiring attention" shows items requiring your attention — or, if it is deliberately an org activity feed, says so and does not count other people's work as yours.
Actual: for a user with no involvement the Inbox is indistinguishable from an org-wide activity log. It advertises 38 items requiring attention, 0 of which involve them. Meanwhile the reviews they were actually assigned do not appear, because the only involvement filter on the page tests a field the API does not return.
Nothing leaks — this is a signal-to-noise defect, not a security one. Every row the Inbox over-fetches is already readable by the same user through the module's own page: comments through the document/review views (verified — a reader retrieves the same review-scoped comments via GET /documents/:docId/comments?review_id=N), tasks/changes/suggestions through their registers. Private tasks are filtered and stay filtered, in both the task pane (db/tasks.go:75-81) and the suggestions pane (api_suggestions.go:218-227). Do not read a confidentiality problem into this.
Component / surface
Web UI
Version
86ccf59 (0.7.x, unreleased master).
Anything else?
This is a product decision before it is a patch, which is why it is filed as a question with evidence rather than a diff. What is the Inbox meant to be?
- (a) A personal work queue. Every pane filters to involvement — assigned to me, created by me, authored by me, awaiting my review. Comments needs a real predicate (author, or a participation join against the review's assignments); tasks and changes need
assignee=<me> passed the way incidents and CAs already do; suggestions needs a rule (mine? or, for managers, all pending?).
- (b) An org activity feed, honestly labelled. Keep the breadth, drop "requiring attention" from the subtitle, and separate "yours" from "the org's" visually.
- (c) Both, with a toggle. A "Mine / All" switch at the page level, defaulting to Mine.
Whichever is chosen, the panes should agree. Today they do not, and the disagreement is not a design stance — incidents and corrective actions were plainly written by someone with (a) in mind and the rest were not.
Two things are worth fixing regardless of which option wins:
- The
_assignments filter. GET /reviews should return assignments (or the Inbox should call the assignments endpoint the way Reviews.vue:1383-1388 does). Right now the reviews most likely to need action are the ones guaranteed not to show.
- The comment at
api_collab.go:3442. It documents behaviour that does not exist. Either implement it or delete it — leaving it is how the next reader concludes the filtering is already handled.
The CLI shares two of these defects, not all of them. GET /inbox (api_collab.go:3399-3455) is narrower than the web page: tasks go through db.ListTasks with the actor as the assignee argument, which becomes a real SQL predicate (db/tasks.go:113-116), and there are no changes or suggestions sections at all. But its comments block is the same org-wide call (:3442-3443), and its reviews sections filter only on RequestedBy == actor (:3412, :3423) — so isms inbox list and isms inbox dump (client.go:679, :688) show you your tasks, the reviews you sent, and everyone's comments, while still hiding the reviews assigned to you. Fixing the comments query and the assigned-reviews lookup fixes both surfaces; the per-pane scoping question is web-only.
Two adjacent issues, cross-referenced rather than restated. #203 covers the Inbox task status button rendering on every visible task with no ownership check — which is exactly what the org-wide task pane makes possible, so the two fixes interact. #191/#192 cover the broader pattern of write affordances that 403.
Found during a multi-user manual test run on 2026-08-05 against a locally-built server (Postgres, file storage backend, iso27001 template scaffolded, accounts across admin/manager/contributor/reader), and re-verified on 2026-08-07. Every code reference above is from 86ccf59. The live comparison was entirely read-only — no rows were created, modified, or deleted.
What happened?
The Inbox page bills itself as personal. Its subtitle renders
{{ totalActionItems }} action item(s) requiring attention(web/src/views/Inbox.vue:22), summing all seven panes (:860-862). But most of those panes fetch the entire organization.Logged in as a reader with no assignments, no authored comments and no open reviews, that banner reads 38 action items requiring attention. Not one of the 38 has anything to do with them.
The panes do not agree with each other about what "mine" means. Two pass the viewer's email; four pass nothing; one filters on a field the API never returns.
GET /comments/openorganization_idandstatus = 'open'Inbox.vue:1024,api.js:268,api_collab.go:1699-1701,db/postgres.go:791-796GET /reviewsrequested_byorrev._assignments— a keyGET /reviewsnever returnsInbox.vue:1033,:1036-1040,api_collab.go:241-254,db/postgres.go:275-294GET /tasksInbox.vue:1091,api.js:294,api_collab.go:2107-2124,:201-207GET /incidents?assignee=<me>Inbox.vue:1149,api.js:403,api_incidents.go:83GET /changesInbox.vue:1140,api.js:321,api_collab.go:2527-2541GET /corrective-actions?assignee=<me>Inbox.vue:1159,api.js:412,api_corrective.go:59GET /suggestions?status=<filter>Inbox.vue:1169,api.js:303,api_suggestions.go:198-227All seven fire from the same
Promise.allinonMounted(Inbox.vue:1264-1295). Incidents and corrective actions passcurrentUserEmail.value; the loaders immediately above and below them do not.(A smaller bug in the same loader: the fetch is filtered to a single status —
suggestionFilterdefaults to'open'atInbox.vue:813— while the computed that feeds the pane acceptsopenorin_review(:858). Anin_reviewsuggestion can therefore never render on the default view.)A code comment promises the filtering that never happens. In
handleInbox:—
internal/isms/api/api_collab.go:3442-3443The storage layer is honest about what that call does:
—
internal/isms/db/postgres.go:790-796There is no author predicate, no review-participation join, no involvement condition of any kind. "documents the user has reviewed" is not implemented anywhere. The same function is called from three places: the REST
GET /comments/openthat the web page uses (api_collab.go:1699-1701),GET /inbox(:3443), andGET /inbox/dump(:3517), the last two backingisms inbox listandisms inbox dump(internal/isms/client/client.go:679,:688).The only client-side narrowing on that pane is
.filter(c => !c.parent_id)(Inbox.vue:1025) — that drops threaded replies, not other people's comments. It is easy to misread as an involvement filter.What scoping does exist is about privacy, not involvement.
taskViewersetsCanSeeAllfrom the role (api_collab.go:201-207) and its SQL clause isAND (t.private = false OR t.created_by = $n OR t.assignee_id = …)(db/tasks.go:75-81). It hides private tasks. It does not make the pane personal, and there is no equivalent for changes, comments, or suggestions.The Reviews pane fails the opposite way. It filters to reviews where the viewer is the requester or an assignee — but
db.Review(db/postgres.go:275-294) has no assignments field andhandleListReviewsreturns the struct as-is (api_collab.go:241-254), sorev._assignmentsis alwaysundefinedand the second clause is dead._assignmentsis populated only byReviews.vue:1383-1388, which fetches per-review assignments client-side on a different page. Net effect: the Inbox never shows you a review you were asked to review — only ones you sent. That is the single item most likely to actually need your attention. The CLI inbox has the same blind spot by a different mechanism (handleInboxfilters onRequestedBy == actoratapi_collab.go:3412and:3423, with no assignment lookup at all), so this is systemic across both human surfaces rather than a Vue bug. Notably, the endpoint built for agents gets it right:handleAgentPendingActionsdocuments "For reviewer agents: reviews with pending assignments for them" and callsListPendingAssignmentsForReviewer(api_collab.go:3621-3634). (Separately,GET /reviewsdefaults tolimit = 50(api_collab.go:261-262) before the client filter runs, so past 50 org-wide reviews the pane silently truncates. Latent; noted, not the main point.)Steps to reproduce
Against
$BASE/api/v1on an org with a few users' worth of activity. Purely read-only.1. Log in as a reader with zero involvement, and as three other users. Hit the endpoints the Inbox page hits, applying the same client-side filters the page applies.
The org-wide panes return the same sets to all four — 6 comments, 14 tasks, 4 changes, 14 suggestions — and they do so by construction, not by coincidence:
AllOpenCommentsandPaginatedChangeRequeststake no viewer argument at all, and the org has zero private tasks, which makestaskViewerandtaskSuggestionHiddenno-ops. Even the one privacy filter that exists produces no difference between a reader and an admin here.(The Inbox route and its sidebar link are not role-gated —
router.js:39-40,App.vue:144— so areaderreaches this page normally.)2. Confirm the Reviews pane misses assignments. Two users each hold two pending review assignments. Fetch
GET /reviewsas each and apply the page's filter:3. Confirm the comments pane is org-wide by construction.
GET /comments/openreturns the same 7 rows to every account including the reader (6 after the page drops the one threaded reply atInbox.vue:1025).handleAllOpenComments(api_collab.go:1699-1701) takes onlyorgID— there is no query parameter that would narrow it.Expected vs. actual
Expected: a page called "Inbox" that says "N action items requiring attention" shows items requiring your attention — or, if it is deliberately an org activity feed, says so and does not count other people's work as yours.
Actual: for a user with no involvement the Inbox is indistinguishable from an org-wide activity log. It advertises 38 items requiring attention, 0 of which involve them. Meanwhile the reviews they were actually assigned do not appear, because the only involvement filter on the page tests a field the API does not return.
Nothing leaks — this is a signal-to-noise defect, not a security one. Every row the Inbox over-fetches is already readable by the same user through the module's own page: comments through the document/review views (verified — a
readerretrieves the same review-scoped comments viaGET /documents/:docId/comments?review_id=N), tasks/changes/suggestions through their registers. Private tasks are filtered and stay filtered, in both the task pane (db/tasks.go:75-81) and the suggestions pane (api_suggestions.go:218-227). Do not read a confidentiality problem into this.Component / surface
Web UI
Version
86ccf59(0.7.x, unreleasedmaster).Anything else?
This is a product decision before it is a patch, which is why it is filed as a question with evidence rather than a diff. What is the Inbox meant to be?
assignee=<me>passed the way incidents and CAs already do; suggestions needs a rule (mine? or, for managers, all pending?).Whichever is chosen, the panes should agree. Today they do not, and the disagreement is not a design stance — incidents and corrective actions were plainly written by someone with (a) in mind and the rest were not.
Two things are worth fixing regardless of which option wins:
_assignmentsfilter.GET /reviewsshould return assignments (or the Inbox should call the assignments endpoint the wayReviews.vue:1383-1388does). Right now the reviews most likely to need action are the ones guaranteed not to show.api_collab.go:3442. It documents behaviour that does not exist. Either implement it or delete it — leaving it is how the next reader concludes the filtering is already handled.The CLI shares two of these defects, not all of them.
GET /inbox(api_collab.go:3399-3455) is narrower than the web page: tasks go throughdb.ListTaskswith the actor as the assignee argument, which becomes a real SQL predicate (db/tasks.go:113-116), and there are no changes or suggestions sections at all. But its comments block is the same org-wide call (:3442-3443), and its reviews sections filter only onRequestedBy == actor(:3412,:3423) — soisms inbox listandisms inbox dump(client.go:679,:688) show you your tasks, the reviews you sent, and everyone's comments, while still hiding the reviews assigned to you. Fixing the comments query and the assigned-reviews lookup fixes both surfaces; the per-pane scoping question is web-only.Two adjacent issues, cross-referenced rather than restated. #203 covers the Inbox task status button rendering on every visible task with no ownership check — which is exactly what the org-wide task pane makes possible, so the two fixes interact. #191/#192 cover the broader pattern of write affordances that 403.
Found during a multi-user manual test run on 2026-08-05 against a locally-built server (Postgres, file storage backend,
iso27001template scaffolded, accounts across admin/manager/contributor/reader), and re-verified on 2026-08-07. Every code reference above is from86ccf59. The live comparison was entirely read-only — no rows were created, modified, or deleted.