diff --git a/frontend/packages/swarm-ui/src/App.tsx b/frontend/packages/swarm-ui/src/App.tsx
index 65d47afe..854f5169 100644
--- a/frontend/packages/swarm-ui/src/App.tsx
+++ b/frontend/packages/swarm-ui/src/App.tsx
@@ -6,6 +6,7 @@ import { AgentsPage } from './pages/AgentsPage.js';
import { ComponentsPage } from './pages/ComponentsPage.js';
import { JobsPage } from './pages/JobsPage.js';
import { HivesPage } from './pages/HivesPage.js';
+import { IssueReportPage } from './pages/IssueReportPage.js';
import { Panel } from './ui/panel/Panel.js';
function NotFound() {
@@ -23,6 +24,7 @@ export function App() {
+
diff --git a/frontend/packages/swarm-ui/src/pages/IssueReportPage.css b/frontend/packages/swarm-ui/src/pages/IssueReportPage.css
new file mode 100644
index 00000000..1419727d
--- /dev/null
+++ b/frontend/packages/swarm-ui/src/pages/IssueReportPage.css
@@ -0,0 +1,43 @@
+/* — the repo-picker + hide-blocked toggle sit in one
+ row (mirrors CreateAgentForm's row/wrap pattern: flex-wrap so narrow
+ viewports stack instead of overflowing), the label chips get their
+ own row below since the set is open-ended and shouldn't fight the
+ controls row for width. Sort buttons live inside the table's own
+ `
` cells (Table.tsx's `header` now accepts real markup for
+ exactly this) so they inherit the table's header styling for free —
+ only the pointer cursor + no-underline reset is this page's to own. */
+.issue-report-controls {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-end;
+ gap: 1.25em;
+ margin-bottom: 1em;
+}
+.issue-report-toggle {
+ display: flex;
+ align-items: center;
+ gap: 0.4em;
+ cursor: pointer;
+}
+.issue-report-labels {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.6em;
+ margin-bottom: 1em;
+}
+.issue-report-label-chip {
+ display: flex;
+ align-items: center;
+ gap: 0.35em;
+ cursor: pointer;
+ color: var(--muted);
+}
+.issue-report-sort-btn {
+ background: none;
+ border: none;
+ padding: 0;
+ margin: 0;
+ font: inherit;
+ color: inherit;
+ cursor: pointer;
+}
diff --git a/frontend/packages/swarm-ui/src/pages/IssueReportPage.tsx b/frontend/packages/swarm-ui/src/pages/IssueReportPage.tsx
new file mode 100644
index 00000000..62171995
--- /dev/null
+++ b/frontend/packages/swarm-ui/src/pages/IssueReportPage.tsx
@@ -0,0 +1,287 @@
+// — mara's custom issue report: browse open issues
+// across every repo that has one, sortable/filterable her own way, with
+// "hide blocked (open dependency)" as the headline filter and a rank
+// column for how many other open issues depend on each one. Backed by
+// swarm-controller's `GET /api/repos` (dropdown source, only repos with
+// an open issue), `GET /api/issue-report` (default — every repo
+// combined) and `GET /api/repos/{org}/{repo}/issue-report` (once a
+// specific repo is picked) — `blocked` and `depended_on_by_count` both
+// arrive pre-resolved per row, so nothing here does its own
+// dependency-graph walk.
+//
+// Sorting and the label/hide-blocked filters are client-side over
+// whatever's currently loaded — only the repo selection re-fetches.
+// Default sort is `depended_on_by_count` descending: mara's own framing
+// ("i would also like to rank them by how many issues depend on them")
+// reads as the report's headline ordering, not just one more column.
+//
+// No `RefreshIntervalPicker`/auto-poll like the roster pages — this
+// reads as a "generate a report" action rather than a live status view
+// (mara: a "generating report" spinner sub-5s is fine, doesn't need to
+// be instant), so it fetches once per repo-selection change instead of
+// polling.
+import { useEffect, useMemo, useState } from 'preact/hooks';
+import { ApiErrorPanel } from '@hive/shared/api-error-panel.js';
+import { readApiError, type ProblemDetails } from '@hive/shared/api-error.js';
+import { Badge } from '@hive/shared/badge.js';
+import { Panel } from '../ui/panel/Panel.js';
+import { SelectField } from '../ui/select-field/SelectField.js';
+import { Table, type TableColumn } from '../ui/table/Table.js';
+import './IssueReportPage.css';
+
+interface IssueReportRow {
+ repo: string;
+ number: number;
+ title: string;
+ labels: string[];
+ assignee: string | null;
+ html_url: string | null;
+ blocked: boolean;
+ depended_on_by_count: number;
+}
+
+type SortKey = 'repo' | 'number' | 'title' | 'assignee' | 'blocked' | 'depended_on_by_count';
+type SortDir = 'asc' | 'desc';
+
+// Sentinel `