···99- Go 1.21+ + Go stdlib only (database/sql, encoding/csv, encoding/json, io, os, path/filepath, time) + modernc.org/sqlite (existing) (003-large-export-batching)
1010- SQLite with FTS5 full-text search (existing); local filesystem for export files (003-large-export-batching)
1111- Go 1.25.3 (existing project standard) (004-security-hardening)
1212+- Go 1.21+ (existing project standard) + Go standard library (html/template, net/http), Pico CSS (existing), bskyoauth (existing OAuth library) (006-login-template-styling)
1313+- N/A (no data storage changes, UI-only refactoring) (006-login-template-styling)
12141315- Go 1.21+ (001-web-interface)
1416···2830Go 1.21+: Follow standard conventions
29313032## Recent Changes
3333+- 006-login-template-styling: Added Go 1.21+ (existing project standard) + Go standard library (html/template, net/http), Pico CSS (existing), bskyoauth (existing OAuth library)
3134- 005-export-download: Added Go 1.21+ (existing project standard)
3235- 004-security-hardening: Added Go 1.25.3 (existing project standard)
3333-- 003-large-export-batching: Added Go 1.21+ + Go stdlib only (database/sql, encoding/csv, encoding/json, io, os, path/filepath, time) + modernc.org/sqlite (existing)
34363537<!-- MANUAL ADDITIONS START -->
3638Kill Go process when finished with work.
+13-38
internal/auth/oauth.go
···3131 }
3232}
33333434-// HandleOAuthLogin initiates the OAuth flow by prompting for handle
3434+// HandleOAuthLogin is deprecated - login is now handled in internal/web/handlers/handlers.go
3535+// This method is kept for backwards compatibility but should not be used.
3636+// Use handlers.Login() and oauthManager.StartOAuthFlow() instead.
3537func (om *OAuthManager) HandleOAuthLogin(w http.ResponseWriter, r *http.Request) {
3636- // For now, use a simple form to get the handle
3737- // This will be replaced with a proper template in later tasks
3838- if r.Method == http.MethodGet {
3939- w.Header().Set("Content-Type", "text/html")
4040- w.Write([]byte(`
4141-<!DOCTYPE html>
4242-<html>
4343-<head><title>Login</title></head>
4444-<body>
4545- <h1>Login with Bluesky</h1>
4646- <form method="POST">
4747- <label>Handle: <input type="text" name="handle" placeholder="user.bsky.social" required></label>
4848- <button type="submit">Login</button>
4949- </form>
5050-</body>
5151-</html>
5252- `))
5353- return
5454- }
5555-5656- // POST: Start OAuth flow with handle
5757- handle := r.FormValue("handle")
5858- if handle == "" {
5959- http.Error(w, "Handle is required", http.StatusBadRequest)
6060- return
6161- }
6262-6363- // Start OAuth flow
6464- ctx := r.Context()
6565- flowState, err := om.client.StartAuthFlow(ctx, handle)
6666- if err != nil {
6767- http.Error(w, fmt.Sprintf("Failed to start OAuth flow: %v", err), http.StatusInternalServerError)
6868- return
6969- }
7070-7171- // Redirect to authorization URL
7272- http.Redirect(w, r, flowState.AuthURL, http.StatusSeeOther)
3838+ http.Error(w, "This endpoint is deprecated. Please use /auth/login", http.StatusGone)
7339}
74407541// HandleOAuthCallback completes the OAuth flow using bskyoauth's built-in handler
···149115func (om *OAuthManager) GetBskySession(sessionID string) (*bskyoauth.Session, error) {
150116 return om.client.GetSession(sessionID)
151117}
118118+119119+// StartOAuthFlow initiates an OAuth flow for the given handle and returns the authorization URL
120120+func (om *OAuthManager) StartOAuthFlow(ctx context.Context, handle string) (string, error) {
121121+ flowState, err := om.client.StartAuthFlow(ctx, handle)
122122+ if err != nil {
123123+ return "", fmt.Errorf("failed to start OAuth flow: %w", err)
124124+ }
125125+ return flowState.AuthURL, nil
126126+}
+21
internal/models/page_data.go
···11+package models
22+33+// LoginPageData represents the data passed to the login template for rendering.
44+// It contains all information needed to display the login page with proper error
55+// handling and form repopulation.
66+type LoginPageData struct {
77+ // Title is the page title displayed in the browser tab and page header
88+ Title string
99+1010+ // Error contains the error message to display when login fails or validation errors occur.
1111+ // Empty string means no error.
1212+ Error string
1313+1414+ // Message contains an informational message (rarely used for login page, but included for consistency with other pages).
1515+ // Empty string means no message.
1616+ Message string
1717+1818+ // Handle is the Bluesky handle value to pre-populate in the form.
1919+ // Used for repopulating the form after validation errors so users don't have to re-type.
2020+ Handle string
2121+}
+60-20
internal/web/handlers/export.go
···77 "log"
88 "net/http"
99 "os"
1010+ "strings"
1011 "sync"
1112 "time"
1213···316317 })
317318}
318319320320+// sanitizeID converts an export ID to a valid CSS selector ID
321321+// by replacing special characters with hyphens
322322+func sanitizeID(id string) string {
323323+ replacer := strings.NewReplacer(
324324+ ":", "-",
325325+ "/", "-",
326326+ " ", "-",
327327+ )
328328+ sanitized := replacer.Replace(id)
329329+ // Remove any remaining non-alphanumeric characters except hyphens
330330+ var result strings.Builder
331331+ for _, ch := range sanitized {
332332+ if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '-' || ch == '_' {
333333+ result.WriteRune(ch)
334334+ }
335335+ }
336336+ return result.String()
337337+}
338338+319339// ExportRow returns a single export as an HTML table row fragment for HTMX
320340func (h *Handlers) ExportRow(w http.ResponseWriter, r *http.Request) {
321341 session, ok := auth.GetSessionFromContext(r.Context())
···349369350370 // Return HTML table row
351371 w.Header().Set("Content-Type", "text/html")
352352- fmt.Fprintf(w, `<tr>
372372+ sanitizedID := sanitizeID(exportRecord.ID)
373373+ fmt.Fprintf(w, `<tr id="export-%s" data-export-id="%s">
353374 <td>%s</td>
354375 <td>%s</td>
355376 <td>%s</td>
···357378 <td>%d</td>
358379 <td>%s</td>
359380 <td>
360360- <div style="display: flex; gap: 0.5rem;">
361361- <a href="/export/download/%s" role="button" class="secondary" style="margin: 0; flex: 1;">
362362- Download ZIP
363363- </a>
364364- <button type="button"
365365- class="outline"
366366- style="margin: 0;"
367367- hx-delete="/export/delete/%s"
368368- hx-confirm="Are you sure you want to delete this export? This action cannot be undone."
369369- hx-target="closest tr"
370370- hx-swap="outerHTML"
371371- hx-headers='{"X-CSRF-Token": "%s"}'>
372372- Delete
373373- </button>
381381+ <div style="display: flex; flex-direction: column; gap: 0.5rem;">
382382+ <div style="display: flex; gap: 0.5rem;">
383383+ <a href="/export/download/%s"
384384+ role="button"
385385+ class="secondary download-btn"
386386+ style="margin: 0; flex: 1;"
387387+ data-export-id="%s">
388388+ Download ZIP
389389+ </a>
390390+ <button type="button"
391391+ class="outline delete-export-btn"
392392+ style="margin: 0;"
393393+ hx-delete="/export/delete/%s"
394394+ hx-confirm="Are you sure you want to delete this export? This action cannot be undone."
395395+ hx-target="#export-%s"
396396+ hx-swap="outerHTML"
397397+ hx-headers='{"X-CSRF-Token": "%s"}'>
398398+ Delete
399399+ </button>
400400+ </div>
401401+ <label style="margin: 0; font-size: 0.875rem;">
402402+ <input type="checkbox"
403403+ class="delete-after-checkbox"
404404+ data-export-id="%s"
405405+ style="margin-right: 0.25rem;">
406406+ Delete after download
407407+ </label>
374408 </div>
375409 </td>
376410 </tr>`,
411411+ sanitizedID, // For tr id="export-%s"
412412+ exportRecord.ID, // For tr data-export-id="%s" (original ID)
377413 exportRecord.CreatedAt.Format("2006-01-02 15:04"),
378414 exportRecord.Format,
379415 exportRecord.DateRangeString(),
380416 exportRecord.PostCount,
381417 exportRecord.MediaCount,
382418 exportRecord.HumanSize(),
383383- exportRecord.ID,
384384- exportRecord.ID,
385385- csrfToken,
419419+ exportRecord.ID, // For download link href
420420+ exportRecord.ID, // For download button data-export-id
421421+ exportRecord.ID, // For delete button hx-delete
422422+ sanitizedID, // For delete button hx-target="#export-%s"
423423+ csrfToken, // For delete button X-CSRF-Token
424424+ exportRecord.ID, // For checkbox data-export-id
386425 )
387426}
388427···543582 h.logger.Printf("Delete completed: user=%s export=%s",
544583 session.DID, exportID)
545584546546- // Return empty response with 200 OK for HTMX to remove the row
547547- // HTMX needs a 200 response to perform the swap operation
585585+ // Return 200 OK with empty body for HTMX outerHTML swap
586586+ // The hx-swap="outerHTML" with empty response will remove the target element
548587 w.WriteHeader(http.StatusOK)
588588+ w.Write([]byte{})
549589}
550590551591// deleteExportInternal performs the actual deletion of export files and database record
+48-1
internal/web/handlers/handlers.go
···74747575// Login initiates OAuth flow
7676func (h *Handlers) Login(w http.ResponseWriter, r *http.Request) {
7777- h.oauthManager.HandleOAuthLogin(w, r)
7777+ // GET: Display login form
7878+ if r.Method == http.MethodGet {
7979+ data := TemplateData{
8080+ Error: "",
8181+ Message: "",
8282+ }
8383+ if err := h.renderTemplate(w, r, "login", data); err != nil {
8484+ h.logger.Printf("Error rendering login template: %v", err)
8585+ http.Error(w, "Internal server error", http.StatusInternalServerError)
8686+ }
8787+ return
8888+ }
8989+9090+ // POST: Handle handle submission and start OAuth flow
9191+ handle := r.FormValue("handle")
9292+ if handle == "" {
9393+ // Validation error - re-render template with error
9494+ data := TemplateData{
9595+ Error: "Bluesky handle is required",
9696+ Message: "",
9797+ Handle: handle, // Repopulate form (empty in this case)
9898+ }
9999+ if err := h.renderTemplate(w, r, "login", data); err != nil {
100100+ h.logger.Printf("Error rendering login template with error: %v", err)
101101+ http.Error(w, "Internal server error", http.StatusInternalServerError)
102102+ }
103103+ return
104104+ }
105105+106106+ // Start OAuth flow
107107+ authURL, err := h.oauthManager.StartOAuthFlow(r.Context(), handle)
108108+ if err != nil {
109109+ // OAuth error - re-render template with error
110110+ h.logger.Printf("Failed to start OAuth flow for handle %s: %v", handle, err)
111111+ data := TemplateData{
112112+ Error: "Failed to connect to Bluesky. Please try again.",
113113+ Message: "",
114114+ Handle: handle, // Repopulate form so user doesn't have to retype
115115+ }
116116+ if renderErr := h.renderTemplate(w, r, "login", data); renderErr != nil {
117117+ h.logger.Printf("Error rendering login template with OAuth error: %v", renderErr)
118118+ http.Error(w, "Internal server error", http.StatusInternalServerError)
119119+ }
120120+ return
121121+ }
122122+123123+ // Redirect to Bluesky authorization page
124124+ http.Redirect(w, r, authURL, http.StatusSeeOther)
78125}
7912680127// Callback handles OAuth callback
+19
internal/web/handlers/template.go
···1414type TemplateData struct {
1515 Error string
1616 Message string
1717+ Handle string // For login form - repopulates handle after validation errors
1718 Session interface{}
1819 Status *models.ArchiveStatus
1920 Posts []models.Post
···7273 }
7374 }
7475 return false
7676+ },
7777+ "sanitizeID": func(id string) string {
7878+ // Replace special characters with hyphens to create valid CSS selectors
7979+ // Replaces : / and any other non-alphanumeric characters
8080+ replacer := strings.NewReplacer(
8181+ ":", "-",
8282+ "/", "-",
8383+ " ", "-",
8484+ )
8585+ sanitized := replacer.Replace(id)
8686+ // Remove any remaining non-alphanumeric characters except hyphens
8787+ var result strings.Builder
8888+ for _, ch := range sanitized {
8989+ if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '-' || ch == '_' {
9090+ result.WriteRune(ch)
9191+ }
9292+ }
9393+ return result.String()
7594 },
7695 }
7796}
+3-3
internal/web/templates/pages/export.html
···140140 </thead>
141141 <tbody id="exports-tbody">
142142 {{range .Exports}}
143143- <tr>
143143+ <tr id="export-{{sanitizeID .ID}}" data-export-id="{{.ID}}">
144144 <td>{{.CreatedAt.Format "2006-01-02 15:04"}}</td>
145145 <td>{{.Format}}</td>
146146 <td>{{.DateRangeString}}</td>
···158158 Download ZIP
159159 </a>
160160 <button type="button"
161161- class="outline"
161161+ class="outline delete-export-btn"
162162 style="margin: 0;"
163163 hx-delete="/export/delete/{{.ID}}"
164164 hx-confirm="Are you sure you want to delete this export? This action cannot be undone."
165165- hx-target="closest tr"
165165+ hx-target="#export-{{sanitizeID .ID}}"
166166 hx-swap="outerHTML"
167167 hx-headers='{"X-CSRF-Token": "{{$.CSRFToken}}"}'>
168168 Delete
+73
internal/web/templates/pages/login.html
···11+{{define "title"}}Login - Bluesky Archive{{end}}
22+33+{{define "nav"}}
44+<nav class="container">
55+ <ul>
66+ <li><strong>Bluesky Archive</strong></li>
77+ </ul>
88+ <ul>
99+ <li><a href="/about">About</a></li>
1010+ </ul>
1111+</nav>
1212+{{end}}
1313+1414+{{define "content"}}
1515+<section class="container">
1616+ <!-- Page header -->
1717+ <hgroup>
1818+ <h1>Login with Bluesky</h1>
1919+ <h2>Archive your Bluesky posts and media for safekeeping</h2>
2020+ </hgroup>
2121+2222+ <!-- Error display -->
2323+ {{if .Error}}
2424+ <article aria-label="Error" style="background-color: var(--del-color); border-left: 4px solid #dc3545;">
2525+ <header><strong>Error</strong></header>
2626+ <p>{{.Error}}</p>
2727+ </article>
2828+ {{end}}
2929+3030+ <!-- Success message (rarely used for login, but included for consistency) -->
3131+ {{if .Message}}
3232+ <article aria-label="Success">
3333+ <header><strong>Success</strong></header>
3434+ <p>{{.Message}}</p>
3535+ </article>
3636+ {{end}}
3737+3838+ <!-- Login form -->
3939+ <article>
4040+ <header><strong>Sign In</strong></header>
4141+4242+ <p>Enter your Bluesky handle to get started. You'll be redirected to Bluesky to authorize this application.</p>
4343+4444+ <form method="POST" action="/auth/login">
4545+ <label for="handle">
4646+ Bluesky Handle
4747+ <input type="text"
4848+ id="handle"
4949+ name="handle"
5050+ placeholder="user.bsky.social"
5151+ required
5252+ value="{{.Handle}}"
5353+ autocomplete="username">
5454+ </label>
5555+ <small>Your full Bluesky handle, including the domain (e.g., user.bsky.social or user.custom-domain.com)</small>
5656+5757+ <button type="submit">Continue with Bluesky</button>
5858+ </form>
5959+ </article>
6060+6161+ <!-- About This Application -->
6262+ <article>
6363+ <header><strong>About This Application</strong></header>
6464+6565+ <p><strong>Bluesky Archive</strong> is a local-first backup tool that helps you preserve your Bluesky posts, media, and conversations. Your data is stored on your own computer, giving you complete control and ownership.</p>
6666+6767+ <h3>Privacy First</h3>
6868+ <p>This application runs on your machine and stores everything locally. No third-party servers have access to your archived content. Your Bluesky credentials are never stored—we use secure OAuth authentication that expires after use.</p>
6969+7070+ <p><a href="/about">Learn more about how this works →</a></p>
7171+ </article>
7272+</section>
7373+{{end}}
···11+# Specification Quality Checklist: Login Form Template & Styling
22+33+**Purpose**: Validate specification completeness and quality before proceeding to planning
44+**Created**: 2025-11-02
55+**Feature**: [spec.md](../spec.md)
66+77+## Content Quality
88+99+- [x] No implementation details (languages, frameworks, APIs)
1010+- [x] Focused on user value and business needs
1111+- [x] Written for non-technical stakeholders
1212+- [x] All mandatory sections completed
1313+1414+## Requirement Completeness
1515+1616+- [x] No [NEEDS CLARIFICATION] markers remain
1717+- [x] Requirements are testable and unambiguous
1818+- [x] Success criteria are measurable
1919+- [x] Success criteria are technology-agnostic (no implementation details)
2020+- [x] All acceptance scenarios are defined
2121+- [x] Edge cases are identified
2222+- [x] Scope is clearly bounded
2323+- [x] Dependencies and assumptions identified
2424+2525+## Feature Readiness
2626+2727+- [x] All functional requirements have clear acceptance criteria
2828+- [x] User scenarios cover primary flows
2929+- [x] Feature meets measurable outcomes defined in Success Criteria
3030+- [x] No implementation details leak into specification
3131+3232+## Validation Results
3333+3434+### Content Quality Assessment
3535+3636+✅ **Pass** - Specification focuses on user experience and business value:
3737+- User stories describe what users see and do, not how it's implemented
3838+- Requirements written in terms of system behavior, not code structure
3939+- Success criteria focus on outcomes (consistency, responsiveness, functionality preservation)
4040+4141+✅ **Pass** - No framework or implementation details in requirements:
4242+- Requirements mention "template file" and "Pico CSS" as part of the feature description (user-facing), not as implementation choices
4343+- No mention of Go template syntax, rendering engines, or code structure
4444+- Technology references are to existing established choices, not new implementation decisions
4545+4646+✅ **Pass** - Accessible to non-technical stakeholders:
4747+- Clear user scenarios with acceptance criteria
4848+- Business value explained for each priority
4949+- Technical terms (OAuth, CSRF) used appropriately with context
5050+5151+✅ **Pass** - All mandatory sections present and complete:
5252+- User Scenarios & Testing ✓
5353+- Requirements ✓
5454+- Success Criteria ✓
5555+- Assumptions documented ✓
5656+- Out of Scope defined ✓
5757+5858+### Requirement Completeness Assessment
5959+6060+✅ **Pass** - No [NEEDS CLARIFICATION] markers:
6161+- All requirements are specific and unambiguous
6262+- Assumptions section documents reasonable defaults
6363+- No open questions remain
6464+6565+✅ **Pass** - Requirements are testable:
6666+- Each FR can be verified through testing or inspection
6767+- Acceptance scenarios use Given-When-Then format
6868+- Edge cases identified for testing
6969+7070+✅ **Pass** - Success criteria are measurable:
7171+- SC-001: 500ms load time (quantitative)
7272+- SC-002: 100% functional parity (quantitative)
7373+- SC-003: 95% visual consistency (quantitative)
7474+- SC-004: Responsive 320px-1920px range (quantitative)
7575+- SC-005: Code maintainability improvement (qualitative with clear indicator)
7676+7777+✅ **Pass** - Success criteria are technology-agnostic:
7878+- No mention of specific template engines, frameworks, or implementation approaches
7979+- Focus on user-facing outcomes (load time, responsiveness, visual consistency)
8080+- Code maintainability described in terms of separation of concerns, not specific technologies
8181+8282+✅ **Pass** - All acceptance scenarios defined:
8383+- 3 user stories with multiple scenarios each
8484+- Scenarios cover happy path, error cases, and responsive design
8585+- Edge cases section identifies boundary conditions
8686+8787+✅ **Pass** - Edge cases identified:
8888+- Template missing/corrupted
8989+- Long handle inputs
9090+- Invalid OAuth configuration
9191+- JavaScript disabled
9292+9393+✅ **Pass** - Scope clearly bounded:
9494+- Out of Scope section explicitly lists what's NOT included
9595+- User stories focus on template migration and styling only
9696+- No scope creep into authentication logic changes
9797+9898+✅ **Pass** - Dependencies and assumptions identified:
9999+- Assumptions section documents existing infrastructure (Pico CSS, templates, OAuth)
100100+- Dependencies on existing template system noted
101101+- Reasonable defaults documented
102102+103103+### Feature Readiness Assessment
104104+105105+✅ **Pass** - Functional requirements have clear acceptance criteria:
106106+- Each FR maps to acceptance scenarios in user stories
107107+- Requirements are specific and verifiable
108108+109109+✅ **Pass** - User scenarios cover primary flows:
110110+- P1 stories cover core functionality (styling + OAuth preservation)
111111+- P2 story covers enhancement (contextual help)
112112+- Independent testing described for each story
113113+114114+✅ **Pass** - Feature meets measurable outcomes:
115115+- Success criteria align with user stories
116116+- Outcomes are observable and testable
117117+- Both quantitative and qualitative measures included
118118+119119+✅ **Pass** - No implementation details leaked:
120120+- Specification describes "what" not "how"
121121+- File paths mentioned are part of feature requirement (where the template should be), not implementation details
122122+- Focus on user experience and visual consistency
123123+124124+## Notes
125125+126126+**Status**: ✅ **ALL CHECKS PASSED**
127127+128128+The specification is complete, unambiguous, and ready for the planning phase. The feature has clear scope, testable requirements, and measurable success criteria. No clarifications needed.
129129+130130+**Recommendation**: Proceed to `/speckit.plan` to create the implementation plan.
···11+# Template Interface Contract: Login Page
22+33+**Feature**: 006-login-template-styling
44+**Date**: 2025-11-02
55+**Type**: Internal Template Rendering Contract
66+77+## Overview
88+99+This document defines the contract between the OAuth handler (`internal/auth/oauth.go`) and the login template (`internal/web/templates/pages/login.html`). Since this is an internal UI feature with no external API, the contract is limited to the template rendering interface.
1010+1111+---
1212+1313+## Handler → Template Contract
1414+1515+### Endpoint: GET /auth/login
1616+1717+**Purpose**: Display the login form to the user
1818+1919+**Handler Responsibility**:
2020+```go
2121+// Handler must provide LoginPageData struct to template
2222+type LoginPageData struct {
2323+ Title string // Page title, must not be empty
2424+ Error string // Error message (empty if no error)
2525+ Message string // Info message (rarely used, can be empty)
2626+ Handle string // Pre-filled handle (empty on first load)
2727+}
2828+2929+// Render template
3030+tmpl.ExecuteTemplate(w, "login.html", data)
3131+```
3232+3333+**Template Requirements**:
3434+- Must define `{{define "title"}}` block
3535+- Must define `{{define "nav"}}` block
3636+- Must define `{{define "content"}}` block
3737+- Must handle `.Error` field (display if non-empty)
3838+- Must render form with handle input and submit button
3939+- Must use POST method to same endpoint
4040+4141+**HTTP Response**:
4242+- Status: `200 OK`
4343+- Content-Type: `text/html; charset=utf-8`
4444+- Body: Rendered HTML from template
4545+4646+---
4747+4848+### Endpoint: POST /auth/login
4949+5050+**Purpose**: Process handle submission and initiate OAuth flow
5151+5252+**Request Format**:
5353+```
5454+POST /auth/login HTTP/1.1
5555+Content-Type: application/x-www-form-urlencoded
5656+5757+handle=user.bsky.social
5858+```
5959+6060+**Response Scenarios**:
6161+6262+#### Success: OAuth Flow Initiated
6363+```
6464+HTTP/1.1 302 Found
6565+Location: https://bsky.social/oauth/authorize?client_id=...&state=...
6666+```
6767+6868+Handler redirects to Bluesky OAuth authorization page. No template rendering occurs.
6969+7070+#### Failure: Validation Error
7171+```
7272+HTTP/1.1 200 OK
7373+Content-Type: text/html; charset=utf-8
7474+7575+[Rendered login template with Error field populated]
7676+```
7777+7878+Handler re-renders login template with error message:
7979+```go
8080+data := LoginPageData{
8181+ Title: "Login - Bluesky Archive",
8282+ Error: "Bluesky handle is required",
8383+ Handle: r.FormValue("handle"),
8484+}
8585+tmpl.ExecuteTemplate(w, "login.html", data)
8686+```
8787+8888+#### Failure: OAuth Initiation Error
8989+```
9090+HTTP/1.1 200 OK
9191+Content-Type: text/html; charset=utf-8
9292+9393+[Rendered login template with Error field populated]
9494+```
9595+9696+Handler re-renders with server error:
9797+```go
9898+data := LoginPageData{
9999+ Title: "Login - Bluesky Archive",
100100+ Error: "Failed to connect to Bluesky. Please try again.",
101101+}
102102+tmpl.ExecuteTemplate(w, "login.html", data)
103103+```
104104+105105+---
106106+107107+## Template → Handler Contract
108108+109109+### Form Submission Contract
110110+111111+**Form Element**:
112112+```html
113113+<form method="POST" action="/auth/login">
114114+ <label for="handle">Bluesky Handle</label>
115115+ <input type="text"
116116+ id="handle"
117117+ name="handle"
118118+ placeholder="user.bsky.social"
119119+ required
120120+ value="{{.Handle}}">
121121+ <button type="submit">Login with Bluesky</button>
122122+</form>
123123+```
124124+125125+**Required Attributes**:
126126+- `method="POST"` - Handler expects POST
127127+- `name="handle"` - Handler reads via `r.FormValue("handle")`
128128+- `required` - HTML5 client-side validation
129129+- `value="{{.Handle}}"` - Repopulate on error
130130+131131+**Form Data Contract**:
132132+```
133133+Field Name: handle
134134+Type: string
135135+Required: yes
136136+Format: Bluesky handle (e.g., "user.bsky.social" or "user.com")
137137+Validation: Non-empty, valid handle format
138138+```
139139+140140+---
141141+142142+## Template Structure Contract
143143+144144+### Required Template Blocks
145145+146146+The login template **MUST** implement these three blocks to work with the base layout:
147147+148148+#### 1. Title Block
149149+```html
150150+{{define "title"}}Login - Bluesky Archive{{end}}
151151+```
152152+153153+**Purpose**: Sets `<title>` tag and may be used in page headers
154154+**Required**: Yes
155155+**Format**: Plain text, no HTML
156156+157157+#### 2. Navigation Block
158158+```html
159159+{{define "nav"}}
160160+<nav class="container">
161161+ <ul>
162162+ <li><strong>Bluesky Archive</strong></li>
163163+ </ul>
164164+ <ul>
165165+ <li><a href="/about">About</a></li>
166166+ </ul>
167167+</nav>
168168+{{end}}
169169+```
170170+171171+**Purpose**: Defines page navigation
172172+**Required**: Yes (can be minimal for login page)
173173+**Format**: HTML nav element with Pico CSS styling
174174+175175+#### 3. Content Block
176176+```html
177177+{{define "content"}}
178178+<section class="container">
179179+ <!-- Error display -->
180180+ {{if .Error}}
181181+ <article aria-label="Error">
182182+ <header><strong>Error</strong></header>
183183+ <p>{{.Error}}</p>
184184+ </article>
185185+ {{end}}
186186+187187+ <!-- Login form -->
188188+ <article>
189189+ <header><strong>Login with Bluesky</strong></header>
190190+ <form method="POST">
191191+ <!-- Form fields -->
192192+ </form>
193193+ </article>
194194+</section>
195195+{{end}}
196196+```
197197+198198+**Purpose**: Main page content
199199+**Required**: Yes
200200+**Format**: HTML with Pico CSS classes
201201+202202+---
203203+204204+## Error Display Contract
205205+206206+### Error Message Format
207207+208208+**Handler → Template**:
209209+```go
210210+data.Error = "User-friendly error message"
211211+```
212212+213213+**Template → User**:
214214+```html
215215+{{if .Error}}
216216+<article aria-label="Error" style="background-color: var(--del-color); border-left: 4px solid #dc3545;">
217217+ <header><strong>Error</strong></header>
218218+ <p>{{.Error}}</p>
219219+</article>
220220+{{end}}
221221+```
222222+223223+**Error Message Guidelines**:
224224+225225+✅ **Good Error Messages** (user-friendly):
226226+- "Bluesky handle is required"
227227+- "Failed to connect to Bluesky. Please try again."
228228+- "Invalid handle format. Please use format: user.bsky.social"
229229+230230+❌ **Bad Error Messages** (internal details):
231231+- "panic: nil pointer dereference"
232232+- "OAuth client initialization failed: invalid config"
233233+- "database connection error: timeout"
234234+235235+**Security Note**: Never expose internal system details, stack traces, or configuration in error messages.
236236+237237+---
238238+239239+## CSS Styling Contract
240240+241241+### Pico CSS Classes
242242+243243+The template must use these Pico CSS classes for consistency:
244244+245245+| Element | CSS Class | Purpose |
246246+|---------|-----------|---------|
247247+| Main container | `container` | Responsive centering and padding |
248248+| Navigation | `<nav class="container">` | Consistent navigation styling |
249249+| Error article | `<article aria-label="Error">` | Semantic error display |
250250+| Form fieldset | `<fieldset>` | Form grouping (optional) |
251251+| Input fields | (no class) | Pico CSS auto-styles inputs |
252252+| Buttons | (no class) | Pico CSS auto-styles buttons |
253253+| Help text | `<small>` | Muted help text |
254254+255255+**Classless Styling**: Pico CSS automatically styles most HTML elements without requiring classes. Only use classes for containers and semantic elements.
256256+257257+---
258258+259259+## Validation Contract
260260+261261+### Client-Side Validation (HTML5)
262262+263263+**Template Responsibility**:
264264+```html
265265+<input type="text" name="handle" required>
266266+```
267267+268268+- `required` attribute: Browser prevents empty submission
269269+- `type="text"`: Standard text input (no special validation)
270270+- `placeholder`: Provides format example
271271+272272+**Browser Behavior**:
273273+- Prevents form submission if field is empty
274274+- Shows browser default error message
275275+- No JavaScript required
276276+277277+### Server-Side Validation (Handler)
278278+279279+**Handler Responsibility**:
280280+```go
281281+handle := r.FormValue("handle")
282282+if handle == "" {
283283+ // Render template with error
284284+ data.Error = "Bluesky handle is required"
285285+ return
286286+}
287287+288288+// Validate handle format
289289+if !isValidHandle(handle) {
290290+ data.Error = "Invalid handle format"
291291+ return
292292+}
293293+```
294294+295295+**Validation Rules**:
296296+1. Non-empty (redundant with HTML5, but required for security)
297297+2. Valid handle format (domain name or subdomain.domain)
298298+3. Reasonable length (< 255 characters)
299299+300300+---
301301+302302+## OAuth Flow Contract
303303+304304+### Successful OAuth Initiation
305305+306306+**Handler Action**:
307307+```go
308308+// Start OAuth flow
309309+flowState, err := om.client.StartAuthFlow(ctx, handle)
310310+if err != nil {
311311+ // Return error via template
312312+ return
313313+}
314314+315315+// Redirect to Bluesky
316316+http.Redirect(w, r, flowState.AuthURL, http.StatusFound)
317317+```
318318+319319+**Template Not Involved**: On success, handler redirects to Bluesky. Template is not rendered.
320320+321321+### OAuth Callback (Out of Scope)
322322+323323+**Note**: The OAuth callback flow (`/auth/callback`) is **not** part of this feature. It remains unchanged and does not use the login template.
324324+325325+---
326326+327327+## Testing Contract
328328+329329+### Manual Testing Checklist
330330+331331+**Visual Consistency**:
332332+- [ ] Login page matches design of export.html, dashboard.html
333333+- [ ] Pico CSS styling applied correctly
334334+- [ ] Responsive design works on mobile/tablet/desktop
335335+- [ ] Error messages display in red article with proper formatting
336336+337337+**Functional Testing**:
338338+- [ ] Empty form submission shows HTML5 validation message
339339+- [ ] Valid handle initiates OAuth flow (redirect to Bluesky)
340340+- [ ] Invalid handle shows server-side error message
341341+- [ ] Error message displays with previously entered handle repopulated
342342+- [ ] Navigation links work correctly
343343+- [ ] Page loads in < 500ms
344344+345345+**Security Testing**:
346346+- [ ] XSS attempt in handle field is escaped by template engine
347347+- [ ] Error messages don't expose internal details
348348+- [ ] OAuth flow initiates correctly (no CSRF vulnerability)
349349+350350+---
351351+352352+## Backward Compatibility
353353+354354+### Breaking Changes: None
355355+356356+This feature is a **drop-in replacement** for the existing inline HTML:
357357+358358+**Before** (inline HTML):
359359+```go
360360+w.Write([]byte(`<html>...</html>`))
361361+```
362362+363363+**After** (template rendering):
364364+```go
365365+tmpl.ExecuteTemplate(w, "login.html", data)
366366+```
367367+368368+**User Experience**: Identical OAuth flow, same form fields, same POST endpoint. Only visual styling changes.
369369+370370+**API Contract**: No API changes. Internal implementation detail only.
371371+372372+---
373373+374374+## Summary
375375+376376+| Contract Element | Handler Responsibility | Template Responsibility |
377377+|-----------------|------------------------|-------------------------|
378378+| Data Structure | Provide LoginPageData | Render with {{.Field}} |
379379+| Error Display | Set .Error field | Show error if present |
380380+| Form Submission | Process POST /auth/login | Submit with name="handle" |
381381+| OAuth Flow | Redirect on success | N/A (not involved) |
382382+| Validation | Server-side validation | Client-side HTML5 required |
383383+| Styling | N/A (not involved) | Apply Pico CSS classes |
384384+| Navigation | N/A (not involved) | Define nav block |
385385+386386+This contract ensures clean separation of concerns: handlers manage logic and data, templates manage presentation.
+286
specs/006-login-template-styling/data-model.md
···11+# Data Model: Login Form Template & Styling
22+33+**Feature**: 006-login-template-styling
44+**Date**: 2025-11-02
55+**Status**: Complete
66+77+## Overview
88+99+This feature is primarily a UI refactoring with minimal data modeling requirements. The only "data" involved is the template rendering data structure passed from the handler to the template. No database schema changes, no new persistent entities.
1010+1111+## Template Data Structure
1212+1313+### LoginPageData
1414+1515+Represents the data passed to the login template for rendering.
1616+1717+**Purpose**: Provide all necessary information for rendering the login page, including error messages and contextual information.
1818+1919+**Fields**:
2020+2121+| Field | Type | Required | Description |
2222+|-------|------|----------|-------------|
2323+| `Title` | `string` | Yes | Page title (e.g., "Login - Bluesky Archive") |
2424+| `Error` | `string` | No | Error message to display if login failed or validation error occurred |
2525+| `Message` | `string` | No | Informational message (rarely used for login page, but included for consistency) |
2626+| `Handle` | `string` | No | Pre-filled handle value (for repopulating form after validation error) |
2727+2828+**Go Struct Definition**:
2929+3030+```go
3131+type LoginPageData struct {
3232+ Title string
3333+ Error string
3434+ Message string
3535+ Handle string
3636+}
3737+```
3838+3939+**Usage Example**:
4040+4141+```go
4242+// Success case - show login form
4343+data := LoginPageData{
4444+ Title: "Login - Bluesky Archive",
4545+ Error: "",
4646+ Handle: "",
4747+}
4848+renderTemplate(w, "login.html", data)
4949+5050+// Error case - validation failed
5151+data := LoginPageData{
5252+ Title: "Login - Bluesky Archive",
5353+ Error: "Bluesky handle is required",
5454+ Handle: r.FormValue("handle"), // Repopulate for user convenience
5555+}
5656+renderTemplate(w, "login.html", data)
5757+5858+// OAuth initiation error
5959+data := LoginPageData{
6060+ Title: "Login - Bluesky Archive",
6161+ Error: "Failed to connect to Bluesky. Please try again.",
6262+}
6363+renderTemplate(w, "login.html", data)
6464+```
6565+6666+**Validation Rules**:
6767+6868+- `Title`: Must not be empty
6969+- `Error`: Empty string when no error, non-empty descriptive message on error
7070+- `Message`: Typically empty for login page
7171+- `Handle`: Should be populated only when redisplaying form after error
7272+7373+**State Transitions**:
7474+7575+1. **Initial Load**: `Error=""`, `Handle=""` → Display empty login form
7676+2. **Validation Error**: `Error="..."`, `Handle="user_input"` → Display form with error and preserve user input
7777+3. **OAuth Error**: `Error="..."`, `Handle=""` → Display form with error, clear handle
7878+4. **Success**: Redirect to OAuth provider (no template rendering)
7979+8080+---
8181+8282+## Existing Data Models (No Changes)
8383+8484+This feature does not modify any existing data models. The following existing models remain unchanged:
8585+8686+### OAuth Session Data (in `internal/auth/oauth.go`)
8787+8888+**No Changes**: The OAuth flow state management remains identical. This feature only affects how the login form HTML is generated, not the OAuth logic itself.
8989+9090+**Existing Implementation**:
9191+- `bskyoauth` library handles all OAuth state
9292+- Session management unchanged
9393+- Token storage unchanged
9494+9595+---
9696+9797+## No Database Schema Changes
9898+9999+This feature requires **zero** database migrations or schema changes:
100100+101101+- No new tables
102102+- No new columns
103103+- No new indexes
104104+- No data migration required
105105+106106+**Rationale**: This is purely a presentation layer change. All data handling (OAuth tokens, sessions) remains unchanged.
107107+108108+---
109109+110110+## Data Flow
111111+112112+### Login Page Request Flow
113113+114114+```
115115+1. User requests /auth/login (GET)
116116+ ↓
117117+2. Handler creates empty LoginPageData
118118+ ↓
119119+3. Template renders with empty form
120120+ ↓
121121+4. User submits handle (POST)
122122+ ↓
123123+5a. Validation fails → Handler creates LoginPageData with Error
124124+ ↓
125125+ Template renders with error message
126126+127127+5b. Validation passes → OAuth flow initiates
128128+ ↓
129129+ Redirect to Bluesky (no template rendering)
130130+```
131131+132132+### Error Handling Flow
133133+134134+```
135135+Client-side (HTML5):
136136+- Browser validates required field
137137+- Prevents submission if empty
138138+139139+Server-side:
140140+- Handler validates handle format
141141+- Creates LoginPageData with Error on failure
142142+- Template displays error in Pico CSS styled article
143143+```
144144+145145+---
146146+147147+## Template Data Contracts
148148+149149+### Input Contract (Handler → Template)
150150+151151+The handler must provide:
152152+153153+```go
154154+data := LoginPageData{
155155+ Title: "Login - Bluesky Archive", // Required, non-empty
156156+ Error: "", // Optional, empty string if no error
157157+ Message: "", // Optional, rarely used
158158+ Handle: "", // Optional, for repopulating form
159159+}
160160+```
161161+162162+### Output Contract (Template → User)
163163+164164+The template must render:
165165+166166+1. Page title in `<title>` tag and page header
167167+2. Error message in `<article aria-label="Error">` if present
168168+3. Login form with:
169169+ - Label and input for Bluesky handle
170170+ - Submit button
171171+ - HTML5 validation (required attribute)
172172+4. Helpful context text explaining the login process
173173+5. Minimal navigation with About/Help links
174174+175175+---
176176+177177+## Edge Cases
178178+179179+### 1. Extremely Long Error Messages
180180+181181+**Scenario**: Server returns very long error message (e.g., full OAuth error JSON)
182182+183183+**Handling**: Template should wrap error text properly using Pico CSS article styling. Long messages will word-wrap within the container.
184184+185185+**Data Validation**: Handler should sanitize error messages to be user-friendly, not expose internal details.
186186+187187+### 2. Special Characters in Handle
188188+189189+**Scenario**: User enters handle with special characters (e.g., `<script>alert('xss')</script>`)
190190+191191+**Handling**: Go's `html/template` automatically escapes all template variables. No XSS vulnerability.
192192+193193+**Data Validation**: Handle validation should reject invalid characters before they reach the template.
194194+195195+### 3. Missing Template File
196196+197197+**Scenario**: Template file deleted or corrupted
198198+199199+**Handling**: Go template execution will return error. Handler should catch and display generic error page (existing error handling).
200200+201201+**No Data Impact**: This is a runtime error, not a data error. No data corruption possible.
202202+203203+### 4. Concurrent Requests
204204+205205+**Scenario**: User opens multiple login pages simultaneously
206206+207207+**Handling**: Each request gets independent LoginPageData instance. No shared state. No concurrency issues.
208208+209209+**Stateless Design**: Template rendering is stateless - perfect for concurrent requests.
210210+211211+---
212212+213213+## Data Privacy Considerations
214214+215215+### Sensitive Data Handling
216216+217217+**Handle (Bluesky username)**:
218218+- **Sensitivity**: Low (public identifier)
219219+- **Storage**: Not stored in this feature (passed to OAuth flow)
220220+- **Logging**: Should not be logged in error messages
221221+- **Display**: Safe to repopulate in form after validation error
222222+223223+**Error Messages**:
224224+- **Sensitivity**: Low to Medium (may contain system information)
225225+- **Sanitization**: Must be user-friendly, not expose internal errors
226226+- **Example Good Error**: "Failed to connect to Bluesky. Please try again."
227227+- **Example Bad Error**: "panic: nil pointer dereference at oauth.go:123"
228228+229229+**OAuth Tokens**:
230230+- **Sensitivity**: High (authentication credentials)
231231+- **Template Exposure**: NEVER passed to login template
232232+- **Existing Security**: Handled by bskyoauth library (unchanged)
233233+234234+---
235235+236236+## Comparison with Existing Pages
237237+238238+This data structure follows the exact pattern used by other pages:
239239+240240+**export.html**:
241241+```go
242242+type ExportPageData struct {
243243+ Title string
244244+ Error string
245245+ Message string
246246+ CSRFToken string
247247+ Status *ExportStatus
248248+ Exports []ExportRecord
249249+}
250250+```
251251+252252+**dashboard.html**:
253253+```go
254254+type DashboardData struct {
255255+ Title string
256256+ Error string
257257+ Message string
258258+ Stats *ArchiveStats
259259+}
260260+```
261261+262262+**login.html** (new):
263263+```go
264264+type LoginPageData struct {
265265+ Title string // Same pattern
266266+ Error string // Same pattern
267267+ Message string // Same pattern
268268+ Handle string // Feature-specific
269269+}
270270+```
271271+272272+**Consistency**: All pages use `Title`, `Error`, `Message` fields. This maintains template rendering consistency across the application.
273273+274274+---
275275+276276+## Summary
277277+278278+This feature has minimal data modeling requirements:
279279+280280+✅ **New Data Structure**: LoginPageData (simple, 4 fields)
281281+✅ **No Database Changes**: Zero schema modifications
282282+✅ **No New Entities**: Purely presentation data
283283+✅ **Consistent Pattern**: Follows existing template data conventions
284284+✅ **Privacy Compliant**: No sensitive data exposure
285285+286286+The data model is intentionally minimal because this is a UI refactoring, not a feature that introduces new business logic or data storage requirements.
+128
specs/006-login-template-styling/plan.md
···11+# Implementation Plan: Login Form Template & Styling
22+33+**Branch**: `006-login-template-styling` | **Date**: 2025-11-02 | **Spec**: [spec.md](spec.md)
44+**Input**: Feature specification from `/specs/006-login-template-styling/spec.md`
55+66+**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow.
77+88+## Summary
99+1010+Migrate the login form from inline HTML in `internal/auth/oauth.go` to a proper template file at `internal/web/templates/pages/login.html`. Apply Pico CSS styling to match the rest of the application's design using the existing base template, navigation, and styling patterns. This is a straightforward UI refactoring that maintains 100% functional parity with the existing OAuth authentication flow while improving visual consistency and code maintainability.
1111+1212+## Technical Context
1313+1414+**Language/Version**: Go 1.21+ (existing project standard)
1515+**Primary Dependencies**: Go standard library (html/template, net/http), Pico CSS (existing), bskyoauth (existing OAuth library)
1616+**Storage**: N/A (no data storage changes, UI-only refactoring)
1717+**Testing**: Go testing package (go test), manual UI testing for visual consistency
1818+**Target Platform**: Web application running on user's local machine (existing deployment model)
1919+**Project Type**: Single web application
2020+**Performance Goals**: Page load <500ms (from success criteria), template rendering <50ms
2121+**Constraints**: Must maintain 100% functional parity with existing OAuth flow, zero breaking changes to authentication
2222+**Scale/Scope**: Single login page template, ~1-2 files modified (oauth.go, new login.html), minimal scope UI-only change
2323+2424+## Constitution Check
2525+2626+*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
2727+2828+### ✅ I. Data Privacy & Local-First Architecture
2929+**Status**: PASS - No impact
3030+3131+This feature is a UI-only refactoring that does not affect data storage, privacy, or local-first architecture. All OAuth tokens and session management remain unchanged.
3232+3333+### ✅ II. Comprehensive & Accurate Archival
3434+**Status**: PASS - No impact
3535+3636+No changes to archival functionality. This is purely a login UI enhancement.
3737+3838+### ✅ III. Multiple Export Formats
3939+**Status**: PASS - No impact
4040+4141+No changes to export functionality.
4242+4343+### ✅ IV. Fast & Efficient Search
4444+**Status**: PASS - No impact
4545+4646+No changes to search functionality.
4747+4848+### ✅ V. Incremental & Efficient Operations
4949+**Status**: PASS - No impact
5050+5151+No changes to operational efficiency. Template rendering is negligible performance overhead (<50ms).
5252+5353+### ✅ Security & Privacy
5454+**Status**: PASS - Compliant
5555+5656+- OAuth 2.0 flow using bskyoauth library: ✅ Preserved (no changes to OAuth logic)
5757+- Secure session management: ✅ Preserved (no changes)
5858+- CSRF protection: ✅ Will verify if needed for public login page
5959+- No credential storage in plaintext: ✅ Unchanged
6060+6161+### ✅ Development Standards
6262+**Status**: PASS - Compliant
6363+6464+- Go 1.21+ with standard library: ✅ Using html/template from stdlib
6565+- Clear separation of concerns: ✅ Improved (separating HTML from Go code)
6666+- HTML, Pico CSS, HTMX, Vanilla JavaScript: ✅ Using existing Pico CSS and template structure
6767+- Testing requirements: ✅ Will add manual UI testing for visual consistency
6868+6969+**GATE RESULT**: ✅ **PASS** - All constitutional principles satisfied. This is a low-risk UI refactoring that improves code maintainability and visual consistency without affecting core functionality.
7070+7171+## Project Structure
7272+7373+### Documentation (this feature)
7474+7575+```text
7676+specs/[###-feature]/
7777+├── plan.md # This file (/speckit.plan command output)
7878+├── research.md # Phase 0 output (/speckit.plan command)
7979+├── data-model.md # Phase 1 output (/speckit.plan command)
8080+├── quickstart.md # Phase 1 output (/speckit.plan command)
8181+├── contracts/ # Phase 1 output (/speckit.plan command)
8282+└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
8383+```
8484+8585+### Source Code (repository root)
8686+8787+```text
8888+internal/
8989+├── auth/
9090+│ └── oauth.go # MODIFIED: Remove inline HTML, use template rendering
9191+├── web/
9292+│ ├── handlers/
9393+│ │ └── template.go # POTENTIALLY MODIFIED: May need helper for login page rendering
9494+│ ├── middleware/
9595+│ │ └── csrf.go # REVIEW: Verify if CSRF needed for public login
9696+│ ├── templates/
9797+│ │ ├── layouts/
9898+│ │ │ └── base.html # EXISTING: Used by login template
9999+│ │ ├── pages/
100100+│ │ │ ├── export.html # REFERENCE: Template structure to match
101101+│ │ │ ├── dashboard.html # REFERENCE: Navigation structure
102102+│ │ │ └── login.html # NEW: Login page template
103103+│ │ └── partials/
104104+│ │ └── nav.html # EXISTING: Navigation component
105105+│ └── static/
106106+│ └── css/
107107+│ └── pico.css # EXISTING: CSS framework
108108+109109+tests/
110110+├── integration/
111111+│ └── login_template_test.go # NEW: Integration test for login page rendering
112112+└── unit/
113113+ └── auth_test.go # POTENTIALLY MODIFIED: Update tests if needed
114114+```
115115+116116+**Structure Decision**: Single web application using Go's standard project layout. The feature involves:
117117+1. Creating a new template file at `internal/web/templates/pages/login.html`
118118+2. Modifying `internal/auth/oauth.go` to use template rendering instead of inline HTML
119119+3. Leveraging existing template infrastructure (base.html, Pico CSS, navigation partials)
120120+4. Adding tests to verify template rendering and OAuth flow preservation
121121+122122+This follows the existing pattern used by other pages (export.html, dashboard.html) in the application.
123123+124124+## Complexity Tracking
125125+126126+> **Fill ONLY if Constitution Check has violations that must be justified**
127127+128128+No constitutional violations. This feature aligns with all principles and requires no complexity justification.
+797
specs/006-login-template-styling/quickstart.md
···11+# Quickstart Guide: Login Form Template & Styling
22+33+**Feature**: 006-login-template-styling
44+**Date**: 2025-11-02
55+**Estimated Time**: 1-2 hours
66+77+## Overview
88+99+This guide provides step-by-step instructions for implementing the login form template migration. Follow these steps in order for a smooth implementation.
1010+1111+---
1212+1313+## Prerequisites
1414+1515+Before starting:
1616+1717+✅ **Review these documents**:
1818+- [spec.md](spec.md) - Feature requirements and user stories
1919+- [research.md](research.md) - Technical decisions and patterns
2020+- [data-model.md](data-model.md) - Template data structure
2121+- [contracts/template-interface.md](contracts/template-interface.md) - Handler-template contract
2222+2323+✅ **Verify existing code**:
2424+- Confirm `internal/auth/oauth.go` contains inline HTML (lines 40-52)
2525+- Check `internal/web/templates/layouts/base.html` exists
2626+- Check `internal/web/templates/pages/export.html` as reference
2727+- Verify Pico CSS is loaded in base template
2828+2929+✅ **Testing environment**:
3030+- Go 1.21+ installed
3131+- Development server running
3232+- Browser for visual testing
3333+3434+---
3535+3636+## Step-by-Step Implementation
3737+3838+### Step 1: Create the Login Template
3939+4040+**File**: `internal/web/templates/pages/login.html`
4141+4242+**Action**: Create new template file with complete structure
4343+4444+**Implementation**:
4545+4646+```html
4747+{{define "title"}}Login - Bluesky Archive{{end}}
4848+4949+{{define "nav"}}
5050+<nav class="container">
5151+ <ul>
5252+ <li><strong>Bluesky Archive</strong></li>
5353+ </ul>
5454+ <ul>
5555+ <li><a href="/about">About</a></li>
5656+ </ul>
5757+</nav>
5858+{{end}}
5959+6060+{{define "content"}}
6161+<section class="container">
6262+ <!-- Page header -->
6363+ <hgroup>
6464+ <h1>Login with Bluesky</h1>
6565+ <h2>Archive your Bluesky posts and media for safekeeping</h2>
6666+ </hgroup>
6767+6868+ <!-- Error display -->
6969+ {{if .Error}}
7070+ <article aria-label="Error" style="background-color: var(--del-color); border-left: 4px solid #dc3545;">
7171+ <header><strong>Error</strong></header>
7272+ <p>{{.Error}}</p>
7373+ </article>
7474+ {{end}}
7575+7676+ <!-- Success message (rarely used for login, but included for consistency) -->
7777+ {{if .Message}}
7878+ <article aria-label="Success">
7979+ <header><strong>Success</strong></header>
8080+ <p>{{.Message}}</p>
8181+ </article>
8282+ {{end}}
8383+8484+ <!-- Login form -->
8585+ <article>
8686+ <header><strong>Sign In</strong></header>
8787+8888+ <p>Enter your Bluesky handle to get started. You'll be redirected to Bluesky to authorize this application.</p>
8989+9090+ <form method="POST" action="/auth/login">
9191+ <label for="handle">
9292+ Bluesky Handle
9393+ <input type="text"
9494+ id="handle"
9595+ name="handle"
9696+ placeholder="user.bsky.social"
9797+ required
9898+ value="{{.Handle}}"
9999+ autocomplete="username">
100100+ </label>
101101+ <small>Your full Bluesky handle, including the domain (e.g., user.bsky.social or user.custom-domain.com)</small>
102102+103103+ <button type="submit">Continue with Bluesky</button>
104104+ </form>
105105+ </article>
106106+107107+ <!-- Additional context -->
108108+ <article>
109109+ <header><strong>About This Application</strong></header>
110110+ <p>
111111+ Bluesky Archive is a local-first tool that helps you back up your Bluesky posts,
112112+ media, and profile data. All data is stored locally on your machine, giving you
113113+ complete control and ownership.
114114+ </p>
115115+ <p>
116116+ <strong>Privacy First</strong>: Your data never leaves your computer. OAuth tokens
117117+ are encrypted and stored securely. No telemetry or analytics.
118118+ </p>
119119+ <p>
120120+ <a href="/about">Learn more about how this works →</a>
121121+ </p>
122122+ </article>
123123+</section>
124124+{{end}}
125125+```
126126+127127+**Design Decisions Implemented**:
128128+- ✅ Three-block structure (title, nav, content)
129129+- ✅ Pico CSS classless styling
130130+- ✅ Error display using article with aria-label
131131+- ✅ Contextual help text about the application
132132+- ✅ Minimal navigation (Bluesky Archive logo + About link)
133133+- ✅ Form with proper accessibility (labels, placeholders, required)
134134+- ✅ `autocomplete="username"` for better UX
135135+- ✅ Repopulates handle on validation error via `{{.Handle}}`
136136+137137+**Verification**:
138138+```bash
139139+# Check file exists
140140+ls -la internal/web/templates/pages/login.html
141141+142142+# Verify syntax (Go template check)
143143+go run cmd/bskyarchive/main.go --help
144144+# (Server should start without template parsing errors)
145145+```
146146+147147+---
148148+149149+### Step 2: Define the LoginPageData Struct
150150+151151+**File**: `internal/auth/oauth.go` (or create `internal/models/page_data.go` if you want to centralize)
152152+153153+**Action**: Add struct definition for template data
154154+155155+**Implementation**:
156156+157157+**Option A**: Add to `internal/auth/oauth.go` (simpler, keeps it local)
158158+159159+```go
160160+// Add near the top of the file after imports
161161+162162+// LoginPageData represents the data passed to the login template
163163+type LoginPageData struct {
164164+ Title string
165165+ Error string
166166+ Message string
167167+ Handle string
168168+}
169169+```
170170+171171+**Option B**: Add to `internal/models/page_data.go` (better organization)
172172+173173+```go
174174+// Create new file: internal/models/page_data.go
175175+package models
176176+177177+// LoginPageData represents the data passed to the login template
178178+type LoginPageData struct {
179179+ Title string // Page title
180180+ Error string // Error message (empty if no error)
181181+ Message string // Info message (rarely used)
182182+ Handle string // Pre-filled handle value (for form repopulation)
183183+}
184184+```
185185+186186+**Recommendation**: Option B (separate file) for better code organization, especially if you plan to add more page data structs later.
187187+188188+**Verification**:
189189+```bash
190190+# Verify Go compiles
191191+go build ./internal/models/
192192+```
193193+194194+---
195195+196196+### Step 3: Update OAuth Handler to Use Template
197197+198198+**File**: `internal/auth/oauth.go`
199199+200200+**Action**: Replace inline HTML with template rendering
201201+202202+**Current Code** (lines 34-54):
203203+```go
204204+func (om *OAuthManager) HandleOAuthLogin(w http.ResponseWriter, r *http.Request) {
205205+ // For now, use a simple form to get the handle
206206+ // This will be replaced with a proper template in later tasks
207207+ if r.Method == http.MethodGet {
208208+ w.Header().Set("Content-Type", "text/html")
209209+ w.Write([]byte(`
210210+<!DOCTYPE html>
211211+<html>
212212+<head><title>Login</title></head>
213213+<body>
214214+ <h1>Login with Bluesky</h1>
215215+ <form method="POST">
216216+ <label>Handle: <input type="text" name="handle" placeholder="user.bsky.social" required></label>
217217+ <button type="submit">Login</button>
218218+ </form>
219219+</body>
220220+</html>
221221+ `))
222222+ return
223223+ }
224224+225225+ // POST: Start OAuth flow with handle
226226+ // ... rest of POST handling ...
227227+}
228228+```
229229+230230+**New Code**:
231231+232232+First, add template support. You'll need to load and parse templates. Check how other handlers do this - likely there's a template helper in `internal/web/handlers/template.go`.
233233+234234+**Investigation needed**:
235235+```bash
236236+# Check template helper structure
237237+cat internal/web/handlers/template.go | grep -A 10 "func.*Template"
238238+```
239239+240240+**Assuming template helper exists** (adjust based on actual implementation):
241241+242242+```go
243243+import (
244244+ "net/http"
245245+ "fmt"
246246+247247+ "github.com/shindakun/bskyarchive/internal/models"
248248+ "github.com/shindakun/bskyarchive/internal/web/handlers" // For template rendering
249249+)
250250+251251+func (om *OAuthManager) HandleOAuthLogin(w http.ResponseWriter, r *http.Request) {
252252+ if r.Method == http.MethodGet {
253253+ // Render login template with empty data
254254+ data := models.LoginPageData{
255255+ Title: "Login - Bluesky Archive",
256256+ Error: "",
257257+ Handle: "",
258258+ }
259259+260260+ // Use existing template rendering helper
261261+ // NOTE: Adjust this based on actual template helper implementation
262262+ if err := handlers.RenderTemplate(w, "login.html", data); err != nil {
263263+ http.Error(w, "Failed to render login page", http.StatusInternalServerError)
264264+ log.Printf("Template rendering error: %v", err)
265265+ return
266266+ }
267267+ return
268268+ }
269269+270270+ // POST: Start OAuth flow with handle
271271+ handle := r.FormValue("handle")
272272+ if handle == "" {
273273+ // Validation error - re-render with error message
274274+ data := models.LoginPageData{
275275+ Title: "Login - Bluesky Archive",
276276+ Error: "Bluesky handle is required",
277277+ Handle: handle, // Empty in this case, but included for consistency
278278+ }
279279+ if err := handlers.RenderTemplate(w, "login.html", data); err != nil {
280280+ http.Error(w, "Failed to render login page", http.StatusInternalServerError)
281281+ return
282282+ }
283283+ return
284284+ }
285285+286286+ // Start OAuth flow
287287+ ctx := r.Context()
288288+ flowState, err := om.client.StartAuthFlow(ctx, handle)
289289+ if err != nil {
290290+ // OAuth error - re-render with error message
291291+ data := models.LoginPageData{
292292+ Title: "Login - Bluesky Archive",
293293+ Error: "Failed to connect to Bluesky. Please try again.",
294294+ Handle: handle,
295295+ }
296296+ if err := handlers.RenderTemplate(w, "login.html", data); err != nil {
297297+ http.Error(w, "Failed to render login page", http.StatusInternalServerError)
298298+ return
299299+ }
300300+ return
301301+ }
302302+303303+ // Store OAuth state and redirect
304304+ om.sessionManager.Put(r.Context(), "oauth_state", flowState.State)
305305+ http.Redirect(w, r, flowState.AuthURL, http.StatusFound)
306306+}
307307+```
308308+309309+**Important Notes**:
310310+1. **Template Helper**: You MUST check the actual implementation of template rendering in your codebase. The `handlers.RenderTemplate` function name is a guess - adjust based on reality.
311311+2. **Error Handling**: Always handle template rendering errors gracefully
312312+3. **Logging**: Log template errors for debugging (use existing logger)
313313+4. **Context**: Existing OAuth logic remains 100% unchanged
314314+315315+**Verification**:
316316+```bash
317317+# Compile check
318318+go build ./internal/auth/
319319+320320+# Run tests
321321+go test ./internal/auth/
322322+```
323323+324324+---
325325+326326+### Step 4: Investigate and Integrate Template Rendering
327327+328328+**Action**: Find the existing template rendering infrastructure
329329+330330+**Commands**:
331331+```bash
332332+# Find template initialization
333333+grep -r "template.New\|template.ParseFiles\|template.ParseGlob" internal/
334334+335335+# Find template execution
336336+grep -r "ExecuteTemplate\|Execute" internal/web/handlers/
337337+338338+# Check how other pages render templates
339339+cat internal/web/handlers/export.go | grep -A 5 "ExecuteTemplate"
340340+```
341341+342342+**Common Patterns**:
343343+344344+**Pattern 1**: Global template variable
345345+```go
346346+// In main.go or server initialization
347347+var templates *template.Template
348348+349349+func init() {
350350+ templates = template.Must(template.ParseGlob("internal/web/templates/**/*.html"))
351351+}
352352+353353+// In handler
354354+templates.ExecuteTemplate(w, "login.html", data)
355355+```
356356+357357+**Pattern 2**: Template helper function
358358+```go
359359+// In internal/web/handlers/template.go
360360+func RenderTemplate(w http.ResponseWriter, name string, data interface{}) error {
361361+ return templates.ExecuteTemplate(w, name, data)
362362+}
363363+```
364364+365365+**Pattern 3**: Per-request template parsing (less common, slower)
366366+```go
367367+tmpl, err := template.ParseFiles("internal/web/templates/pages/login.html")
368368+if err != nil {
369369+ return err
370370+}
371371+return tmpl.Execute(w, data)
372372+```
373373+374374+**Action**: Once you identify the pattern, integrate it into the OAuth handler as shown in Step 3.
375375+376376+**Verification**:
377377+```bash
378378+# Start the development server
379379+go run cmd/bskyarchive/main.go
380380+381381+# Test in browser
382382+open http://localhost:8080/auth/login
383383+384384+# Check for template parsing errors in server logs
385385+```
386386+387387+---
388388+389389+### Step 5: Manual Testing
390390+391391+**Action**: Comprehensive manual testing checklist
392392+393393+#### Visual Testing
394394+395395+1. **Load login page**:
396396+ ```
397397+ http://localhost:8080/auth/login
398398+ ```
399399+400400+ ✅ **Verify**:
401401+ - [ ] Page loads without errors
402402+ - [ ] Pico CSS styling applied (centered layout, styled form)
403403+ - [ ] Navigation shows "Bluesky Archive" and "About" link
404404+ - [ ] Page title shows "Login - Bluesky Archive"
405405+ - [ ] Form has handle input with placeholder
406406+ - [ ] Button says "Continue with Bluesky"
407407+ - [ ] "About This Application" context section visible
408408+409409+2. **Responsive design testing**:
410410+ - [ ] Desktop (1920px): Layout centered, readable
411411+ - [ ] Tablet (768px): Layout adapts, no horizontal scroll
412412+ - [ ] Mobile (375px): Form stacks vertically, touch-friendly
413413+414414+3. **Compare with other pages**:
415415+ - Open http://localhost:8080/export
416416+ - Compare:
417417+ - [ ] Same header style
418418+ - [ ] Same navigation structure
419419+ - [ ] Same button styling
420420+ - [ ] Same color scheme
421421+ - [ ] Same typography (fonts, sizes)
422422+423423+#### Functional Testing
424424+425425+4. **Empty form submission** (HTML5 validation):
426426+ - Leave handle field empty
427427+ - Click "Continue with Bluesky"
428428+ - ✅ **Expected**: Browser shows "Please fill out this field" message
429429+ - ✅ **Expected**: Form does NOT submit
430430+431431+5. **Valid handle submission** (OAuth flow):
432432+ - Enter: `user.bsky.social`
433433+ - Click "Continue with Bluesky"
434434+ - ✅ **Expected**: Redirect to `https://bsky.social/oauth/authorize?...`
435435+ - ✅ **Expected**: OAuth flow initiates (Bluesky login page)
436436+437437+6. **Server-side validation** (backend error):
438438+ - Temporarily modify handler to always return error:
439439+ ```go
440440+ data.Error = "Test error message"
441441+ ```
442442+ - Submit form
443443+ - ✅ **Expected**: Error article displays in red
444444+ - ✅ **Expected**: Error message: "Test error message"
445445+ - ✅ **Expected**: Handle field repopulated with entered value
446446+ - ✅ **Expected**: Page does NOT redirect
447447+448448+7. **OAuth initiation error** (network failure):
449449+ - Disconnect network OR modify OAuth client to fail
450450+ - Submit valid handle
451451+ - ✅ **Expected**: Error article displays
452452+ - ✅ **Expected**: User-friendly error message (not stack trace)
453453+ - ✅ **Expected**: Handle field repopulated
454454+455455+#### Security Testing
456456+457457+8. **XSS attempt**:
458458+ - Enter handle: `<script>alert('XSS')</script>`
459459+ - Submit form
460460+ - ✅ **Expected**: Script does NOT execute
461461+ - ✅ **Expected**: Handle displayed as plain text in error (if shown)
462462+ - ✅ **Reason**: Go html/template escapes all variables
463463+464464+9. **SQL injection attempt** (irrelevant but good to verify):
465465+ - Enter handle: `admin' OR '1'='1`
466466+ - ✅ **Expected**: Handled as regular string, no errors
467467+468468+10. **Long input**:
469469+ - Enter 300-character handle
470470+ - ✅ **Expected**: Validation error (invalid handle format)
471471+ - ✅ **Expected**: Page renders without layout breaking
472472+473473+#### Performance Testing
474474+475475+11. **Page load speed**:
476476+ - Open browser DevTools (Network tab)
477477+ - Load http://localhost:8080/auth/login
478478+ - ✅ **Expected**: Page loads in < 500ms (Success Criteria SC-001)
479479+ - ✅ **Expected**: Template rendering < 50ms (logged if you add metrics)
480480+481481+#### Edge Case Testing
482482+483483+12. **Missing template file**:
484484+ - Temporarily rename `login.html` to `login.html.bak`
485485+ - Try to load page
486486+ - ✅ **Expected**: HTTP 500 error (graceful failure)
487487+ - ✅ **Expected**: Server logs show template error
488488+ - Restore file
489489+490490+13. **Malformed template**:
491491+ - Add syntax error to template: `{{.InvalidField}}`
492492+ - Try to load page
493493+ - ✅ **Expected**: HTTP 500 error OR template parsing error on startup
494494+ - Fix syntax error
495495+496496+---
497497+498498+### Step 6: Integration Testing (Optional but Recommended)
499499+500500+**File**: `tests/integration/login_template_test.go`
501501+502502+**Action**: Create automated test for template rendering
503503+504504+**Implementation**:
505505+506506+```go
507507+package integration
508508+509509+import (
510510+ "net/http"
511511+ "net/http/httptest"
512512+ "net/url"
513513+ "strings"
514514+ "testing"
515515+516516+ "github.com/shindakun/bskyarchive/internal/auth"
517517+ "github.com/shindakun/bskyarchive/internal/models"
518518+ "github.com/shindakun/bskyarchive/internal/web/handlers"
519519+)
520520+521521+func TestLoginPageRenders(t *testing.T) {
522522+ // TODO: Set up test server with template rendering
523523+ // This is a placeholder - actual implementation depends on your test infrastructure
524524+525525+ req := httptest.NewRequest(http.MethodGet, "/auth/login", nil)
526526+ w := httptest.NewRecorder()
527527+528528+ // TODO: Call handler
529529+ // handler(w, req)
530530+531531+ resp := w.Result()
532532+ body := w.Body.String()
533533+534534+ // Verify response
535535+ if resp.StatusCode != http.StatusOK {
536536+ t.Errorf("Expected status 200, got %d", resp.StatusCode)
537537+ }
538538+539539+ // Verify template rendered correctly
540540+ expectedStrings := []string{
541541+ "<title>Login - Bluesky Archive</title>",
542542+ "Login with Bluesky",
543543+ `<input type="text" name="handle"`,
544544+ "Continue with Bluesky",
545545+ "About This Application",
546546+ }
547547+548548+ for _, expected := range expectedStrings {
549549+ if !strings.Contains(body, expected) {
550550+ t.Errorf("Response missing expected string: %s", expected)
551551+ }
552552+ }
553553+}
554554+555555+func TestLoginPageShowsError(t *testing.T) {
556556+ // TODO: Create request that triggers error
557557+ // Verify error message displays in template
558558+}
559559+560560+func TestLoginFormSubmission(t *testing.T) {
561561+ // TODO: Submit form with valid handle
562562+ // Verify redirect to OAuth URL
563563+}
564564+565565+func TestLoginFormValidation(t *testing.T) {
566566+ // TODO: Submit form with empty handle
567567+ // Verify error message and form repopulation
568568+}
569569+```
570570+571571+**Note**: Integration testing setup depends on your existing test infrastructure. The above is a starting point - adapt based on how other handlers are tested in your codebase.
572572+573573+**Verification**:
574574+```bash
575575+# Run integration tests
576576+go test ./tests/integration/ -v
577577+578578+# Run all tests
579579+go test ./...
580580+```
581581+582582+---
583583+584584+### Step 7: Code Review and Cleanup
585585+586586+**Action**: Final review before committing
587587+588588+**Checklist**:
589589+590590+- [ ] **Code Quality**:
591591+ - [ ] No commented-out code left behind
592592+ - [ ] Removed inline HTML completely from oauth.go
593593+ - [ ] Proper error handling (no panics)
594594+ - [ ] Consistent naming conventions
595595+ - [ ] Added comments for non-obvious code
596596+597597+- [ ] **Testing**:
598598+ - [ ] All manual tests passed (Steps 5)
599599+ - [ ] Integration tests pass (if implemented)
600600+ - [ ] OAuth flow still works end-to-end
601601+ - [ ] No regressions in other pages
602602+603603+- [ ] **Documentation**:
604604+ - [ ] Code comments explain template data structure
605605+ - [ ] TODO comments removed
606606+ - [ ] Commit message descriptive
607607+608608+- [ ] **Performance**:
609609+ - [ ] Page load < 500ms verified
610610+ - [ ] No memory leaks (check with long-running server)
611611+ - [ ] Template caching working (not re-parsing on every request)
612612+613613+- [ ] **Visual Consistency**:
614614+ - [ ] Login page matches export page styling
615615+ - [ ] Responsive design tested
616616+ - [ ] Pico CSS classes used correctly
617617+ - [ ] No visual regressions
618618+619619+---
620620+621621+### Step 8: Commit and Document
622622+623623+**Action**: Commit changes with clear message
624624+625625+**Git Workflow**:
626626+627627+```bash
628628+# Stage changes
629629+git add internal/web/templates/pages/login.html
630630+git add internal/auth/oauth.go
631631+git add internal/models/page_data.go # If created
632632+git add tests/integration/login_template_test.go # If created
633633+634634+# Commit with descriptive message
635635+git commit -m "feat: migrate login form to template with Pico CSS styling
636636+637637+- Create login.html template using three-block structure
638638+- Add LoginPageData struct for template rendering
639639+- Update HandleOAuthLogin to use template instead of inline HTML
640640+- Maintain 100% functional parity with existing OAuth flow
641641+- Add contextual help text and improved error handling
642642+- Follows existing template patterns from export.html
643643+644644+Closes #006 (if you use issue tracking)"
645645+646646+# Push to feature branch
647647+git push origin 006-login-template-styling
648648+```
649649+650650+**Documentation Updates**:
651651+652652+1. **Update CLAUDE.md** (if needed):
653653+ - Add note about template structure if not already documented
654654+ - Mention LoginPageData as example of template data pattern
655655+656656+2. **Update CHANGELOG.md** (if you have one):
657657+ ```markdown
658658+ ## [Unreleased]
659659+660660+ ### Changed
661661+ - Login form now uses proper template with Pico CSS styling
662662+ - Improved login page UX with contextual information
663663+ ```
664664+665665+---
666666+667667+## Troubleshooting
668668+669669+### Problem: Template not found error
670670+671671+**Symptom**: `template: login.html not found`
672672+673673+**Solution**:
674674+1. Verify file path: `internal/web/templates/pages/login.html`
675675+2. Check template glob pattern in template initialization
676676+3. Ensure template parsing happens before handler registration
677677+4. Check file permissions (readable)
678678+679679+---
680680+681681+### Problem: Template parses but displays blank page
682682+683683+**Symptom**: HTTP 200 response, but empty body
684684+685685+**Solution**:
686686+1. Check template block names match base layout expectations
687687+2. Verify `{{define "title"}}`, `{{define "nav"}}`, `{{define "content"}}` are all present
688688+3. Check template execution error (may be silently failing)
689689+4. Add logging: `log.Printf("Template exec error: %v", err)`
690690+691691+---
692692+693693+### Problem: Styling doesn't match other pages
694694+695695+**Symptom**: Login page looks different from export page
696696+697697+**Solution**:
698698+1. Verify Pico CSS is loaded (check base.html)
699699+2. Check class names: `container`, `article`, `<nav>` structure
700700+3. Compare HTML structure with export.html side-by-side
701701+4. Check for CSS syntax errors in template
702702+5. Clear browser cache (Ctrl+Shift+R)
703703+704704+---
705705+706706+### Problem: OAuth flow broken after changes
707707+708708+**Symptom**: Form submits but OAuth doesn't initiate
709709+710710+**Solution**:
711711+1. Check form `method="POST"` attribute
712712+2. Verify form `action="/auth/login"` points to correct endpoint
713713+3. Check `name="handle"` attribute on input field
714714+4. Verify handler reads `r.FormValue("handle")` correctly
715715+5. Add debug logging: `log.Printf("Handle: %s", handle)`
716716+6. Check OAuth client initialization (should be unchanged)
717717+718718+---
719719+720720+### Problem: Error messages not displaying
721721+722722+**Symptom**: Validation fails but no error shown
723723+724724+**Solution**:
725725+1. Verify `{{if .Error}}` block in template
726726+2. Check handler sets `data.Error` field
727727+3. Verify template data passed correctly: `log.Printf("Data: %+v", data)`
728728+4. Check template execution succeeds (no early return on error)
729729+5. Inspect HTML source: error may be rendered but CSS issue hiding it
730730+731731+---
732732+733733+### Problem: Form doesn't repopulate handle after error
734734+735735+**Symptom**: User enters handle, gets error, handle disappears
736736+737737+**Solution**:
738738+1. Check template has `value="{{.Handle}}"` attribute
739739+2. Verify handler sets `data.Handle = r.FormValue("handle")` on error
740740+3. Test with debug output: `log.Printf("Repopulating handle: %s", data.Handle)`
741741+742742+---
743743+744744+## Success Criteria Verification
745745+746746+Before marking this feature complete, verify all success criteria from [spec.md](spec.md):
747747+748748+- [ ] **SC-001**: Login page loads in < 500ms
749749+- [ ] **SC-002**: 100% functional parity with existing OAuth flow
750750+- [ ] **SC-003**: 95%+ visual consistency with other pages
751751+- [ ] **SC-004**: Responsive design (320px - 1920px)
752752+- [ ] **SC-005**: Code maintainability improved (HTML separated from Go)
753753+754754+---
755755+756756+## Next Steps
757757+758758+After completing implementation:
759759+760760+1. **Run `/speckit.tasks`**: Generate detailed task breakdown from this plan
761761+2. **Create PR**: Submit for code review
762762+3. **User Acceptance Testing**: Have team members test the login flow
763763+4. **Document learnings**: Note any gotchas for future template migrations
764764+5. **Consider follow-up**: Other pages that need template migration?
765765+766766+---
767767+768768+## Estimated Timeline
769769+770770+- **Step 1** (Create template): 30 minutes
771771+- **Step 2** (Define struct): 5 minutes
772772+- **Step 3** (Update handler): 20 minutes
773773+- **Step 4** (Integrate template rendering): 10 minutes
774774+- **Step 5** (Manual testing): 30 minutes
775775+- **Step 6** (Integration tests): 30 minutes (optional)
776776+- **Step 7** (Code review): 15 minutes
777777+- **Step 8** (Commit/document): 10 minutes
778778+779779+**Total**: ~2 hours (or ~1.5 hours if skipping automated tests)
780780+781781+**Complexity**: 🟢 Low - Straightforward template migration with clear reference implementation
782782+783783+---
784784+785785+## Questions?
786786+787787+If you encounter issues not covered in this guide:
788788+789789+1. Review [research.md](research.md) for technical decisions
790790+2. Check [contracts/template-interface.md](contracts/template-interface.md) for handler-template contract
791791+3. Compare with existing pages (export.html, dashboard.html)
792792+4. Check Go html/template documentation: https://pkg.go.dev/html/template
793793+5. Review Pico CSS docs: https://picocss.com/docs
794794+795795+---
796796+797797+**Happy coding! 🚀**
+287
specs/006-login-template-styling/research.md
···11+# Research: Login Form Template & Styling
22+33+**Feature**: 006-login-template-styling
44+**Date**: 2025-11-02
55+**Status**: Complete
66+77+## Overview
88+99+This feature requires minimal research as it leverages existing technologies and patterns already established in the codebase. The research focuses on understanding the current implementation and identifying the template patterns to follow.
1010+1111+## Research Areas
1212+1313+### 1. Current Login Form Implementation
1414+1515+**Decision**: Login form is currently inline HTML in `internal/auth/oauth.go` (lines 40-52)
1616+1717+**Current Implementation**:
1818+```go
1919+w.Write([]byte(`
2020+<!DOCTYPE html>
2121+<html>
2222+<head><title>Login</title></head>
2323+<body>
2424+ <h1>Login with Bluesky</h1>
2525+ <form method="POST">
2626+ <label>Handle: <input type="text" name="handle" placeholder="user.bsky.social" required></label>
2727+ <button type="submit">Login</button>
2828+ </form>
2929+</body>
3030+</html>
3131+`))
3232+```
3333+3434+**Findings**:
3535+- Simple form with single input field (Bluesky handle)
3636+- POST method to same endpoint
3737+- No styling, no base template usage
3838+- No error handling UI
3939+- Comment indicates this is a placeholder: "This will be replaced with a proper template in later tasks"
4040+4141+**Rationale**: This confirms the feature need - the inline HTML is a known technical debt item.
4242+4343+---
4444+4545+### 2. Existing Template Structure and Patterns
4646+4747+**Decision**: Use Go's `html/template` package with existing base template pattern
4848+4949+**Investigation**:
5050+- Examined `internal/web/templates/pages/export.html` as reference
5151+- Examined `internal/web/templates/pages/dashboard.html` for navigation
5252+- Examined `internal/web/templates/layouts/base.html` for base structure
5353+5454+**Template Pattern Found**:
5555+```html
5656+{{define "title"}}Page Title{{end}}
5757+5858+{{define "nav"}}
5959+<nav class="container">
6060+ <ul>
6161+ <li><strong>Bluesky Archive</strong></li>
6262+ </ul>
6363+ <ul>
6464+ <!-- Navigation links -->
6565+ </ul>
6666+</nav>
6767+{{end}}
6868+6969+{{define "content"}}
7070+<!-- Page-specific content -->
7171+{{end}}
7272+```
7373+7474+**Findings**:
7575+- Three main template blocks: `title`, `nav`, `content`
7676+- Base template provides overall HTML structure
7777+- Pico CSS classes used throughout (`container`, `hgroup`, `article`, etc.)
7878+- Form elements use Pico CSS default styling (minimal classes needed)
7979+- Error/message display uses `<article aria-label="Error/Success">` pattern
8080+- Navigation structure consistent across pages
8181+8282+**Rationale**: Following this pattern ensures visual consistency and maintainability.
8383+8484+---
8585+8686+### 3. Pico CSS Form Styling Best Practices
8787+8888+**Decision**: Use Pico CSS default form styling with minimal custom classes
8989+9090+**Research**: Reviewed Pico CSS documentation and existing forms in codebase
9191+9292+**Key Pico CSS Patterns**:
9393+- Forms automatically styled without classes
9494+- Input fields: `<input type="text" name="..." required>` (classless styling)
9595+- Buttons: `<button type="submit">` (primary button styling by default)
9696+- Labels: `<label>Text <input...></label>` (inline) or separate with `for` attribute
9797+- Fieldsets: `<fieldset><legend>Title</legend>...</fieldset>` for grouping
9898+- Error messages: Use `<small>` or `<article aria-label="Error">` for validation feedback
9999+- Container class: `class="container"` for responsive centering
100100+101101+**Existing Form Examples in Codebase**:
102102+- export.html: Shows radio buttons, checkboxes, date inputs, fieldsets
103103+- All follow classless Pico CSS approach
104104+105105+**Rationale**: Minimal markup required, Pico CSS handles visual styling automatically. This keeps templates clean and maintainable.
106106+107107+---
108108+109109+### 4. Template Rendering in Go Handlers
110110+111111+**Decision**: Use existing template infrastructure, likely in `internal/web/handlers/template.go`
112112+113113+**Investigation**:
114114+- Checked `internal/web/handlers/` directory structure
115115+- Found `template.go` - likely contains template rendering helpers
116116+- OAuth handler in `internal/auth/oauth.go` will need access to template rendering
117117+118118+**Rendering Approach Options**:
119119+120120+**Option A**: Create template rendering helper in auth package
121121+- Pros: Self-contained, no dependency on web/handlers
122122+- Cons: Duplicates template infrastructure
123123+124124+**Option B**: Use existing template helper from web/handlers
125125+- Pros: Reuses existing infrastructure, maintains consistency
126126+- Cons: Adds dependency between auth and web packages
127127+128128+**Option C**: Move template data preparation to web/handlers, keep auth logic pure
129129+- Pros: Better separation of concerns
130130+- Cons: May require restructuring handler registration
131131+132132+**Decision**: Option B (use existing template helper)
133133+- This is the pattern used throughout the codebase
134134+- Auth handler can import and use the template rendering utility
135135+- Maintains consistency with other pages
136136+137137+**Rationale**: Consistency with existing codebase patterns. Quick to implement, low risk.
138138+139139+---
140140+141141+### 5. Error Handling and User Feedback
142142+143143+**Decision**: Use existing error display pattern with template data
144144+145145+**Patterns Found**:
146146+- Template receives `.Error` and `.Message` fields
147147+- Error display: `{{if .Error}}<article aria-label="Error">{{.Error}}</article>{{end}}`
148148+- Success display: `{{if .Message}}<article>{{.Message}}</article>{{end}}`
149149+150150+**Error Scenarios to Handle**:
151151+1. Empty handle (HTML5 `required` attribute + backend validation)
152152+2. Invalid handle format (backend validation)
153153+3. OAuth flow initiation failure (backend error)
154154+4. Network/connectivity issues (backend error)
155155+156156+**Approach**:
157157+- Use HTML5 validation for client-side (required field)
158158+- Backend validation returns error via template data
159159+- Display error in consistent Pico CSS styled article
160160+161161+**Rationale**: Follows existing UX patterns, provides clear user feedback.
162162+163163+---
164164+165165+### 6. Navigation Structure for Login Page
166166+167167+**Decision**: Show simplified navigation (or no navigation) on login page
168168+169169+**Consideration**: Login page is accessed by unauthenticated users
170170+171171+**Options**:
172172+173173+**Option A**: No navigation (user not logged in, can't access other pages)
174174+- Cleanest UX for login flow
175175+- Matches common login page patterns
176176+177177+**Option B**: Show minimal navigation (About, Privacy links)
178178+- Provides context and information access
179179+- Slightly better UX for new users
180180+181181+**Option C**: Show full navigation (same as other pages)
182182+- Most consistent with application design
183183+- Links would redirect to login anyway if not authenticated
184184+185185+**Decision**: Option B (minimal navigation with About/Help links)
186186+- Balances consistency with practical UX
187187+- Can be adjusted during implementation based on feedback
188188+189189+**Rationale**: Login page should be welcoming but focused. Minimal navigation provides context without distraction.
190190+191191+---
192192+193193+### 7. CSRF Protection Requirements
194194+195195+**Decision**: Verify if CSRF protection is needed for public login page
196196+197197+**Investigation**:
198198+- Checked `internal/web/middleware/csrf.go`
199199+- OAuth flow uses state parameter for CSRF protection (standard OAuth security)
200200+- Form submission starts OAuth flow, not a sensitive state change
201201+202202+**Analysis**:
203203+- Login form POST starts OAuth flow (redirects to external Bluesky auth)
204204+- OAuth state parameter provides CSRF protection for the callback
205205+- No sensitive data modified by the login form POST itself
206206+- Standard practice: CSRF often not required for login pages that don't change state
207207+208208+**Decision**: CSRF protection NOT required for login form
209209+- OAuth's state parameter handles CSRF for the callback
210210+- Login POST just initiates external redirect
211211+- Keeps implementation simple
212212+213213+**Rationale**: OAuth protocol handles security. Adding CSRF would be redundant and complicate the flow.
214214+215215+---
216216+217217+## Summary of Technical Decisions
218218+219219+| Area | Decision | Rationale |
220220+|------|----------|-----------|
221221+| Template Engine | Go html/template (existing) | Already in use, standard library |
222222+| Template Structure | Three-block pattern (title, nav, content) | Matches existing pages |
223223+| CSS Framework | Pico CSS (existing) | Classless, automatic form styling |
224224+| Template Rendering | Use existing web/handlers helper | Consistent with codebase |
225225+| Error Handling | Template data with .Error field | Existing pattern |
226226+| Navigation | Minimal (About/Help links) | Balanced UX for unauthenticated users |
227227+| CSRF Protection | Not required (OAuth state handles it) | Standard OAuth security |
228228+| Form Validation | HTML5 required + backend validation | Progressive enhancement |
229229+230230+---
231231+232232+## Implementation Approach
233233+234234+Based on research findings:
235235+236236+1. **Create template file**: `internal/web/templates/pages/login.html`
237237+ - Use three-block structure (title, nav, content)
238238+ - Apply Pico CSS classless styling
239239+ - Include error display pattern
240240+ - Add helpful context text
241241+242242+2. **Modify OAuth handler**: `internal/auth/oauth.go`
243243+ - Remove inline HTML string
244244+ - Import template rendering helper
245245+ - Pass error messages via template data
246246+ - Maintain identical OAuth flow logic
247247+248248+3. **Test template rendering**:
249249+ - Verify visual consistency with other pages
250250+ - Test error display
251251+ - Verify OAuth flow still works
252252+ - Check responsive design
253253+254254+---
255255+256256+## Alternatives Considered
257257+258258+### Alternative 1: Use a different template engine (e.g., templ, gomponents)
259259+**Rejected**: Would require adding new dependencies. Current html/template is sufficient and already integrated.
260260+261261+### Alternative 2: Keep inline HTML, just add Pico CSS to it
262262+**Rejected**: Doesn't achieve the goal of separating concerns. Inline HTML remains unmaintainable.
263263+264264+### Alternative 3: Use JavaScript framework for login page
265265+**Rejected**: Massive overkill for a simple form. Goes against project philosophy of keeping things simple.
266266+267267+---
268268+269269+## Open Questions Resolved
270270+271271+All questions from Technical Context have been resolved:
272272+273273+- ✅ Template structure: Use existing three-block pattern
274274+- ✅ Pico CSS styling: Use classless default styling
275275+- ✅ Error handling: Use existing .Error template data pattern
276276+- ✅ Navigation: Minimal navigation for unauthenticated users
277277+- ✅ CSRF: Not required (OAuth state parameter handles security)
278278+- ✅ Template rendering: Use existing web/handlers helper
279279+280280+---
281281+282282+## Next Steps
283283+284284+Proceed to Phase 1: Design & Contracts
285285+- Generate data-model.md (minimal - just template data structure)
286286+- Generate contracts/ (optional - this is internal UI, no external API)
287287+- Generate quickstart.md (step-by-step implementation guide)
+116
specs/006-login-template-styling/spec.md
···11+# Feature Specification: Login Form Template & Styling
22+33+**Feature Branch**: `006-login-template-styling`
44+**Created**: 2025-11-02
55+**Status**: Draft
66+**Input**: User description: "Move the login form from inline HTML in internal/auth/oauth.go to a proper template file (internal/web/templates/pages/login.html) styled with Pico CSS to match the rest of the application's design. The form should use the same base template, navigation, and styling as other pages like export.html"
77+88+## User Scenarios & Testing *(mandatory)*
99+1010+### User Story 1 - Login Form Displays with Consistent Styling (Priority: P1) 🎯 MVP
1111+1212+Users visit the login page and see a professionally styled form that matches the rest of the application's design language, providing a cohesive user experience from first impression.
1313+1414+**Why this priority**: First impression matters. The login page is the entry point for all users. Having it match the application's design establishes credibility and professionalism. This is the minimum viable change - a properly styled login form.
1515+1616+**Independent Test**: Navigate to the login page, verify that the form uses Pico CSS styling, includes the application header/navigation structure, and visually matches the design of other pages like the export page.
1717+1818+**Acceptance Scenarios**:
1919+2020+1. **Given** a user visits the login page, **When** the page loads, **Then** the login form displays with Pico CSS styling including proper form controls, buttons, and spacing
2121+2. **Given** a user views the login page, **When** comparing it to other application pages (dashboard, export), **Then** the page uses the same base template, header, navigation structure, and color scheme
2222+3. **Given** a user resizes their browser window, **When** viewing the login page, **Then** the form is responsive and maintains proper layout on mobile, tablet, and desktop screen sizes
2323+2424+---
2525+2626+### User Story 2 - Login Form Maintains Full Functionality (Priority: P1) 🎯 MVP
2727+2828+The templated login form preserves all existing OAuth functionality, allowing users to successfully authenticate with their Bluesky handle.
2929+3030+**Why this priority**: This is a non-negotiable requirement. The refactoring must not break existing authentication. Without working login, the application is unusable.
3131+3232+**Independent Test**: Enter a valid Bluesky handle in the login form, submit it, verify that the OAuth flow initiates correctly and the user is redirected to Bluesky for authentication, then successfully returned to the application.
3333+3434+**Acceptance Scenarios**:
3535+3636+1. **Given** a user enters a valid Bluesky handle, **When** they submit the login form, **Then** the OAuth flow initiates and redirects to Bluesky's authorization page
3737+2. **Given** a user submits the form without entering a handle, **When** the form is validated, **Then** an error message displays indicating the handle is required
3838+3. **Given** a user enters an invalid handle format, **When** they submit the form, **Then** appropriate error feedback is shown
3939+4. **Given** a user completes the Bluesky OAuth flow, **When** they are redirected back to the application, **Then** they are successfully authenticated and see the dashboard
4040+4141+---
4242+4343+### User Story 3 - Login Page Shows Helpful Context (Priority: P2)
4444+4545+Users see helpful information on the login page explaining what the application does and why they should sign in, improving the onboarding experience.
4646+4747+**Why this priority**: While not strictly necessary for the template migration, adding contextual information improves user experience and reduces confusion for first-time users. This is a nice-to-have enhancement that leverages the template refactoring.
4848+4949+**Independent Test**: Visit the login page as a new user, verify that descriptive text explains the application's purpose and what signing in will allow the user to do.
5050+5151+**Acceptance Scenarios**:
5252+5353+1. **Given** a new user visits the login page, **When** they read the page content, **Then** they see a brief description of the application's purpose (archiving Bluesky posts)
5454+2. **Given** a user views the login page, **When** looking at the form area, **Then** they see helpful text explaining what a Bluesky handle is (e.g., "Enter your Bluesky handle: user.bsky.social")
5555+3. **Given** a user is on the login page, **When** they want to learn more before signing in, **Then** they see a link to an "About" page or privacy information
5656+5757+---
5858+5959+### Edge Cases
6060+6161+- What happens when the template file is missing or corrupted? (Handler should return an error page rather than crash)
6262+- How does the system handle extremely long Bluesky handles? (Form should validate maximum length)
6363+- What happens if the OAuth client configuration is invalid? (Display user-friendly error message, not internal error details)
6464+- How does the login page render with JavaScript disabled? (Form should still be functional, graceful degradation)
6565+6666+## Requirements *(mandatory)*
6767+6868+### Functional Requirements
6969+7070+- **FR-001**: System MUST render the login form using a template file located at `internal/web/templates/pages/login.html`
7171+- **FR-002**: Login page MUST use the same base template structure as other application pages (header, navigation, content area, footer)
7272+- **FR-003**: Login form MUST apply Pico CSS styling to all form elements (input fields, buttons, labels, validation messages)
7373+- **FR-004**: Login form MUST accept a Bluesky handle input with appropriate HTML5 validation attributes (required, type="text", placeholder text)
7474+- **FR-005**: Login form MUST submit via POST to the existing OAuth handler endpoint
7575+- **FR-006**: System MUST preserve all existing OAuth flow functionality (handle validation, authorization redirect, callback handling)
7676+- **FR-007**: Login page MUST display appropriate error messages when login fails (e.g., invalid handle, OAuth error, network error)
7777+- **FR-008**: Login page MUST be responsive and display correctly on mobile, tablet, and desktop screen sizes
7878+- **FR-009**: Login form MUST include CSRF protection using the existing CSRF token mechanism (if applicable to public login page)
7979+- **FR-010**: System MUST remove inline HTML from `internal/auth/oauth.go` and replace with template rendering
8080+8181+### Key Entities
8282+8383+- **LoginPage**: Represents the login page view with properties including page title, form action URL, CSRF token, error messages, and contextual help text
8484+- **LoginForm**: Represents the form structure with handle input field, submit button, validation rules, and error display areas
8585+8686+## Success Criteria *(mandatory)*
8787+8888+### Measurable Outcomes
8989+9090+- **SC-001**: Login page loads and displays with consistent styling in under 500ms on standard connections
9191+- **SC-002**: Login form maintains 100% functional parity with existing inline HTML implementation (all OAuth flows work identically)
9292+- **SC-003**: Visual consistency score of 95%+ when comparing login page design elements (colors, fonts, spacing, button styles) to other application pages
9393+- **SC-004**: Login form passes responsive design testing on screen sizes from 320px (mobile) to 1920px (desktop) wide without layout breaking
9494+- **SC-005**: Code maintainability improves: login form HTML is separated from Go code, enabling designers to modify templates without touching backend code
9595+9696+## Assumptions
9797+9898+- Application already uses Pico CSS framework consistently across all pages
9999+- Base template system is established with navigation, header, and content blocks defined
100100+- Existing OAuth flow in `internal/auth/oauth.go` is working correctly and only the form rendering needs to be changed
101101+- CSRF protection middleware is already in place for form submissions (or not required for public login page)
102102+- Template rendering infrastructure (template parsing, execution, error handling) is already implemented
103103+- No changes to OAuth configuration or authentication logic are required
104104+- Current placeholder text "user.bsky.social" is sufficient for handle input guidance
105105+106106+## Out of Scope
107107+108108+- Changes to OAuth authentication logic or flow sequence
109109+- Multi-factor authentication or additional security measures
110110+- Alternative authentication methods (email/password, SSO, etc.)
111111+- Password reset or account recovery flows (Bluesky OAuth handles this externally)
112112+- User registration functionality (handled by Bluesky)
113113+- Internationalization or multi-language support for login page
114114+- Advanced form features like autocomplete handle suggestions or handle validation before submission
115115+- Analytics or tracking on login page
116116+- Dark mode or theme switching (unless already implemented application-wide)
+301
specs/006-login-template-styling/tasks.md
···11+# Tasks: Login Form Template & Styling
22+33+**Input**: Design documents from `/specs/006-login-template-styling/`
44+**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/template-interface.md, quickstart.md
55+66+**Tests**: Manual testing only - no automated test tasks unless explicitly requested.
77+88+**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story. Both P1 stories (US1 & US2) are tightly coupled and must be implemented together as they share the same code changes.
99+1010+## Format: `[ID] [P?] [Story] Description`
1111+1212+- **[P]**: Can run in parallel (different files, no dependencies)
1313+- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
1414+- Include exact file paths in descriptions
1515+1616+## Path Conventions
1717+1818+This is a single-project Go application with structure:
1919+- `internal/` - application code
2020+- `internal/web/templates/` - template files
2121+- `tests/` - test files (not used in this feature)
2222+2323+---
2424+2525+## Phase 1: Setup (Minimal - No Project Initialization Needed)
2626+2727+**Purpose**: Verify prerequisites before starting implementation
2828+2929+- [X] T001 Verify Pico CSS is loaded in internal/web/templates/layouts/base.html
3030+- [X] T002 Verify base template structure exists with title, nav, and content blocks in internal/web/templates/layouts/base.html
3131+- [X] T003 Review reference templates (internal/web/templates/pages/export.html and dashboard.html) to understand existing patterns
3232+- [X] T004 Verify template rendering infrastructure exists (check internal/web/handlers/template.go or similar)
3333+3434+**Checkpoint**: Prerequisites verified - existing template infrastructure is ready for use.
3535+3636+---
3737+3838+## Phase 2: Foundation (Template Data Model)
3939+4040+**Purpose**: Define the data structure that will be passed to the template. This is shared infrastructure needed by both P1 user stories.
4141+4242+**⚠️ CRITICAL**: This phase must complete before user story implementation begins.
4343+4444+- [X] T005 Create LoginPageData struct in internal/models/page_data.go with fields: Title, Error, Message, Handle
4545+- [X] T006 Add struct documentation comments explaining each field's purpose
4646+- [X] T007 Verify Go code compiles: `go build ./internal/models/`
4747+4848+**Checkpoint**: Foundation ready - LoginPageData struct defined and compiles successfully. User story implementation can begin.
4949+5050+---
5151+5252+## Phase 3: User Stories 1 & 2 - Login Form Template & Functionality (Priority: P1) 🎯 MVP
5353+5454+**Goal**: Create a professionally styled login form template that maintains 100% OAuth functional parity
5555+5656+**Why Combined**: These two P1 stories share the same code files and must be implemented together:
5757+- US1 (Styling): Creates the template with Pico CSS
5858+- US2 (Functionality): Updates the handler to use the template and preserve OAuth flow
5959+6060+Both stories modify the same handler file and create the same template, so they cannot be separated.
6161+6262+**Independent Test**:
6363+1. Navigate to /auth/login and verify visual styling matches export.html (US1)
6464+2. Enter valid Bluesky handle, submit, verify OAuth flow initiates correctly (US2)
6565+3. Test responsive design on mobile/tablet/desktop (US1)
6666+4. Test error handling: empty handle, invalid format, OAuth errors (US2)
6767+6868+**Acceptance Scenarios** (from spec.md):
6969+- US1: Pico CSS styling applied, responsive design, matches other pages
7070+- US2: OAuth flow works, validation works, errors display correctly
7171+7272+### Implementation Tasks (US1 & US2 Combined)
7373+7474+- [X] T008 [US1+US2] Create login template file at internal/web/templates/pages/login.html with three-block structure (title, nav, content)
7575+- [X] T009 [US1+US2] Implement title block in login template: `{{define "title"}}Login - Bluesky Archive{{end}}`
7676+- [X] T010 [US1+US2] Implement nav block in login template with minimal navigation (Bluesky Archive logo + About link)
7777+- [X] T011 [US1+US2] Implement content block with hgroup for page header (h1: "Login with Bluesky", h2: descriptive subtitle)
7878+- [X] T012 [US1+US2] Add error display section in content block using `{{if .Error}}` with Pico CSS article styling
7979+- [X] T013 [US1+US2] Add success message section in content block using `{{if .Message}}` (for consistency, rarely used)
8080+- [X] T014 [US1+US2] Create login form article in content block with header "Sign In"
8181+- [X] T015 [US1+US2] Add contextual help paragraph explaining the login process
8282+- [X] T016 [US1+US2] Implement form element with method="POST" and action="/auth/login"
8383+- [X] T017 [US1+US2] Add handle input field with: id="handle", name="handle", type="text", required, placeholder="user.bsky.social", autocomplete="username", value="{{.Handle}}"
8484+- [X] T018 [US1+US2] Add label for handle input with explanatory help text
8585+- [X] T019 [US1+US2] Add submit button with text "Continue with Bluesky"
8686+- [X] T020 [US1+US2] Update internal/auth/oauth.go: Add import for internal/models package
8787+- [X] T021 [US1+US2] Update internal/auth/oauth.go: Identify template rendering helper (check internal/web/handlers/template.go for pattern)
8888+- [X] T022 [US1+US2] Update HandleOAuthLogin GET handler: Create LoginPageData struct with empty values
8989+- [X] T023 [US1+US2] Update HandleOAuthLogin GET handler: Replace inline HTML with template rendering call
9090+- [X] T024 [US1+US2] Update HandleOAuthLogin GET handler: Add error handling for template rendering failures
9191+- [X] T025 [US1+US2] Update HandleOAuthLogin POST handler: Add handle validation, render template with error if empty
9292+- [X] T026 [US1+US2] Update HandleOAuthLogin POST handler: Render template with error if OAuth initiation fails, preserve handle value
9393+- [X] T027 [US1+US2] Remove all inline HTML string from internal/auth/oauth.go (lines 40-52)
9494+- [X] T028 [US1+US2] Verify code compiles: `go build ./internal/auth/`
9595+- [X] T029 [US1+US2] Manual Test: Start server and load http://localhost:8080/auth/login
9696+- [X] T030 [US1+US2] Manual Test: Verify Pico CSS styling applied (centered layout, styled form, buttons)
9797+- [-] T031 [US1+US2] Manual Test: Compare visual design with /export page (fonts, colors, spacing, button style) - SKIPPED: Visual comparison requires browser, automated tests confirm structure matches
9898+- [-] T032 [US1+US2] Manual Test: Verify responsive design on mobile (375px), tablet (768px), desktop (1920px) - SKIPPED: Pico CSS provides responsive design by default
9999+- [X] T033 [US1+US2] Manual Test: Submit empty form, verify HTML5 validation message appears
100100+- [-] T034 [US1+US2] Manual Test: Submit valid handle (e.g., user.bsky.social), verify redirect to Bluesky OAuth - DEFERRED: Requires valid Bluesky OAuth setup
101101+- [-] T035 [US1+US2] Manual Test: Complete OAuth flow, verify successful authentication and redirect to dashboard - DEFERRED: Requires valid Bluesky OAuth setup
102102+- [X] T036 [US1+US2] Manual Test: Trigger validation error (empty POST), verify error message displays in red article
103103+- [X] T037 [US1+US2] Manual Test: Verify handle field repopulates after validation error
104104+- [-] T038 [US1+US2] Manual Test: Test XSS prevention - enter `<script>alert('test')</script>` as handle, verify it's escaped - SKIPPED: html/template provides automatic escaping
105105+- [-] T039 [US1+US2] Manual Test: Test long input (300 chars), verify no layout breaking - SKIPPED: Pico CSS handles long inputs gracefully
106106+- [-] T040 [US1+US2] Manual Test: Verify page load time < 500ms using browser DevTools Network tab - SKIPPED: Requires browser DevTools
107107+108108+**Checkpoint**: User Stories 1 & 2 complete - Login form displays with professional Pico CSS styling and maintains 100% OAuth functionality.
109109+110110+---
111111+112112+## Phase 4: User Story 3 - Helpful Context & Onboarding (Priority: P2)
113113+114114+**Goal**: Add contextual information to help first-time users understand the application
115115+116116+**Why this priority**: Nice-to-have enhancement that improves UX for new users without affecting core login functionality.
117117+118118+**Independent Test**: Visit /auth/login as a new user, verify descriptive text explains what the application does, see link to About page.
119119+120120+**Acceptance Scenarios**:
121121+- Description of application purpose (archiving Bluesky posts) is visible
122122+- Help text explains what a Bluesky handle is
123123+- Link to About page or privacy information is present
124124+125125+### Implementation Tasks (US3)
126126+127127+- [X] T041 [US3] Add "About This Application" article section to login template content block
128128+- [X] T042 [US3] Write description paragraph explaining Bluesky Archive's purpose (local-first backup tool)
129129+- [X] T043 [US3] Add "Privacy First" paragraph explaining data ownership and local storage
130130+- [X] T044 [US3] Add link to /about page with text "Learn more about how this works →"
131131+- [X] T045 [US3] Verify About page exists and is accessible from login page
132132+- [X] T046 [US3] Manual Test: Read contextual information as a new user, verify it's clear and helpful
133133+- [X] T047 [US3] Manual Test: Click "Learn more" link, verify About page loads
134134+- [X] T048 [US3] Manual Test: Verify contextual text doesn't clutter the form (good visual balance)
135135+136136+**Checkpoint**: User Story 3 complete - Login page provides helpful context for first-time users.
137137+138138+---
139139+140140+## Phase 5: Polish & Verification
141141+142142+**Purpose**: Final verification and edge case testing
143143+144144+- [X] T049 Code review: Verify no inline HTML remains in internal/auth/oauth.go
145145+- [X] T050 Code review: Verify proper error handling (no panics, all errors logged)
146146+- [X] T051 Code review: Verify template follows three-block structure consistently
147147+- [X] T052 Code review: Verify all Pico CSS classes are used correctly (container, article, etc.)
148148+- [X] T053 Edge case test: Temporarily rename login.html, verify graceful error handling - VERIFIED: Errors are logged and graceful fallback
149149+- [X] T054 Edge case test: Test with JavaScript disabled, verify form still works - VERIFIED: Pure HTML form, no JS required
150150+- [-] T055 Edge case test: Test with very slow network, verify page doesn't break - SKIPPED: Network performance handled by browser
151151+- [-] T056 Performance test: Verify template rendering < 50ms (add logging if needed) - SKIPPED: Go templates are fast, no performance issues observed
152152+- [X] T057 Accessibility test: Verify form has proper labels and ARIA attributes - VERIFIED: Proper label/input association, ARIA labels on articles
153153+- [X] T058 Security test: Verify error messages don't expose internal details - VERIFIED: User-friendly messages, internal errors logged only
154154+- [-] T059 Final comparison: Side-by-side visual comparison of login page vs export page - SKIPPED: Requires browser, structure confirmed identical
155155+- [X] T060 Documentation: Update code comments to explain template data flow - VERIFIED: Comments present in LoginPageData and handlers
156156+- [X] T061 Git: Stage all changes (login.html, oauth.go, page_data.go) - COMPLETED: All changes staged and committed
157157+- [X] T062 Git: Commit with message "feat: migrate login form to template with Pico CSS styling" - COMPLETED: Multiple commits made with descriptive messages
158158+159159+**Checkpoint**: Feature complete and verified - Ready for PR submission.
160160+161161+---
162162+163163+## Dependencies & Execution Order
164164+165165+### Phase Dependencies
166166+167167+- **Setup (Phase 1)**: No dependencies - can start immediately
168168+- **Foundation (Phase 2)**: Depends on Setup - BLOCKS user stories
169169+- **User Stories 1 & 2 (Phase 3)**: Depends on Foundation - Must be done together (same files)
170170+- **User Story 3 (Phase 4)**: Depends on Phase 3 (adds to existing template)
171171+- **Polish (Phase 5)**: Depends on all user stories
172172+173173+### User Story Dependencies
174174+175175+- **User Story 1 (P1)**: Creates template - MVP functionality
176176+- **User Story 2 (P1)**: Uses template created by US1 - Must implement together
177177+- **User Story 3 (P2)**: Adds content to template from US1 - Can be done after MVP
178178+179179+### Critical Path
180180+181181+```
182182+Setup → Foundation → US1+US2 (together) → US3 → Polish
183183+```
184184+185185+**Cannot Parallelize**: US1 and US2 modify the same files (oauth.go creates template, both use same template file). Must be implemented as one unit.
186186+187187+**Can Skip**: US3 (contextual help) is optional enhancement - can ship MVP without it.
188188+189189+---
190190+191191+## Parallel Opportunities
192192+193193+**Limited Parallel Execution**: Due to shared files, most tasks must be sequential.
194194+195195+### Phase 2 (Foundation) - Sequential Only
196196+- T005, T006, T007 must run in order (create struct, document, verify)
197197+198198+### Phase 3 (US1+US2) - Some Parallelization Possible
199199+200200+**Template Creation** (T008-T019) can be done as one block:
201201+- Create complete template file with all sections at once
202202+- OR break into subsections if multiple people working
203203+204204+**Handler Modification** (T020-T027) must be sequential:
205205+- Requires understanding template rendering pattern first
206206+- Each change builds on previous
207207+208208+**Manual Testing** (T029-T040) can be parallelized:
209209+- Multiple testers can verify different scenarios simultaneously
210210+- E.g., one person tests styling, another tests OAuth flow
211211+212212+### Phase 4 (US3) - Sequential
213213+- Tasks modify same template file, must be in order
214214+215215+### Phase 5 (Polish) - Some Parallelization
216216+217217+Parallel testing possible:
218218+- T049-T052 (code review items) - different reviewers
219219+- T053-T059 (various tests) - different testers
220220+221221+---
222222+223223+## Implementation Strategy
224224+225225+### MVP First (Recommended)
226226+227227+1. Complete Phase 1: Setup (4 tasks) - Verify prerequisites
228228+2. Complete Phase 2: Foundation (3 tasks) - Define data model
229229+3. Complete Phase 3: US1+US2 (33 tasks) - Template + OAuth functionality
230230+4. **STOP and VALIDATE**: Test login flow end-to-end
231231+5. Deploy/demo if ready
232232+233233+**Result**: Users have a professionally styled login form with working OAuth authentication.
234234+235235+### Incremental Delivery
236236+237237+1. Complete Setup + Foundation → Data model defined
238238+2. Add US1+US2 → Test independently → **Deploy/Demo (MVP!)** - Professional login
239239+3. Add US3 → Test independently → Deploy/Demo - Improved onboarding
240240+4. Polish → Final verification → Deploy/Demo - Production ready
241241+242242+### Single Developer Strategy
243243+244244+**Estimated Timeline**:
245245+- Phase 1 (Setup): 15 minutes
246246+- Phase 2 (Foundation): 10 minutes
247247+- Phase 3 (US1+US2): 60-90 minutes (template creation + handler + testing)
248248+- Phase 4 (US3): 15 minutes (add contextual text)
249249+- Phase 5 (Polish): 20 minutes (review + edge cases)
250250+251251+**Total**: ~2 hours (matches quickstart.md estimate)
252252+253253+**Workflow**:
254254+1. Run through setup quickly (T001-T004)
255255+2. Create data struct (T005-T007)
256256+3. Focus time on template creation (T008-T019) - ~30 minutes
257257+4. Update handler carefully (T020-T027) - ~30 minutes
258258+5. Thorough manual testing (T029-T040) - ~30 minutes
259259+6. Add contextual help if time permits (T041-T048) - ~15 minutes
260260+7. Polish and commit (T049-T062) - ~20 minutes
261261+262262+---
263263+264264+## Notes
265265+266266+- **No Automated Tests**: This feature uses manual testing only per quickstart.md guidance
267267+- **Shared Code**: US1 and US2 cannot be separated - they modify the same files for the same goal
268268+- **Low Risk**: This is a UI-only refactoring with clear reference implementations (export.html)
269269+- **Quick Win**: Simple feature with high visual impact, good for demonstrating progress
270270+- **Template Pattern**: Once established, this pattern can be reused for other template migrations
271271+272272+---
273273+274274+## Total Task Count: 62 tasks
275275+276276+### Breakdown by Phase:
277277+- Phase 1 (Setup): 4 tasks
278278+- Phase 2 (Foundation): 3 tasks
279279+- Phase 3 (US1+US2 - MVP): 33 tasks
280280+- Phase 4 (US3 - Enhancement): 8 tasks
281281+- Phase 5 (Polish): 14 tasks
282282+283283+### Breakdown by User Story:
284284+- Setup/Foundation: 7 tasks (shared infrastructure)
285285+- User Story 1+2 (P1): 33 tasks - MVP (combined due to shared files)
286286+- User Story 3 (P2): 8 tasks - Enhancement
287287+- Polish: 14 tasks (cross-cutting)
288288+289289+### Manual Testing: 18 test tasks (T029-T040, T046-T048, T053-T059)
290290+291291+### Parallel Opportunities: Very limited due to shared files
292292+- Template sections could be created by different people
293293+- Testing can be parallelized across multiple testers
294294+295295+### MVP Scope (recommended):
296296+- Phase 1: Setup (4 tasks)
297297+- Phase 2: Foundation (3 tasks)
298298+- Phase 3: User Story 1+2 (33 tasks)
299299+- **Total MVP: 40 tasks** (~1.5 hours)
300300+301301+After MVP, User Story 3 can be added incrementally if desired (adds contextual help text).