Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 22 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# instanode.dev Dashboard

React 18 + TypeScript + Vite frontend for the customer dashboard. This is where users log in, view their provisioned resources, upgrade their plan, and manage their team. It talks exclusively to `dashboard-api/` (port 8081, NodePort 30082) — not directly to the agent-facing `api/`.
React 18 + TypeScript + Vite frontend for the customer dashboard. This is where users log in, view their provisioned resources, upgrade their plan, and manage their team. It talks directly to the agent-facing API at `api.instanode.dev`.

---

## Why Two APIs?
## Architecture

`api/` (port 8080) is designed for agents and automation: anonymous-friendly, no sessions, simple HTTP, no cookies. It intentionally has no concept of "logged-in user."
The dashboard is a single-page app that calls the agent API at `api.instanode.dev` for every operation: auth, claim, billing, team management, resource CRUD, and stacks. There is no intermediate backend — the browser holds a bearer token (`localStorage.instanode.token`) and includes it on every request.

`dashboard-api/` (port 8081) is designed for humans: it manages JWT sessions, team membership, billing state, and exposes resource management UI that proxies reads from the platform database. The two services have different auth models, different latency tolerances, and different security concerns. Keeping them separate means a bug in the human-facing session layer cannot affect agent provisioning, and vice versa.
In dev, Vite proxies `/api`, `/auth`, `/claim`, `/db`, `/cache`, `/nosql`, `/queue`, `/storage`, `/webhook`, `/.well-known` to `AGENT_API_URL` (default `http://api.instanode.dev`). In prod, the dashboard ships as a static bundle on GitHub Pages and issues cross-origin fetches directly to `https://api.instanode.dev` (set via the `VITE_API_URL` build env).

---

Expand All @@ -20,9 +20,9 @@ npm install
npm run dev # Vite dev server at http://localhost:5173
```

Requires `dashboard-api` running and reachable at `http://localhost:30082` (k8s NodePort). If you're not running k8s, start it with docker-compose:
To point the dev proxy at a local k8s cluster:
```bash
cd infra && docker compose up -d
AGENT_API_URL=http://localhost:30080 npm run dev
```

To run unit tests:
Expand All @@ -37,16 +37,16 @@ npm test
```
src/
├── hooks/
│ ├── useAuth.ts # JWT session management — login, logout, auto-refresh
│ └── useResources.ts # Fetches and caches the resource list from dashboard-api
│ ├── useAuth.ts # Bearer-token session management
│ └── useResources.ts # Fetches and caches the resource list
├── pages/
│ ├── LoginPage.tsx # GitHub OAuth / magic link entry point
│ ├── LoginPage.tsx # Email magic link / PAT entry point
│ ├── DashboardPage.tsx # Main resource list view
│ ├── ClaimPage.tsx # Anonymous → account conversion (arrives via /start?t=jwt)
│ ├── BillingPage.tsx # Plan status + upgrade flow
│ ├── SettingsPage.tsx # Team name, member management
│ ├── ResourceDetailPage.tsx # Per-resource view + rotate credentials
│ └── DeployPage.tsx # (Phase 6) Container deploy entrypoint
│ └── DeployPage.tsx # Container deploy entrypoint
└── components/
├── Layout/ # Sidebar + top nav shell
├── ResourceCard/ # Resource summary card used in DashboardPage
Expand All @@ -59,27 +59,26 @@ src/

## Auth Flow

1. User clicks "Login with GitHub" on `LoginPage` — browser goes to `dashboard-api/auth/github`.
2. OAuth redirect returns to `dashboard-api/auth/callback`, which issues a JWT and sets a `__session` HttpOnly cookie.
3. `useAuth.ts` calls `/auth/me` on mount to hydrate session state. The JWT is kept in memory (not localStorage) to avoid XSS exposure.
4. `useAuth.ts` silently calls `/auth/refresh` every 23 hours to extend the session without prompting the user.
5. On logout, `/auth/logout` clears the cookie and the in-memory token.
1. User pastes a PAT or completes the email magic-link flow on `LoginPage`.
2. The bearer token is stored in `localStorage.instanode.token` and attached as `Authorization: Bearer <token>` on every subsequent request.
3. `useAuth.ts` calls `GET /auth/me` on mount to hydrate session state.
4. On 401, the client clears the token, stores the current path under `instanode.return_to`, and redirects to `/login`.

---

## The Claim Page (Anonymous to Account)

When an anonymous user hits a resource limit, `api/` embeds an upgrade URL in the response:
When an anonymous user hits a resource limit, the agent API embeds an upgrade URL in the response:
```
https://instanode.dev/start?t=<signed-jwt>
```

That URL hits `api/GET /start`, which validates the JWT and issues a 302 redirect to:
That URL hits `GET /start` on the agent API, which validates the JWT and issues a 302 redirect to:
```
http://localhost:5173/claim?t=<jwt>
```

`ClaimPage.tsx` picks up the `t` parameter, lets the user choose a login method, and calls `api/POST /claim` to atomically convert the anonymous session into a full account. The JWT in the claim is single-use — a second call returns 409 Conflict, preventing double-conversion.
`ClaimPage.tsx` picks up the `t` parameter, lets the user choose a login method, and calls `POST /claim` on the agent API to atomically convert the anonymous session into a full account. The JWT in the claim is single-use — a second call returns 409 Conflict, preventing double-conversion.

---

Expand All @@ -88,7 +87,7 @@ http://localhost:5173/claim?t=<jwt>
107 tests covering auth guards, the upgrade journey, and resource interactions.

```bash
# Requires: Vite dev server running (npm run dev) + k8s API at localhost:30080
# Requires: Vite dev server running (npm run dev) + agent API at localhost:30080
E2E_API_URL=http://localhost:30080 npx playwright test --project=chromium

# Run a single spec
Expand All @@ -106,13 +105,14 @@ npx playwright test --headed --project=chromium

| Variable | Purpose | Default |
|---|---|---|
| `VITE_API_URL` | dashboard-api base URL | `http://localhost:30082` |
| `AGENT_API_URL` | Upstream the Vite dev proxy points at | `http://api.instanode.dev` |
| `VITE_API_URL` | Build-time override for the production bundle | `https://api.instanode.dev` |
| `VITE_NO_PROXY` | Disables Vite proxy (set to `1` in E2E) | unset |
| `E2E_API_URL` | Agent API base URL used by Playwright tests | `http://localhost:30080` |

---

## Known Gaps

- **RotateCredentials**: the UI calls `POST /api/v1/resources/:id/rotate` on dashboard-api, which proxies to `api/`. Rotation is implemented for Postgres, Redis, and MongoDB.
- **Razorpay Checkout**: the "Upgrade to Pro" button opens `instanode.dev/pricing` when checkout is not configured. A real `POST /api/v1/billing/checkout` endpoint in dashboard-api returns a Razorpay short URL when keys are configured.
- **RotateCredentials**: the UI calls `POST /api/v1/resources/:id/rotate-credentials` on the agent API. Rotation is implemented for Postgres, Redis, and MongoDB.
- **Razorpay Checkout**: the "Upgrade to Pro" button calls `POST /api/v1/billing/checkout` on the agent API and redirects to the returned Razorpay short URL. When Razorpay isn't configured (503), the button falls back to `instanode.dev/pricing`.
4 changes: 2 additions & 2 deletions src/api/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Stubbed API client — adds a small fake-network delay so loading states
// behave like the real thing. Swap this for `apiFetch()` against
// dashboard-api when backend is ready.
// behave like the real thing. Swap this for `apiFetch()` against the agent
// API when wiring an endpoint that's still fixture-backed.
//
// Exact contract reference (when wiring real backend):
// GET /api/v1/resources — locked
Expand Down
2 changes: 1 addition & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Real API surface — talks to api.instanode.dev (via Vite proxy in dev,
// same-origin in prod). Endpoints that do NOT exist on the live backend
// (vault list, activity feed, dashboard-team, members CRUD) fall back to
// (vault list, activity feed, team metadata, members CRUD) fall back to
// fixtures so the dashboard remains usable end-to-end while engineering
// catches up. Each fallback is annotated `[FIXTURE]` in the comment.

Expand Down
5 changes: 2 additions & 3 deletions src/api/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// ------------------------------------------------------------------
// Types — mirror the locked dashboard-api + proto contracts exactly.
// Source of truth: /InstaNode/proto/dashboard/v1/dashboard.proto
// /InstaNode/dashboard-api/internal/handlers/*
// Types — mirror the agent API JSON shapes the dashboard consumes.
// Source of truth: /InstaNode/api/internal/handlers/*
// ------------------------------------------------------------------

export type Tier = 'anonymous' | 'hobby' | 'pro' | 'team' | 'growth'
Expand Down
8 changes: 4 additions & 4 deletions src/pages/ContractsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function ContractsPage() {
<SummaryStat color="blue" label="delegated" v="3" sub="via agent api" subCls="dim" />
</div>

<SectionH label="LOCKED" badgeBg="var(--accent)" title="26 endpoints · ready for parallel build" sub="source · /dashboard-api/internal/handlers/" />
<SectionH label="LOCKED" badgeBg="var(--accent)" title="26 endpoints · ready for parallel build" sub="source · /api/internal/handlers/" />
<Card style={{ padding: 0 }}>
<Group title="Resources · 4">
<ContractLine method="GET" path="/api/v1/resources" status="→ {ok, items: Resource[], total}" />
Expand Down Expand Up @@ -132,7 +132,7 @@ export function ContractsPage() {
<BlockedCard
icon="⟳"
title="Deploy actions & logs · 4 endpoints"
intro="Redeploy/rollback/stop and logs SSE all go directly to agent api today. Should proxy via dashboard-api for auth + audit."
intro="Redeploy/rollback/stop and logs SSE all go directly to agent api today. Audit hooks need wiring on the agent side."
contracts={[
['POST', '/api/v1/stacks/:slug/redeploy', 'proxy → agent'],
['POST', '/api/v1/stacks/:slug/rollback', 'propose'],
Expand Down Expand Up @@ -161,7 +161,7 @@ data: {}`}</>}
<strong>Trial vs. immediate Hobby.</strong> <code>plans.yaml</code> declares <code>trial_days: 14</code>; <code>auth.go:151</code> assigns <code>hobby</code> with no trial fields. Brief journey 1 assumes a trial. <strong>Lock:</strong> add <code>teams.trial_ends_at</code> + worker, OR drop trial language from copy.
</ContractBanner>
<ContractBanner kind="warning" badge="#2">
<strong>"Deployments" vs "Stacks".</strong> Brief uses "Deployments"; proto + dashboard-api use "Stacks". <strong>Lock:</strong> dashboard URL is <code>/deployments</code> (user language), API stays <code>/stacks</code> (existing).
<strong>"Deployments" vs "Stacks".</strong> Brief uses "Deployments"; the API uses "Stacks". <strong>Lock:</strong> dashboard URL is <code>/deployments</code> (user language), API stays <code>/stacks</code> (existing).
</ContractBanner>
<ContractBanner kind="warning" badge="#3">
<strong>Multi-env scoping.</strong> Resource shape includes <code>env</code> but list endpoint has no <code>?env=</code> filter. <strong>Lock:</strong> add server-side filter param + <code>teams.default_env</code> in PATCH body.
Expand All @@ -174,7 +174,7 @@ data: {}`}</>}
</ContractBanner>
</div>

<SectionH label="DELEGATED" badgeBg="var(--blue)" title="3 surfaces · routes to agent api" sub="not in dashboard-api · already documented in /flows" />
<SectionH label="DELEGATED" badgeBg="var(--blue)" title="3 surfaces · routes to agent api" sub="anonymous / cross-origin paths · already documented in /flows" />
<Card style={{ padding: '18px 20px' }}>
<ContractLine method="POST" path="api.instanode.dev/db/new · /cache/new · /mongo/new · /queue/new · /storage/new · /webhook/new · /deploy/new" status="→ agent api" />
<ContractLine method="POST" path="api.instanode.dev/claim · /start?t=jwt · /claim/preview" status="→ agent api" />
Expand Down
2 changes: 1 addition & 1 deletion src/pages/DeployDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export function DeployDetailPage() {
</ROBanner>

<ContractBanner kind="blocked" badge="🔒 blocked">
<strong>Redeploy / Rollback / Stop are missing from dashboard-api.</strong> <code>POST /api/v1/stacks/:slug/redeploy</code> currently routes to the agent API directly. Rollback and Stop don't exist anywhere yet.
<strong>Redeploy / Rollback / Stop are partially wired.</strong> <code>POST /api/v1/stacks/:slug/redeploy</code> routes to the agent API. Rollback and Stop don't exist on the agent API yet.
</ContractBanner>

<div className="tabs">
Expand Down
2 changes: 1 addition & 1 deletion src/pages/DeploymentsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function DeploymentsPage() {
return (
<>
<ContractBanner kind="warning" badge="naming gap">
<strong>"Deployments" in the brief = "Stacks" in the code.</strong> The dashboard-api exposes <code>GET /api/v1/stacks</code>{' '}
<strong>"Deployments" in the brief = "Stacks" in the code.</strong> The agent API exposes <code>GET /api/v1/stacks</code>{' '}
(returns <code>DashboardStack</code>). UI keeps <code>/deployments</code> (user language); API stays <code>/stacks</code> (existing).
</ContractBanner>

Expand Down
3 changes: 1 addition & 2 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import react from '@vitejs/plugin-react';

// AGENT_API_URL — the agent-facing instanode.dev API (defaults to the live
// cluster; override locally with AGENT_API_URL=http://localhost:30080).
// All dashboard fetches go through this single upstream — no separate
// dashboard-api in this build of the project.
// All dashboard fetches go through this single upstream.
const agentApiURL = process.env.AGENT_API_URL || 'http://api.instanode.dev';

// In tests we set VITE_NO_PROXY=1 so Playwright's page.route() globs match
Expand Down
Loading