diff --git a/DEV_PORTAL_M05_FRONTEND.md b/DEV_PORTAL_M05_FRONTEND.md
index 746f02e..a99befe 100644
--- a/DEV_PORTAL_M05_FRONTEND.md
+++ b/DEV_PORTAL_M05_FRONTEND.md
@@ -1,8 +1,48 @@
# Milestone 5: Developer Portal Frontend
-**Status**: Planning
+**Status**: Decided
**Goal**: Web interface for developer account and app management.
+## Decision
+
+**htmx + Go Templates** for server-rendered UI from the single Go container:
+
+```
+Rendering: Go html/template
+Interactivity: htmx (partial page updates)
+Styling: Tailwind CSS (compiled at build time)
+Charts: Chart.js (lightweight)
+Icons: Heroicons or Lucide
+Build: Embed static assets in Go binary
+```
+
+### Rationale
+
+1. **Single container** - No separate Node.js server needed
+2. **Server-rendered** - All HTML generated by Go templates
+3. **htmx for interactivity** - AJAX without JavaScript framework
+4. **Embedded assets** - CSS/JS bundled into Go binary via `embed`
+5. **Simple deployment** - Just the Go binary, nothing else
+6. **Low resource usage** - Perfect for Synology NAS
+
+### Architecture
+
+```
+┌─────────────────────────────────────────┐
+│ mosis-portal container │
+│ ┌────────────────────────────────────┐ │
+│ │ Go binary │ │
+│ │ ├── Chi router │ │
+│ │ ├── html/template rendering │ │
+│ │ ├── Embedded static assets │ │
+│ │ │ ├── tailwind.css (built) │ │
+│ │ │ ├── htmx.min.js │ │
+│ │ │ └── chart.min.js │ │
+│ │ └── SQLite database │ │
+│ └────────────────────────────────────┘ │
+└─────────────────────────────────────────┘
+```
+
---
## Overview
@@ -246,136 +286,224 @@ Approach: npm package
---
-## Key Features
+## Key Features (htmx Implementation)
-### File Upload
+### File Upload with Progress
-```typescript
-// Drag and drop with progress
-
-
- Drop your package here
-
+```html
+
+
+
+
```
-### Real-time Validation
+### Dynamic App List
-```typescript
-// Validate package client-side before upload
-async function validatePackage(file: File) {
- const zip = await JSZip.loadAsync(file);
-
- // Check manifest
- const manifest = await zip.file('manifest.json')?.async('text');
- if (!manifest) throw new Error('Missing manifest.json');
-
- const parsed = JSON.parse(manifest);
- ManifestSchema.parse(parsed);
-
- // Check required files
- const entry = parsed.entry;
- if (!zip.file(entry)) {
- throw new Error(`Entry file not found: ${entry}`);
- }
-
- return parsed;
-}
+```html
+
+
+ {{range .Apps}}
+
+

+
{{.Name}}
+
{{.Status}}
+
View →
+
+ {{end}}
+
```
-### Analytics Dashboard
+### Form with Validation
-```typescript
-// Recharts example
-
-
-
-
-
-
+```html
+
+
+```
+
+### Analytics Chart
+
+```html
+
+
+
+
+
+
```
---
-## State Management
+## Go Template Structure
-### Server State (React Query)
+### Base Layout
-```typescript
-// Fetch apps
-const { data: apps, isLoading } = useQuery({
- queryKey: ['apps'],
- queryFn: () => api.get('/apps'),
-});
-
-// Create app mutation
-const createApp = useMutation({
- mutationFn: (data) => api.post('/apps', data),
- onSuccess: () => {
- queryClient.invalidateQueries(['apps']);
- },
-});
+```go
+// templates/layouts/base.html
+{{define "base"}}
+
+
+
+
+
+ {{.Title}} - Mosis Developer Portal
+
+
+
+
+ {{template "navbar" .}}
+
+ {{template "content" .}}
+
+
+
+{{end}}
```
-### Client State (Zustand)
+### Page Template
-```typescript
-// UI state
-const useStore = create((set) => ({
- sidebarOpen: true,
- toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
-}));
+```go
+// templates/pages/dashboard.html
+{{define "content"}}
+Dashboard
+
+
+ {{template "stat-card" dict "Label" "Total Apps" "Value" .Stats.TotalApps}}
+ {{template "stat-card" dict "Label" "Downloads" "Value" .Stats.Downloads}}
+ {{template "stat-card" dict "Label" "Active Users" "Value" .Stats.ActiveUsers}}
+
+
+
+
+
+ Loading...
+
+{{end}}
```
---
## Authentication Flow
-### Login Page
+### Login Page (Go Template)
-```tsx
-export default function LoginPage() {
- return (
-
-
-
- Sign in to Mosis
-
-
-
-
-
-
-
-
+```html
+{{define "content"}}
+
+
+
Sign in to Mosis
+
+
- );
-}
+
+
+{{end}}
```
-### Protected Routes
+### Auth Middleware (Go)
-```tsx
-// Middleware (Next.js)
-export function middleware(request: NextRequest) {
- const token = request.cookies.get('token');
-
- if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
- return NextResponse.redirect(new URL('/login', request.url));
- }
+```go
+// middleware/auth.go
+func RequireAuth(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ session, err := sessionStore.Get(r, "session")
+ if err != nil || session.Values["developer_id"] == nil {
+ http.Redirect(w, r, "/login", http.StatusSeeOther)
+ return
+ }
+ next.ServeHTTP(w, r)
+ })
}
+
+// Usage in router
+r.Group(func(r chi.Router) {
+ r.Use(RequireAuth)
+ r.Get("/dashboard", handlers.Dashboard)
+ r.Get("/apps", handlers.AppList)
+ r.Get("/apps/{id}", handlers.AppDetail)
+})
```
---
@@ -433,11 +561,11 @@ npm install @axe-core/react
## Deliverables
-- [ ] Framework selection
-- [ ] UI component library selection
+- [x] Framework selection (htmx + Go Templates)
+- [x] UI component library selection (Tailwind CSS + Chart.js)
- [ ] Design system (colors, typography)
-- [ ] Page wireframes
-- [ ] Authentication flow
+- [x] Page wireframes (see above)
+- [x] Authentication flow (server sessions + OAuth)
- [ ] Dashboard implementation
- [ ] App management pages
- [ ] Version submission flow
@@ -449,10 +577,10 @@ npm install @axe-core/react
## Open Questions
-1. Dark mode support?
-2. Internationalization (i18n)?
-3. Custom domain for docs vs integrated?
-4. Email notifications UI?
+1. ~~Dark mode support?~~ → Defer to post-MVP (Tailwind makes it easy to add later)
+2. ~~Internationalization (i18n)?~~ → English only for MVP
+3. ~~Custom domain for docs vs integrated?~~ → Integrated into same Go server at /docs
+4. Email notifications UI? → Consider for v1.1
---