A quick vibecoded webapp on exe.dev that I liked enough to save the source for.
at main 413 lines 11 kB view raw
1package srv 2 3import ( 4 "database/sql" 5 "fmt" 6 "html/template" 7 "log/slog" 8 "net/http" 9 "net/url" 10 "path/filepath" 11 "runtime" 12 "sort" 13 "strconv" 14 "strings" 15 "time" 16 17 "srv.exe.dev/db" 18 "srv.exe.dev/db/dbgen" 19) 20 21type Server struct { 22 DB *sql.DB 23 Hostname string 24 TemplatesDir string 25 StaticDir string 26} 27 28func New(dbPath, hostname string) (*Server, error) { 29 _, thisFile, _, _ := runtime.Caller(0) 30 baseDir := filepath.Dir(thisFile) 31 srv := &Server{ 32 Hostname: hostname, 33 TemplatesDir: filepath.Join(baseDir, "templates"), 34 StaticDir: filepath.Join(baseDir, "static"), 35 } 36 if err := srv.setUpDatabase(dbPath); err != nil { 37 return nil, err 38 } 39 return srv, nil 40} 41 42type ContactView struct { 43 ID int64 44 Name string 45 FrequencyDays int64 46 LastInteraction *time.Time 47 DaysOverdue int 48 Status string // "overdue", "due-soon", "ok" 49} 50 51type pageData struct { 52 Hostname string 53 UserEmail string 54 LoginURL string 55 LogoutURL string 56 Contacts []ContactView 57} 58 59func (s *Server) HandleRoot(w http.ResponseWriter, r *http.Request) { 60 userID := strings.TrimSpace(r.Header.Get("X-ExeDev-UserID")) 61 userEmail := strings.TrimSpace(r.Header.Get("X-ExeDev-Email")) 62 63 data := pageData{ 64 Hostname: s.Hostname, 65 UserEmail: userEmail, 66 LoginURL: loginURLForRequest(r), 67 LogoutURL: "/__exe.dev/logout", 68 Contacts: []ContactView{}, 69 } 70 71 if userID != "" && s.DB != nil { 72 q := dbgen.New(s.DB) 73 contacts, err := q.GetContactsByUser(r.Context(), userID) 74 if err != nil { 75 slog.Warn("get contacts", "error", err) 76 } else { 77 now := time.Now() 78 for _, c := range contacts { 79 cv := ContactView{ 80 ID: c.ID, 81 Name: c.Name, 82 FrequencyDays: c.FrequencyDays, 83 Status: "ok", 84 } 85 if c.LastInteraction != nil { 86 if lastStr, ok := c.LastInteraction.(string); ok { 87 t := parseFlexibleTime(lastStr) 88 if !t.IsZero() { 89 cv.LastInteraction = &t 90 } 91 } 92 } 93 if cv.LastInteraction != nil { 94 daysSince := int(now.Sub(*cv.LastInteraction).Hours() / 24) 95 cv.DaysOverdue = daysSince - int(c.FrequencyDays) 96 if cv.DaysOverdue > 0 { 97 cv.Status = "overdue" 98 } else if cv.DaysOverdue > -7 { 99 cv.Status = "due-soon" 100 } 101 } else { 102 // Never interacted - consider overdue 103 cv.Status = "overdue" 104 cv.DaysOverdue = int(c.FrequencyDays) 105 } 106 data.Contacts = append(data.Contacts, cv) 107 } 108 // Sort by most overdue first 109 sort.Slice(data.Contacts, func(i, j int) bool { 110 return data.Contacts[i].DaysOverdue > data.Contacts[j].DaysOverdue 111 }) 112 } 113 } 114 115 w.Header().Set("Content-Type", "text/html; charset=utf-8") 116 if err := s.renderTemplate(w, "index.html", data); err != nil { 117 slog.Warn("render template", "url", r.URL.Path, "error", err) 118 } 119} 120 121func (s *Server) HandleAddContact(w http.ResponseWriter, r *http.Request) { 122 userID := strings.TrimSpace(r.Header.Get("X-ExeDev-UserID")) 123 if userID == "" { 124 http.Redirect(w, r, "/__exe.dev/login", http.StatusSeeOther) 125 return 126 } 127 128 name := strings.TrimSpace(r.FormValue("name")) 129 freqStr := r.FormValue("frequency_days") 130 freq, err := strconv.ParseInt(freqStr, 10, 64) 131 if err != nil || freq < 1 { 132 freq = 30 133 } 134 135 if name != "" { 136 q := dbgen.New(s.DB) 137 _, err := q.CreateContact(r.Context(), dbgen.CreateContactParams{ 138 UserID: userID, 139 Name: name, 140 FrequencyDays: freq, 141 CreatedAt: time.Now(), 142 }) 143 if err != nil { 144 slog.Warn("create contact", "error", err) 145 } 146 } 147 148 http.Redirect(w, r, "/", http.StatusSeeOther) 149} 150 151func (s *Server) HandleLogInteraction(w http.ResponseWriter, r *http.Request) { 152 userID := strings.TrimSpace(r.Header.Get("X-ExeDev-UserID")) 153 if userID == "" { 154 http.Redirect(w, r, "/__exe.dev/login", http.StatusSeeOther) 155 return 156 } 157 158 contactIDStr := r.PathValue("id") 159 contactID, err := strconv.ParseInt(contactIDStr, 10, 64) 160 if err != nil { 161 http.Error(w, "Invalid contact ID", http.StatusBadRequest) 162 return 163 } 164 165 q := dbgen.New(s.DB) 166 // Verify contact belongs to user 167 _, err = q.GetContact(r.Context(), dbgen.GetContactParams{ID: contactID, UserID: userID}) 168 if err != nil { 169 http.Error(w, "Contact not found", http.StatusNotFound) 170 return 171 } 172 173 notes := strings.TrimSpace(r.FormValue("notes")) 174 var notesPtr *string 175 if notes != "" { 176 notesPtr = &notes 177 } 178 179 _, err = q.CreateInteraction(r.Context(), dbgen.CreateInteractionParams{ 180 ContactID: contactID, 181 Notes: notesPtr, 182 InteractedAt: time.Now().UTC(), 183 }) 184 if err != nil { 185 slog.Warn("create interaction", "error", err) 186 } 187 188 http.Redirect(w, r, "/", http.StatusSeeOther) 189} 190 191type contactDetailData struct { 192 Hostname string 193 UserEmail string 194 LoginURL string 195 LogoutURL string 196 Contact dbgen.Contact 197 Interactions []dbgen.Interaction 198} 199 200func (s *Server) HandleContactDetail(w http.ResponseWriter, r *http.Request) { 201 userID := strings.TrimSpace(r.Header.Get("X-ExeDev-UserID")) 202 if userID == "" { 203 http.Redirect(w, r, "/__exe.dev/login", http.StatusSeeOther) 204 return 205 } 206 207 contactIDStr := r.PathValue("id") 208 contactID, err := strconv.ParseInt(contactIDStr, 10, 64) 209 if err != nil { 210 http.Error(w, "Invalid contact ID", http.StatusBadRequest) 211 return 212 } 213 214 q := dbgen.New(s.DB) 215 contact, err := q.GetContact(r.Context(), dbgen.GetContactParams{ID: contactID, UserID: userID}) 216 if err != nil { 217 http.Error(w, "Contact not found", http.StatusNotFound) 218 return 219 } 220 221 interactions, err := q.GetInteractionsByContact(r.Context(), contactID) 222 if err != nil { 223 slog.Warn("get interactions", "error", err) 224 interactions = []dbgen.Interaction{} 225 } 226 227 data := contactDetailData{ 228 Hostname: s.Hostname, 229 UserEmail: strings.TrimSpace(r.Header.Get("X-ExeDev-Email")), 230 LoginURL: loginURLForRequest(r), 231 LogoutURL: "/__exe.dev/logout", 232 Contact: contact, 233 Interactions: interactions, 234 } 235 236 w.Header().Set("Content-Type", "text/html; charset=utf-8") 237 if err := s.renderTemplate(w, "contact.html", data); err != nil { 238 slog.Warn("render template", "error", err) 239 } 240} 241 242func (s *Server) HandleUpdateContact(w http.ResponseWriter, r *http.Request) { 243 userID := strings.TrimSpace(r.Header.Get("X-ExeDev-UserID")) 244 if userID == "" { 245 http.Redirect(w, r, "/__exe.dev/login", http.StatusSeeOther) 246 return 247 } 248 249 contactIDStr := r.PathValue("id") 250 contactID, err := strconv.ParseInt(contactIDStr, 10, 64) 251 if err != nil { 252 http.Error(w, "Invalid contact ID", http.StatusBadRequest) 253 return 254 } 255 256 name := strings.TrimSpace(r.FormValue("name")) 257 freqStr := r.FormValue("frequency_days") 258 freq, err := strconv.ParseInt(freqStr, 10, 64) 259 if err != nil || freq < 1 { 260 freq = 30 261 } 262 263 q := dbgen.New(s.DB) 264 err = q.UpdateContact(r.Context(), dbgen.UpdateContactParams{ 265 ID: contactID, 266 UserID: userID, 267 Name: name, 268 FrequencyDays: freq, 269 }) 270 if err != nil { 271 slog.Warn("update contact", "error", err) 272 } 273 274 http.Redirect(w, r, fmt.Sprintf("/contact/%d", contactID), http.StatusSeeOther) 275} 276 277func (s *Server) HandleUpdateInteraction(w http.ResponseWriter, r *http.Request) { 278 userID := strings.TrimSpace(r.Header.Get("X-ExeDev-UserID")) 279 if userID == "" { 280 http.Redirect(w, r, "/__exe.dev/login", http.StatusSeeOther) 281 return 282 } 283 284 contactIDStr := r.PathValue("id") 285 contactID, err := strconv.ParseInt(contactIDStr, 10, 64) 286 if err != nil { 287 http.Error(w, "Invalid contact ID", http.StatusBadRequest) 288 return 289 } 290 291 interactionIDStr := r.PathValue("interactionId") 292 interactionID, err := strconv.ParseInt(interactionIDStr, 10, 64) 293 if err != nil { 294 http.Error(w, "Invalid interaction ID", http.StatusBadRequest) 295 return 296 } 297 298 q := dbgen.New(s.DB) 299 // Verify contact belongs to user 300 _, err = q.GetContact(r.Context(), dbgen.GetContactParams{ID: contactID, UserID: userID}) 301 if err != nil { 302 http.Error(w, "Contact not found", http.StatusNotFound) 303 return 304 } 305 306 notes := strings.TrimSpace(r.FormValue("notes")) 307 var notesPtr *string 308 if notes != "" { 309 notesPtr = &notes 310 } 311 312 err = q.UpdateInteractionNotes(r.Context(), dbgen.UpdateInteractionNotesParams{ 313 Notes: notesPtr, 314 ID: interactionID, 315 }) 316 if err != nil { 317 slog.Warn("update interaction", "error", err) 318 } 319 320 http.Redirect(w, r, fmt.Sprintf("/contact/%d", contactID), http.StatusSeeOther) 321} 322 323func (s *Server) HandleDeleteContact(w http.ResponseWriter, r *http.Request) { 324 userID := strings.TrimSpace(r.Header.Get("X-ExeDev-UserID")) 325 if userID == "" { 326 http.Redirect(w, r, "/__exe.dev/login", http.StatusSeeOther) 327 return 328 } 329 330 contactIDStr := r.PathValue("id") 331 contactID, err := strconv.ParseInt(contactIDStr, 10, 64) 332 if err != nil { 333 http.Error(w, "Invalid contact ID", http.StatusBadRequest) 334 return 335 } 336 337 q := dbgen.New(s.DB) 338 err = q.DeleteContact(r.Context(), dbgen.DeleteContactParams{ 339 ID: contactID, 340 UserID: userID, 341 }) 342 if err != nil { 343 slog.Warn("delete contact", "error", err) 344 } 345 346 http.Redirect(w, r, "/", http.StatusSeeOther) 347} 348 349func parseFlexibleTime(s string) time.Time { 350 formats := []string{ 351 "2006-01-02 15:04:05.999999999 -0700 MST", 352 "2006-01-02 15:04:05.999999999 +0000 UTC", 353 "2006-01-02 15:04:05-07:00", 354 "2006-01-02 15:04:05", 355 time.RFC3339, 356 time.RFC3339Nano, 357 } 358 // Strip any monotonic clock part 359 if idx := strings.Index(s, " m="); idx > 0 { 360 s = s[:idx] 361 } 362 for _, f := range formats { 363 if t, err := time.Parse(f, s); err == nil { 364 return t 365 } 366 } 367 return time.Time{} 368} 369 370func loginURLForRequest(r *http.Request) string { 371 path := r.URL.RequestURI() 372 v := url.Values{} 373 v.Set("redirect", path) 374 return "/__exe.dev/login?" + v.Encode() 375} 376 377func (s *Server) renderTemplate(w http.ResponseWriter, name string, data any) error { 378 path := filepath.Join(s.TemplatesDir, name) 379 tmpl, err := template.ParseFiles(path) 380 if err != nil { 381 return fmt.Errorf("parse template %q: %w", name, err) 382 } 383 if err := tmpl.Execute(w, data); err != nil { 384 return fmt.Errorf("execute template %q: %w", name, err) 385 } 386 return nil 387} 388 389func (s *Server) setUpDatabase(dbPath string) error { 390 wdb, err := db.Open(dbPath) 391 if err != nil { 392 return fmt.Errorf("failed to open db: %w", err) 393 } 394 s.DB = wdb 395 if err := db.RunMigrations(wdb); err != nil { 396 return fmt.Errorf("failed to run migrations: %w", err) 397 } 398 return nil 399} 400 401func (s *Server) Serve(addr string) error { 402 mux := http.NewServeMux() 403 mux.HandleFunc("GET /{$}", s.HandleRoot) 404 mux.HandleFunc("POST /contact", s.HandleAddContact) 405 mux.HandleFunc("GET /contact/{id}", s.HandleContactDetail) 406 mux.HandleFunc("POST /contact/{id}", s.HandleUpdateContact) 407 mux.HandleFunc("POST /contact/{id}/delete", s.HandleDeleteContact) 408 mux.HandleFunc("POST /contact/{id}/interaction", s.HandleLogInteraction) 409 mux.HandleFunc("POST /contact/{id}/interaction/{interactionId}", s.HandleUpdateInteraction) 410 mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(s.StaticDir)))) 411 slog.Info("starting server", "addr", addr) 412 return http.ListenAndServe(addr, mux) 413}