Repository navigation
V0.5 GRANULAR PLAN
Current State: 192 pages, 222 API routes, 93 components Goal: Make EVERY page, tab, component, chart, feature 100% production-ready Timeline: 6 weeks (detailed daily breakdown)
- Foundation & Infrastructure
- Page-by-Page Audit & Completion
- Component Library Completion
- API Route Hardening
- Testing Strategy
- Performance Optimization
- Documentation & Help
Why: 50+ pages need live updates instead of polling
Implementation:
// src/lib/websocket/server.ts
- WebSocket server setup (Socket.io)
- Room-based architecture (per-user, per-project)
- Event types: service_status, build_progress, log_stream, deployment_status
- Heartbeat ping/pong (30s interval)
- Auto-reconnect client-side with exponential backoffEvents to implement:
| Event | Payload | Used By |
|---|---|---|
service:status |
{ service: string, status: 'running'|'stopped' } |
Dashboard, Services |
build:progress |
{ step: string, progress: number } |
Build page |
deploy:progress |
{ stage: string, logs: string[] } |
Deploy pages |
logs:stream |
{ service: string, line: string } |
Logs viewer |
docker:stats |
{ stats: ContainerStats } |
Monitor pages |
db:query:result |
{ queryId: string, rows: any[] } |
Database console |
Files to create:
-
src/lib/websocket/server.ts(150 lines) -
src/lib/websocket/client.ts(80 lines) -
src/lib/websocket/events.ts(types, 100 lines) -
src/hooks/useWebSocket.ts(50 lines) -
src/hooks/useServiceStatus.ts(40 lines) -
src/hooks/useBuildProgress.ts(40 lines) -
src/app/api/ws/route.ts(200 lines)
Testing:
- WebSocket connects on page load
- Auto-reconnects after disconnect
- Events received in <100ms
- Multiple tabs work independently
- Graceful shutdown
Why: Current errors show generic "Failed" messages
Global Error Types:
enum ErrorCode {
// Network errors (1xxx)
NETWORK_ERROR = 1000,
TIMEOUT = 1001,
OFFLINE = 1002,
// Auth errors (2xxx)
UNAUTHORIZED = 2000,
SESSION_EXPIRED = 2001,
INVALID_CREDENTIALS = 2002,
// CLI errors (3xxx)
CLI_NOT_FOUND = 3000,
CLI_EXECUTION_FAILED = 3001,
CLI_TIMEOUT = 3002,
// Docker errors (4xxx)
DOCKER_NOT_RUNNING = 4000,
CONTAINER_NOT_FOUND = 4001,
// Database errors (5xxx)
DB_CONNECTION_FAILED = 5000,
QUERY_ERROR = 5001,
// Validation errors (6xxx)
VALIDATION_ERROR = 6000,
INVALID_INPUT = 6001,
}Error Response Format:
interface APIError {
success: false
error: {
code: ErrorCode
message: string // Technical message
userMessage: string // User-friendly
action?: string // What to do next
retryable: boolean
details?: Record<string, any>
}
}Files to create:
-
src/lib/errors/codes.ts(100 error codes) -
src/lib/errors/messages.ts(error code → user message map) -
src/lib/errors/ErrorBoundary.tsx(global React error boundary) -
src/lib/errors/ApiError.ts(custom error class) -
src/lib/api-client.ts(fetch wrapper with retry logic) -
src/components/ErrorDisplay.tsx(error UI component) -
src/components/OfflineBanner.tsx(network status banner)
Retry Logic:
- Max 3 retries for network errors
- Exponential backoff: 1s, 2s, 4s
- Don't retry auth errors (401/403)
- Don't retry validation errors (400)
Testing:
- Network error shows offline banner
- API errors show user-friendly messages
- Retry logic works (network failures)
- Error boundary catches React errors
- All error codes have messages
Why: Many pages show blank screens while loading
Skeleton Components to Create:
// src/components/skeletons/
;-DashboardSkeleton.tsx - // 4x4 card grid
TableSkeleton.tsx - // Data table with headers
FormSkeleton.tsx - // Form fields
CardGridSkeleton.tsx - // 3-column card grid
ListSkeleton.tsx - // Vertical list items
ChartSkeleton.tsx - // Graph placeholder
CodeEditorSkeleton.tsx - // Monaco editor placeholder
LogViewerSkeleton.tsx - // Log lines
MetricCardSkeleton.tsx - // Stat card
TimelineSkeleton.tsx // Deployment timelinePages needing skeletons (priority order):
- Dashboard (
/) - 4x4 card grid - Services (
/services) - Card grid - Database Console (
/database/console) - Code editor - Logs (
/logs) - Log viewer - Config Env (
/config/env) - Form - Monitor Metrics (
/monitor/metrics) - Charts - Database Schema (
/database/schema) - Table - Plugins (
/plugins) - Card grid - Deploy (
/deploy/*) - Timeline - All other pages - Generic skeleton
Implementation Pattern:
export default async function Page() {
return (
<Suspense fallback={<DashboardSkeleton />}>
<DashboardContent />
</Suspense>
)
}Testing:
- Every page shows skeleton on first load
- Skeleton matches final layout
- No layout shift when content loads
- Perceived load time <500ms
Why: Sessions expire without warning, no refresh mechanism
Features to add:
- Session Refresh: Auto-refresh at 20h mark (sessions last 24h)
- Remember Me: 30-day sessions
- Active Sessions Page: View/revoke sessions
- CSRF Protection: Token on all mutations
- Idle Timeout Warning: Warn at 22h, logout at 24h
Files to create:
-
src/app/settings/sessions/page.tsx- Active sessions list -
src/app/api/auth/sessions/route.ts- List sessions (GET) -
src/app/api/auth/sessions/revoke/route.ts- Revoke session (POST) -
src/app/api/auth/refresh/route.ts- Refresh session (POST) -
src/lib/csrf.ts- CSRF token generation/validation -
src/components/SessionWarning.tsx- Idle timeout modal -
src/hooks/useSession.ts- Session refresh hook
Session Database Schema (LokiJS):
interface Session {
token: string
userId: string
expiresAt: Date
createdAt: Date
lastActive: Date
ip: string
userAgent: string
rememberMe: boolean
csrfToken: string
}Active Sessions Page UI:
- Table: Device, Browser, IP, Location, Last Active, Created
- "This device" badge on current session
- Revoke button (except current session)
- "Revoke All Others" button
CSRF Implementation:
- Generate token on login
- Send in cookie (httpOnly, sameSite: strict)
- Require in header for POST/PUT/DELETE
- Validate on server side
- Regenerate on refresh
Testing:
- Session refreshes at 20h
- Remember me works (30 days)
- Active sessions page shows all sessions
- Revoke works
- CSRF protection blocks invalid requests
- Idle timeout warning appears at 22h
File: src/app/config/env/page.tsx
Problem: VariableRow component defined inside parent, causes full remount on every state change
Current Code (line ~50):
export default function EnvPage() {
// VariableRow defined here - BAD
const VariableRow = ({ variable }: { variable: Variable }) => {
// ...
}
return (
// uses VariableRow
)
}Fix:
// Move outside component
function VariableRow({ variable, onEdit, onDelete }: VariableRowProps) {
// ...
}
export default function EnvPage() {
// now uses stable VariableRow reference
}Impact:
- Prevents unnecessary re-renders
- Preserves input focus when editing
- Better performance on large env files (100+ vars)
Testing:
- Type in variable value, doesn't lose focus
- Edit multiple variables without rerenders
- Performance: <16ms render time with 100 vars
File: src/lib/navigation.ts
Problem: NavLink has properties that are never rendered: icon, collapsed, position, external, submenu
Options:
- Remove unused properties (simplest)
- Implement rendering (adds value)
Recommendation: Implement rendering
Implementation:
// src/components/Navigation.tsx
;<NavLink href={link.href}>
{link.icon && <Icon name={link.icon} />}
{link.title}
{link.badge && <Badge>{link.badge}</Badge>}
{link.external && <ExternalLinkIcon />}
</NavLink>
{
link.submenu && <Submenu items={link.submenu} />
}Files to update:
-
src/components/Navigation.tsx- Add icon, badge, external rendering -
src/components/Submenu.tsx- Create submenu component -
src/lib/navigation.ts- Keep all properties (now used)
Testing:
- Icons render on nav links
- Badges show correctly
- External links have icon
- Submenu expands/collapses
- Keyboard navigation works
File: src/app/help/page.tsx
Problem: 2340 lines in a single file, hard to maintain
Split into:
-
/help- Overview + search -
/help/commands- CLI command reference -
/help/troubleshooting- Common issues -
/help/faq- Frequently asked questions -
/help/tutorials- Step-by-step guides -
/help/api- API documentation
Files to create:
-
src/app/help/page.tsx(100 lines) - Overview with search -
src/app/help/commands/page.tsx(800 lines) - CLI reference -
src/app/help/troubleshooting/page.tsx(400 lines) - Issues & fixes -
src/app/help/faq/page.tsx(300 lines) - Q&A -
src/app/help/tutorials/page.tsx(400 lines) - Guides -
src/app/help/api/page.tsx(440 lines) - API docs -
src/components/HelpSearch.tsx(150 lines) - Search component -
src/lib/help/search-index.ts(200 lines) - FlexSearch index
Search Implementation:
- FlexSearch for client-side search
- Index all help content
- Search across: title, description, tags, content
- Show results grouped by section
- Keyboard shortcuts: Cmd+K to open
Navigation: Tabs at top of help pages
Testing:
- All help content accessible
- Search works across all sections
- Links between help pages work
- Mobile-friendly layout
- Cmd+K opens search
File: src/lib/nselfCLI.ts
Function: nselfDbConsole()
Problem: Calls nself db console which opens interactive psql (doesn't work in exec)
Fix: Remove function entirely OR replace with SQL query endpoint
Recommendation: Replace with SQL execution
Implementation:
// Remove:
export async function nselfDbConsole() {
return executeNselfCommand(['db', 'console']) // BROKEN
}
// Replace with:
export async function executeDbQuery(sql: string) {
return executeNselfCommand(['db', 'query', '--sql', sql])
}Files to update:
-
src/lib/nselfCLI.ts- Remove nselfDbConsole, add executeDbQuery -
src/app/database/console/page.tsx- Use executeDbQuery -
src/app/api/database/query/route.ts- Use executeDbQuery
Testing:
- Database console executes queries
- Results display correctly
- Error handling works
- Query history persists
File: src/lib/nselfCLI.ts
Problem: catch (error: any) in many places
Fix: Use proper Error types
Pattern:
// Before:
try {
// ...
} catch (error: any) {
console.error('Error:', error.message)
}
// After:
try {
// ...
} catch (error) {
if (error instanceof Error) {
console.error('Error:', error.message)
} else {
console.error('Unknown error:', error)
}
}
// Or use helper:
import { getErrorMessage } from '@/lib/errors/utils'
try {
// ...
} catch (error) {
console.error('Error:', getErrorMessage(error))
}Files to update (~30 files with any errors):
- All files in
src/app/api/with catch blocks src/lib/nselfCLI.tssrc/lib/docker-api.tssrc/lib/database.ts
Create:
-
src/lib/errors/utils.ts- Error handling utilities
Testing:
- TypeScript strict mode enabled
- Zero
anytypes remaining - Error messages still display correctly
Files: src/app/config/export/page.tsx, import/page.tsx, sync/page.tsx
Problem: Toast notifications don't auto-dismiss
Current:
toast.success('Exported!')
// Stays foreverFix:
toast.success('Exported!', { duration: 3000 })Auto-dismiss durations:
- Success: 3s
- Info: 4s
- Warning: 5s
- Error: 7s (longer to read)
Implementation:
// src/lib/toast.ts
import { toast as sonnerToast } from 'sonner'
export const toast = {
success: (message: string, options?) =>
sonnerToast.success(message, { duration: 3000, ...options }),
error: (message: string, options?) => sonnerToast.error(message, { duration: 7000, ...options }),
// ...
}Files to update (all files using toast):
- Find:
toast.success(,toast.error(, etc. - Replace imports to use custom toast wrapper
- Verify auto-dismiss works
Testing:
- Success toasts dismiss after 3s
- Error toasts dismiss after 7s
- Can manually dismiss before timeout
- Multiple toasts stack correctly
File: src/app/api/config/validate/route.ts
Problem: 200+ lines of regex parsing for nself doctor output
Current: All parsing logic inline in route handler
Fix: Extract to dedicated parser module
Create:
// src/lib/parsers/nself-doctor.ts
export interface DoctorCheckResult {
category: string
status: 'pass' | 'warn' | 'fail'
message: string
details?: string
}
export function parseNselfDoctorOutput(output: string): DoctorCheckResult[] {
// All parsing logic here
}
// src/lib/parsers/nself-build.ts
export function parseNselfBuildOutput(output: string): BuildStep[] {}
// src/lib/parsers/nself-logs.ts
export function parseNselfLogsOutput(output: string): LogEntry[] {}Files to create:
-
src/lib/parsers/nself-doctor.ts(150 lines) -
src/lib/parsers/nself-build.ts(100 lines) -
src/lib/parsers/nself-logs.ts(80 lines) -
src/lib/parsers/__tests__/nself-doctor.test.ts(unit tests)
Files to update:
-
src/app/api/config/validate/route.ts- Use parser -
src/app/api/nself/build/route.ts- Use parser -
src/app/api/logs/stream/route.ts- Use parser
Testing:
- All parsers tested with real CLI output
- Edge cases handled (empty, malformed)
- Performance: <10ms parse time
File: src/app/config/vault/page.tsx
Problem: Uses raw <div>, <input> instead of shadcn components
Current:
<div className="...">
<input type="text" className="..." />
<button className="...">Click</button>
</div>Fix:
import { Card, CardHeader, CardContent } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
;<Card>
<CardHeader>
<CardTitle>Vault Status</CardTitle>
</CardHeader>
<CardContent>
<Input type="text" placeholder="..." />
<Button>Click</Button>
</CardContent>
</Card>Components to replace:
-
<div>→<Card> -
<input>→<Input> -
<button>→<Button> -
<select>→<Select> -
<label>→<Label>
Testing:
- Visual appearance identical
- All functionality works
- Consistent with other pages
- Accessibility improved
For each of the 192 pages, check:
- Loading state: Has skeleton/spinner
- Empty state: Helpful message + CTA when no data
- Error state: User-friendly error with action
- Success state: Clear feedback on actions
- Mobile responsive: Usable on tablet, readable on phone
- Accessibility: Keyboard nav, ARIA labels, focus visible
- Real-time: Uses WebSocket where applicable
- Performance: Loads <1s, renders <16ms
-
Type safety: No
anytypes, proper interfaces - Testing: Unit + E2E tests exist
Current: Service cards, basic stats Make 100%:
- Real-time service status via WebSocket
- Shows running/stopped/unhealthy immediately
- No polling, instant updates
- Animated status transitions
- Service starting: pulsing indicator
- Service stopping: fade out
- Health check: green pulse
- Quick actions on each card
- Start/Stop/Restart buttons
- View logs (opens drawer)
- View details (navigates)
- Resource usage sparklines
- CPU, Memory, Network mini-graphs
- Last 1 hour trend
- Recharts implementation
- Alerts section
- Critical: Red banner with count
- Warnings: Yellow card
- Info: Blue notification
- Recent activity feed
- Last 10 operations
- Timestamps (relative: "2m ago")
- User attribution (for multi-user)
- Environment indicator
- Badge showing Local/Staging/Prod
- Color-coded (green/yellow/red)
- System health score
- 0-100 score based on:
- Services running
- No errors in logs
- Resource usage OK
- Color indicator
- Loading state: DashboardSkeleton
- Empty state: "No services found - Run 'nself init'"
- Mobile: 1-column layout, collapsible cards
- Tests: Dashboard.test.tsx (unit), dashboard.spec.ts (E2E)
Components to create:
-
<ServiceCard>- Service status card -
<ResourceSparkline>- Mini CPU/memory graph -
<ActivityFeed>- Recent operations -
<HealthScore>- System health indicator -
<EnvironmentBadge>- Env indicator
Current: Basic password form Make 100%:
- First-time setup flow
- "Welcome to nAdmin" message
- Password requirements shown
- Confirm password field
- Password strength indicator
- Login flow
- Username (fixed: "admin")
- Password field (visible toggle)
- Remember me checkbox
- Forgot password (for multi-user v0.6+)
- Rate limiting feedback
- After 3 failed attempts: "Too many attempts. Try again in 1 minute"
- Show countdown timer
- Disable form during lockout
- Session persistence
- Remember me → 30 days
- Default → 24 hours
- Security features
- CSRF token on submit
- No password in URL params
- Secure cookie (httpOnly, sameSite)
- Loading state: Spinner on button
- Error state: Clear messages below form
- Success state: Redirect with toast
- Mobile: Full-screen, centered
- Tests: Login.test.tsx, login.spec.ts
UI improvements:
- Password strength bar (weak/medium/strong)
- Caps lock warning
- Enter key submits form
- Auto-focus password field
Current: Build button with output Make 100%:
- Pre-build checks
- Check .env files exist
- Check Docker running
- Check nself CLI accessible
- Show warning if any missing
- Build progress
- Real-time output stream via WebSocket
- Progress bar (0-100%)
- Current step indicator
- Elapsed time
- Build steps visualization
- Timeline view of steps:
- Validating config
- Generating docker-compose.yml
- Creating networks
- Building images
- Pulling images
- Starting containers
- Each step: pending/in-progress/complete/failed
- Build logs
- Auto-scroll to bottom
- Pause auto-scroll button
- Download logs button
- Search logs
- Syntax highlighting for errors
- Build output filters
- Show: All / Errors Only / Warnings Only
- Checkbox toggles
- Post-build actions
- Success: "Build complete! Go to dashboard"
- Failure: "Build failed. View errors below"
- Retry button
- Reset button
- Loading state: BuildProgressSkeleton
- Empty state: "No build in progress"
- Mobile: Full-width log viewer
- Tests: Build.test.tsx, build.spec.ts
Components:
-
<BuildTimeline>- Step-by-step progress -
<LogViewer>- Real-time log display -
<BuildProgress>- Progress bar with stats
Current: Grid of service links Make 100%:
- Service cards with live status
- Service name + icon
- Status: running (green) / stopped (gray) / error (red)
- Health check: healthy / unhealthy / pending
- Uptime (e.g., "5d 3h 12m")
- Resource usage bars (CPU, Memory)
- Quick actions per card
- Start/Stop toggle
- Restart button
- View logs
- View details
- Access UI (if applicable)
- Bulk actions
- Select multiple services
- Start all selected
- Stop all selected
- Restart all selected
- Filters & search
- Search by name
- Filter: Running / Stopped / All
- Filter: Healthy / Unhealthy / All
- Sort: Name / Status / Uptime / CPU
- Service groups
- Core Stack (Postgres, Hasura, Auth)
- Data (Redis, MinIO)
- Infrastructure (Nginx, Mailpit)
- Custom (user-added services)
- Collapsible groups
- Add custom service button
- Opens scaffold wizard
- Loading state: CardGridSkeleton
- Empty state: "No services found. Run nself init"
- Mobile: 1-column, smaller cards
- Tests: ServicesList.test.tsx
Components:
-
<ServiceCard>- Individual service -
<ServiceActions>- Action buttons -
<ServiceFilters>- Search + filters -
<ServiceGroups>- Grouped view
Current: Monaco editor with execute button Make 100%:
- SQL Editor (Monaco)
- Syntax highlighting
- Auto-completion (tables, columns)
- Error highlighting
- Multi-line queries
- Keyboard shortcuts (Cmd+Enter to run)
- Line numbers
- Minimap
- Dark mode support
- Query execution
- Execute button (Cmd+Enter)
- Execute selected text only
- Cancel running query
- Query timeout (30s default)
- Real-time results via WebSocket
- Results display
- Table view (default)
- JSON view (toggle)
- CSV export
- Copy to clipboard
- Row count + execution time
- Pagination (100 rows/page)
- Query history
- Last 50 queries
- Click to load into editor
- Search history
- Delete from history
- Star favorites
- Saved queries
- Save current query with name
- Folder organization
- Quick access sidebar
- Import/export
- Schema browser sidebar
- List all tables
- Expand to see columns + types
- Click table name to INSERT template
- Click column to add to SELECT
- Query templates
- Common queries dropdown:
- SELECT * FROM users LIMIT 10
- Show table structure
- List all tables
- Count rows
- etc.
- Error handling
- Syntax errors highlighted inline
- Clear error messages
- Suggestions for fixes
- Loading state: CodeEditorSkeleton
- Empty state: Template query loaded
- Mobile: Read-only on phone, usable on tablet
- Tests: DatabaseConsole.test.tsx
Components:
-
<SQLEditor>- Monaco wrapper -
<ResultsTable>- Query results -
<QueryHistory>- History sidebar -
<SchemaBrowser>- Table list -
<SavedQueries>- Saved query manager
Current: Key-value editor with tabs Make 100%:
- Environment tabs
- Local (.env.local)
- Dev (.env.dev)
- Staging (.env.staging)
- Production (.env.prod)
- Secrets (.env.secrets)
- Access-based visibility (dev can't see prod)
- Variable list
- Table: Key, Value, Description
- Sortable columns
- Search/filter variables
- Group by prefix (DB*, HASURA*, etc.)
- Add variable
- Key input (validation: A-Z, 0-9, _)
- Value input (multiline for long values)
- Description input
- Secret checkbox (masks value)
- Add button
- Edit variable
- Inline editing
- Click to edit mode
- Save/Cancel buttons
- Validation before save
- Delete variable
- Trash icon
- Confirm modal
- Undo (5s window)
- Secret masking
- Secrets show as
******** - Reveal button (eye icon)
- Copy to clipboard (masked)
- Bulk operations
- Import from .env file
- Export to .env file
- Copy from another environment
- Find & replace
- Validation
- Required variables highlighted
- Invalid values (ports, URLs) shown
- Missing variables from template
- Changes tracking
- Unsaved changes indicator
- Discard changes button
- Save all button
- Auto-save draft (localStorage)
- Post-save actions
- "Rebuild required" banner
- Quick rebuild button
- Loading state: FormSkeleton
- Empty state: "No variables. Click Add to create"
- Mobile: Full-width inputs, scrollable table
- Tests: ConfigEnv.test.tsx
Components:
-
<EnvVariableRow>- Single variable (FIXED from parent) -
<EnvTabBar>- Environment switcher -
<EnvImportExport>- Bulk operations -
<SecretInput>- Masked input field
Current: Service selector + log output Make 100%:
- Service selector
- Dropdown: All services
- Multi-select (view logs from multiple)
- Recently viewed services
- Real-time log streaming
- WebSocket connection
- Auto-scroll to bottom
- Pause button (stops auto-scroll)
- Resume button
- Connection status indicator
- Log display
- Timestamp (absolute + relative)
- Service name (color-coded)
- Log level badge (INFO, WARN, ERROR)
- Message text
- Monospace font
- Syntax highlighting (JSON logs)
- Filters
- Search text
- Filter by level: All / INFO / WARN / ERROR
- Filter by service
- Time range: Last 5m / 1h / 24h / Custom
- Regex search toggle
- Actions
- Download logs (last 1000 lines)
- Clear view (doesn't affect actual logs)
- Refresh
- Share link (with filters)
- Performance
- Virtual scrolling (1000+ lines)
- Throttle updates (max 10 lines/s)
- Buffer management (max 10k lines in memory)
- Log insights
- Error count badge
- Warning count badge
- Patterns detected (repeated errors)
- Loading state: LogViewerSkeleton
- Empty state: "No logs yet. Start a service to see logs"
- Error state: "Connection lost. Reconnecting..."
- Mobile: Horizontal scroll, smaller font
- Tests: LogsViewer.test.tsx
Components:
-
<LogStream>- Real-time log display -
<LogFilters>- Filter controls -
<LogLine>- Single log entry -
<ServiceSelector>- Multi-select dropdown
Make 100%:
- Secret list table (key, created date, last used)
- Add secret modal (key validation)
- Edit secret (show warning about overwrite)
- Delete secret (confirm modal)
- Rotate secret (generates new value)
- Export secrets (encrypted)
- Import secrets (validate format)
- Search/filter secrets
- Secret usage tracking (which services use it)
- Secret strength indicator
- Generate random secret button
- Copy to clipboard
- Loading state: TableSkeleton
- Empty state: "No secrets. Add your first secret"
- Mobile: Responsive table
- Tests: Secrets.test.tsx
Make 100%:
- Current certificate status (valid/expired/none)
- Certificate details (issuer, expiry date, domains)
- Auto-renew toggle
- Generate local cert (mkcert)
- Domain input
- Generate button
- Progress indicator
- Success message with install instructions
- Let's Encrypt setup
- Email input
- Domain input
- DNS verification status
- Challenge type selector (HTTP-01/DNS-01)
- Generate button
- Auto-renew schedule
- Trust certificate button (macOS/Linux)
- Certificate viewer (PEM format)
- Upload custom certificate
- Certificate chain validation
- Loading state: FormSkeleton
- Empty state: "No SSL certificate configured"
- Mobile: Stacked layout
- Tests: SSL.test.tsx
Make 100% (convert to shadcn components):
- Vault status (connected/disconnected)
- Connection form (URL, token)
- Test connection button
- Initialize Vault button
- Unseal Vault UI (key shards)
- Seal Vault button (with warning)
- Vault health metrics
- Secret engine list
- Enable/disable secret engines
- Loading state: CardSkeleton
- Error state: Connection failed message
- Mobile: Full-width cards
- Tests: Vault.test.tsx
Make 100%:
- Run validation button
- Validation results by category:
- Docker ✓/✗
- Database ✓/✗
- Environment variables ✓/✗
- Services ✓/✗
- Network ✓/✗
- Health score (0-100)
- Detailed results per check
- Fix suggestions for each failure
- Re-run individual check button
- Export validation report
- Historical validation results
- Loading state: Validation in progress
- Empty state: "Click Validate to check your config"
- Mobile: Collapsible sections
- Tests: Validate.test.tsx
Make 100%:
- All use consistent pattern
- Progress indicators
- Auto-dismiss toasts (FIX 6)
- Error handling with recovery steps
- Confirmation modals
- Success feedback
- Loading states
- Tests for each
Make 100%:
- Database status card (connected/disconnected)
- Connection info (host, port, database name)
- Quick stats:
- Total tables
- Total rows
- Database size
- Active connections
- Recent queries (last 10)
- Quick actions:
- Open console
- Run migrations
- Create backup
- View schema
- Loading state: DashboardSkeleton
- Error state: "Cannot connect to database"
- Tests: DatabaseOverview.test.tsx
Make 100%:
- Table list (sidebar)
- Search tables
- Group by schema
- Row count per table
- Table details (main area)
- Column list (name, type, nullable, default)
- Primary keys highlighted
- Foreign keys with links
- Indexes list
- Constraints
- Relationship diagram
- Visual ERD (Entity-Relationship Diagram)
- Zoom/pan controls
- Export as image
- Table actions:
- View data (opens in new tab)
- Export schema (SQL)
- Generate TypeScript types
- Copy CREATE TABLE statement
- Loading state: TableSkeleton
- Empty state: "No tables found"
- Tests: Schema.test.tsx
Make 100%:
- Migration list
- Applied migrations (green checkmark)
- Pending migrations (yellow dot)
- Failed migrations (red X)
- Migration name, timestamp, status
- Run migrations button
- Confirm modal
- Progress indicator
- Success/failure feedback
- Rollback migration
- Select migration
- Confirm rollback
- Show affected tables
- Create new migration
- Name input
- Template selector (create table, alter table, etc.)
- SQL editor
- Save migration
- Migration diff viewer
- Show SQL changes
- Syntax highlighting
- Loading state: TableSkeleton
- Empty state: "No migrations"
- Tests: Migrations.test.tsx
Make 100%:
- Backup list (name, size, date, type)
- Create backup button
- Name input
- Type selector (full/schema-only/data-only)
- Compression toggle
- Progress bar
- Estimated time
- Download backup
- Delete backup (confirm)
- Restore backup
- Select backup
- Target database selector
- Confirm modal with warnings
- Progress indicator
- Verification step
- Scheduled backups
- Cron schedule editor
- Enable/disable
- Retention policy (keep last N)
- Loading state: TableSkeleton
- Empty state: "No backups. Create your first backup"
- Tests: Backup.test.tsx, Restore.test.tsx
Make 100%:
- Seed file list
- Run seed button
- Seed preview (what will be inserted)
- Upload custom seed file
- Generate mock data
- Select tables
- Row count input
- Faker.js integration
- Preview generated data
- Execute button
- Loading state: Processing seed
- Empty state: "No seed files"
- Tests: Seed.test.tsx
For each service page (/services/*), implement:
Components every service page needs:
- Status Card
- Running/Stopped indicator
- Health check status
- Uptime
- Resource usage (CPU, Memory)
- Quick actions (Start/Stop/Restart)
- Configuration Section
- Service-specific env vars
- Edit inline
- Save button
- Restart warning
- Logs Section
- Real-time log stream
- Filter by level
- Download logs
- Clear logs
- Metrics Section
- CPU usage graph (last 1h)
- Memory usage graph
- Network I/O graph
- Request rate (if applicable)
- Actions Section
- Access UI (if has web interface)
- View in Docker
- View compose config
- Remove service
Service-specific additions:
- Database list
- Active connections
- Query performance stats
- Vacuum status
- Replication status (if configured)
- GraphQL endpoint link
- Console link (external)
- Metadata status
- Tracked tables count
- Relationships count
- Permissions summary
- User count
- Active sessions
- OAuth providers enabled
- MFA enabled users
- Recent logins
- Key count
- Memory usage
- Hit rate %
- Eviction stats
- Slow log
- Bucket list
- Total storage used
- Object count
- Bandwidth usage
Enhance to 100%:
- Add real-time deployment status
- Add function invocation metrics
- Add log filtering
- Add test invocation UI improvements
- Add function templates
Enhance to 100%:
- Add email preview
- Add template variables autocomplete
- Add send history
- Add delivery statistics
Enhance to 100%:
- Add cache hit/miss metrics
- Add key browser
- Add TTL management
- Add cache warmup tools
Enhance to 100%:
- Add image preview
- Add batch operations
- Add access control UI
- Add CDN configuration
Enhance to 100%:
- Add index statistics
- Add query builder UI
- Add relevance tuning
- Add synonym management
Enhance to 100%:
- Add experiment comparison
- Add model deployment UI
- Add artifact browser
- Add metrics visualization
Enhance to 100%:
- Add channel analytics
- Add message replay
- Add presence tracking UI
- Add rate limiting config
Enhance to 100%:
- Add template preview
- Add customization wizard
- Add post-scaffold setup guide
- Add template marketplace link
Make 100%:
- Installed plugins grid
- Plugin status (active/inactive)
- Plugin version
- Update available badge
- Quick actions (configure, disable, remove)
- Search plugins
- Filter: Active / All
- Sort: Name / Date installed
- Loading state: CardGridSkeleton
- Empty state: "No plugins installed"
Make 100%:
- Available plugins grid
- Plugin details modal (description, features, screenshots)
- Install button
- Category filter (Payments, DevOps, E-commerce)
- Search
- Sort: Popular / Recent / Name
- Loading state: CardGridSkeleton
- Empty state: "No plugins available"
Make 100%:
- Revenue metrics
- MRR (Monthly Recurring Revenue)
- ARR (Annual Recurring Revenue)
- Total revenue
- Growth %
- Revenue chart (last 12 months)
- Recent transactions (last 20)
- Customer count
- Subscription count
- Quick links:
- View customers
- View subscriptions
- View invoices
- Stripe dashboard (external)
Make 100%:
- Customer list table
- Name, Email, Created, Lifetime Value, Status
- Search customers
- Filter: Active / All
- Sort: Name / Created / LTV
- Customer details modal
- Contact info
- Subscriptions
- Invoices
- Payment methods
- Actions: View in Stripe, Email customer
- Pagination (50/page)
- Loading state: TableSkeleton
- Empty state: "No customers"
Make 100%:
- Subscription list table
- Customer, Plan, Status, Amount, Next billing
- Search subscriptions
- Filter: Active / Canceled / All
- Sort: Created / Amount
- Subscription details modal
- Actions: Cancel, Pause, Resume
- Pagination
- Loading state: TableSkeleton
- Empty state: "No subscriptions"
Make 100%:
- Invoice list table
- Customer, Amount, Status, Date, Due date
- Search invoices
- Filter: Paid / Unpaid / All
- Sort: Date / Amount
- Invoice details modal (line items, PDF link)
- Actions: View PDF, Send reminder
- Pagination
- Loading state: TableSkeleton
- Empty state: "No invoices"
Make 100%:
- Repository list (linked repos)
- Recent commits
- Open issues count
- Open PRs count
- CI/CD status (latest runs)
- Activity feed
Make 100%:
- Repository list table
- Name, Language, Stars, Forks, Updated
- Search repos
- Filter: Has issues / Has PRs / All
- Sort: Name / Updated / Stars
- Repo details modal
- Actions: View on GitHub, Settings
- Loading state: TableSkeleton
- Empty state: "No repositories linked"
Make 100%:
- Issue list table
- Title, Repo, Author, Status, Created
- Search issues
- Filter: Open / Closed / All
- Sort: Created / Updated
- Issue details modal
- Actions: View on GitHub, Close, Comment
- Pagination
- Loading state: TableSkeleton
- Empty state: "No issues"
Make 100%:
- PR list table
- Title, Repo, Author, Status, Checks, Created
- Search PRs
- Filter: Open / Merged / Closed
- Sort: Created / Updated
- PR details modal (checks, reviews, files changed)
- Actions: View on GitHub, Merge, Close
- Pagination
- Loading state: TableSkeleton
- Empty state: "No pull requests"
Make 100%:
- Store metrics
- Total products
- Total orders
- Revenue (today/week/month)
- Recent orders (last 10)
- Low stock alerts
- Quick links
Make 100% (similar pattern to Stripe):
- List tables
- Search/filter/sort
- Details modals
- Actions
- Pagination
- Loading/empty states
Make 100%:
- Environment selector (local → staging)
- Diff viewer (what will change)
- Env vars changed
- Services added/removed
- Version changes
- Validation checks
- All required vars set
- No conflicting ports
- Valid URLs
- Deploy button (if valid)
- Deployment log stream
- Post-deploy verification
- Rollback button
Make 100%:
- Canary configuration
- Traffic split % (10/90, 50/50, etc.)
- Duration (1h, 4h, 24h)
- Success criteria (error rate, latency)
- Deploy canary button
- Live metrics comparison
- Old version vs canary
- Error rates
- Response times
- Promote/rollback buttons
- Automatic rollback on failure
Make 100%:
- Current version (blue)
- New version (green)
- Deploy to green button
- Health check status (green)
- Switch traffic button
- Rollback button (switch back to blue)
- Zero-downtime guarantee indicator
Make 100%:
- Environment status
- Running/Stopped
- Last deployed
- Version/commit
- Health checks
- Deploy from local button
- Deploy from git button
- Environment variables (read-only or editable based on access)
- Deployment history (last 20)
- Rollback to previous version
- SSH access (if configured)
Make 100%:
- System health score (0-100)
- Active alerts count
- Service status grid (all services)
- Resource usage summary
- CPU average across services
- Memory average
- Disk usage
- Network I/O
- Recent events (last 20)
- Quick links (Grafana, alerts, metrics)
Make 100%:
- Metric selector dropdown
- CPU usage
- Memory usage
- Disk I/O
- Network I/O
- Request rate
- Error rate
- Response time
- Time range selector (1h, 6h, 24h, 7d, 30d)
- Chart display (Recharts)
- Line chart for time series
- Area chart for cumulative
- Zoom/pan controls
- Multi-metric comparison
- Overlay multiple metrics
- Dual Y-axis
- Export chart (PNG, CSV)
- Auto-refresh toggle (30s interval)
- Loading state: ChartSkeleton
- Empty state: "No metrics available"
Make 100%:
- Alert rules list
- Name, Condition, Severity, Status
- Create alert rule
- Name input
- Metric selector
- Condition (>, <, =, between)
- Threshold value
- Duration (alert if true for X minutes)
- Severity (critical, warning, info)
- Notification channels (email, webhook)
- Edit/delete alert rules
- Alert history
- Triggered alerts
- Resolution time
- Actions taken
- Test alert (send test notification)
- Loading state: TableSkeleton
- Empty state: "No alerts configured"
Make 100%:
- Embedded Grafana dashboard (iframe)
- Dashboard selector dropdown
- Full-screen toggle
- Auto-refresh toggle
- Open in new tab link
- Loading state: "Loading Grafana..."
- Error state: "Grafana not configured"
Make 100%:
- Provider cards (AWS, GCP, Azure, DO, Linode, Hetzner)
- Connection status per provider
- Add provider button
- Provider configuration modal
- Provider selector
- Credentials form (API key, secret, etc.)
- Region selector
- Test connection button
- Edit/remove provider
- Loading state: CardGridSkeleton
- Empty state: "No cloud providers configured"
Make 100%:
- Server list table
- Name, Provider, IP, Status, CPU, Memory
- Create server button
- Provider selector
- Size/type selector
- Region selector
- Name input
- SSH key selector
- Create button
- Progress indicator
- Server details (
/cloud/servers/[name]) - Overview (IP, status, uptime)
- Metrics (CPU, memory, disk)
- Firewall rules
- Snapshots
- Console access
- Actions: Start, Stop, Restart, Destroy
- Loading state: TableSkeleton
- Empty state: "No servers"
Make 100%:
- Cluster list
- Connect to cluster button
- Kubeconfig upload
- Context selector
- Test connection
- Cluster details
- Nodes count
- Pods count
- Namespaces count
- Version
- Health checks
- Remove cluster
- Loading state: TableSkeleton
- Empty state: "No Kubernetes clusters connected"
Make 100%:
- Namespace selector
- Deployment method selector
- Helm chart
- kubectl apply
- Custom YAML
- Helm chart option:
- Chart selector (nself official chart)
- Values editor (YAML)
- Preview generated manifests
- Install button
- kubectl option:
- YAML editor
- Validate button
- Apply button
- Deployment progress
- Rollout status
- Pod creation
- Service exposure
- Post-deploy verification
- Loading state: FormSkeleton
- Empty state: "Select deployment method"
Make 100% (similar pattern):
- List tables
- Search/filter/sort
- Actions
- Details modals
- Loading/empty states
Make 100%:
- Search bar (Cmd+K)
- FlexSearch index
- Search across all help content
- Results grouped by section
- Keyboard navigation
- Quick links grid
- Getting Started
- CLI Commands
- Troubleshooting
- FAQ
- API Reference
- Video Tutorials
- Popular topics
- Recent updates
- Help feedback (was this helpful?)
Make 100%:
- Command list (all
nselfcommands) - Search commands
- Filter by category
- Command detail view
- Usage
- Description
- Options
- Examples
- Related commands
- Copy command button
- Try command button (opens relevant page)
Make 100%:
- Issue categories
- Installation
- Configuration
- Services
- Database
- Deployment
- Performance
- Common issues list (30+ issues)
- Issue detail view
- Symptoms
- Cause
- Solution (step-by-step)
- Prevention
- Search issues
- "Still stuck? Contact support" link
Make 100%:
- FAQ list (30+ questions)
- Categories
- General
- Setup
- Services
- Deployment
- Troubleshooting
- Collapsible Q&A
- Search FAQs
- "Ask a question" link (feedback form)
Make 100%:
- Tutorial list
- Getting started (step-by-step)
- First deployment
- Adding a custom service
- Database backup/restore
- Multi-environment setup
- CI/CD integration
- Tutorial detail view
- Step-by-step instructions
- Screenshots
- Code examples
- Video embed (if available)
- Estimated time
- Difficulty level
Make 100%:
- API endpoint list
- Grouped by category (Auth, Services, Database, etc.)
- Endpoint detail view
- HTTP method + path
- Description
- Parameters
- Request body schema
- Response schema
- Example request
- Example response
- Try it button (API playground)
Make 100%:
- Current version display
- Update check button
- Available updates list
- nself CLI updates
- nAdmin updates
- Service updates (Postgres, Redis, etc.)
- Update button per item
- Update all button
- Release notes viewer
- Auto-update toggle
- Update history (last 10)
Make 100%:
- Run diagnostics button
- Diagnostic checks
- Docker running
- nself CLI version
- Services status
- Database connectivity
- Disk space
- Memory usage
- Network connectivity
- Results display (pass/fail/warn)
- Export diagnostics report
- Share report button (creates shareable link)
- Already planned in Foundation
-
<Form>- Form wrapper with validation -
<FormField>- Field with label + error -
<FormSection>- Grouped fields -
<FormActions>- Submit/cancel buttons -
<MultiSelect>- Select multiple options -
<DatePicker>- Date selection -
<TimePicker>- Time selection -
<ColorPicker>- Color selection -
<FileUpload>- Drag-drop file upload -
<CodeEditor>- Monaco wrapper -
<MarkdownEditor>- MDX editor
-
<DataTable>- Sortable, filterable table - Column config
- Pagination
- Row selection
- Bulk actions
- Export (CSV, JSON)
-
<StatCard>- Metric display card -
<ProgressBar>- Progress indicator -
<ProgressRing>- Circular progress -
<Timeline>- Event timeline -
<Chart>- Recharts wrapper - Line chart
- Bar chart
- Area chart
- Pie chart
- Donut chart
-
<Sparkline>- Mini inline chart -
<Badge>- Status badge -
<StatusIndicator>- Running/stopped dot -
<HealthCheck>- Health status -
<EmptyState>- No data message
-
<Breadcrumbs>- Page path -
<Tabs>- Tab navigation -
<Sidebar>- Side navigation -
<Pagination>- Page navigation -
<CommandPalette>- Cmd+K search (already exists?)
-
<Alert>- Alert banner -
<Toast>- Notification (Sonner) -
<ConfirmDialog>- Confirm action modal -
<LoadingSpinner>- Loading indicator -
<ProgressModal>- Long operation progress -
<ErrorBoundary>- Error catcher -
<OfflineBanner>- Network status
-
<PageTemplate>- Standard page layout -
<PageShell>- Page container -
<PageHeader>- Page title + actions -
<PageContent>- Content area -
<TwoColumnLayout>- Sidebar + content -
<ThreeColumnLayout>- Sidebar + content + sidebar -
<Card>- Content card (already exists) -
<CardGrid>- Responsive card grid -
<Drawer>- Slide-out panel -
<Modal>- Dialog modal
-
<ServiceCard>- Service status card -
<ServiceActions>- Service action buttons -
<ResourceUsage>- CPU/memory bars -
<LogViewer>- Log display -
<TerminalOutput>- CLI output display -
<DockerStats>- Container stats
Every API route must implement:
-
Authentication
const session = await validateSession(request) if (!session) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) }
-
CSRF Protection (POST/PUT/DELETE only)
const csrfToken = request.headers.get('x-csrf-token') if (!validateCsrfToken(csrfToken, session)) { return NextResponse.json({ error: 'Invalid CSRF token' }, { status: 403 }) }
-
Input Validation
import { z } from 'zod' const schema = z.object({ name: z.string().min(1).max(100), value: z.string(), }) const result = schema.safeParse(await request.json()) if (!result.success) { return NextResponse.json( { error: 'Validation failed', details: result.error.issues, }, { status: 400 } ) }
-
Error Handling
try { // operation } catch (error) { return NextResponse.json( { success: false, error: { code: ErrorCode.OPERATION_FAILED, message: getErrorMessage(error), userMessage: 'Operation failed. Please try again.', retryable: true, }, }, { status: 500 } ) }
-
Rate Limiting (auth endpoints)
const rateLimitResult = await checkRateLimit(session.userId, 'login') if (!rateLimitResult.allowed) { return NextResponse.json( { error: 'Too many requests', retryAfter: rateLimitResult.retryAfter, }, { status: 429 } ) }
-
Logging
logger.info('Operation started', { userId: session.userId, operation: 'backup_create', })
-
Consistent Response Format
// Success return NextResponse.json({ success: true, data: result, }) // Error return NextResponse.json({ success: false, error: { code: ErrorCode.XXX, message: 'Technical message', userMessage: 'User-friendly message', retryable: boolean, details?: any } }, { status: number })
- Rate limiting (5 attempts/minute)
- Brute force protection
- Session management
- CSRF tokens
- CLI command validation
- Command injection prevention (execFile only)
- Output parsing
- Timeout handling (30s default)
- Docker socket availability check
- Container existence validation
- Resource limit validation
- SQL injection prevention
- Query timeout (30s)
- Result size limit (1MB)
- Connection pooling
- Service existence check
- State validation (can't stop already stopped)
- Dependency validation
- File permission checks
- Backup before write
- Atomic writes
- Validation before save
- Plugin availability check
- Version compatibility
- Schema validation
Goal: >80% code coverage
Utilities (src/lib/__tests__/)
-
auth-db.test.ts- Session management -
database.test.ts- LokiJS operations -
project-utils.test.ts- Project detection -
nselfCLI.test.ts- CLI execution -
docker-api.test.ts- Docker operations -
errors/codes.test.ts- Error code mapping -
errors/messages.test.ts- Message generation -
parsers/nself-doctor.test.ts- Parser output -
websocket/server.test.ts- WebSocket server -
websocket/client.test.ts- WebSocket client
API Routes (src/app/api/**/__tests__/)
-
All auth routes (10 files)
-
All nself routes (8 files)
-
All database routes (10 files)
-
All service routes (50+ files)
-
Sample:
auth/login.test.tsdescribe('POST /api/auth/login', () => { it('returns 400 with no password', async () => { const res = await POST(createRequest({})) expect(res.status).toBe(400) }) it('returns 401 with wrong password', async () => { const res = await POST(createRequest({ password: 'wrong' })) expect(res.status).toBe(401) }) it('returns 200 with correct password', async () => { const res = await POST(createRequest({ password: 'correct' })) expect(res.status).toBe(200) expect(await res.json()).toHaveProperty('token') }) it('rate limits after 5 attempts', async () => { // Make 5 failed attempts for (let i = 0; i < 5; i++) { await POST(createRequest({ password: 'wrong' })) } const res = await POST(createRequest({ password: 'wrong' })) expect(res.status).toBe(429) }) })
Components (src/components/__tests__/)
-
All skeleton components (10 files)
-
All form components (11 files)
-
All data display components (12 files)
-
All feedback components (7 files)
-
Sample:
ServiceCard.test.tsxdescribe('ServiceCard', () => { it('renders service name', () => { render(<ServiceCard service={mockService} />) expect(screen.getByText('PostgreSQL')).toBeInTheDocument() }) it('shows running status', () => { render(<ServiceCard service={{ ...mockService, status: 'running' }} />) expect(screen.getByText('Running')).toBeInTheDocument() }) it('calls onStart when start button clicked', () => { const onStart = jest.fn() render(<ServiceCard service={mockService} onStart={onStart} />) fireEvent.click(screen.getByText('Start')) expect(onStart).toHaveBeenCalledWith(mockService.name) }) })
Hooks (src/hooks/__tests__/)
-
useWebSocket.test.ts -
useServiceStatus.test.ts -
useSession.test.ts -
useBuildProgress.test.ts
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts', '!src/**/__tests__/**'],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
}
// jest.setup.ts
import '@testing-library/jest-dom'
import { TextEncoder, TextDecoder } from 'util'
global.TextEncoder = TextEncoder
global.TextDecoder = TextDecoder
// Mock Docker API
jest.mock('@/lib/docker-api', () => ({
getContainers: jest.fn(() => Promise.resolve([])),
// ...
}))
// Mock LokiJS
jest.mock('@/lib/database', () => ({
getDatabase: jest.fn(() => mockDatabase),
// ...
}))Goal: 10 critical user flows tested
e2e/auth.spec.ts
test('complete login flow', async ({ page }) => {
await page.goto('http://localhost:3021/login')
// First time setup
await page.fill('[name="password"]', 'TestPassword123!')
await page.fill('[name="confirmPassword"]', 'TestPassword123!')
await page.click('button[type="submit"]')
// Should redirect to dashboard
await page.waitForURL('http://localhost:3021/')
await expect(page.locator('h1')).toContainText('Dashboard')
})
test('logout and login again', async ({ page }) => {
await loginAsAdmin(page)
// Logout
await page.click('[aria-label="User menu"]')
await page.click('text=Logout')
// Should redirect to login
await page.waitForURL('http://localhost:3021/login')
// Login again
await page.fill('[name="password"]', 'TestPassword123!')
await page.click('button[type="submit"]')
await page.waitForURL('http://localhost:3021/')
})e2e/services.spec.ts
test('view services and start/stop', async ({ page }) => {
await loginAsAdmin(page)
await page.goto('http://localhost:3021/services')
// Check service card visible
await expect(page.locator('text=PostgreSQL')).toBeVisible()
// Click stop button
await page.click('[data-service="postgres"] button:has-text("Stop")')
// Wait for status change
await expect(page.locator('[data-service="postgres"] [data-status]')).toHaveAttribute(
'data-status',
'stopped'
)
// Start again
await page.click('[data-service="postgres"] button:has-text("Start")')
await expect(page.locator('[data-service="postgres"] [data-status]')).toHaveAttribute(
'data-status',
'running'
)
})e2e/database.spec.ts
test('execute SQL query', async ({ page }) => {
await loginAsAdmin(page)
await page.goto('http://localhost:3021/database/console')
// Type query
await page.fill('.monaco-editor textarea', 'SELECT 1')
// Execute
await page.click('button:has-text("Execute")')
// Check results
await expect(page.locator('table')).toBeVisible()
await expect(page.locator('table td')).toContainText('1')
})e2e/config.spec.ts
test('edit environment variable', async ({ page }) => {
await loginAsAdmin(page)
await page.goto('http://localhost:3021/config/env')
// Add new variable
await page.click('button:has-text("Add Variable")')
await page.fill('[name="key"]', 'TEST_VAR')
await page.fill('[name="value"]', 'test_value')
await page.click('button:has-text("Save")')
// Check saved
await expect(page.locator('text=TEST_VAR')).toBeVisible()
// Edit variable
await page.click('[data-key="TEST_VAR"] button:has-text("Edit")')
await page.fill('[data-key="TEST_VAR"] [name="value"]', 'new_value')
await page.click('button:has-text("Save")')
// Check updated
await expect(page.locator('[data-key="TEST_VAR"]')).toContainText('new_value')
})e2e/logs.spec.ts
test('view service logs', async ({ page }) => {
await loginAsAdmin(page)
await page.goto('http://localhost:3021/logs')
// Select service
await page.selectOption('select[name="service"]', 'postgres')
// Check logs appear
await expect(page.locator('.log-line')).toHaveCount(gt(0))
// Filter by error
await page.click('button:has-text("ERROR")')
// Check only error logs shown
const logLines = await page.locator('.log-line').all()
for (const line of logLines) {
await expect(line).toContainText('ERROR')
}
})e2e/deploy.spec.ts
test('deploy to staging', async ({ page }) => {
await loginAsAdmin(page)
await page.goto('http://localhost:3021/deploy/preview')
// Select staging
await page.selectOption('select[name="environment"]', 'staging')
// Check diff
await expect(page.locator('.diff-viewer')).toBeVisible()
// Deploy
await page.click('button:has-text("Deploy")')
// Confirm
await page.click('button:has-text("Confirm Deploy")')
// Wait for completion
await expect(page.locator('text=Deployment complete')).toBeVisible({
timeout: 60000,
})
})e2e/plugins.spec.ts
test('install and configure plugin', async ({ page }) => {
await loginAsAdmin(page)
await page.goto('http://localhost:3021/plugins/marketplace')
// Find Stripe plugin
await page.click('[data-plugin="stripe"]')
// Install
await page.click('button:has-text("Install")')
// Configure
await page.fill('[name="api_key"]', 'sk_test_...')
await page.click('button:has-text("Save Configuration")')
// Check installed
await page.goto('http://localhost:3021/plugins')
await expect(page.locator('[data-plugin="stripe"]')).toBeVisible()
})e2e/backup.spec.ts
test('create and restore backup', async ({ page }) => {
await loginAsAdmin(page)
await page.goto('http://localhost:3021/database/backup')
// Create backup
await page.fill('[name="name"]', 'test-backup')
await page.click('button:has-text("Create Backup")')
// Wait for completion
await expect(page.locator('text=Backup created successfully')).toBeVisible({
timeout: 30000,
})
// Restore backup
await page.click('[data-backup="test-backup"] button:has-text("Restore")')
await page.click('button:has-text("Confirm Restore")')
// Wait for completion
await expect(page.locator('text=Restore complete')).toBeVisible({
timeout: 30000,
})
})e2e/help.spec.ts
test('search help content', async ({ page }) => {
await loginAsAdmin(page)
// Press Cmd+K
await page.keyboard.press('Meta+K')
// Search
await page.fill('[placeholder="Search help..."]', 'docker')
// Check results
await expect(page.locator('.search-results')).toBeVisible()
await expect(page.locator('.search-result')).toHaveCount(gt(0))
// Click result
await page.click('.search-result:first-child')
// Check navigated
await expect(page.locator('h1')).toContainText('Docker')
})e2e/session.spec.ts
test('session persistence with remember me', async ({ page, context }) => {
await page.goto('http://localhost:3021/login')
// Login with remember me
await page.fill('[name="password"]', 'TestPassword123!')
await page.check('[name="rememberMe"]')
await page.click('button[type="submit"]')
await page.waitForURL('http://localhost:3021/')
// Close and reopen browser
await page.close()
const newPage = await context.newPage()
// Should still be logged in
await newPage.goto('http://localhost:3021/')
await expect(newPage.locator('h1')).toContainText('Dashboard')
})Goals:
- Lighthouse score >90
- Bundle size <500KB
- Page load <1s
- TTI <2s
Tools:
- Lighthouse CI
- Bundle Analyzer
- React DevTools Profiler
Tests to run:
-
Bundle Analysis
npm run build npx webpack-bundle-analyzer .next/static/chunks/*.js
Check:
- Main bundle <200KB
- Vendor bundle <300KB
- Page chunks <50KB each
- Identify heavy dependencies
- Remove unused code
-
Lighthouse CI
# .github/workflows/lighthouse.yml - name: Lighthouse CI run: | npm install -g @lhci/cli lhci autorun
Metrics to achieve:
- Performance: >90
- Accessibility: >90
- Best Practices: >90
- SEO: >80
-
Load Testing
// Load test with k6 import http from 'k6/http' import { check } from 'k6' export let options = { vus: 10, // 10 virtual users duration: '30s', } export default function () { let res = http.get('http://localhost:3021/') check(res, { 'status is 200': (r) => r.status === 200, 'response time <1s': (r) => r.timings.duration < 1000, }) }
-
React Performance
- Use React DevTools Profiler
- Find slow renders (>16ms)
- Memoize expensive components
- Use
React.memowhere appropriate - Use
useMemofor expensive calculations - Use
useCallbackfor event handlers
Optimizations to apply:
- Code splitting (route-based)
- Dynamic imports for heavy components
- Image optimization (next/image)
- Font optimization (next/font)
- Remove unused dependencies
- Tree-shaking (lucide-react icons)
- Lazy load below-the-fold content
- Service Worker for offline support
- CDN for static assets
- Gzip/Brotli compression
/* Mobile: 0-639px */
/* Tablet: 640-1023px */
/* Desktop: 1024px+ */Priority tiers:
- Tier 1 (Must be usable on tablet, readable on mobile):
- Dashboard (
/) - Login (
/login) - Services (
/services) - Logs (
/logs) - Database Console (
/database/console) - Config Env (
/config/env)
- Tier 2 (Must be usable on tablet):
- All service pages
- All database pages
- All config pages
- Monitor pages
- Deploy pages
- Tier 3 (Desktop-optimized, but readable on tablet):
- Help pages
- Plugin pages
- Cloud/K8s pages
- System pages
Mobile layout patterns:
- Card grid → 1 column on mobile
- Table → Horizontal scroll or card view
- Multi-column → Stack vertically
- Sidebar → Hamburger menu
- Forms → Full-width inputs
- Charts → Responsive, maintain aspect ratio
- Code editor → Read-only on phone, usable on tablet
Touch-friendly:
- Buttons min 44x44px
- Adequate spacing (16px min)
- No hover-only interactions
- Swipe gestures (optional)
Goal: WCAG 2.1 AA compliance
- Semantic HTML
- Use
<nav>,<main>,<article>,<aside> - Use
<h1>-<h6>hierarchy - Use
<button>for actions,<a>for links - Use
<label>for all inputs
- Keyboard Navigation
- All interactive elements tabbable
- Logical tab order
- Focus visible (outline)
- Escape closes modals
- Enter/Space activates buttons
- Arrow keys for lists
- ARIA Labels
-
aria-labelon icon-only buttons -
aria-labelledbyfor sections -
aria-describedbyfor help text -
aria-livefor dynamic content -
roleattributes where needed
- Color Contrast
- Text: 4.5:1 min
- Large text: 3:1 min
- Interactive elements: 3:1 min
- Test with tools (axe, Lighthouse)
- Screen Reader Support
- Test with NVDA (Windows)
- Test with VoiceOver (macOS)
- Meaningful alt text
- Skip to content link
- Descriptive link text (no "click here")
- Forms
- Labels associated with inputs
- Error messages clear
- Required fields indicated
- Validation feedback accessible
- All images have alt text
- All buttons have labels
- All links have meaningful text
- Heading hierarchy correct
- Focus visible on all interactive elements
- Keyboard navigation works
- Color contrast passes
- Screen reader announces correctly
- No keyboard traps
- Skip to content link present
- Error messages announced
- Loading states announced
- Dynamic content announces (
aria-live)
Current state: next-themes installed, some inconsistencies
Goal: Perfect dark mode on all pages
// Tailwind colors
const colors = {
// Light mode
light: {
background: 'white',
foreground: 'zinc-900',
card: 'white',
border: 'zinc-200',
primary: 'blue-600',
secondary: 'zinc-100',
muted: 'zinc-100',
accent: 'blue-50',
destructive: 'red-600',
},
// Dark mode
dark: {
background: 'zinc-900',
foreground: 'zinc-50',
card: 'zinc-800',
border: 'zinc-700',
primary: 'blue-500',
secondary: 'zinc-800',
muted: 'zinc-800',
accent: 'zinc-800',
destructive: 'red-500',
},
}- Remove all hard-coded colors
- Search for:
bg-white,bg-zinc-900, etc. - Replace with:
bg-background,bg-card, etc.
-
Use CSS variables
:root { --background: white; --foreground: #18181b; } .dark { --background: #18181b; --foreground: white; }
-
Test all pages
- Switch between light/dark
- Check contrast ratios
- Check syntax highlighting (Monaco, code blocks)
- Check charts (Recharts)
- Check shadows (visible in both modes)
- Theme toggle
- Add to header
- Icon: Sun/Moon
- Persist preference
- System preference detection
- Password hashing (bcrypt) - DONE
- Session tokens (httpOnly cookies) - DONE
- Session expiry (24h) - DONE
- Session refresh mechanism - TODO
- CSRF protection - TODO
- Rate limiting (auth endpoints) - TODO
- Brute force protection - TODO
- Remember me (30 days) - TODO
- Active sessions list - TODO
- Revoke sessions - TODO
- Zod schemas on all API routes
- SQL injection prevention (parameterized queries)
- Command injection prevention (execFile only, no shell)
- Path traversal prevention (path.resolve + validation)
- XSS prevention (React auto-escaping + CSP headers)
- File upload validation (type, size, content)
// next.config.js
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-XSS-Protection', value: '1; mode=block' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'", // Monaco editor needs unsafe-eval
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self' data:",
"connect-src 'self' ws: wss:",
"frame-ancestors 'none'",
].join('; '),
},
],
},
]
}- Never commit secrets to git (.gitignore)
- Validate required vars on startup
- Use .env.example template
- Encrypt .env.secrets
- Never expose server-side vars to client
- Non-root user - DONE
- Read-only filesystem where possible
- No privileged mode
- Resource limits (CPU, memory)
- Security scanning (Trivy)
- Regular
npm audit - Dependabot enabled
- Pin versions (no
^or~) - Review updates before merging
Already covered in Help Pages section above.
# nself-admin
> Web UI for the nself CLI - Manage your local dev stack visually
## Features
- Service management
- Database tools
- Configuration editor
- Real-time monitoring
- Deployment tools
- Plugin system
## Quick Start
```bash
nself admin
# Opens http://localhost:3021
```[Include 5-10 screenshots]
MIT
#### docs/INSTALLATION.md
```markdown
# Installation Guide
## Prerequisites
- nself CLI v0.4.8+
- Docker Desktop
- Node.js 20+ (for development)
## Via nself CLI (Recommended)
```bash
nself admin
docker run -p 3021:3021 \
-v $(pwd):/workspace \
nself/nself-admin:latestgit clone https://github.com/nself-org/admin
cd nself-admin
npm install
npm run build
npm start- Navigate to http://localhost:3021
- Create admin password
- Configure project
#### docs/USER-GUIDE.md
```markdown
# User Guide
## Getting Started
[Step-by-step first use]
## Services
[How to manage services]
## Database
[Database tools guide]
## Configuration
[Config management guide]
## Deployment
[Deploy to staging/prod]
## Troubleshooting
[Common issues]
# API Reference
## Authentication
[Auth endpoints]
## Services
[Service endpoints]
## Database
[Database endpoints]
## Configuration
[Config endpoints]
## Webhooks
[Webhook format]# Development Guide
## Setup
```bash
git clone...
npm install
npm run dev
```[Directory layout]
[ESLint, Prettier]
[How to run tests]
[Contribution guidelines]
---
## RELEASE CHECKLIST
### Pre-Release (Week 6, Day 1-3)
- [ ] All 192 pages reviewed and tested
- [ ] All 222 API routes hardened
- [ ] All 93+ components built
- [ ] 80%+ test coverage
- [ ] 10 E2E tests passing
- [ ] Lighthouse score >90
- [ ] Zero TypeScript errors (strict mode)
- [ ] Zero ESLint warnings
- [ ] Zero security vulnerabilities
- [ ] All tech debt fixed
- [ ] Mobile responsive
- [ ] Accessibility audit passed
- [ ] Dark mode working
- [ ] Documentation complete
### Release Preparation (Week 6, Day 4-5)
- [ ] Update CHANGELOG.md
- [ ] Update version to 0.5.0
- package.json
- src/lib/constants.ts
- [ ] Create migration guide (v0.4 → v0.5)
- [ ] Update README.md
- [ ] Update screenshots
- [ ] Build Docker image
- [ ] Test Docker image
- [ ] Tag release (v0.5.0)
- [ ] Create GitHub release
- [ ] Push to Docker Hub
### Post-Release (Week 6, Day 6)
- [ ] Announce release
- [ ] Monitor for issues
- [ ] Create v0.5.1 milestone for bugs
- [ ] Plan v0.6.0 (multi-user, RBAC)
---
## TIMELINE SUMMARY
| Week | Days | Focus | Deliverables |
|------|------|-------|--------------|
| 1 | 1-2 | Real-time, errors, loading, sessions | WebSocket, error system, skeletons, CSRF |
| 1 | 3-5 | Tech debt fixes | 8 issues resolved, strict mode |
| 2 | 1-5 | Core pages (7 pages) | Dashboard, Login, Build, Services, Console, Env, Logs 100% |
| 3 | 1-3 | Config + Database pages (30 pages) | All config pages, all database pages 100% |
| 3 | 4-5 | Testing | 80% coverage, 10 E2E tests, performance tests |
| 4 | 1-5 | Service + Plugin pages (80 pages) | All service pages, all plugin pages 100% |
| 5 | 1-3 | Deploy + Monitor + Cloud pages (40 pages) | Deploy, monitor, cloud/k8s pages 100% |
| 5 | 4-5 | Help + System pages (25 pages) | Help pages split, system pages 100% |
| 6 | 1-2 | Mobile + a11y + dark mode | All pages responsive, accessible |
| 6 | 3-4 | Security + docs | Security audit, all docs complete |
| 6 | 5-6 | Release prep + QA | Final testing, release v0.5.0 |
---
## SUCCESS METRICS
### Quantitative
- [x] **192 pages** reviewed
- [x] **222 API routes** hardened
- [ ] **>80% test coverage**
- [ ] **>90 Lighthouse score**
- [ ] **<500KB bundle size**
- [ ] **<1s page load (FCP)**
- [ ] **<2s time to interactive**
- [ ] **Zero critical bugs**
- [ ] **Zero TypeScript errors**
- [ ] **Zero ESLint warnings**
### Qualitative
- [ ] **Every page** has loading state
- [ ] **Every page** has empty state
- [ ] **Every page** has error handling
- [ ] **Every page** is mobile-friendly
- [ ] **Every page** is accessible
- [ ] **Every feature** works as expected
- [ ] **User feedback** is immediate
- [ ] **Documentation** is full
---
## DONE MEANS DONE
**A page is only "100%" when**:
1. ✅ Loading state exists and works
2. ✅ Empty state is helpful
3. ✅ Error handling is user-friendly
4. ✅ Success feedback is clear
5. ✅ Mobile responsive (tablet usable)
6. ✅ Accessibility compliant (keyboard nav, ARIA, contrast)
7. ✅ Real-time updates (where applicable)
8. ✅ Performance optimized (<1s load, <16ms render)
9. ✅ Type-safe (no `any`)
10. ✅ Tested (unit + E2E)
**When we ship v0.5.0, we ship QUALITY.**
Version: 1.0.0 | Updated: 2026-10-19 11:40 UTC | GitHub