307 lines
7.1 KiB
Go
307 lines
7.1 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// cmdWork dispatches work subcommands.
|
|
func cmdWork(args []string, flags Flags) {
|
|
if len(args) == 0 {
|
|
fmt.Fprintln(os.Stderr, "usage: dev_console work <today|dashboard> [args]")
|
|
os.Exit(1)
|
|
}
|
|
sub := args[0]
|
|
|
|
switch sub {
|
|
case "today":
|
|
workToday(flags)
|
|
case "dashboard":
|
|
workDashboard(flags)
|
|
// v2 stubs
|
|
case "weekly", "search":
|
|
fmt.Fprintf(os.Stderr, "TODO v2: work %s not yet implemented\n", sub)
|
|
os.Exit(2)
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "unknown work subcommand: %s\n", sub)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
type workItem struct {
|
|
Kind string `json:"kind"`
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Prio string `json:"prio"`
|
|
Status string `json:"status"`
|
|
Next string `json:"next"`
|
|
}
|
|
|
|
// workToday shows a prioritized list of up to 10 items to work on today.
|
|
func workToday(flags Flags) {
|
|
root := mustRegistryRoot()
|
|
|
|
// Load issues
|
|
issues, err := LoadOpenIssues(root)
|
|
if err != nil {
|
|
fatalf("load issues: %v", err)
|
|
}
|
|
ComputeDepsResolved(issues)
|
|
|
|
// Load flows
|
|
flows, err := LoadAllFlows(root)
|
|
if err != nil {
|
|
fatalf("load flows: %v", err)
|
|
}
|
|
|
|
var items []workItem
|
|
|
|
// Priority order: alta > media > baja
|
|
prioPriority := map[string]int{
|
|
"alta": 0,
|
|
"media": 1,
|
|
"baja": 2,
|
|
"high": 0,
|
|
"medium": 1,
|
|
"low": 2,
|
|
"": 3,
|
|
}
|
|
|
|
statusPriority := map[string]int{
|
|
"in-progress": 0,
|
|
"pendiente": 1,
|
|
"pending": 1,
|
|
"bloqueado": 2,
|
|
"blocked": 2,
|
|
"deferred": 3,
|
|
}
|
|
|
|
// Filter issues: only pendiente or in-progress
|
|
for _, iss := range issues {
|
|
st := strings.ToLower(iss.Status)
|
|
if st == "completado" || st == "done" || st == "completed" || st == "deferred" {
|
|
continue
|
|
}
|
|
next := issueNext(iss)
|
|
items = append(items, workItem{
|
|
Kind: "issue",
|
|
ID: iss.ID,
|
|
Title: iss.Title,
|
|
Prio: iss.Priority,
|
|
Status: st,
|
|
Next: next,
|
|
})
|
|
}
|
|
|
|
// Add flows that are not completed
|
|
for _, fl := range flows {
|
|
st := strings.ToLower(fl.Status)
|
|
if st == "completado" || st == "done" || st == "completed" {
|
|
continue
|
|
}
|
|
next := flowNext(fl)
|
|
items = append(items, workItem{
|
|
Kind: "flow",
|
|
ID: fl.ID,
|
|
Title: fl.Name,
|
|
Prio: fl.Priority,
|
|
Status: st,
|
|
Next: next,
|
|
})
|
|
}
|
|
|
|
// Sort: first by status priority, then by prio priority, then by ID
|
|
sort.Slice(items, func(i, j int) bool {
|
|
si := statusPriority[items[i].Status]
|
|
sj := statusPriority[items[j].Status]
|
|
if si != sj {
|
|
return si < sj
|
|
}
|
|
pi := prioPriority[strings.ToLower(items[i].Prio)]
|
|
pj := prioPriority[strings.ToLower(items[j].Prio)]
|
|
if pi != pj {
|
|
return pi < pj
|
|
}
|
|
return items[i].ID < items[j].ID
|
|
})
|
|
|
|
// Take top 10
|
|
if len(items) > 10 {
|
|
items = items[:10]
|
|
}
|
|
|
|
if flags.JSON {
|
|
printJSON(items)
|
|
return
|
|
}
|
|
|
|
headers := []string{"KIND", "ID", "TITLE", "PRIO", "STATUS", "NEXT"}
|
|
var rows [][]string
|
|
for _, item := range items {
|
|
rows = append(rows, []string{
|
|
item.Kind,
|
|
item.ID,
|
|
truncate(item.Title, 40),
|
|
item.Prio,
|
|
item.Status,
|
|
truncate(item.Next, 30),
|
|
})
|
|
}
|
|
printTable(os.Stdout, headers, rows)
|
|
}
|
|
|
|
// issueNext returns a human-readable "next action" for an issue.
|
|
func issueNext(iss Issue) string {
|
|
st := strings.ToLower(iss.Status)
|
|
if st == "in-progress" {
|
|
return "iterate"
|
|
}
|
|
if !iss.DepsResolved {
|
|
return "blocked: resolve deps"
|
|
}
|
|
if iss.AcceptancePct == 0 {
|
|
return "implement (deps ok)"
|
|
}
|
|
if iss.AcceptancePct < 100 {
|
|
return fmt.Sprintf("implement (%d%% done)", iss.AcceptancePct)
|
|
}
|
|
return "review DoD"
|
|
}
|
|
|
|
// flowNext returns a human-readable "next action" for a flow.
|
|
func flowNext(fl Flow) string {
|
|
st := strings.ToLower(fl.Status)
|
|
if st == "in-progress" {
|
|
return "iterate"
|
|
}
|
|
if fl.DoDPct == 100 {
|
|
return "mark done"
|
|
}
|
|
if fl.UserFacingPct < 100 && fl.UserFacingPct > 0 {
|
|
return fmt.Sprintf("DoD user-facing %d%%", fl.UserFacingPct)
|
|
}
|
|
if fl.AcceptancePct < 100 {
|
|
return fmt.Sprintf("acceptance %d%%", fl.AcceptancePct)
|
|
}
|
|
return "check DoD"
|
|
}
|
|
|
|
// workDashboard prints a JSON dashboard for the work tab.
|
|
func workDashboard(flags Flags) {
|
|
root := mustRegistryRoot()
|
|
|
|
issues, err := LoadAllIssues(root)
|
|
if err != nil {
|
|
fatalf("load issues: %v", err)
|
|
}
|
|
ComputeDepsResolved(issues)
|
|
|
|
flows, err := LoadAllFlows(root)
|
|
if err != nil {
|
|
fatalf("load flows: %v", err)
|
|
}
|
|
|
|
// Build stats
|
|
type stats struct {
|
|
Total int `json:"total"`
|
|
Pendiente int `json:"pendiente"`
|
|
InProgress int `json:"in_progress"`
|
|
Bloqueado int `json:"bloqueado"`
|
|
Completado int `json:"completado"`
|
|
}
|
|
|
|
var ist stats
|
|
for _, iss := range issues {
|
|
ist.Total++
|
|
switch strings.ToLower(iss.Status) {
|
|
case "pendiente", "pending":
|
|
ist.Pendiente++
|
|
case "in-progress":
|
|
ist.InProgress++
|
|
case "bloqueado", "blocked":
|
|
ist.Bloqueado++
|
|
case "completado", "done", "completed":
|
|
ist.Completado++
|
|
}
|
|
}
|
|
|
|
var fst stats
|
|
for _, fl := range flows {
|
|
fst.Total++
|
|
switch strings.ToLower(fl.Status) {
|
|
case "pendiente", "pending":
|
|
fst.Pendiente++
|
|
case "in-progress":
|
|
fst.InProgress++
|
|
case "completado", "done", "completed":
|
|
fst.Completado++
|
|
}
|
|
}
|
|
|
|
// Top priority issues
|
|
var topIssues []Issue
|
|
for _, iss := range issues {
|
|
if iss.Priority == "alta" {
|
|
st := strings.ToLower(iss.Status)
|
|
if st != "completado" && st != "done" && st != "completed" && st != "deferred" {
|
|
topIssues = append(topIssues, iss)
|
|
}
|
|
}
|
|
}
|
|
sort.Slice(topIssues, func(i, j int) bool {
|
|
return topIssues[i].ID < topIssues[j].ID
|
|
})
|
|
if len(topIssues) > 10 {
|
|
topIssues = topIssues[:10]
|
|
}
|
|
|
|
type issueSlim struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Status string `json:"status"`
|
|
Type string `json:"type"`
|
|
Domain []string `json:"domain"`
|
|
Priority string `json:"priority"`
|
|
Depends []string `json:"depends"`
|
|
DepsResolved bool `json:"deps_resolved"`
|
|
AcceptancePct int `json:"acceptance_pct"`
|
|
}
|
|
type flowSlim struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
Pattern string `json:"pattern"`
|
|
Risk string `json:"risk"`
|
|
Priority string `json:"priority"`
|
|
Apps []string `json:"apps"`
|
|
AcceptancePct int `json:"acceptance_pct"`
|
|
DoDPct int `json:"dod_pct"`
|
|
UserFacingPct int `json:"user_facing_pct"`
|
|
}
|
|
slimIssues := make([]issueSlim, 0, len(topIssues))
|
|
for _, iss := range topIssues {
|
|
slimIssues = append(slimIssues, issueSlim{
|
|
ID: iss.ID, Title: iss.Title, Status: iss.Status, Type: iss.Type,
|
|
Domain: iss.Domain, Priority: iss.Priority, Depends: iss.Depends,
|
|
DepsResolved: iss.DepsResolved, AcceptancePct: iss.AcceptancePct,
|
|
})
|
|
}
|
|
slimFlows := make([]flowSlim, 0, len(flows))
|
|
for _, fl := range flows {
|
|
slimFlows = append(slimFlows, flowSlim{
|
|
ID: fl.ID, Name: fl.Name, Status: fl.Status, Pattern: fl.Pattern,
|
|
Risk: fl.Risk, Priority: fl.Priority, Apps: fl.Apps,
|
|
AcceptancePct: fl.AcceptancePct, DoDPct: fl.DoDPct, UserFacingPct: fl.UserFacingPct,
|
|
})
|
|
}
|
|
out := map[string]any{
|
|
"issue_stats": ist,
|
|
"flow_stats": fst,
|
|
"top_issues": slimIssues,
|
|
"flows": slimFlows,
|
|
}
|
|
printJSON(out)
|
|
}
|