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
- Variables
- func Listen(port int) (net.Listener, error)
- func NewLogger(w io.Writer) *log.Logger
- func OpenBrowser(url string) error
- func OpenLog(path string) (*os.File, error)
- type Check
- type Epic
- type EpicEdit
- type EpicFields
- type Lane
- type Note
- type Rejection
- type Server
- type Source
- type Sync
- type Ticket
- type TicketDetail
Constants ¶
const ( LaneWork = "work" LaneHuman = "human" LaneTerminal = "terminal" LaneParking = "parking" )
The kinds a Lane has.
Variables ¶
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 OpenBrowser ¶
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.
Types ¶
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"`
// Signoff counts the tickets waiting in the signoff lane for a person to
// accept them; they are not in Done.
Signoff int `json:"signoff"`
Total int `json:"total"`
Tickets []Ticket `json:"tickets"`
// Closed is an epic 'jaira epic close' closed: it takes a ticket only
// with force, and the page hides it unless asked to show it.
Closed bool `json:"closed"`
}
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
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 ¶
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.
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. A closed epic refuses it with
// ErrConflict unless force is set, and stays closed when it is.
Assign(epic, from, ticketID string, force bool) error
Unassign(epic, ticketID string) error
// SetClosed closes an epic or opens it again. Closing one that still
// counts an unfinished ticket is refused with ErrConflict unless force
// is set.
SetClosed(epic string, closed, force bool) 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"`
// RejectedWhy is the reason 'jaira reject' gave, shown on the card.
RejectedWhy string `json:"rejected_why,omitzero"`
// BehindRef says the ticket file in this checkout is older than the
// ticket's ref, and what the page shows is the ref.
BehindRef bool `json:"behind_ref,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"`
// BehindRef: as on Ticket.
BehindRef bool `json:"behind_ref,omitzero"`
}
TicketDetail is what the page shows of one ticket when it is opened.