Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ jobs:
- name: Install dependencies
run: go mod download

- name: Create frontend stub for embed
run: mkdir -p web/dist && touch web/dist/.gitkeep

- name: Lint
uses: golangci/golangci-lint-action@v7
with:
version: v2.10

# Build internal packages and cmd/server (excludes Wails app which needs web/dist)
- name: Build
run: |
Expand Down
105 changes: 105 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
version: "2"

run:
timeout: 5m

linters:
enable:
- errcheck
- govet
- staticcheck
- unused
- ineffassign
- gocritic
- misspell
- bodyclose
- durationcheck
- errname
- nilerr
- sqlclosecheck
- unconvert
- unparam
- wastedassign

settings:
errcheck:
check-type-assertions: false
check-blank: false
exclude-functions:
# Network cleanup — errors not actionable
- (net.Conn).Close
- (net.Conn).SetDeadline
- (net.Conn).SetReadDeadline
- (*net.UDPConn).Close
- (*net.UDPConn).SetDeadline
- (*net.UDPConn).SetReadDeadline
- (*net.UDPConn).WriteToUDP
- (*database/sql.DB).Close
- (*os.File).Close
- (io.Closer).Close
- (*io.PipeWriter).Close
- (*mime/multipart.Writer).Close
- (*net/http.Response).Body.Close
- os.Remove
- os.MkdirAll
- encoding/json.Unmarshal
- (*encoding/json.Encoder).Encode
- (*encoding/json.Decoder).Decode
gocritic:
enabled-tags:
- diagnostic
- performance
disabled-checks:
- hugeParam
- rangeValCopy
- ifElseChain
- filepathJoin
- exitAfterDefer
misspell:
locale: US

exclusions:
paths:
- web
- site
- build
- migrations
rules:
# Test files: relax errcheck and unparam
- path: _test\.go
linters:
- unparam
- errcheck
- gocritic
# Defer cleanup — error is not actionable
- linters:
- errcheck
source: "defer "
# Deprecated websocket library — tracked separately
- linters:
- staticcheck
text: "SA1019:"
# IPv6 hostport in printer discovery
- linters:
- govet
text: "hostport"
# "cancelled" is used in JSON struct tags and API responses
- linters:
- misspell
text: "(?i)cancelled"
# Best-effort event logging / audit trail
- linters:
- errcheck
source: "AddEvent|AppendEvent"
# Best-effort cleanup of OAuth state
- linters:
- errcheck
source: "DeleteOAuthState|UpdateLastSync|UpdateWebhookEventProcessed"
# Background printer connections (fire-and-forget goroutines)
- linters:
- errcheck
source: "go s\\.(manager|printerMgr)\\.Connect"
# Migration ALTER TABLE — intentionally ignores "column already exists"
- linters:
- errcheck
source: "// Ignore error if"
14 changes: 13 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help dev build run test clean frontend backend stop start restart show-version bump-patch bump-minor bump-major release site site-build
.PHONY: help dev build run test clean frontend backend stop start restart show-version bump-patch bump-minor bump-major release site site-build lint lint-go lint-web

# Version
VERSION := $(shell cat VERSION | tr -d 'v\n')
Expand All @@ -23,6 +23,9 @@ help:
@echo " make frontend - Run React frontend only"
@echo " make build - Build production binaries"
@echo " make test - Run tests"
@echo " make lint - Run all linters"
@echo " make lint-go - Run Go linter (golangci-lint)"
@echo " make lint-web - Run frontend linter (ESLint)"
@echo " make clean - Clean build artifacts"
@echo ""
@echo "Site:"
Expand Down Expand Up @@ -68,6 +71,15 @@ test-coverage:
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

# Linting
lint: lint-go lint-web

lint-go:
golangci-lint run ./...

lint-web:
cd web && npm run lint

# Cleanup
clean:
rm -rf bin/
Expand Down
5 changes: 0 additions & 5 deletions internal/api/dispatch_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,3 @@ func (h *DispatchHandler) UpdatePrinterSettings(w http.ResponseWriter, r *http.R

respondJSON(w, http.StatusOK, settings)
}

// PrintJobHandler extension for priority update
type printJobPriorityRequest struct {
Priority int `json:"priority"`
}
20 changes: 18 additions & 2 deletions internal/api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ func (h *DesignHandler) Download(w http.ResponseWriter, r *http.Request) {

w.Header().Set("Content-Disposition", "attachment; filename="+design.FileName)
w.Header().Set("Content-Type", "application/octet-stream")
io.Copy(w, reader)
io.Copy(w, reader) //nolint:errcheck // best-effort streaming to HTTP client
}

// OpenExternal opens a design file in an external application.
Expand Down Expand Up @@ -918,6 +918,22 @@ func (h *SpoolHandler) Get(w http.ResponseWriter, r *http.Request) {
respondJSON(w, http.StatusOK, spool)
}

// Delete deletes a spool by ID.
func (h *SpoolHandler) Delete(w http.ResponseWriter, r *http.Request) {
id, err := parseUUID(r, "id")
if err != nil {
respondError(w, http.StatusBadRequest, "invalid spool ID")
return
}

if err := h.service.Delete(r.Context(), id); err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}

w.WriteHeader(http.StatusNoContent)
}

// PrintJobHandler handles print job endpoints.
type PrintJobHandler struct {
service *service.PrintJobService
Expand Down Expand Up @@ -1396,7 +1412,7 @@ func (h *FileHandler) Get(w http.ResponseWriter, r *http.Request) {

w.Header().Set("Content-Disposition", "attachment; filename="+file.OriginalName)
w.Header().Set("Content-Type", file.ContentType)
io.Copy(w, reader)
io.Copy(w, reader) //nolint:errcheck // best-effort streaming to HTTP client
}

// ExpenseHandler handles expense endpoints.
Expand Down
1 change: 1 addition & 0 deletions internal/api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ func NewRouter(services *service.Services, hub *realtime.Hub) http.Handler {
r.Get("/", spoolHandler.List)
r.Post("/", spoolHandler.Create)
r.Get("/{id}", spoolHandler.Get)
r.Delete("/{id}", spoolHandler.Delete)
})

// Print Jobs
Expand Down
8 changes: 0 additions & 8 deletions internal/api/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,6 @@ func (m *MockTemplateService) CreateProjectFromTemplate(ctx context.Context, tem
return project, []model.PrintJob{}, nil
}

// Test helper to create a template handler with mock service
func setupTemplateHandler() (*TemplateHandler, *MockTemplateService) {
mock := NewMockTemplateService()
// We need to wrap the mock in a real service struct for the handler
// For now, we'll test via HTTP handlers directly
return nil, mock
}

func TestTemplateHandler_Create(t *testing.T) {
// Create a test template
template := model.Template{
Expand Down
4 changes: 2 additions & 2 deletions internal/database/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ CREATE INDEX IF NOT EXISTS idx_projects_sku ON projects(sku);
-- Tasks table (Work Instances - created when processing orders)
CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL REFERENCES projects(id),
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
order_id TEXT REFERENCES orders(id),
order_item_id TEXT REFERENCES order_items(id),
name TEXT NOT NULL,
Expand Down Expand Up @@ -816,7 +816,7 @@ CREATE TABLE IF NOT EXISTS quotes (
CREATE INDEX IF NOT EXISTS idx_quotes_status ON quotes(status);
CREATE INDEX IF NOT EXISTS idx_quotes_customer ON quotes(customer_id);
CREATE INDEX IF NOT EXISTS idx_quotes_quote_number ON quotes(quote_number);
CREATE UNIQUE INDEX IF NOT EXISTS idx_quotes_share_token ON quotes(share_token);
-- idx_quotes_share_token is created in sqlite.go after ALTER TABLE adds the column for existing databases

CREATE TABLE IF NOT EXISTS quote_options (
id TEXT PRIMARY KEY,
Expand Down
2 changes: 1 addition & 1 deletion internal/database/sqlite.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func RunMigrations(db *sql.DB) error {
}

// Create indexes that may not exist
db.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_quotes_share_token ON quotes(share_token)`)
db.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_quotes_share_token ON quotes(share_token)`) //nolint:errcheck // best-effort index creation

return nil
}
Expand Down
4 changes: 2 additions & 2 deletions internal/printer/bambu.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ func (c *BambuClient) requestPushAll() {
Command: "pushall",
},
}
c.sendCommand(cmd)
c.sendCommand(cmd) //nolint:errcheck // fire-and-forget status refresh
}

// sendCommand sends a command to the printer via MQTT.
Expand Down Expand Up @@ -415,7 +415,7 @@ func (c *BambuClient) uploadFile(localPath string, remoteName string) (string, e
remoteDir := "/cache"
if err := conn.ChangeDir(remoteDir); err != nil {
remoteDir = "/"
conn.ChangeDir(remoteDir)
conn.ChangeDir(remoteDir) //nolint:errcheck // fallback to root dir
}

// Upload file
Expand Down
12 changes: 6 additions & 6 deletions internal/printer/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ func (d *Discovery) checkChiTu(ctx context.Context, host string, port int) *Disc
strings.Contains(bodyLower, "msla")

if isChiTu {
name := "Resin Printer"
var name string
manufacturer := "Unknown"

// Try to identify manufacturer
Expand Down Expand Up @@ -368,7 +368,7 @@ func (d *Discovery) probeBambuUDPUnicast(host string) *bambuUDPInfo {
}
defer conn.Close()

conn.SetDeadline(time.Now().Add(3 * time.Second))
conn.SetDeadline(time.Now().Add(3 * time.Second)) //nolint:errcheck // best-effort deadline

if _, err := conn.Write([]byte("M99999")); err != nil {
slog.Debug("Bambu UDP unicast write failed", "host", host, "error", err)
Expand Down Expand Up @@ -398,7 +398,7 @@ func (d *Discovery) probeBambuUDPBroadcast(targetHost string) *bambuUDPInfo {
}
defer conn.Close()

conn.SetDeadline(time.Now().Add(4 * time.Second))
conn.SetDeadline(time.Now().Add(4 * time.Second)) //nolint:errcheck // best-effort deadline

// Send broadcast discovery
for i := 0; i < 2; i++ {
Expand Down Expand Up @@ -543,7 +543,7 @@ func (d *Discovery) ScanSSDPBambu(ctx context.Context) ([]DiscoveredPrinter, err
}

// sendSSDPSearch sends an SSDP M-SEARCH and collects Bambu responses.
func (d *Discovery) sendSSDPSearch(ctx context.Context, searchTarget string) ([]DiscoveredPrinter, error) {
func (d *Discovery) sendSSDPSearch(_ context.Context, searchTarget string) ([]DiscoveredPrinter, error) {
var printers []DiscoveredPrinter

// SSDP M-SEARCH request
Expand Down Expand Up @@ -574,7 +574,7 @@ func (d *Discovery) sendSSDPSearch(ctx context.Context, searchTarget string) ([]
}

// Set read deadline
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
conn.SetReadDeadline(time.Now().Add(5 * time.Second)) //nolint:errcheck // best-effort deadline

// Read responses
buffer := make([]byte, 4096)
Expand Down Expand Up @@ -705,7 +705,7 @@ func (d *Discovery) ScanBambuUDP(ctx context.Context) ([]DiscoveredPrinter, erro
}

// Set timeout
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
conn.SetReadDeadline(time.Now().Add(5 * time.Second)) //nolint:errcheck // best-effort deadline

// Read responses
buffer := make([]byte, 4096)
Expand Down
4 changes: 2 additions & 2 deletions internal/printer/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func (m *Manager) Disconnect(id uuid.UUID) {
defer m.mu.Unlock()

if client, ok := m.clients[id]; ok {
client.Disconnect()
client.Disconnect() //nolint:errcheck // best-effort cleanup
delete(m.clients, id)
}
delete(m.states, id)
Expand All @@ -178,7 +178,7 @@ func (m *Manager) DisconnectAll() {

for id, client := range m.clients {
slog.Info("disconnecting printer", "printer_id", id)
client.Disconnect()
client.Disconnect() //nolint:errcheck // best-effort shutdown cleanup
delete(m.clients, id)
}
// Clear all states
Expand Down
10 changes: 4 additions & 6 deletions internal/printer/moonraker.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,9 @@ func (c *MoonrakerClient) GetStatus() (*model.PrinterState, error) {
// Get printer status
resp, err := c.doRequest("GET", "/printer/objects/query?print_stats&extruder&heater_bed", nil)
if err != nil {
return &model.PrinterState{
PrinterID: c.printerID,
Status: model.PrinterStatusOffline,
UpdatedAt: time.Now(),
}, nil
// Connection failure means the printer is offline, not an application error
offlineState := &model.PrinterState{PrinterID: c.printerID, Status: model.PrinterStatusOffline, UpdatedAt: time.Now()}
return offlineState, nil //nolint:nilerr
}

state := c.parseState(resp)
Expand Down Expand Up @@ -110,7 +108,7 @@ func (c *MoonrakerClient) SetStatusCallback(cb func(*model.PrinterState)) {
}

// doRequest performs an HTTP request to the Moonraker API.
func (c *MoonrakerClient) doRequest(method string, path string, body []byte) ([]byte, error) {
func (c *MoonrakerClient) doRequest(method string, path string, body []byte) ([]byte, error) { //nolint:unparam // body kept for future POST/PUT support
var bodyReader io.Reader
if body != nil {
bodyReader = bytes.NewReader(body)
Expand Down
8 changes: 3 additions & 5 deletions internal/printer/octoprint.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,9 @@ func (c *OctoPrintClient) GetStatus() (*model.PrinterState, error) {
// Get printer state
printerResp, err := c.doRequest("GET", "/api/printer", nil)
if err != nil {
return &model.PrinterState{
PrinterID: c.printerID,
Status: model.PrinterStatusOffline,
UpdatedAt: time.Now(),
}, nil
// Connection failure means the printer is offline, not an application error
offlineState := &model.PrinterState{PrinterID: c.printerID, Status: model.PrinterStatusOffline, UpdatedAt: time.Now()}
return offlineState, nil //nolint:nilerr
}

// Get job state
Expand Down
Loading
Loading