mirror of
https://github.com/Dvorinka/Dash.git
synced 2026-06-03 23:12:56 +00:00
🚀 Dash - Homelab Dashboard
A clean, customizable homelab dashboard inspired by CasaOS. Features: - Empty-first dashboard (no demo data) - 3 themes: Light, Dark, CasaOS glassmorphism - Widgets: Clock (multi-timezone), Pi-hole, Memos, Immich, Image - Drag & drop app organization - Grid + list view for apps - Groups with collapse/expand - Proper widget refresh handling - Visual timezone picker - Square app cards with hover effects Stack: Go + Gin + PostgreSQL + Next.js 15 + React 19 + Tailwind CSS + shadcn/ui
This commit is contained in:
+442
@@ -0,0 +1,442 @@
|
||||
# FrontendPlan.md
|
||||
|
||||
## Mission
|
||||
|
||||
Build the complete frontend for the self-hosted homelab dashboard. The frontend owns the Next.js app, shadcn UI composition, dashboard interactions, drag/drop UX, theme system, generated API client usage, frontend tests, and production-grade responsive design.
|
||||
|
||||
This agent may edit:
|
||||
|
||||
- `/frontend`
|
||||
|
||||
This agent must not edit `/backend`, `/db`, or `/openapi`. Read `openapi/openapi.yaml` only to generate client/types. If the API contract is missing or wrong, document the needed backend change instead of creating duplicate schemas.
|
||||
|
||||
## Product Context
|
||||
|
||||
The app is a fast, clean dashboard for home lab services. It should feel closer to Vercel, Linear, and a Nothing-style instrument panel than older dashboard tools. The first screen is the actual dashboard, not a landing page.
|
||||
|
||||
Core frontend responsibilities:
|
||||
|
||||
- Show date/time/timezones.
|
||||
- Show dashboard widgets.
|
||||
- Show grouped and ungrouped services.
|
||||
- Add/edit/delete services with multiple URLs.
|
||||
- Let user choose local/external/custom URL before opening when service has multiple URLs.
|
||||
- Create/edit/collapse/delete groups.
|
||||
- Drag/drop reorder groups, services, and widgets.
|
||||
- Support dark and light mode.
|
||||
- Use generated API types/client only.
|
||||
|
||||
## Stack
|
||||
|
||||
- Next.js App Router.
|
||||
- React with TypeScript strict mode.
|
||||
- Tailwind CSS.
|
||||
- shadcn/ui.
|
||||
- `@dnd-kit` for drag/drop.
|
||||
- `@tanstack/react-query` for server state.
|
||||
- `openapi-typescript` for generated types.
|
||||
- `openapi-fetch` or a thin generated client wrapper for requests.
|
||||
- Playwright for end-to-end tests.
|
||||
- MSW or local fixtures while backend is incomplete.
|
||||
|
||||
## Repository Layout
|
||||
|
||||
Use this frontend layout:
|
||||
|
||||
```text
|
||||
frontend/
|
||||
app/
|
||||
layout.tsx
|
||||
page.tsx
|
||||
globals.css
|
||||
components/
|
||||
dashboard/
|
||||
groups/
|
||||
services/
|
||||
widgets/
|
||||
shell/
|
||||
ui/
|
||||
lib/
|
||||
api/
|
||||
mocks/
|
||||
theme/
|
||||
utils/
|
||||
hooks/
|
||||
tests/
|
||||
e2e/
|
||||
```
|
||||
|
||||
Intent:
|
||||
|
||||
- `app`: Next.js routes and global shell.
|
||||
- `components/ui`: shadcn components only.
|
||||
- `components/dashboard`: dashboard composition and layout.
|
||||
- `components/groups`: group panels, group controls, group reorder.
|
||||
- `components/services`: service cards, service form, URL picker.
|
||||
- `components/widgets`: clock, image, Pi-hole widget cards, widget form.
|
||||
- `lib/api`: generated client and React Query hooks.
|
||||
- `lib/mocks`: fixtures/MSW handlers matching OpenAPI.
|
||||
- `lib/theme`: theme constants and helpers.
|
||||
|
||||
## API Contract
|
||||
|
||||
Frontend consumes `../openapi/openapi.yaml`.
|
||||
|
||||
Generate types/client through scripts:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"api:generate": "openapi-typescript ../openapi/openapi.yaml -o lib/api/schema.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:e2e": "playwright test"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Do not hand-write API response types.
|
||||
- All API DTOs come from generated OpenAPI types.
|
||||
- UI view models may exist, but must be derived from generated types.
|
||||
- If backend is unavailable, use MSW fixtures shaped exactly like generated types.
|
||||
- API base URL comes from `NEXT_PUBLIC_API_BASE_URL`, default `http://localhost:8080`.
|
||||
|
||||
Expected resources:
|
||||
|
||||
```text
|
||||
Dashboard
|
||||
Group
|
||||
Service
|
||||
ServiceUrl
|
||||
WidgetInstance
|
||||
WidgetData
|
||||
AssetFile
|
||||
ErrorResponse
|
||||
```
|
||||
|
||||
Required frontend API hooks:
|
||||
|
||||
```text
|
||||
useDashboard()
|
||||
useCreateGroup()
|
||||
useUpdateGroup()
|
||||
useDeleteGroup()
|
||||
useCreateService()
|
||||
useUpdateService()
|
||||
useDeleteService()
|
||||
useUpdateLayout()
|
||||
useUploadIcon()
|
||||
useCreateWidget()
|
||||
useUpdateWidget()
|
||||
useDeleteWidget()
|
||||
useWidgetData(widgetId)
|
||||
useRefreshWidget(widgetId)
|
||||
```
|
||||
|
||||
Mutation behavior:
|
||||
|
||||
- Optimistically update layout reorder where safe.
|
||||
- Roll back reorder on API failure.
|
||||
- For CRUD forms, close dialog only after success.
|
||||
- Render inline errors as `[ERROR: message]`.
|
||||
- Avoid toast dependency for core flows; inline status preferred.
|
||||
|
||||
## Design Direction
|
||||
|
||||
Primary design direction:
|
||||
|
||||
- Dark-first dashboard.
|
||||
- Light mode complete and polished.
|
||||
- Vercel-inspired structure: restrained cards, shadow-as-border, tight typography.
|
||||
- Nothing-inspired accents: OLED black, mono uppercase labels, red signal accent only when meaningful.
|
||||
|
||||
Fonts:
|
||||
|
||||
- Load `Geist` and `Geist_Mono` with `next/font/google`.
|
||||
- Optional `Doto` only for large clock/hero metric. Do not use Doto for body text.
|
||||
|
||||
Visual tokens:
|
||||
|
||||
- Background dark: near/OLED black.
|
||||
- Background light: off-white/white.
|
||||
- Text dark mode: white/soft gray hierarchy.
|
||||
- Text light mode: `#171717` and neutral grays.
|
||||
- Radius: 6px for controls, 8px for cards, no large rounded card stacks.
|
||||
- Borders: use shadow-as-border or tokenized border, not heavy outlines.
|
||||
- Accent: red only for alert/active signal/destructive state.
|
||||
- Icons: monoline Lucide via shadcn project icon library.
|
||||
|
||||
Nothing/Vercel constraints:
|
||||
|
||||
- No gradients in app chrome.
|
||||
- No decorative blobs/orbs.
|
||||
- No emojis.
|
||||
- No nested cards.
|
||||
- No large marketing hero.
|
||||
- No skeleton-only loading screens; prefer `[LOADING...]` or compact shadcn loading state.
|
||||
- No visible instructional paragraphs explaining obvious UI.
|
||||
- Use strong spacing and hierarchy instead of decorative containers.
|
||||
|
||||
## shadcn Rules
|
||||
|
||||
Use shadcn components before custom markup:
|
||||
|
||||
- Button
|
||||
- Card
|
||||
- Dialog
|
||||
- Sheet
|
||||
- Input
|
||||
- Select
|
||||
- Switch
|
||||
- Tabs
|
||||
- Badge
|
||||
- DropdownMenu
|
||||
- Tooltip
|
||||
- Collapsible
|
||||
- Alert
|
||||
- Empty
|
||||
- Separator
|
||||
- ScrollArea
|
||||
- Field / FieldGroup where available
|
||||
|
||||
Rules:
|
||||
|
||||
- Forms use `FieldGroup` and `Field` patterns.
|
||||
- Dialogs, Sheets, and Drawers always have titles.
|
||||
- Buttons with icons use `data-icon`.
|
||||
- Use `gap-*`, never `space-x-*` or `space-y-*`.
|
||||
- Use semantic Tailwind tokens, not random raw colors in component classes.
|
||||
- Use `cn()` for conditional class names.
|
||||
- Use shadcn `Empty` for no services/no groups.
|
||||
- Use shadcn `Alert` for blocking errors.
|
||||
|
||||
## Screens and Components
|
||||
|
||||
### Dashboard Page
|
||||
|
||||
The first viewport is the dashboard.
|
||||
|
||||
Layout:
|
||||
|
||||
- Top shell with app name, current date/time, theme toggle, add button.
|
||||
- Widget strip below header.
|
||||
- Service area below widgets.
|
||||
- Groups render as collapsible sections.
|
||||
- Ungrouped services render in their own section when present.
|
||||
- Empty dashboard renders a minimal empty state with primary add action.
|
||||
|
||||
Desktop:
|
||||
|
||||
- Max width around 1200-1400px.
|
||||
- Dense but calm grid.
|
||||
- Service cards use responsive columns.
|
||||
|
||||
Mobile:
|
||||
|
||||
- Single-column service grid.
|
||||
- Add/edit forms use full-screen or near-full-height Sheet/Dialog.
|
||||
- Drag handles must remain tappable.
|
||||
|
||||
### Service Cards
|
||||
|
||||
Show:
|
||||
|
||||
- Icon or generated initials fallback.
|
||||
- Service name.
|
||||
- URL kind badges or compact count.
|
||||
- Optional primary URL label.
|
||||
- Menu for edit/delete/move.
|
||||
|
||||
Click behavior:
|
||||
|
||||
- If service has one URL, open it directly in same tab or new tab based on app setting/default.
|
||||
- If service has multiple URLs, open URL picker dialog.
|
||||
- URL picker lists label, kind, and hostname.
|
||||
|
||||
Drag behavior:
|
||||
|
||||
- Drag service within group.
|
||||
- Drag service across groups.
|
||||
- Drag service to ungrouped.
|
||||
- Show clear drop target state.
|
||||
- Persist through `PUT /api/v1/layout`.
|
||||
|
||||
### Service Form
|
||||
|
||||
Fields:
|
||||
|
||||
- Name.
|
||||
- Icon mode: icon URL or upload file.
|
||||
- URLs dynamic list:
|
||||
- label
|
||||
- kind: local, external, custom
|
||||
- URL
|
||||
- primary toggle
|
||||
- Group select.
|
||||
|
||||
Validation:
|
||||
|
||||
- Name required.
|
||||
- At least one URL required.
|
||||
- URL must be absolute `http` or `https`.
|
||||
- Exactly one primary URL in UI; if user never chooses, first is primary.
|
||||
- Show field-level errors, not generic banners only.
|
||||
|
||||
### Groups
|
||||
|
||||
Group section includes:
|
||||
|
||||
- Name.
|
||||
- Service count.
|
||||
- Collapse/expand control.
|
||||
- Drag handle.
|
||||
- Menu for rename/delete.
|
||||
|
||||
Behavior:
|
||||
|
||||
- Collapsed state persists via group PATCH.
|
||||
- Empty group can be deleted.
|
||||
- Non-empty group delete should ask user to move services to ungrouped, matching backend option.
|
||||
- Group reorder persists through layout endpoint.
|
||||
|
||||
### Widgets
|
||||
|
||||
Widget strip supports:
|
||||
|
||||
- Clock widget.
|
||||
- Image widget.
|
||||
- Pi-hole widget.
|
||||
|
||||
Clock:
|
||||
|
||||
- Shows large time and date.
|
||||
- Supports configured timezones when backend exposes config.
|
||||
- Uses Geist Mono or optional Doto for display moment.
|
||||
|
||||
Image:
|
||||
|
||||
- Shows configured image in restrained card.
|
||||
- Optional link click.
|
||||
- No decorative cropping that hides important content.
|
||||
|
||||
Pi-hole:
|
||||
|
||||
- Shows status, blocked count, query count, percent blocked.
|
||||
- Loading: `[LOADING...]`.
|
||||
- Error: `[ERROR: ...]`.
|
||||
- Stale data: show data with compact stale label.
|
||||
- Manual refresh button.
|
||||
|
||||
Widget management:
|
||||
|
||||
- Add/edit widget Sheet.
|
||||
- Widget type select.
|
||||
- Type-specific config fields.
|
||||
- Widget drag reorder persisted through layout endpoint.
|
||||
|
||||
## State Management
|
||||
|
||||
Use React Query for server state:
|
||||
|
||||
- Dashboard query is primary source for layout.
|
||||
- Widget data can be separate query per widget.
|
||||
- Invalidate dashboard after CRUD mutations.
|
||||
- Use optimistic mutation for layout reorder.
|
||||
|
||||
Local UI state:
|
||||
|
||||
- Dialog open/closed.
|
||||
- Drag active item.
|
||||
- Theme.
|
||||
- Form draft state.
|
||||
|
||||
Do not mirror entire dashboard into global client state unless needed for drag preview. If local reorder state is needed, keep it scoped to dashboard components and reconcile from query data.
|
||||
|
||||
## Accessibility
|
||||
|
||||
Required:
|
||||
|
||||
- Keyboard focus visible on all controls.
|
||||
- Dialog title for every Dialog/Sheet.
|
||||
- Service cards keyboard-accessible.
|
||||
- Drag/drop has keyboard fallback or at least accessible controls for move up/down.
|
||||
- Form fields have labels and error messages.
|
||||
- Theme toggle has accessible label.
|
||||
- Icon-only buttons have labels/tooltips.
|
||||
- Color is not only signal for URL kind or errors.
|
||||
|
||||
## Testing Plan
|
||||
|
||||
Unit/component tests:
|
||||
|
||||
- Service form validates required name and URL.
|
||||
- Dynamic URL rows add/remove correctly.
|
||||
- URL picker opens for multi-URL service.
|
||||
- Group collapse button updates state.
|
||||
- Widget cards render loading/error/stale/data states.
|
||||
|
||||
Playwright tests:
|
||||
|
||||
- Dashboard loads fixture data.
|
||||
- Add service with two URLs.
|
||||
- Click multi-URL service and choose local/external URL.
|
||||
- Create group and move service into group.
|
||||
- Collapse group and confirm persisted state after reload.
|
||||
- Drag reorder services and verify layout mutation.
|
||||
- Toggle dark/light mode.
|
||||
- Pi-hole widget shows data and refresh button.
|
||||
|
||||
Build checks:
|
||||
|
||||
```bash
|
||||
npm run api:generate
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
npm run build
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
Use the package manager chosen during app scaffold. If using pnpm, scripts remain same but commands run as `pnpm ...`.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Scaffold Next.js App Router project in `/frontend` with TypeScript and Tailwind.
|
||||
2. Initialize shadcn/ui and install required base components.
|
||||
3. Add API generation script from `../openapi/openapi.yaml`.
|
||||
4. Create generated client wrapper and React Query provider.
|
||||
5. Build MSW/fixture data matching OpenAPI.
|
||||
6. Implement theme tokens, fonts, dark/light modes, and shell.
|
||||
7. Build dashboard page with fixture data.
|
||||
8. Build service cards and URL picker.
|
||||
9. Build add/edit service form with dynamic URLs and icon upload field.
|
||||
10. Build group sections, collapse, menus, and group forms.
|
||||
11. Add drag/drop for services, groups, and widgets.
|
||||
12. Build widgets: clock, image, Pi-hole.
|
||||
13. Wire all CRUD and layout mutations to API client.
|
||||
14. Add tests and responsive polish.
|
||||
15. Run build/typecheck/e2e.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- `/frontend` builds successfully.
|
||||
- Frontend generates types from `../openapi/openapi.yaml`.
|
||||
- No hand-written API DTO duplication.
|
||||
- Dashboard works with MSW fixtures before backend is ready.
|
||||
- Dashboard works against backend API once available by changing `NEXT_PUBLIC_API_BASE_URL`.
|
||||
- Add/edit/delete service flows work.
|
||||
- Multiple URL picker works.
|
||||
- Groups can be created, collapsed, reordered, and deleted safely.
|
||||
- Drag/drop persists through layout API.
|
||||
- Clock, image, and Pi-hole widgets render expected states.
|
||||
- Dark and light modes are polished.
|
||||
- Mobile and desktop layouts do not overlap or clip text.
|
||||
|
||||
## Parallel Coordination
|
||||
|
||||
- Frontend can start with fixtures while backend builds API.
|
||||
- Do not invent contract fields outside OpenAPI.
|
||||
- If UI needs a new field, request OpenAPI change from backend agent.
|
||||
- Regenerate API client after any OpenAPI update.
|
||||
- Keep all frontend work isolated inside `/frontend`.
|
||||
|
||||
Reference in New Issue
Block a user