Feature #34
Updated by Suresh Kumar about 2 months ago
# Canofy Control Centre — Frontend Development Journal
This document explains **what was built, why, and how**, across all development
phases of the Canofy Control Centre frontend (Next.js) and its wiring to the
Frappe backend (`canofy_control_centre`). It's meant as a reference for anyone
picking up this codebase — including a future version of the person who built it.
---
## 1. How this app is put together
Before the phases make sense, it helps to understand the shape of the app:
- **Frontend**: Next.js (App Router) + TypeScript + Tailwind + SWR for data
fetching. Lives entirely under `src/`.
- **Backend**: an existing Frappe app, `canofy_control_centre`, with its own
doctypes (AWS Server, Client Site, Change Request, etc.) and a handful of
custom whitelisted Python methods (`api/*.py`, `page/*/*.py`).
- **The bridge**: every Next.js route under `src/app/api/**/route.ts` is a
thin server-side proxy. It either calls one of the backend's custom methods,
or — increasingly, as the app grew — calls Frappe's own generic
`frappe.client.get_list` / `set_value` / `insert` methods directly. This
means most new features **don't require touching backend Python code at
all**; they just compose existing, already-permissioned generic endpoints.
- **Mock fallback**: every API route checks `process.env.FRAPPE_URL` first.
If it's not set, the route returns realistic mock data from
`src/lib/mock-data.ts` instead of calling Frappe. This is what lets the
entire UI run and be clicked through with zero backend connected — every
page in every phase was built and verified this way before ever touching a
live Frappe instance.
- **Client-side data layer**: `src/lib/api.ts` wraps every API route in an
SWR hook (`useXxxData()`) or a plain async function (`doXxxAction()`).
Pages never call `fetch()` directly — they import from here.
- **Types**: `src/lib/types.ts` mirrors the *exact* shape returned by each
backend method/route, verified field-by-field against the real doctype
JSON files, not guessed.
Keeping this pattern consistent across all four phases is what makes the app
predictable to extend — every new page follows the same recipe:
`type → mock data → API route → SWR hook → page component`.
---
## 2. Phase 1 — Backend wiring + UI foundation
**Goal:** get the dashboard and core entity pages (Servers, Clients,
Credentials, Version Matrix, Upgrade Candidates) rendering real data from the
Frappe backend, and establish the UI system everything else would build on.
**What was built:**
- `src/lib/frappe-client.ts` — the server-side-only Axios client that holds
the Frappe API key/secret and a `FRAPPE_METHODS` map of every backend
endpoint this app calls. Never imported into a `"use client"` component.
- `src/lib/types.ts` and `src/lib/mock-data.ts` — established for the
dashboard first (`DashboardData` and friends), matching
`control_centre_home.py` exactly.
- The **API proxy layer**: `src/app/api/dashboard`, `/clients`, `/servers`,
`/servers/[id]`, `/credentials` (+ `reveal`/`rotate`), `/versions/*` — each
following the mock-fallback pattern described above.
- The **dashboard page** (`src/app/dashboard/page.tsx`) and its component
breakdown in `src/components/dashboard/*` (SummaryCards, ServerHealthGrid,
AlertsFeed, VersionDistribution, BackupStatus, RecentDeployments,
ExpiryWatchlist).
- Core entity pages: `clients/page.tsx`, `servers/page.tsx` +
`servers/[id]/page.tsx` (with a live 24h trend chart via Recharts),
`credentials/page.tsx` (masked reveal/rotate), `version-matrix/page.tsx`,
`upgrade-candidates/page.tsx`.
- The **shared UI kit** (`src/components/ui/*`): `Card`, `Badge`,
`ProgressBar`, `EmptyState`, `DropdownChip`, `Sparkline` — every later page
reuses these instead of inventing new patterns.
- App shell: `layout.tsx`, `Sidebar.tsx`, `TopBar.tsx`.
**Key decision:** API routes always resolve field names by reading the
backend's actual doctype JSON / Python source first, never by guessing from
the frontend side. This discipline is what kept the "hook it up to real data"
step low-risk in every later phase.
---
## 3. Phase 2 — Dark theme
**Goal:** apply a consistent dark visual theme across every page built in
Phase 1.
**What was built:**
- All color, spacing, and font tokens were centralized as CSS custom
properties in `src/app/globals.css` (`--bg`, `--surface`, `--text-primary`,
`--ok` / `--warn` / `--crit` / `--info`, brand accent + indigo, etc.), then
mapped into `tailwind.config.ts` so every component uses semantic utility
classes (`bg-surface`, `text-text-dim`, `border-border-soft`) instead of
raw hex values.
- Every page/component from Phase 1 was restyled against these tokens.
**Why it mattered later:** because no component ever hardcodes a color, the
Phase 4 **theme toggle** (light/dark switch) only needed one new CSS block —
zero component changes. This is a direct payoff of doing Phase 2 properly.
---
## 4. Phase 3 — New v2 modules
**Goal:** build pages for three new backend modules that didn't exist in the
original app: CI/CD Pipeline tracking, Client Onboarding tracking, and GitHub
version drift detection.
**What was built:**
### CI/CD Pipelines (`/pipelines`, `/pipelines/[id]`)
- Lists `Deployment Pipeline` records with live status polling (15s on the
list, 4s on an in-progress pipeline's detail page — stages move fast).
- **"Start Pipeline"** modal only offers **Approved** Change Requests
(`/api/pipelines/eligible-change-requests`) — the approval gate is enforced
by the backend (`api/pipeline.py:start_pipeline`), the UI just reflects it
honestly rather than pretending to bypass it.
- Detail page shows stage-by-stage progress (backup → snapshot → git-pull →
migrate → restart) with rollback action.
### Client Onboarding (`/onboarding`, `/onboarding/[id]`)
- Same shape as Pipelines: list + polling detail page, 14-stage automated
provisioning engine (EC2 → S3 → IAM → bench → site → apps), with
per-stage retry on failure.
### GitHub Sync (`/github`)
- Repository list with "Sync Now" action
(`api/github.py:sync_repositories`), plus a per-client Git status view
(`get_client_git_status`) showing whether a site's deployed commit matches
its recorded ref — the "version drift" detector.
**Pattern reused from Phase 1:** every one of these follows the same
mock-fallback → API route → SWR hook → page recipe. Nothing new was invented
architecturally; the app just grew sideways.
---
## 5. Phase 4 — Backlog: Toasts, Client Detail, Change Requests, Search
This phase had an explicit constraint: **do not touch Phase 1–3 pages or any
backend Python code.** Everything below was built additively.
### Toast Notifications
- `src/components/ui/Toast.tsx` — a `ToastProvider` (mounted once in
`layout.tsx`) + a `useToast()` hook (`.success()` / `.error()` / `.info()`)
usable from any client component. Falls back to no-ops if used outside the
provider, so it can never crash a page.
- Wired into the two new Phase 4 pages (Change Requests, and available for
any future page) without touching Phase 1–3 action handlers.
### Client Detail Page (`/clients/[id]`)
- There was no existing backend method for "everything about one client," so
rather than write new Python, `src/app/api/clients/[id]/route.ts` composes
the view from **eight parallel `frappe.client.get_list` calls** (servers,
sites, credentials, incidents, alerts, change requests, version map) plus
one batched call for installed apps (a child table of Client Site, queried
directly by `parenttype`/`parent` — the same trick used for Site Detail
later).
- Trade-off, documented honestly: this is more round-trips than the
single-Python-method pattern used elsewhere (e.g. `get_server_detail`). If
it ever needs to match that architecture, the clean fix is a
`get_client_detail()` backend method — a small, additive change, not a
rewrite.
- Client cards on `/clients` now link here (the one necessary touch to
Phase 1's `clients/page.tsx` — wrapping the card in an `<a>`).
### Change Request Queue (`/change-requests`)
- Full lifecycle UI for `Change Request`: status tabs (Draft → Pending
Approval → Approved → In Progress → Completed / Rejected / Rolled Back), a
"New Change Request" creation modal (`frappe.client.insert`), and
Submit/Approve/Reject actions (`frappe.client.set_value` on `status`) —
mirroring exactly how `alerts/ack` and `alerts/resolve` already did status
updates in Phase 1.
- Approved requests link straight into the existing Pipelines "Start
Pipeline" flow from Phase 3.
- **Known constraint (by design, not a bug):** per the backend's own
permission matrix, only System Manager / Senior Engineer / Support Lead can
Approve/Reject (Write permission on Change Request) — Support Engineer can
only Create. The UI doesn't try to route around this.
### Global Search (TopBar)
- The search box in `TopBar.tsx` was purely decorative before this phase. It
now debounces input, calls `/api/search`, and shows a results dropdown
spanning Clients, Servers, Change Requests, Pipelines, and Onboarding
Requests. `⌘K` / `Ctrl+K` focuses it from anywhere; `Esc` closes it.
- `/api/search` uses simple `like` filters against each doctype's
human-readable field (`company_name`, `server_name`, `title`,
`pipeline_name`, `client`) — same generic-`get_list` philosophy as
everything else.
---
## 6. Phase 4 extension — Theme toggle, Alerts, Backups, Sites
A follow-up round, same "don't touch Phase 1–3" constraint, closing out
sidebar links that had existed as dead-ends since Phase 1 (`/alerts`,
`/backups`) and adding one brand-new module (`/sites`).
### Light/Dark theme toggle
- Because Phase 2 centralized every color into CSS variables, this needed
only one addition: an `html[data-theme="light"]` override block in
`globals.css`, plus `src/components/theme/ThemeProvider.tsx` (context +
`localStorage` persistence) and a tiny inline script in `layout.tsx`'s
`<head>` that applies the saved theme *before* paint, avoiding a
dark-flash-then-light flicker on reload.
- The Sun/Moon button already existed in `TopBar.tsx` (decorative since
Phase 1) — it now calls `toggleTheme()`.
### Alerts page (`/alerts`)
- The `/api/alerts` route (list) and `/api/alerts/ack` + `/api/alerts/resolve`
(actions) already existed from Phase 1 — used by the dashboard's alerts
feed — but no full-page UI consumed them. This page adds that: separate
Health Alerts and Incidents tables, with Acknowledge / Resolve actions and
toast feedback.
### Backups page (`/backups`)
- New `/api/backups` route (full `Backup Log` list via `get_list`) — the
dashboard only ever showed a summary + failed list, never the full log.
Page adds status summary cards, a status filter, and a Retry action for
failed backups (reusing the existing `/api/backup/retry` endpoint from
Phase 1).
### Sites (`/sites`, `/sites/[id]`) — new sidebar tab
- New `Client Site` list page: searchable/filterable, shows app count and
live up/down status per site.
- New Site Detail page answers, in order: **which client owns this site,
which server it runs on, how many other clients share that server** (via
the server's `Shared Server Client` child table — only shown when
`server_type = Shared`), **which apps are installed and at what version**
(via the `Installed App` child table), plus creation date, backup/uptime
status, recent backups, and credentials.
- Same architectural trade-off as Client Detail: composed from `get_list`
calls (including two direct child-table queries) rather than a new
backend method — flagged as something to verify permissions-wise once
connected to a real Frappe instance.
---
## 7. Directory map (as of the end of Phase 4)
```
src/
├── app/
│ ├── alerts/page.tsx Phase 4 ext — Alerts & Incidents
│ ├── backups/page.tsx Phase 4 ext — Backup Log
│ ├── change-requests/page.tsx Phase 4 — Change Request Queue
│ ├── clients/page.tsx Phase 1 (links to detail added in Phase 4)
│ ├── clients/[id]/page.tsx Phase 4 — Client Detail
│ ├── credentials/page.tsx Phase 1
│ ├── dashboard/page.tsx Phase 1
│ ├── github/page.tsx Phase 3
│ ├── onboarding/page.tsx, [id]/page.tsx Phase 3
│ ├── pipelines/page.tsx, [id]/page.tsx Phase 3
│ ├── servers/page.tsx, [id]/page.tsx Phase 1
│ ├── sites/page.tsx Phase 4 ext — Sites list
│ ├── sites/[id]/page.tsx Phase 4 ext — Site Detail
│ ├── upgrade-candidates/page.tsx Phase 1
│ ├── version-matrix/page.tsx Phase 1
│ ├── layout.tsx Phase 1 (+ Toast/Theme providers, Phase 4)
│ ├── globals.css Phase 2 (dark) + Phase 4 ext (light)
│ └── api/**/route.ts One per feature above, same pattern throughout
├── components/
│ ├── dashboard/* Phase 1
│ ├── layout/Sidebar.tsx, TopBar.tsx Phase 1 (+ search/theme, Phase 4)
│ ├── theme/ThemeProvider.tsx Phase 4 ext
│ └── ui/* Phase 1 (+ Toast.tsx, Phase 4)
└── lib/
├── frappe-client.ts Phase 1 (+ insertDoc method, Phase 4)
├── types.ts Grown incrementally every phase
├── mock-data.ts Grown incrementally every phase
└── api.ts Grown incrementally every phase
```
---
## 8. Connecting to a real Frappe backend
Every route already supports it — set these three in `.env.local` and mock
data is replaced by live data everywhere, with no code changes:
```
FRAPPE_URL=https://your-canofy-instance.example.com
FRAPPE_API_KEY=...
FRAPPE_API_SECRET=...
```
Two things to verify on first real connection (flagged honestly, not
guaranteed, since only mock-mode has been tested end-to-end):
1. **Role permissions** — Change Request Approve/Reject needs Write access
(System Manager / Senior Engineer / Support Lead). If the API key's user
is only a Support Engineer, those two actions will fail with a Frappe
permission error — by design, not a bug.
2. **Child-table queries** — Installed App and Shared Server Client are
queried directly (`parenttype`/`parent` filters) in the Client Detail and
Site Detail routes. This is an unusual access pattern for a child table;
if it 403s on your instance, the fix is additive (either a permission
rule on the child doctype, or switching that call to fetch the full
parent document instead).
---
## 9. What was deliberately *not* changed
To respect "don't touch anything else" across every phase after Phase 1:
- No backend Python file was ever modified — every new feature uses either
an existing custom method or Frappe's own generic `get_list` / `set_value`
/ `insert`.
- Existing Phase 1–3 page logic was never rewritten — only additive touches
were made (a provider wrapped around `children`, a nav link added, a card
wrapped in a link), each called out explicitly when it happened.