signs

package
v0.0.0-...-ddef314 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 6, 2026 License: MIT Imports: 25 Imported by: 0

README

signs

Member-facing sign printer. Active members pick a template at /signs/{slug}, fill out the dynamic fields, and a worker delivers a rendered PDF to a network printer over IPP.

Pieces

  • module.go — module wiring, HTTP routes, workqueue (GetItem/ProcessItem/ UpdateItem), submission handlers, and the signs_config / signs_print_queue SQLite migration.
  • config.go — typed Config (printer host/port/queue + []Template) and FieldDef/Template types. Includes the seed DefaultMaintenanceTemplate.
  • render.goRenderSign executes a Go text/template body and renders a small markdown subset to a Letter-size PDF via go-pdf/fpdf.
  • ipp.goPrinter interface, NewIPPPrinter (raw IPP Print-Job request), and the noopPrinter fallback.
  • admin.go — leadership-only template editor at /admin/signs/templates/... with a live PDF preview endpoint, plus the templates panel embedded in the signs config page.
  • signs.templ / admin.templ — UI.

Behavior worth knowing

  • Auth gate. All /signs routes require an active member; non-active users get 403. Template editing requires leadership.
  • Worker. Polled at 1 Hz, rate-limited to printRPS = 1 job/sec. A separate hourly cleanup deletes rows older than printQueueTTL (1 hour).
  • Retry semantics. ProcessItem returns *RenderError for non-retryable failures (missing template, template execute error, PDF too large). The engine doesn't pass the error to UpdateItem, so failures currently bump attempts with exponential backoff (min(maxBackoffSeconds, 2^attempts), cap 1 h) and rely on the TTL cleanup to eventually drop bad rows.
  • Printer fallback. If printer_host or printer_queue is empty the module installs noopPrinter, which errors on every job so rows back off in-queue rather than being silently dropped. SetPrinter(nil) reinstalls the noop. reloadConfig will not overwrite a test-injected printer with a real IPP one unless real config is present.
  • Brother quirks (ipp.go). A custom brotherAdapter overrides the go-ipp library's hardcoded /printers/<queue> URL so the configured queue path is used verbatim (Brother exposes /ipp/print). printer-uri is set to the actual printer URI rather than localhost. job-priority and copies are deliberately not sent — Brother firmware silently drops jobs that include job-priority. IPP statuses in 0x0000–0x00FF are treated as success (RFC 8011 §15.1) even though go-ipp surfaces them as errors.
  • Submission limits. Per-member: ≤5 submits/min and ≤20 outstanding prints (HTTP 429 otherwise). Body capped at 64 KiB; per-field length ≤2000 chars; rendered PDF capped at 10 MiB.
  • Field storage. New rows store form values in fields_json; legacy machine_name/issue columns are still written empty for back-compat and read as a fallback by buildSignData / printRecord.FieldSummary.
  • Always-available template vars. {{.DiscordHandle}} (member's Discord handle, falling back to email local-part, then "unknown") and {{.Date}} (formatted print-creation time). Other variables come from the template's FieldDefs.
  • Markdown subset. # / ## / ### headings, ---/*** rules, -/* bullets, **bold** inline. Long unbreakable tokens are character-soft-wrapped to avoid right-margin overflow. Page has a Conway-green accent bar at the top.
  • Default template seeding. On first run (no signs_config row) the DefaultMaintenanceTemplate is inserted. Subsequent saves are honored verbatim, including an empty templates list — clearing the picker is treated as an intentional admin choice, not an error.
  • Preview endpoint (POST /admin/signs/preview) accepts both application/x-www-form-urlencoded and multipart/form-data; the editor's JS uses FormData, which is multipart. Sample values come from preview_<FieldName>, falling back to placeholder, then (<Label>).
  • Generated files. *_templ.go are produced by templ generate (go:generate directive in module.go); don't edit by hand.

Documentation

Overview

templ: version: v0.3.1001

templ: version: v0.3.1001

Index

Constants

This section is empty.

Variables

View Source
var DefaultMaintenanceTemplate = Template{
	Slug:        "maintenance",
	Name:        "Out of Service",
	Description: "Mark a machine or piece of equipment as out of service.",
	Orientation: "portrait",
	Body: `# OUT OF SERVICE
{{if .MachineName}}
## {{.MachineName}}
{{end}}
{{.Issue}}

---

Reported by **@{{.DiscordHandle}}**

{{.Date}}
`,
	FieldsJSON: mustMarshalFields([]FieldDef{
		{
			Name:        "MachineName",
			Label:       "Machine / equipment name",
			Placeholder: "e.g. Bambu Lab Printer 2",
			Required:    true,
		},
		{
			Name:        "Issue",
			Label:       "What's wrong? (1-2 sentences)",
			Placeholder: "Describe the issue clearly so the next person knows what's broken.",
			Required:    true,
			Multiline:   true,
		},
	}),
}

DefaultMaintenanceTemplate is the seed template installed on first run.

Functions

func RenderSign

func RenderSign(t Template, data SignData) ([]byte, error)

RenderSign executes the template's Go-text/template body against data, then converts the resulting markdown into a Letter-size PDF.

Types

type Config

type Config struct {
	PrinterHost  string     `` /* 146-byte string literal not displayed */
	PrinterPort  int        `json:"printer_port" config:"label=Printer Port,default=631,min=1,max=65535,section=printer"`
	PrinterQueue string     `` /* 146-byte string literal not displayed */
	Templates    []Template `json:"templates" config:"key=Slug"`
}

Config holds the signs module configuration.

type FieldDef

type FieldDef struct {
	// Name is the template variable name (e.g. "MachineName"). Must be a
	// valid Go template identifier (letters/digits/underscores, starts
	// with a letter).
	Name string `json:"name"`

	// Label is the human-readable label shown on the form.
	Label string `json:"label"`

	// Placeholder is optional hint text inside the input.
	Placeholder string `json:"placeholder,omitempty"`

	// Required marks the field as mandatory. The submit handler rejects
	// empty values for required fields.
	Required bool `json:"required,omitempty"`

	// Multiline renders the field as a <textarea> instead of a single-line
	// <input>.
	Multiline bool `json:"multiline,omitempty"`
}

FieldDef describes a user-facing form field that the template expects. Templates declare their own fields, and the sign form renders them dynamically. Field values are passed to the Go template body as {{.FieldName}}.

type Module

type Module struct {
	// contains filtered or unexported fields
}

func New

func New(db *sql.DB, eventLogger *engine.EventLogger) *Module

New creates the signs module. Until SetPrinter is called the module uses an internal noop printer that errors on every job.

func (*Module) AttachRoutes

func (m *Module) AttachRoutes(r *engine.Router)

AttachRoutes registers HTTP routes.

func (*Module) AttachWorkers

func (m *Module) AttachWorkers(mgr *engine.ProcMgr)

AttachWorkers registers background workers with the engine. Cleanup is registered first to mirror the discordwebhook module ordering.

func (*Module) ConfigSpec

func (m *Module) ConfigSpec() config.Spec

ConfigSpec returns the signs module configuration specification.

func (*Module) GetItem

func (m *Module) GetItem(ctx context.Context) (*queuedPrint, error)

func (*Module) ProcessItem

func (m *Module) ProcessItem(ctx context.Context, item *queuedPrint) error

func (*Module) ProcessOne

func (m *Module) ProcessOne(ctx context.Context) bool

ProcessOne runs one iteration of the workqueue (helper for tests). Returns true if an item was processed. Bypasses the rate limiter and the configChanged check used by the production poller.

func (*Module) SetConfigLoader

func (m *Module) SetConfigLoader(store *config.Store)

SetConfigLoader wires up typed config loading. Called once during app registration after the config registry has been populated.

func (*Module) SetPrinter

func (m *Module) SetPrinter(p Printer)

SetPrinter overrides the printer used by the worker. Intended for tests that want to inject a fake; production wires the IPP printer from config.

func (*Module) UpdateItem

func (m *Module) UpdateItem(ctx context.Context, item *queuedPrint, success bool) error

type PrintJob

type PrintJob struct {
	JobName string // Human-readable job name shown in printer queues.
	PDF     []byte // The PDF bytes to print.
}

PrintJob represents a single document to send to a printer.

type Printer

type Printer interface {
	Print(ctx context.Context, job PrintJob) error
}

Printer is an abstraction over a network print target. Used by the queue worker to deliver rendered sign PDFs. Tests inject a fake.

func NewIPPPrinter

func NewIPPPrinter(target PrinterTarget) Printer

NewIPPPrinter returns a Printer that delivers jobs to a network printer over IPP (port 631 by default).

Security note: this client speaks plain (unauthenticated) IPP and is only suitable for printers reachable on the lab's trusted LAN. The print queue host/port are configured by leadership through the admin UI; do not point it at anything routable from the public internet.

type PrinterTarget

type PrinterTarget struct {
	Host  string
	Port  int
	Queue string // IPP path on the printer (e.g. "ipp/print" for Brother, "printers/<name>" for CUPS).
}

PrinterTarget describes how to reach a network printer over IPP.

type RenderError

type RenderError struct{ Err error }

RenderError marks a non-retryable failure originating in the template/PDF pipeline. ProcessItem returns these via errors.Is so UpdateItem can drop the row instead of backing off.

func (*RenderError) Error

func (e *RenderError) Error() string

func (*RenderError) Unwrap

func (e *RenderError) Unwrap() error

type SignData

type SignData map[string]string

SignData is the variable bag passed to a sign template's body. It is a string map so that templates can define arbitrary custom fields. The keys "DiscordHandle" and "Date" are always present; other keys come from the template's FieldDef definitions.

type Template

type Template struct {
	Slug        string `json:"slug"`
	Name        string `json:"name"`
	Description string `json:"description"`
	Orientation string `json:"orientation"`
	Body        string `json:"body"`
	// FieldsJSON is the wire/storage form of the template's form-field
	// definitions: a JSON-encoded []FieldDef. It is no longer edited as
	// raw JSON in the admin UI — the dedicated template editor exposes a
	// structured fields editor that round-trips through this field.
	FieldsJSON string `json:"fields_json,omitempty"`
}

Template describes a single printable sign template.

Templates use Go text/template syntax over a markdown body. The following variables are always available:

  • {{.DiscordHandle}}: Discord username of the user who initiated the print
  • {{.Date}}: human-readable date the print was initiated

Additional variables are provided by the template's Fields definitions.

func (Template) ParsedFields

func (t Template) ParsedFields() []FieldDef

ParsedFields returns the FieldDef list parsed from the FieldsJSON string. Returns nil on empty or malformed JSON.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL