From 26c0cb0f43108b5da28644859239b1ea2b28959e Mon Sep 17 00:00:00 2001 From: Brittany Ellich Date: Fri, 15 May 2026 11:14:54 -0700 Subject: [PATCH] feat(about,demo): port /about-atproto, /demo/clock, /healthz Fill in three remaining routes from the old atmoquest app that were missing after the feature-folded migration: - features/about/{routes.go, pages/about.templ}: short "What's ATProto?" explainer page linked from the guest landing. - features/demo/routes.go: Datastar SSE clock endpoint that streams the current UTC time into the statusbar #clock once a second. - router/router.go: register the two new feature subrouters and add the /healthz probe endpoint. - features/index/pages/index.templ: restore the live clock by binding #clock to data-on-load="@get('/demo/clock')" on both the authed and guest statusbars (was a static --:--:-- placeholder). Build clean, all tests pass, smoke-tested on :9090: /healthz 200 /about-atproto 200 (2369 bytes) /demo/clock SSE patch frames streaming / 200 (guest landing unchanged) --- features/about/pages/about.templ | 43 +++++++++++ features/about/pages/about_templ.go | 77 +++++++++++++++++++ features/about/routes.go | 26 +++++++ features/demo/routes.go | 48 ++++++++++++ features/index/pages/index.templ | 4 +- features/index/pages/index_templ.go | 114 +++++++++++++++++----------- router/router.go | 9 +++ 7 files changed, 275 insertions(+), 46 deletions(-) create mode 100644 features/about/pages/about.templ create mode 100644 features/about/pages/about_templ.go create mode 100644 features/about/routes.go create mode 100644 features/demo/routes.go diff --git a/features/about/pages/about.templ b/features/about/pages/about.templ new file mode 100644 index 0000000..64dffdb --- /dev/null +++ b/features/about/pages/about.templ @@ -0,0 +1,43 @@ +package pages + +import "atmoquest/features/common/layouts" + +// AboutATProto is a short explainer page linked from the guest landing. +// Intentionally tiny — its job is to give a curious visitor enough context +// to understand what "your data goes to your repo, not ours" means before +// they sign in. +templ AboutATProto() { + @layouts.Base("What's ATProto? — atmo.quest", "ATProto is an open social protocol — your records live in your repo, not ours.") { +
+
+
+
+
+
man atproto
+
+
+
+ you{ "@" }atmo.quest:~$ + cat /etc/atproto/about.md +
+ +
+ protocol · open · yours +

your data

+

+ ATProto is an open social protocol. Instead of accounts living inside one company's database, your records live in your repo — a personal data store you can take with you. +

+
+ +

+ atmo.quest writes connection, check-in, and badge records to your PDS. If atmo.quest disappears tomorrow, those records still exist in your repo and can be read by any other ATProto-aware app. +

+ + +
+
+ } +} diff --git a/features/about/pages/about_templ.go b/features/about/pages/about_templ.go new file mode 100644 index 0000000..1d95bcf --- /dev/null +++ b/features/about/pages/about_templ.go @@ -0,0 +1,77 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package pages + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import "atmoquest/features/common/layouts" + +// AboutATProto is a short explainer page linked from the guest landing. +// Intentionally tiny — its job is to give a curious visitor enough context +// to understand what "your data goes to your repo, not ours" means before +// they sign in. +func AboutATProto() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
man atproto
you") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs("@") + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/about/pages/about.templ`, Line: 20, Col: 56} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "atmo.quest:~$ cat /etc/atproto/about.md
protocol · open · yours

your data

ATProto is an open social protocol. Instead of accounts living inside one company's database, your records live in your repo — a personal data store you can take with you.

atmo.quest writes connection, check-in, and badge records to your PDS. If atmo.quest disappears tomorrow, those records still exist in your repo and can be read by any other ATProto-aware app.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) + templ_7745c5c3_Err = layouts.Base("What's ATProto? — atmo.quest", "ATProto is an open social protocol — your records live in your repo, not ours.").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/features/about/routes.go b/features/about/routes.go new file mode 100644 index 0000000..3f2d79b --- /dev/null +++ b/features/about/routes.go @@ -0,0 +1,26 @@ +// Package about hosts the /about-atproto explainer page. It's intentionally +// scope-minimal — a single GET that renders a static templ — but lives in +// its own feature folder so the routes file in the router stays tidy and +// future related pages (e.g. /about-collective) have an obvious home. +package about + +import ( + "log/slog" + "net/http" + + "github.com/go-chi/chi/v5" + + "atmoquest/features/about/pages" +) + +// SetupRoutes registers the /about-atproto route. +func SetupRoutes(router chi.Router) { + router.Get("/about-atproto", aboutATProto) +} + +func aboutATProto(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := pages.AboutATProto().Render(r.Context(), w); err != nil { + slog.Error("render about-atproto", "err", err) + } +} diff --git a/features/demo/routes.go b/features/demo/routes.go new file mode 100644 index 0000000..edc7f45 --- /dev/null +++ b/features/demo/routes.go @@ -0,0 +1,48 @@ +// Package demo hosts small Datastar SSE sanity-check endpoints. Right now +// that's just /demo/clock, which streams the current UTC time into the +// statusbar #clock element so we can visually confirm the +// Datastar Go SDK + chi + SSE plumbing all work end-to-end. +// +// The home page wires it up with: +// +//
--:--:--
+// +// Delete this feature once a real interaction replaces the demo. +package demo + +import ( + "fmt" + "log/slog" + "net/http" + "time" + + "github.com/go-chi/chi/v5" + "github.com/starfederation/datastar-go/datastar" +) + +// SetupRoutes registers the demo SSE endpoints. +func SetupRoutes(router chi.Router) { + router.Get("/demo/clock", clock) +} + +// clock streams the current UTC time into #clock once per second. The +// loop exits naturally when the client disconnects (r.Context().Done()) +// or when the SSE write fails. +func clock(w http.ResponseWriter, r *http.Request) { + sse := datastar.NewSSE(w, r) + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + + for { + select { + case <-r.Context().Done(): + return + case t := <-ticker.C: + html := fmt.Sprintf(`
%s
`, t.UTC().Format("15:04:05 UTC")) + if err := sse.PatchElements(html); err != nil { + slog.Debug("clock sse closed", "err", err) + return + } + } + } +} diff --git a/features/index/pages/index.templ b/features/index/pages/index.templ index 2ef0b3e..5c9c080 100644 --- a/features/index/pages/index.templ +++ b/features/index/pages/index.templ @@ -116,7 +116,7 @@ templ indexAuthed(v IndexView) { { "@" + v.Handle } } -
--:--:--
+
--:--:--
@@ -187,7 +187,7 @@ templ indexGuest() {
● ATProto · OAuth ready · guest mode enabled
-
--:--:--
+
--:--:--
diff --git a/features/index/pages/index_templ.go b/features/index/pages/index_templ.go index 8187103..4b5b0a4 100644 --- a/features/index/pages/index_templ.go +++ b/features/index/pages/index_templ.go @@ -280,7 +280,20 @@ func indexAuthed(v IndexView) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
--:--:--
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
--:--:--
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -313,89 +326,89 @@ func indexEventCard(ev *IndexEventCard) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var11 := templ.GetChildren(ctx) - if templ_7745c5c3_Var11 == nil { - templ_7745c5c3_Var11 = templ.NopComponent + templ_7745c5c3_Var12 := templ.GetChildren(ctx) + if templ_7745c5c3_Var12 == nil { + templ_7745c5c3_Var12 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -420,12 +433,12 @@ func indexGuest() templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var16 := templ.GetChildren(ctx) - if templ_7745c5c3_Var16 == nil { - templ_7745c5c3_Var16 = templ.NopComponent + templ_7745c5c3_Var17 := templ.GetChildren(ctx) + if templ_7745c5c3_Var17 == nil { + templ_7745c5c3_Var17 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Var17 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_Var18 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) if !templ_7745c5c3_IsBuffer { @@ -437,52 +450,65 @@ func indexGuest() templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "
~/atmo.quest — zsh
you") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
~/atmo.quest — zsh
you") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var18 string - templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs("@") + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs("@") if templ_7745c5c3_Err != nil { return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/index/pages/index.templ`, Line: 169, Col: 56} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "cascadiajs:~$ whoami
a person at a conference. you just met someone interesting and now their handle is gone.
init: v0.1 · CascadiaJS 2026

atmo.quest 

An event companion ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "cascadiajs:~$ whoami

a person at a conference. you just met someone interesting and now their handle is gone.
init: v0.1 · CascadiaJS 2026

atmo.quest 

An event companion ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var19 string - templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs("for") + var templ_7745c5c3_Var20 string + templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs("for") if templ_7745c5c3_Err != nil { return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/index/pages/index.templ`, Line: 180, Col: 56} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " the open social web. Scan a QR, write a note, leave with a real follow-up list. Your data goes to your repo, not ours.

● ATProto · OAuth ready · guest mode enabled
--:--:--
quest_log.md

Side Quests ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " the open social web. Scan a QR, write a note, leave with a real follow-up list. Your data goes to your repo, not ours.

● ATProto · OAuth ready · guest mode enabled
--:--:--
quest_log.md

Side Quests ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var22 string + templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs("// available") if templ_7745c5c3_Err != nil { return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/index/pages/index.templ`, Line: 203, Col: 57} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "

◆
Make your first connection
Scan someone's QR code. A quest.atmo.connection record gets written to your PDS. They get one too. That's the protocol bit.
+1 badge first connect
◆
Check in to CascadiaJS
Drops a checkin record dated June 1–2, 2026. You'll find it in your repo forever.
+1 badge attendee
◆
Take a note while it's fresh
Private notes, attached to each connection. Read them Monday morning when you forget who Sarah was.
unlocks with first connection
◆
Help unlock the conference rewards
As more people join, new things appear — leaderboard, interest matching, end-of-conf stats. Earned together.
group quest see /unlocks
built on atproto · open source · made by PDX ATProto
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "
◆
Make your first connection
Scan someone's QR code. A quest.atmo.connection record gets written to your PDS. They get one too. That's the protocol bit.
+1 badge first connect
◆
Check in to CascadiaJS
Drops a checkin record dated June 1–2, 2026. You'll find it in your repo forever.
+1 badge attendee
◆
Take a note while it's fresh
Private notes, attached to each connection. Read them Monday morning when you forget who Sarah was.
unlocks with first connection
◆
Help unlock the conference rewards
As more people join, new things appear — leaderboard, interest matching, end-of-conf stats. Earned together.
group quest see /unlocks
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } return nil }) - templ_7745c5c3_Err = layouts.Base("atmo.quest — conference companion on ATProto", "Scan a QR, write a note, leave with a real follow-up list. Your data goes to your repo, not ours.").Render(templ.WithChildren(ctx, templ_7745c5c3_Var17), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = layouts.Base("atmo.quest — conference companion on ATProto", "Scan a QR, write a note, leave with a real follow-up list. Your data goes to your repo, not ours.").Render(templ.WithChildren(ctx, templ_7745c5c3_Var18), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/router/router.go b/router/router.go index 69d4b13..72e9962 100644 --- a/router/router.go +++ b/router/router.go @@ -12,9 +12,11 @@ import ( "github.com/starfederation/datastar-go/datastar" "atmoquest/config" + "atmoquest/features/about" "atmoquest/features/admin" "atmoquest/features/auth" "atmoquest/features/connect" + "atmoquest/features/demo" "atmoquest/features/event" "atmoquest/features/index" "atmoquest/features/profile" @@ -55,8 +57,15 @@ func SetupRoutes( connect.SetupRoutes(router, conn, authH, connQueue) event.SetupRoutes(router, conn, authH) admin.SetupRoutes(router, conn, authH, signer) + about.SetupRoutes(router) + demo.SetupRoutes(router) index.SetupRoutes(router, conn, authH) + router.Get("/healthz", func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) + }) + return nil } -- 2.51.2