Skip to content

Commit dd6d860

Browse files
committed
feat(mesher/client): add live dashboard flows
1 parent d3b3da5 commit dd6d860

34 files changed

+11532
-1865
lines changed

mesher/api/helpers.mpl

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Provides common utilities used across search, dashboard, and team handlers.
33
# All functions are pub for cross-module import.
44

5-
from Storage.Queries import get_project_id_by_slug
5+
from Storage.Queries import get_project_id_by_slug, get_org_id_by_slug
66

77
# Cluster-aware registry lookup.
88
# In cluster mode (Node.self returns non-empty), uses Global.whereis for
@@ -35,6 +35,23 @@ pub fn resolve_project_id(pool :: PoolHandle, raw_id :: String) -> String do
3535
end
3636
end
3737

38+
# Resolve an org identifier to a UUID.
39+
# If the identifier is 36 chars (UUID format), returns it directly.
40+
# Otherwise, treats it as a slug and looks up the org UUID from the database.
41+
# Returns the UUID string on success, or an empty string if slug not found.
42+
43+
pub fn resolve_org_id(pool :: PoolHandle, raw_id :: String) -> String do
44+
if String.length(raw_id) == 36 do
45+
raw_id
46+
else
47+
let result = get_org_id_by_slug(pool, raw_id)
48+
case result do
49+
Ok( uuid) -> uuid
50+
Err( _) -> ""
51+
end
52+
end
53+
end
54+
3855
# Extract optional query parameter with a default value.
3956
# Request.query returns Option<String>; case match to Some/None.
4057

mesher/api/team.mpl

Lines changed: 78 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ from Storage.Queries import (
1515
revoke_api_key
1616
)
1717
from Types.User import User, OrgMembership
18-
from Api.Helpers import query_or_default, to_json_array, require_param, get_registry, resolve_project_id
18+
from Types.Project import ApiKey
19+
from Api.Helpers import query_or_default, to_json_array, require_param, get_registry, resolve_org_id, resolve_project_id
1920

2021
# --- Shared helpers (leaf functions first, per define-before-use requirement) ---
2122
# Serialize a member row (with user info) to JSON.
@@ -54,16 +55,32 @@ end
5455

5556
# Serialize an API key row to JSON.
5657
# Fields: id, project_id, key_value, label, created_at, revoked_at
57-
# If revoked_at is empty string, emit null instead of quoted empty string.
58+
# Uses Json.encode + Json.get so string fields remain truthful values.
59+
60+
fn api_key_row_to_api_key(row) -> ApiKey do
61+
ApiKey {
62+
id : Map.get(row, "id"),
63+
project_id : Map.get(row, "project_id"),
64+
key_value : Map.get(row, "key_value"),
65+
label : Map.get(row, "label"),
66+
created_at : Map.get(row, "created_at"),
67+
revoked_at : if String.length(Map.get(row, "revoked_at")) == 0 do
68+
None
69+
else
70+
Some(Map.get(row, "revoked_at"))
71+
end
72+
}
73+
end
5874

5975
fn api_key_to_json(row) -> String do
60-
let id = Map.get(row, "id")
61-
let project_id = Map.get(row, "project_id")
62-
let key_value = Map.get(row, "key_value")
63-
let label = Map.get(row, "label")
64-
let created_at = Map.get(row, "created_at")
65-
let revoked_at_raw = Map.get(row, "revoked_at")
66-
let revoked_at = if String.length(revoked_at_raw) == 0 do
76+
let api_key_json = Json.encode(api_key_row_to_api_key(row))
77+
let id = Json.get(api_key_json, "id")
78+
let project_id = Json.get(api_key_json, "project_id")
79+
let key_value = Json.get(api_key_json, "key_value")
80+
let label = Json.get(api_key_json, "label")
81+
let created_at = Json.get(api_key_json, "created_at")
82+
let revoked_at_raw = Json.get(api_key_json, "revoked_at")
83+
let revoked_at = if revoked_at_raw == "" or revoked_at_raw == "null" do
6784
"null"
6885
else
6986
"\"#{revoked_at_raw}\""
@@ -78,6 +95,12 @@ fn extract_json_field(pool :: PoolHandle, body :: String, field :: String) -> St
7895
Ok(Json.get(body, field))
7996
end
8097

98+
# Helper: resolve org path identifiers (UUID or slug) and fail explicitly when unknown.
99+
100+
fn resolve_team_org_id(pool :: PoolHandle, raw_id :: String) -> String do
101+
resolve_org_id(pool, raw_id)
102+
end
103+
81104
# --- Team membership helper functions for case arm extraction (ORG-04) ---
82105
# Helper: handle successful add_member result.
83106

@@ -164,7 +187,11 @@ end
164187
fn perform_role_update(pool :: PoolHandle, membership_id :: String, role :: String) do
165188
let result = update_member_role(pool, membership_id, role)
166189
case result do
167-
Ok( _) -> update_role_success()
190+
Ok( count) -> if count > 0 do
191+
update_role_success()
192+
else
193+
HTTP.response(400, json { error : "invalid role or membership not found" })
194+
end
168195
Err( e) -> HTTP.response(500, json { error : e })
169196
end
170197
end
@@ -213,14 +240,19 @@ end
213240
pub fn handle_list_members(request) do
214241
let reg_pid = get_registry()
215242
let pool = PipelineRegistry.get_pool(reg_pid)
216-
let org_id = require_param(request, "org_id")
217-
let result = get_members_with_users(pool, org_id)
218-
case result do
219-
Ok( rows) -> HTTP.response(200,
220-
rows
221-
|> List.map(fn (row) do member_to_json(row) end)
222-
|> to_json_array())
223-
Err( e) -> HTTP.response(500, json { error : e })
243+
let raw_id = require_param(request, "org_id")
244+
let org_id = resolve_team_org_id(pool, raw_id)
245+
if String.length(org_id) == 0 do
246+
HTTP.response(404, json { error : "organization not found" })
247+
else
248+
let result = get_members_with_users(pool, org_id)
249+
case result do
250+
Ok( rows) -> HTTP.response(200,
251+
rows
252+
|> List.map(fn (row) do member_to_json(row) end)
253+
|> to_json_array())
254+
Err( e) -> HTTP.response(500, json { error : e })
255+
end
224256
end
225257
end
226258

@@ -231,9 +263,14 @@ end
231263
pub fn handle_add_member(request) do
232264
let reg_pid = get_registry()
233265
let pool = PipelineRegistry.get_pool(reg_pid)
234-
let org_id = require_param(request, "org_id")
266+
let raw_id = require_param(request, "org_id")
267+
let org_id = resolve_team_org_id(pool, raw_id)
235268
let body = Request.body(request)
236-
validate_add_member(pool, org_id, body)
269+
if String.length(org_id) == 0 do
270+
HTTP.response(404, json { error : "organization not found" })
271+
else
272+
validate_add_member(pool, org_id, body)
273+
end
237274
end
238275

239276
# Handle POST /api/v1/orgs/:org_id/members/:membership_id/role
@@ -242,9 +279,15 @@ end
242279
pub fn handle_update_member_role(request) do
243280
let reg_pid = get_registry()
244281
let pool = PipelineRegistry.get_pool(reg_pid)
282+
let raw_id = require_param(request, "org_id")
283+
let org_id = resolve_team_org_id(pool, raw_id)
245284
let membership_id = require_param(request, "membership_id")
246285
let body = Request.body(request)
247-
do_update_role(pool, membership_id, body)
286+
if String.length(org_id) == 0 do
287+
HTTP.response(404, json { error : "organization not found" })
288+
else
289+
do_update_role(pool, membership_id, body)
290+
end
248291
end
249292

250293
# Handle POST /api/v1/orgs/:org_id/members/:membership_id/remove
@@ -253,11 +296,21 @@ end
253296
pub fn handle_remove_member(request) do
254297
let reg_pid = get_registry()
255298
let pool = PipelineRegistry.get_pool(reg_pid)
299+
let raw_id = require_param(request, "org_id")
300+
let org_id = resolve_team_org_id(pool, raw_id)
256301
let membership_id = require_param(request, "membership_id")
257-
let result = remove_member(pool, membership_id)
258-
case result do
259-
Ok( _) -> remove_success()
260-
Err( e) -> HTTP.response(500, json { error : e })
302+
if String.length(org_id) == 0 do
303+
HTTP.response(404, json { error : "organization not found" })
304+
else
305+
let result = remove_member(pool, membership_id)
306+
case result do
307+
Ok( count) -> if count > 0 do
308+
remove_success()
309+
else
310+
HTTP.response(404, json { error : "membership not found" })
311+
end
312+
Err( e) -> HTTP.response(500, json { error : e })
313+
end
261314
end
262315
end
263316

mesher/client/README.md

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This package is the canonical TanStack dashboard app for Mesher.
44

5-
It runs as a Vite-powered TanStack Start app from `hyperpush-mono/mesher/client/` and stays on the existing mock-data and client-state contract. Do not add backend calls, server functions, or widened URL/search-param semantics here as part of path-migration work.
5+
It runs as a Vite-powered TanStack Start app from `hyperpush-mono/mesher/client/` and now exercises the Issues route through a provider-owned same-origin `/api/v1` seam for both live reads and the supported maintainer actions. The shell stays visually intact by overlaying truthful Mesher issue/detail/timeline state onto the existing mock-shaped dashboard model, so unsupported fields remain visible and explicitly shell-only instead of disappearing or pretending to be live.
66

77
## Package root
88

@@ -21,8 +21,9 @@ npm --prefix mesher/client ci
2121
npm --prefix mesher/client run dev
2222
npm --prefix mesher/client run build
2323
PORT=3001 npm --prefix mesher/client run start
24-
npm --prefix mesher/client run test:e2e:dev
25-
npm --prefix mesher/client run test:e2e:prod
24+
bash mesher/scripts/seed-live-issue.sh
25+
npm --prefix mesher/client run test:e2e:dev -- --grep "issues live"
26+
npm --prefix mesher/client run test:e2e:prod -- --grep "issues live"
2627
```
2728

2829
From this package directory:
@@ -32,40 +33,69 @@ npm ci
3233
vite dev
3334
vite build
3435
node server.mjs
35-
npm run test:e2e:dev
36-
npm run test:e2e:prod
36+
bash ../scripts/seed-live-issue.sh
37+
npm run test:e2e:dev -- --grep "issues live"
38+
npm run test:e2e:prod -- --grep "issues live"
3739
```
3840

3941
## Runtime contract
4042

4143
- `vite dev` starts the local dashboard dev server on port `3000` by default.
4244
- `vite build` produces the production bundle in `dist/`.
4345
- `node server.mjs` serves the built app and static assets from `dist/client`.
44-
- `test:e2e:dev` verifies the direct-entry route and shell-state parity against the dev server.
45-
- `test:e2e:prod` verifies the same parity contract against the built production server.
46+
- `test:e2e:dev` verifies the live Issues seam against the dev server and same-origin Mesher proxy.
47+
- `test:e2e:prod` verifies the same live seam against the built production server.
48+
- `bash mesher/scripts/seed-live-issue.sh` deterministically seeds the read-proof issue and resets the action-proof issue back to an open/unresolved state before live verification.
49+
50+
## Live Issues seam
51+
52+
- Overview reads go through same-origin `/api/v1/projects/default/*` routes.
53+
- Selecting an issue reads `/api/v1/issues/:issue_id/events?limit=1`, `/api/v1/events/:event_id`, and `/api/v1/issues/:issue_id/timeline` through the provider-owned state path.
54+
- Supported live maintainer actions are `Resolve`, `Reopen`, and `Ignore`, which call same-origin `/api/v1/issues/:issue_id/{resolve,unresolve,archive}` from the existing detail action row.
55+
- The UI intentionally uses a mixed live/mock overlay: live Mesher truth replaces supported fields, while unsupported shell sections keep explicit fallback values.
56+
- `AI Analysis`, issue-link chrome, bounty chrome, and the retained proof-rail buttons remain visible but shell-only; they are not claimed as live backend actions.
57+
- Backend read and mutation failures are surfaced through the mounted Radix toaster and `issues-shell` / detail-panel `data-*` attributes instead of silently reverting.
4658

4759
## Important files
4860

4961
- `src/routes/` — TanStack route tree for the dashboard shell and direct-entry pages.
5062
- `src/router.tsx` and `src/routeTree.gen.ts` — router assembly.
5163
- `server.mjs` — package-local production bridge for the built app.
5264
- `playwright.config.ts` — package-local dev/prod Playwright harness.
53-
- `tests/e2e/dashboard-route-parity.spec.ts` — route/UI parity proof on mock data.
65+
- `tests/e2e/issues-live-read.spec.ts` — live Issues read proof, including detail/timeline reads, sparse fallback retention, and failure toasts.
66+
- `tests/e2e/issues-live-actions.spec.ts` — live Issues action proof for resolve/reopen/ignore, same-origin routing, and destructive toast coverage.
67+
- `components/dashboard/dashboard-issues-state.tsx` — provider-owned overview + selected-issue Mesher read orchestration.
68+
- `components/ui/toaster.tsx` and `hooks/use-toast.ts` — mounted Radix toast surface reused for live read and mutation failures.
5469
- `app/globals.css` — shared global styles imported by the TanStack root route.
5570

5671
## Verification notes
5772

58-
The canonical proof surface for this package is package-local:
73+
The canonical full-shell proof rail layers both seed helpers with the assembled walkthrough and the existing live Issues/admin+ops suites:
5974

6075
```bash
61-
npm --prefix mesher/client run build
62-
npm --prefix mesher/client run test:e2e:dev
63-
npm --prefix mesher/client run test:e2e:prod
76+
bash mesher/scripts/seed-live-issue.sh
77+
bash mesher/scripts/seed-live-admin-ops.sh
78+
npm --prefix mesher/client run test:e2e:dev -- --grep "issues live|admin and ops live|seeded walkthrough"
79+
npm --prefix mesher/client run test:e2e:prod -- --grep "issues live|admin and ops live|seeded walkthrough"
80+
```
81+
82+
- `tests/e2e/seeded-walkthrough.spec.ts` is the canonical route-to-route shell proof. It covers direct-entry and in-app navigation parity across Issues, Performance, Solana Programs, Releases, Alerts, Bounties, Treasury, and Settings.
83+
- `tests/e2e/live-runtime-helpers.ts` owns the shared same-origin request tracking, direct-backend rejection, and filtered runtime diagnostics used by the walkthrough and route-level live suites.
84+
- `bash mesher/scripts/seed-live-issue.sh` resets the seeded live Issues read/action fixtures.
85+
- `bash mesher/scripts/seed-live-admin-ops.sh` resets the seeded Alerts and Settings admin/ops fixtures used by the assembled rail.
86+
87+
The narrower route-level verification commands remain useful when you are only iterating on the Issues seam:
88+
89+
```bash
90+
bash mesher/scripts/seed-live-issue.sh
91+
npm --prefix mesher/client run test:e2e:dev -- --grep "issues live"
92+
npm --prefix mesher/client run test:e2e:prod -- --grep "issues live"
6493
```
6594

66-
When a move or refactor breaks the package contract, expect the first signal to appear as one of:
95+
When the seam regresses, the first signal should appear as one of:
6796

68-
- a missing file under `mesher/client/`
97+
- a failed same-origin `/api/v1` request in Playwright request tracking
98+
- a visible destructive toast for mutation or selected-issue read failures
99+
- a mismatched `issues-shell` or `issue-detail-panel` `data-*` attribute
69100
- a broken `dev`, `build`, `start`, or `test:e2e:*` script
70-
- a Playwright console/request failure in `tests/e2e/dashboard-route-parity.spec.ts`
71101
- a `node server.mjs` boot failure after build

0 commit comments

Comments
 (0)