A quick vibecoded webapp on exe.dev that I liked enough to save the source for.
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 = ¬es
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 = ¬es
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}