feat(web): universe dashboard — meshes + incoming invitations
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled

New /dashboard landing that surfaces meshes and invitations-to-you
in one view. Replaces the simple mesh grid at /dashboard (preserved
at /dashboard/legacy).

Backend additions:
- GET /api/my/invites/incoming — pending_invite rows addressed to
  the authed user's email, joined with invite for role + expiry and
  user/mesh for display. Unaccepted + unrevoked + unexpired only.
- DELETE /api/my/invites/incoming/:id — dismiss a pending invite
  (revokes the pending_invite row only; underlying invite code stays
  valid so the inviter can re-send).

Web additions (all under apps/web/src/modules/dashboard/universe/):
- welcome.tsx — editorial serif header with mesh + invite counts
- invitations.tsx — client card with Accept (→ /i/:code claim flow)
  and optimistic Decline
- meshes-grid.tsx — hero card + compact grid, linked to mesh detail
- reveal.tsx — fade-up motion matching marketing _reveal.tsx

Styling uses the existing claudemesh design tokens (--cm-clay,
--cm-bg-elevated, Anthropic Sans/Serif/Mono) — nothing redefined.

Onboarding redirect (0 meshes → /meshes/new?onboarding=1) preserved,
now gated on 0 invitations too so users with pending invites still
land on the dashboard.

Sidebar icon switched to Atom for the "universe" concept.

Standalone prototype saved at prototypes/live-dashboard.html for
reference.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-19 21:31:15 +01:00
parent 2abf86d540
commit 0664180a54
13 changed files with 1734 additions and 66 deletions

View File

@@ -164,6 +164,38 @@ export const archiveMyMesh = async ({
return updated;
};
/**
* Decline an incoming pending invite addressed to this user's email.
* Marks the pending_invite row as revoked so it no longer surfaces
* in /invites/incoming. The underlying short-code invite is NOT revoked
* (inviter may re-send), only this user's copy is dismissed.
*/
export const declineIncomingInvite = async ({
email,
pendingInviteId,
}: {
email: string;
pendingInviteId: string;
}) => {
const [updated] = await db
.update(pendingInvite)
.set({ revokedAt: new Date() })
.where(
and(
eq(pendingInvite.id, pendingInviteId),
eq(pendingInvite.email, email),
isNull(pendingInvite.acceptedAt),
isNull(pendingInvite.revokedAt),
),
)
.returning({ id: pendingInvite.id });
if (!updated) {
throw new Error("Invitation not found or already resolved.");
}
return updated;
};
export const leaveMyMesh = async ({
userId,
meshId,