web

package
v0.5.5 Latest Latest
Warning

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

Go to latest
Published: Oct 26, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package web is the review portal 'jaira web' serves: a page in the browser for a person who wants to see and sort the work without an agent and without the terminal board.

There is no server in the deployment sense. The binary listens on loopback for as long as the command runs, and everything it shows comes from the same files and refs the CLI reads; sync stays git, a fetch in the background and a push after each change. That is also why this package holds no data of its own: a Source hands it the epics and the tickets, and the command that starts it decides where they come from.

The page is a Svelte build under ui/dist, committed and embedded, so a 'go install' without Node still ships it. CI rebuilds it from ui/src and fails when the committed files differ.

Index

Constants

View Source
const (
	LaneWork     = "work"
	LaneHuman    = "human"
	LaneTerminal = "terminal"
	LaneParking  = "parking"
)

The kinds a Lane has.

Variables

View Source
var (
	ErrNotFound = errors.New("not found")
	ErrExists   = errors.New("exists already")
	ErrInvalid  = errors.New("invalid")
	ErrConflict = errors.New("does not apply")
)

The errors a Source wraps so the handler answers with what went wrong rather than 500: no such epic or ticket, a name already taken or not usable, and a change that does not apply (a follow-up has no member line to take out).

Functions

func Listen

func Listen(port int) (net.Listener, error)

Listen opens a listener on loopback. port 0 picks a free one.

func NewLogger added in v0.5.5

func NewLogger(w io.Writer) *log.Logger

NewLogger writes to w with the date and time on every line.

func OpenBrowser

func OpenBrowser(url string) error

OpenBrowser opens url in the default browser and does not wait for it.

It is best effort: a machine with no browser (a container, an ssh session) still gets the address printed by the caller, which is the fallback a person can always act on.

func OpenLog added in v0.5.5

func OpenLog(path string) (*os.File, error)

OpenLog opens the log 'jaira web' writes at path, for appending.

The page runs where nobody watches a terminal, so stderr is not a record: a tab closed, a session detached, and what the process said on the way down is gone. The file is what is left to read afterwards.

Types

type Check

type Check struct {
	Text string `json:"text"`
	Done bool   `json:"done"`
}

Check is one item of a ticket's definition of done.

type Epic

type Epic struct {
	Name        string   `json:"name"`
	Title       string   `json:"title"`
	Description string   `json:"description"`
	Body        string   `json:"body"`
	Text        string   `json:"text"`
	Done        int      `json:"done"`
	Total       int      `json:"total"`
	Tickets     []Ticket `json:"tickets"`
}

Epic is one function of the product with its progress. Body is its markdown text as the file has it, for the editor; Text is the same as a person reads it, without the heading that repeats the title and without comments, for the page.

type EpicEdit added in v0.5.5

type EpicEdit struct {
	Title       *string     `json:"title"`
	Description *string     `json:"description"`
	Body        *string     `json:"body"`
	Base        *EpicFields `json:"base"`
}

EpicEdit rewrites an epic: a nil field stays as it is. Base is what the editor read; a field it changes that somebody else changed since is refused with ErrConflict, and nothing is written.

type EpicFields added in v0.5.5

type EpicFields struct {
	Title       string `json:"title"`
	Description string `json:"description"`
	Body        string `json:"body"`
}

EpicFields are what a person writes about an epic.

type Lane added in v0.5.5

type Lane struct {
	ID   string `json:"id"`
	Name string `json:"name"`
	Kind string `json:"kind"`
}

Lane is one lane of the board, in the board's order. Kind is what the page colours by beside the position: work, human (a person moves it on), terminal or parking.

type Note

type Note struct {
	At     time.Time `json:"at"`
	Author string    `json:"author"`
	Text   string    `json:"text"`
}

Note is one progress note; Text is markdown as its author wrote it.

type Rejection added in v0.5.5

type Rejection struct {
	Kind string    `json:"kind"`
	Why  string    `json:"why"`
	By   string    `json:"by,omitzero"`
	At   time.Time `json:"at,omitzero"`
}

Rejection is why nobody will do a ticket, as 'jaira reject' wrote it.

type Server

type Server struct {
	Source Source
	// Fetch brings the remote's refs into this clone; Push sends what Assign
	// queued. Either may be nil on a board with no remote.
	Fetch func() error
	Push  func() error
	// Log records every error answer, failed fetch or push, and panic, with
	// its time and cause. nil records nothing.
	Log *log.Logger
	// contains filtered or unexported fields
}

Server serves the API and the embedded page.

func (*Server) FetchNow

func (s *Server) FetchNow()

FetchNow runs one fetch and records its outcome. It is what the background loop calls every interval.

func (*Server) Handler

func (s *Server) Handler() http.Handler

Handler returns the routes: the JSON API under /api/ and the page for everything else.

Every request must name a loopback host. Binding to 127.0.0.1 keeps other machines out, but not a web page in the same browser: through DNS rebinding a foreign site can reach a port on loopback under its own host name. Checking Host refuses that, and checking Origin on writes refuses a cross-site form post.

func (*Server) Loop

func (s *Server) Loop(every time.Duration, stop <-chan struct{})

Loop fetches every interval until stop is closed. The first fetch runs at once, so the page opened right after the command shows today's state.

func (*Server) SyncState

func (s *Server) SyncState() Sync

SyncState returns a copy of the sync record.

type Source

type Source interface {
	Epics() ([]Epic, error)
	// Unassigned is the open tickets in no epic; with all set, every ticket
	// in no epic that ever was — finished, logged and archived ones too.
	Unassigned(all bool) ([]Ticket, error)
	Ticket(id string) (TicketDetail, error)
	Lanes() ([]Lane, error)
	CreateEpic(name, title, description string) error
	SetEpic(name string, edit EpicEdit) error
	// Assign puts a ticket into epic; with from set it takes it out of that
	// epic as well, which is a move.
	Assign(epic, from, ticketID string) error
	Unassign(epic, ticketID string) error
}

Source is where the page's data comes from and where a change goes.

The writes only record the change locally (and queue its ref); sending it is Push's job, so a remote that is down never makes the change itself fail.

type Sync

type Sync struct {
	LastFetch  time.Time `json:"last_fetch,omitzero"`
	FetchError string    `json:"fetch_error,omitzero"`
	LastPush   time.Time `json:"last_push,omitzero"`
	PushError  string    `json:"push_error,omitzero"`
}

Sync is what the page says about the last round trip to the remote.

type Ticket

type Ticket struct {
	ID     string `json:"id"`
	Handle string `json:"handle"`
	Title  string `json:"title"`
	Lane   string `json:"lane"`
	Done   bool   `json:"done"`
	// Direct is false for a ticket that belongs to an epic only because it
	// follows, or is a child of, one of its members.
	Direct bool `json:"direct"`
	// Place is where the ticket lies: board, ref, logbook or archive. A
	// filed ticket is shown, never dragged.
	Place string `json:"place"`
	// The fields the page filters by.
	Goal      string    `json:"goal,omitzero"`
	Tags      []string  `json:"tags,omitempty"`
	Creator   string    `json:"creator,omitzero"`
	Assignee  string    `json:"assignee,omitzero"`
	UpdatedBy string    `json:"updated_by,omitzero"`
	CreatedAt time.Time `json:"created_at,omitzero"`
	UpdatedAt time.Time `json:"updated_at,omitzero"`
	// FiledAt is the day it left the board: the logbook folder's date, or
	// for the archive the day it was rejected. Zero when nobody knows.
	FiledAt time.Time `json:"filed_at,omitzero"`
	// Rejected is the kind of no, for a ticket 'jaira reject' archived.
	Rejected string `json:"rejected,omitzero"`
}

Ticket is one card on the page.

type TicketDetail

type TicketDetail struct {
	ID      string  `json:"id"`
	Handle  string  `json:"handle"`
	Title   string  `json:"title"`
	Lane    string  `json:"lane"`
	Place   string  `json:"place"`
	Goal    string  `json:"goal"`
	Context string  `json:"context"`
	DoD     []Check `json:"dod"`
	Notes   []Note  `json:"notes"`
	// Rejected is nil for a ticket that was never rejected.
	Rejected *Rejection `json:"rejected,omitempty"`
}

TicketDetail is what the page shows of one ticket when it is opened.

Jump to

Keyboard shortcuts

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