From 051cf1da5db1fcec8feb1d1605507dc4279b3a8e Mon Sep 17 00:00:00 2001 From: Angel Beltran Date: Sun, 28 Dec 2025 14:38:06 -0700 Subject: [PATCH] prototype report page --- README.md | 4 + go.mod | 4 + internal/domains/raw_events/events.go | 65 ++++++++++ internal/site/scripts/_hyperscript.min.js.gz | Bin 0 -> 25763 bytes internal/site/scripts/htmx.min.js.gz | Bin 0 -> 15677 bytes internal/site/site.go | 120 ++++++++++++++++-- internal/site/styles/index.css | 10 ++ .../component_bodies/nav_bar.html.tmpl | 16 +++ internal/site/templates/layout.html.tmpl | 19 +++ .../site/templates/page_bodies/home.html.tmpl | 3 + .../templates/page_bodies/reports.html.tmpl | 89 +++++++++++++ internal/webhooks/etsy/webhooks.go | 10 +- main.go | 7 +- 13 files changed, 330 insertions(+), 17 deletions(-) create mode 100644 internal/site/scripts/_hyperscript.min.js.gz create mode 100644 internal/site/scripts/htmx.min.js.gz create mode 100644 internal/site/styles/index.css create mode 100644 internal/site/templates/component_bodies/nav_bar.html.tmpl create mode 100644 internal/site/templates/layout.html.tmpl create mode 100644 internal/site/templates/page_bodies/home.html.tmpl create mode 100644 internal/site/templates/page_bodies/reports.html.tmpl diff --git a/README.md b/README.md index 280b8fa..4c6a1f2 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,10 @@ - [ ] Start with just a list of events for a given store (use a static test store) - [ ] ... +## Nice to haves + +- [ ] Drop in a good logger + Looking to follow the [CQRS](https://martinfowler.com/bliki/CQRS.html) pattern. diff --git a/go.mod b/go.mod index f436bec..c7308e3 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,12 @@ module ruben/inventory2 go 1.24.2 +replace github.com/angelbeltran/templater v0.1.0 => ../../github.com/angelbeltran/templater + require github.com/jackc/pgx/v5 v5.7.6 +require github.com/angelbeltran/templater v0.1.0 + require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect diff --git a/internal/domains/raw_events/events.go b/internal/domains/raw_events/events.go index d40e472..840f382 100644 --- a/internal/domains/raw_events/events.go +++ b/internal/domains/raw_events/events.go @@ -16,6 +16,12 @@ type ( db *pgxpool.Pool } + // StoreWithContext allows currying all Store methods by context.Context + StoreWithContext struct { + ctx context.Context + db *Store + } + Event struct { Platform string StoreID string @@ -51,6 +57,13 @@ func newPool(ctx context.Context) (*pgxpool.Pool, error) { return pool, nil } +func (db *Store) WithContext(ctx context.Context) *StoreWithContext { + return &StoreWithContext{ + ctx: ctx, + db: db, + } +} + func (db *Store) Save(ctx context.Context, e *Event) error { _, err := db.db.Exec( ctx, @@ -87,3 +100,55 @@ func (db *Store) Save(ctx context.Context, e *Event) error { return nil } + +func (db *StoreWithContext) Save(e *Event) error { + return db.db.Save(db.ctx, e) +} + +func (db *Store) LoadEventsForStore( + ctx context.Context, + platform string, + storeID string, +) ([]Event, error) { + rows, err := db.db.Query( + ctx, + ` + SELECT + platform as Platform, + store_id as StoreID, + event_timestamp as EventTimestamp, + event_id as EventID, + raw_payload as Payload + FROM + raw_store_events + WHERE + platform = @platform + AND store_id = @store_id + ORDER BY + event_timestamp DESC + LIMIT + 100 + `, + pgx.NamedArgs{ + "platform": platform, + "store_id": storeID, + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to perform query: %w", err) + } + + evts, err := pgx.CollectRows(rows, pgx.RowToStructByNameLax[Event]) + if err != nil { + return nil, fmt.Errorf("failed to scan events: %w", err) + } + + return evts, nil +} + +func (db *StoreWithContext) LoadEventsForStore( + platform string, + storeID string, +) ([]Event, error) { + return db.db.LoadEventsForStore(db.ctx, platform, storeID) +} diff --git a/internal/site/scripts/_hyperscript.min.js.gz b/internal/site/scripts/_hyperscript.min.js.gz new file mode 100644 index 0000000000000000000000000000000000000000..0ec533974fdaec5897dd3923eddaa1921a5c0d13 GIT binary patch literal 25763 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

#gf@xRW350z=X( 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 literal 0 HcmV?d00001 diff --git a/internal/site/scripts/htmx.min.js.gz b/internal/site/scripts/htmx.min.js.gz new file mode 100644 index 0000000000000000000000000000000000000000..396e9d4c644bd49cafa93b0ba4bd9bf7626e7143 GIT binary patch literal 15677 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%0DEqdf%B;ph@g6gQMBRYgjth9^XVi=%~Y;tAG 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 literal 0 HcmV?d00001 diff --git a/internal/site/site.go b/internal/site/site.go index 4c8b836..82c9d7d 100644 --- a/internal/site/site.go +++ b/internal/site/site.go @@ -1,26 +1,120 @@ package site import ( + "encoding/json" + "fmt" + "html/template" "net/http" + "net/url" + "path" + "strings" + + "github.com/angelbeltran/templater" + + "ruben/inventory2/internal/domains/raw_events" ) -func NewSiteHandler() http.Handler { +func NewSiteHandler(dir string, db *raw_events.Store) http.Handler { mux := http.NewServeMux() + // non-html routes + + scfs := http.FileServer(http.Dir(dir + "/scripts")) + mux.Handle("/scripts/", http.StripPrefix("/scripts", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/javascript") + if path.Ext(r.URL.Path) == ".gz" { + w.Header().Set("Content-Encoding", "gzip") + } + scfs.ServeHTTP(w, r) + }))) + mux.Handle("/styles/", http.StripPrefix("/styles", http.FileServer(http.Dir(dir+"/styles")))) + + // html page routes + + tmplr := templater.NewTemplater( + dir+"/templates", + func() template.FuncMap { + return template.FuncMap{ + "buildSitePath": func(parts ...any) string { + strParts := make([]string, len(parts)) + for i, p := range parts { + strParts[i] = fmt.Sprint(p) + } + + // TODO: make "/site" dynamic somehow + return path.Join(append([]string{"/site"}, strParts...)...) + }, + + "prettyPrintJSON": func(j json.RawMessage) string { + b, err := json.MarshalIndent(j, " ", "") + if err != nil { + return string(j) + } + return string(b) + }, + + "addInt": func(a, b int) int { + return a + b + }, + "subInt": func(a, b int) int { + return a - b + }, + "multInt": func(a, b int) int { + return a * b + }, + } + }, + ) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/html") - w.Write([]byte(` - - - - TEST - - - TEST - - -`)) + b, err := tmplr.ExecutePage( + getPageTemplateNameForURL(r.URL), + "Request", + r, + // add services here + "RawEvents", + db.WithContext(r.Context()), + "URLCalc", + newURLCalculator(r.URL), + ) + if err != nil { + // TODO: handle 'not found' as a 404? + fmt.Println("[ERROR]: failed to load or parse layout template:", err) + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return + } + + w.Write(b) }) return mux } + +func getPageTemplateNameForURL(u *url.URL) string { + filepath := strings.TrimPrefix(strings.TrimSuffix(u.Path, ".html"), "/") + if filepath == "" { + // "/" maps to "/home" + filepath = "home" + } + + return filepath +} + +type URLCalculator struct { + url *url.URL +} + +func newURLCalculator(u *url.URL) URLCalculator { + cpy := *u + return URLCalculator{ + url: &cpy, + } +} + +func (c URLCalculator) SetQueryParam(k string, v any) string { + u := *c.url + q := u.Query() + q.Set(k, fmt.Sprint(v)) + u.RawQuery = q.Encode() + + return u.String() +} diff --git a/internal/site/styles/index.css b/internal/site/styles/index.css new file mode 100644 index 0000000..6d29716 --- /dev/null +++ b/internal/site/styles/index.css @@ -0,0 +1,10 @@ +ul, ol { + /* remove default agent spacing */ + margin: 0; + padding: 0; + list-style: none; + + & > li { + display: inline-block; + } +} diff --git a/internal/site/templates/component_bodies/nav_bar.html.tmpl b/internal/site/templates/component_bodies/nav_bar.html.tmpl new file mode 100644 index 0000000..fb83940 --- /dev/null +++ b/internal/site/templates/component_bodies/nav_bar.html.tmpl @@ -0,0 +1,16 @@ +

+ diff --git a/internal/site/templates/layout.html.tmpl b/internal/site/templates/layout.html.tmpl new file mode 100644 index 0000000..662b5c9 --- /dev/null +++ b/internal/site/templates/layout.html.tmpl @@ -0,0 +1,19 @@ + + + + WIP + + + + + + + {{/* This is where the page_head template will be inserted into the page */}} + {{- block "head" . }}{{ end }} + + + + {{/* This is where the page_body template will be inserted into the page */}} + {{- block "body" . }}{{ end }} + + diff --git a/internal/site/templates/page_bodies/home.html.tmpl b/internal/site/templates/page_bodies/home.html.tmpl new file mode 100644 index 0000000..7cce70e --- /dev/null +++ b/internal/site/templates/page_bodies/home.html.tmpl @@ -0,0 +1,3 @@ +{{ componentBody "nav_bar" }} + +

Home

diff --git a/internal/site/templates/page_bodies/reports.html.tmpl b/internal/site/templates/page_bodies/reports.html.tmpl new file mode 100644 index 0000000..570787e --- /dev/null +++ b/internal/site/templates/page_bodies/reports.html.tmpl @@ -0,0 +1,89 @@ +{{ componentBody "nav_bar" }} + +{{ $storeID := .Request.URL.Query.Get "store-id" }} + +

Reports

+ +
+ + + {{- if $storeID }} +
+
+ Date +
+
+ Time +
+
+ ID +
+
+ Payload +
+ {{ $events := .RawEvents.LoadEventsForStore "etsy" $storeID }} + {{ range $e := $events }} +
+ {{ printf "%d/%02d/%02d" $e.EventTimestamp.Year $e.EventTimestamp.Month $e.EventTimestamp.Day }} +
+
+ {{ $suffix := "am" }} + {{ $ts := $e.EventTimestamp }} + {{- $hr := $ts.Hour }} + {{- if eq $hr 0 }} + {{- $hr = 12 }} + {{- else if eq $hr 12 }} + {{- $suffix = "pm" }} + {{- else if gt $hr 12 }} + {{- $hr = subInt $hr 12 }} + {{- $suffix = "pm" }} + {{- end }} + + {{- $hrColorHex := multInt $hr 10 | subInt (subInt 256 128) | printf "%02x" }} + {{- $minColorHex := multInt $ts.Minute 2 | subInt (subInt 256 128) | printf "%02x" }} + {{- $secColorHex := multInt $ts.Second 2 | subInt (subInt 256 128) | printf "%02x" }} + {{ printf "%02d" $hr }}:{{ printf "%02d" $ts.Minute }}:{{ printf "%02d" $ts.Second }}{{ printf " %s" $suffix }} +
+
+ {{ $e.EventID }} +
+
+ {{ prettyPrintJSON $e.Payload }} +
+ {{ end }} +
+ {{- end }} +
diff --git a/internal/webhooks/etsy/webhooks.go b/internal/webhooks/etsy/webhooks.go index 5d60a75..38305c9 100644 --- a/internal/webhooks/etsy/webhooks.go +++ b/internal/webhooks/etsy/webhooks.go @@ -21,9 +21,17 @@ func NewWebhookHandler(db *raw_events.Store) http.Handler { ts := time.Now().UTC() + storeID := "test-store-id" + var payloadObject struct { + StoreID string + } + if err := json.Unmarshal(body, &payloadObject); err == nil && payloadObject.StoreID != "" { + storeID = payloadObject.StoreID + } + err := db.Save(r.Context(), &raw_events.Event{ Platform: "etsy", - StoreID: "test-store-1", + StoreID: storeID, EventID: fmt.Sprint(ts.Unix()), EventTimestamp: ts, Payload: body, diff --git a/main.go b/main.go index 3642c82..8bdde4a 100644 --- a/main.go +++ b/main.go @@ -73,7 +73,7 @@ func runApp(ctx context.Context) error { func runServer(ctx context.Context, db *raw_events.Store) <-chan error { srv := &http.Server{ - Addr: ":9000", // local + Addr: ":8082", // local Handler: buildHTTPHandler(db), } @@ -86,7 +86,7 @@ func runServer(ctx context.Context, db *raw_events.Store) <-chan error { defer cancel() defer close(alreadyShutdownCh) - fmt.Println("server running on 9000...") + fmt.Println("server running on 8082...") if err := srv.ListenAndServe(); err != nil { if !errors.Is(err, http.ErrServerClosed) { runningErrCh <- fmt.Errorf("server experienced error: %w", err) @@ -129,7 +129,8 @@ func buildHTTPHandler(db *raw_events.Store) http.Handler { mux := http.NewServeMux() mux.Handle("/webhooks/", http.StripPrefix("/webhooks", webhooks.New(db))) - mux.Handle("/site/", http.StripPrefix("/site", site.NewSiteHandler())) + mux.Handle("/site/", http.StripPrefix("/site", site.NewSiteHandler("./internal/site", db))) + mux.Handle("/", http.RedirectHandler("/site", http.StatusPermanentRedirect)) return mux }