Skip to content

Commit 02dd52c

Browse files
authored
chore: code review cleanup/reorganization (#9)
* fix(web): remove debug log leaking printer access code * fix(web): correct broken responsive grid class * fix(web): treat main AMS unit (id 0) as loaded * fix(web): drop unsound non-null assertions in printer actions * fix(web): cache filament catalog to avoid refetch * refactor(web): remove dead printer manager code * refactor(web): consolidate bambu color parsing into shared util * refactor(web): dedup Go2RTCPlayer helpers and drop any casts * fix(server): guard against panic on empty go2rtc producers * fix(server): add timeout and status check to go2rtc camera client * fix(server): correct accesCode typo * refactor(server): standardize logging on slog * refactor(server): typed constants for print/notification states * refactor(server): collapse duplicated printer control methods * refactor(server): use requireAdmin helper consistently * refactor(server): support multiple socketio connect handlers * refactor(server): move go2rtc camera client out of repositories * style(server): gofmt struct alignment * refactor(server): centralize environment configuration * chore: move hardcoded docker-compose values to env * ci: run lint, typecheck, and tests
1 parent 984abb8 commit 02dd52c

36 files changed

Lines changed: 385 additions & 262 deletions

.env.example

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copy to .env and adjust for your environment. docker-compose reads this file
2+
# too, so values here also override the compose defaults.
3+
4+
# Timezone for the go2rtc container (used by docker-compose).
5+
TZ=UTC
6+
7+
# TCP port the server listens on.
8+
PORT=3000
9+
10+
# Directory for persistent data (the SQLite database).
11+
DATA_DIR=./data
12+
13+
# Directory of the built web frontend to serve. Leave unset in dev (Vite
14+
# serves the frontend); set in production so the Go server serves both.
15+
# WEB_STATIC_PATH=/app/web
16+
17+
# go2rtc endpoints for camera streaming.
18+
GO2RTC_WS_URL=ws://localhost:1984
19+
GO2RTC_API_URL=http://localhost:1984
20+
21+
# Extra comma-separated origins allowed for WebSocket upgrades, beyond
22+
# same-origin. Set this to your web origin when the app is served elsewhere.
23+
# WS_ALLOWED_ORIGINS=http://localhost:5173
24+
25+
# Web push (VAPID). Leave the keys unset to have the server generate and
26+
# persist a keypair on first run. Subject is a mailto: or URL contact.
27+
VAPID_SUBJECT=crosshatch@bwees.io
28+
# VAPID_PUBLIC_KEY=
29+
# VAPID_PRIVATE_KEY=

.github/workflows/build.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ jobs:
1717
working-directory: server
1818
run: go build ./...
1919

20+
- name: Test server
21+
working-directory: server
22+
run: go test ./...
23+
2024
web:
2125
runs-on: ubuntu-latest
2226
steps:

.github/workflows/lint.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,9 @@ jobs:
2222

2323
- name: Fail if formatting changed files
2424
run: git diff --exit-code
25+
26+
- name: Lint
27+
run: mise run lint
28+
29+
- name: Type-check
30+
run: mise run check

docker-compose.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ services:
77
- "8555:8555" # webrtc
88
restart: unless-stopped
99
environment:
10-
- TZ=America/Chicago
10+
- TZ=${TZ:-UTC}
1111

1212
server:
1313
build:
@@ -20,8 +20,9 @@ services:
2020
- GO2RTC_WS_URL=ws://go2rtc:1984
2121
- GO2RTC_API_URL=http://go2rtc:1984
2222
- DATA_DIR=/data
23-
# Trust the Vite dev server for Websocket connections (CORS).
24-
- WS_ALLOWED_ORIGINS=http://localhost:5173,https://brandon-macbook-pro.tail72746.ts.net
23+
# Trust the Vite dev server for Websocket connections (CORS). Set
24+
# WS_ALLOWED_ORIGINS in a local .env to add your own origins.
25+
- WS_ALLOWED_ORIGINS=${WS_ALLOWED_ORIGINS:-http://localhost:5173}
2526
volumes:
2627
- ./server:/app/server
2728
- ./docker/data:/data

mise.toml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,31 @@ run = "pnpm format"
5252
[tasks.format]
5353
description = "Format the server and web codebases"
5454
depends = ["format:server", "format:web"]
55+
56+
[tasks."lint:server"]
57+
description = "Vet the Go server codebase"
58+
dir = "server"
59+
run = "go vet ./..."
60+
61+
[tasks."lint:web"]
62+
description = "Lint the web codebase (prettier + eslint)"
63+
dir = "web"
64+
run = "pnpm lint"
65+
66+
[tasks.lint]
67+
description = "Lint the server and web codebases"
68+
depends = ["lint:server", "lint:web"]
69+
70+
[tasks.check]
71+
description = "Type-check the web codebase"
72+
dir = "web"
73+
run = "pnpm check"
74+
75+
[tasks."test:server"]
76+
description = "Run the Go server tests"
77+
dir = "server"
78+
run = "go test ./..."
79+
80+
[tasks.test]
81+
description = "Run all tests"
82+
depends = ["test:server"]

server/internal/bambu/client.go

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"crypto/tls"
66
"encoding/json"
77
"fmt"
8+
"log/slog"
89
"math"
910
"strconv"
1011
"sync"
@@ -31,9 +32,9 @@ const (
3132
type StatusUpdateHandler func(serial string, state *dtos.BambuPrintState)
3233

3334
type BambuClient struct {
34-
ip string
35-
accesCode string
36-
serial string
35+
ip string
36+
accessCode string
37+
serial string
3738

3839
mqttClient mqtt.Client
3940

@@ -62,10 +63,10 @@ func (c *BambuClient) State() *dtos.BambuPrintState {
6263
}
6364

6465
func (c *BambuClient) onConnect(client mqtt.Client) {
65-
fmt.Printf("Connected to Bambu printer %s\n", c.serial)
66+
slog.Info("connected to bambu printer", "serial", c.serial)
6667

6768
if token := client.Subscribe(c.reportTopic(), 0, c.onMessage); token.Wait() && token.Error() != nil {
68-
fmt.Printf("Failed to subscribe to %q: %v\n", c.reportTopic(), token.Error())
69+
slog.Error("failed to subscribe to report topic", "topic", c.reportTopic(), "error", token.Error())
6970
}
7071
}
7172

@@ -74,7 +75,7 @@ func (c *BambuClient) onMessage(_ mqtt.Client, msg mqtt.Message) {
7475
Print json.RawMessage `json:"print"`
7576
}
7677
if err := json.Unmarshal(msg.Payload(), &envelope); err != nil {
77-
fmt.Printf("Received invalid MQTT message on %q: %v\n", c.reportTopic(), err)
78+
slog.Error("received invalid MQTT message", "topic", c.reportTopic(), "error", err)
7879
return
7980
}
8081

@@ -91,7 +92,7 @@ func (c *BambuClient) onMessage(_ mqtt.Client, msg mqtt.Message) {
9192
// replaced wholesale.
9293
if err := json.Unmarshal(envelope.Print, c.state); err != nil {
9394
c.stateMu.Unlock()
94-
fmt.Printf("Failed to decode print state on %q: %v\n", c.reportTopic(), err)
95+
slog.Error("failed to decode print state", "topic", c.reportTopic(), "error", err)
9596
return
9697
}
9798
state := c.state
@@ -103,7 +104,7 @@ func (c *BambuClient) onMessage(_ mqtt.Client, msg mqtt.Message) {
103104
}
104105

105106
func (c *BambuClient) onDisconnect(client mqtt.Client, err error) {
106-
fmt.Printf("Disconnected: %v\n", err)
107+
slog.Warn("disconnected from bambu printer", "serial", c.serial, "error", err)
107108
}
108109

109110
func (c *BambuClient) Close() {
@@ -258,10 +259,10 @@ func (c *BambuClient) UnloadMaterial(amsID int) error {
258259
})
259260
}
260261

261-
func NewBambuClient(ip string, accesCode string, serial string, onStatusUpdate StatusUpdateHandler) *BambuClient {
262+
func NewBambuClient(ip string, accessCode string, serial string, onStatusUpdate StatusUpdateHandler) *BambuClient {
262263
client := &BambuClient{
263264
ip: ip,
264-
accesCode: accesCode,
265+
accessCode: accessCode,
265266
serial: serial,
266267
onStatusUpdate: onStatusUpdate,
267268
}
@@ -270,7 +271,7 @@ func NewBambuClient(ip string, accesCode string, serial string, onStatusUpdate S
270271
opts.AddBroker(fmt.Sprintf("mqtts://%s:%d", ip, 8883))
271272
opts.SetClientID(fmt.Sprintf("crosshatch-%s", serial))
272273
opts.SetUsername("bblp")
273-
opts.SetPassword(accesCode)
274+
opts.SetPassword(accessCode)
274275
opts.SetKeepAlive(60)
275276
opts.SetAutoReconnect(true)
276277
opts.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
@@ -282,7 +283,7 @@ func NewBambuClient(ip string, accesCode string, serial string, onStatusUpdate S
282283

283284
go func() {
284285
if token := client.mqttClient.Connect(); token.Wait() && token.Error() != nil {
285-
fmt.Printf("Error connecting to MQTT broker: %v\n", token.Error())
286+
slog.Error("failed to connect to MQTT broker", "serial", serial, "error", token.Error())
286287
}
287288
}()
288289

server/internal/config/config.go

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,70 @@
1+
// Package config is the single source of truth for environment-derived
2+
// configuration. Every setting is read here so defaults live in one place.
13
package config
24

3-
import "os"
5+
import (
6+
"os"
7+
"strings"
8+
)
49

5-
// DataDir returns the directory where persistent data (the SQLite database) is
10+
func env(key, fallback string) string {
11+
if v := os.Getenv(key); v != "" {
12+
return v
13+
}
14+
return fallback
15+
}
16+
17+
// DataDir is the directory where persistent data (the SQLite database) is
618
// stored. Defaults to the current directory for local development; production
719
// sets DATA_DIR to a mounted volume.
820
func DataDir() string {
9-
dir := os.Getenv("DATA_DIR")
10-
if dir == "" {
11-
return "."
21+
return env("DATA_DIR", ".")
22+
}
23+
24+
// Port is the TCP port the HTTP server listens on.
25+
func Port() string {
26+
return env("PORT", "3000")
27+
}
28+
29+
// WebStaticPath is the directory of the built web frontend to serve. When
30+
// empty (the dev default) the server does not serve the frontend.
31+
func WebStaticPath() string {
32+
return os.Getenv("WEB_STATIC_PATH")
33+
}
34+
35+
// Go2RTCWSURL is the go2rtc WebSocket base URL the camera proxy relays to.
36+
func Go2RTCWSURL() string {
37+
return env("GO2RTC_WS_URL", "ws://localhost:1984")
38+
}
39+
40+
// Go2RTCAPIURL is the go2rtc HTTP API base URL used to manage streams.
41+
func Go2RTCAPIURL() string {
42+
return env("GO2RTC_API_URL", "http://localhost:1984")
43+
}
44+
45+
// VapidSubject is the "sub" claim (a mailto: or URL) sent with web push.
46+
func VapidSubject() string {
47+
return env("VAPID_SUBJECT", "crosshatch@bwees.io")
48+
}
49+
50+
// VapidPublicKey and VapidPrivateKey are the web-push VAPID keys. When either
51+
// is empty they are loaded from, or generated and persisted to, the database.
52+
func VapidPublicKey() string { return os.Getenv("VAPID_PUBLIC_KEY") }
53+
func VapidPrivateKey() string { return os.Getenv("VAPID_PRIVATE_KEY") }
54+
55+
// AllowedOrigins is the list of extra origins permitted for WebSocket upgrades,
56+
// beyond same-origin requests.
57+
func AllowedOrigins() []string {
58+
raw := os.Getenv("WS_ALLOWED_ORIGINS")
59+
if raw == "" {
60+
return nil
61+
}
62+
63+
var origins []string
64+
for _, o := range strings.Split(raw, ",") {
65+
if o = strings.TrimSpace(o); o != "" {
66+
origins = append(origins, o)
67+
}
1268
}
13-
return dir
69+
return origins
1470
}

server/internal/controllers/users.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package controllers
22

33
import (
4+
"context"
5+
46
"crosshatch/internal/dtos"
57
"crosshatch/internal/services"
68

@@ -11,8 +13,8 @@ type UsersController struct {
1113
svc *services.AuthService
1214
}
1315

14-
func (c *UsersController) requireAdmin(ctx fuego.ContextNoBody) error {
15-
user := userFromContext(ctx.Request().Context())
16+
func (c *UsersController) requireAdmin(ctx context.Context) error {
17+
user := userFromContext(ctx)
1618
if user == nil || !user.IsAdmin {
1719
return services.ErrForbidden
1820
}
@@ -23,7 +25,7 @@ func (c *UsersController) Register(api *fuego.Server) {
2325
route := fuego.Group(api, "/users")
2426

2527
fuego.Get(route, "/", func(ctx fuego.ContextNoBody) ([]dtos.UserDto, error) {
26-
if err := c.requireAdmin(ctx); err != nil {
28+
if err := c.requireAdmin(ctx.Request().Context()); err != nil {
2729
return nil, err
2830
}
2931

@@ -42,9 +44,8 @@ func (c *UsersController) Register(api *fuego.Server) {
4244
)
4345

4446
fuego.Post(route, "/", func(ctx fuego.ContextWithBody[dtos.CreateUserDto]) (dtos.UserDto, error) {
45-
user := userFromContext(ctx.Request().Context())
46-
if user == nil || !user.IsAdmin {
47-
return dtos.UserDto{}, services.ErrForbidden
47+
if err := c.requireAdmin(ctx.Request().Context()); err != nil {
48+
return dtos.UserDto{}, err
4849
}
4950

5051
dto, err := ctx.Body()
@@ -63,7 +64,7 @@ func (c *UsersController) Register(api *fuego.Server) {
6364
)
6465

6566
fuego.Delete(route, "/{id}", func(ctx fuego.ContextNoBody) (any, error) {
66-
if err := c.requireAdmin(ctx); err != nil {
67+
if err := c.requireAdmin(ctx.Request().Context()); err != nil {
6768
return nil, err
6869
}
6970

server/internal/dtos/notification.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
package dtos
22

3+
// NotificationEvent identifies a printer status change worth notifying about.
4+
type NotificationEvent string
5+
6+
const (
7+
EventComplete NotificationEvent = "complete"
8+
EventError NotificationEvent = "error"
9+
)
10+
311
type VapidDto struct {
412
PublicKey string `json:"publicKey" validate:"required"`
513
}

server/internal/dtos/printer_status.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ import (
88
// PrinterStage mirrors the numeric stage codes reported by the printer.
99
type PrinterStage int
1010

11+
// Gcode states reported by the printer in its "gcode_state" field.
12+
const (
13+
GcodeRunning = "RUNNING"
14+
GcodeFinish = "FINISH"
15+
GcodeFailed = "FAILED"
16+
)
17+
1118
type Temperature struct {
1219
Temperature float64 `json:"temperature" validate:"required"`
1320
TargetTemperature float64 `json:"targetTemperature" validate:"required"`

0 commit comments

Comments
 (0)