chore: auto-commit (10 archivos)
- chat.log - db.go - frontend/src/App.tsx - frontend/src/api.ts - frontend/src/components/CardForm.tsx - frontend/src/components/Dashboard.tsx - frontend/src/components/KanbanCard.tsx - frontend/src/types.ts - handlers.go - metrics.go Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,10 @@ package main
|
||||
import (
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fn-registry/functions/core"
|
||||
@@ -25,21 +28,22 @@ type Column struct {
|
||||
}
|
||||
|
||||
type Card struct {
|
||||
ID string `json:"id"`
|
||||
Requester string `json:"requester"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Color string `json:"color"`
|
||||
ColumnID string `json:"column_id"`
|
||||
Position int `json:"position"`
|
||||
Locked bool `json:"locked"`
|
||||
AssigneeID *string `json:"assignee_id"`
|
||||
CompletedAt *string `json:"completed_at"`
|
||||
DeletedAt *string `json:"deleted_at"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
EnteredAt string `json:"entered_at"`
|
||||
TimeInColumn int64 `json:"time_in_column_ms"`
|
||||
ID string `json:"id"`
|
||||
Requester string `json:"requester"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Color string `json:"color"`
|
||||
ColumnID string `json:"column_id"`
|
||||
Position int `json:"position"`
|
||||
Locked bool `json:"locked"`
|
||||
AssigneeID *string `json:"assignee_id"`
|
||||
CompletedAt *string `json:"completed_at"`
|
||||
DeletedAt *string `json:"deleted_at"`
|
||||
Tags []string `json:"tags"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
EnteredAt string `json:"entered_at"`
|
||||
TimeInColumn int64 `json:"time_in_column_ms"`
|
||||
}
|
||||
|
||||
type HistoryEntry struct {
|
||||
@@ -101,6 +105,7 @@ func ensureColumns(conn *sql.DB) error {
|
||||
{"cards", "assignee_id", "TEXT"},
|
||||
{"cards", "completed_at", "TEXT"},
|
||||
{"cards", "deleted_at", "TEXT"},
|
||||
{"cards", "tags", "TEXT NOT NULL DEFAULT '[]'"},
|
||||
{"card_column_history", "actor_id", "TEXT"},
|
||||
{"card_lock_history", "actor_id", "TEXT"},
|
||||
}
|
||||
@@ -156,6 +161,81 @@ func newID() string {
|
||||
|
||||
func nowRFC3339() string { return time.Now().UTC().Format(time.RFC3339Nano) }
|
||||
|
||||
func parseTags(s string) []string {
|
||||
out := []string{}
|
||||
if s == "" {
|
||||
return out
|
||||
}
|
||||
if err := json.Unmarshal([]byte(s), &out); err != nil {
|
||||
return []string{}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeTags(in []string) []string {
|
||||
seen := map[string]struct{}{}
|
||||
out := []string{}
|
||||
for _, t := range in {
|
||||
t = strings.TrimSpace(t)
|
||||
if t == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[t]; ok {
|
||||
continue
|
||||
}
|
||||
seen[t] = struct{}{}
|
||||
out = append(out, t)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func encodeTags(in []string) string {
|
||||
b, _ := json.Marshal(normalizeTags(in))
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (db *DB) ListAllTags() ([]string, error) {
|
||||
rows, err := db.conn.Query(`SELECT DISTINCT tags FROM cards WHERE deleted_at IS NULL`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
seen := map[string]struct{}{}
|
||||
for rows.Next() {
|
||||
var s string
|
||||
if err := rows.Scan(&s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, t := range parseTags(s) {
|
||||
seen[t] = struct{}{}
|
||||
}
|
||||
}
|
||||
out := make([]string, 0, len(seen))
|
||||
for k := range seen {
|
||||
out = append(out, k)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (db *DB) ListDistinctRequesters() ([]string, error) {
|
||||
rows, err := db.conn.Query(`SELECT DISTINCT requester FROM cards WHERE deleted_at IS NULL AND requester != '' ORDER BY requester`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := []string{}
|
||||
for rows.Next() {
|
||||
var s string
|
||||
if err := rows.Scan(&s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, s)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func nullableActor(actorID string) any {
|
||||
if actorID == "" {
|
||||
return nil
|
||||
@@ -298,7 +378,7 @@ func (db *DB) ReorderColumns(ids []string) error {
|
||||
|
||||
func (db *DB) ListCardsWithTime() ([]Card, error) {
|
||||
rows, err := db.conn.Query(`
|
||||
SELECT c.id, c.requester, c.title, c.description, c.color, c.column_id, c.position, c.locked, c.assignee_id, c.completed_at, c.deleted_at, c.created_at, c.updated_at,
|
||||
SELECT c.id, c.requester, c.title, c.description, c.color, c.column_id, c.position, c.locked, c.assignee_id, c.completed_at, c.deleted_at, c.tags, c.created_at, c.updated_at,
|
||||
h.entered_at
|
||||
FROM cards c
|
||||
LEFT JOIN card_column_history h
|
||||
@@ -318,8 +398,9 @@ func (db *DB) ListCardsWithTime() ([]Card, error) {
|
||||
var assignee sql.NullString
|
||||
var completed sql.NullString
|
||||
var deleted sql.NullString
|
||||
var tagsJSON string
|
||||
var locked int
|
||||
if err := rows.Scan(&c.ID, &c.Requester, &c.Title, &c.Description, &c.Color, &c.ColumnID, &c.Position, &locked, &assignee, &completed, &deleted, &c.CreatedAt, &c.UpdatedAt, &entered); err != nil {
|
||||
if err := rows.Scan(&c.ID, &c.Requester, &c.Title, &c.Description, &c.Color, &c.ColumnID, &c.Position, &locked, &assignee, &completed, &deleted, &tagsJSON, &c.CreatedAt, &c.UpdatedAt, &entered); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Locked = locked != 0
|
||||
@@ -335,6 +416,7 @@ func (db *DB) ListCardsWithTime() ([]Card, error) {
|
||||
s := deleted.String
|
||||
c.DeletedAt = &s
|
||||
}
|
||||
c.Tags = parseTags(tagsJSON)
|
||||
if entered.Valid {
|
||||
c.EnteredAt = entered.String
|
||||
if t, err := time.Parse(time.RFC3339Nano, entered.String); err == nil {
|
||||
@@ -358,6 +440,7 @@ func (db *DB) CreateCard(columnID, requester, title, description, actorID string
|
||||
now := nowRFC3339()
|
||||
c := Card{
|
||||
ID: newID(), Requester: requester, Title: title, Description: description, ColumnID: columnID, Position: pos,
|
||||
Tags: []string{},
|
||||
CreatedAt: now, UpdatedAt: now, EnteredAt: now,
|
||||
}
|
||||
tx, err := db.conn.Begin()
|
||||
@@ -366,8 +449,8 @@ func (db *DB) CreateCard(columnID, requester, title, description, actorID string
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if _, err := tx.Exec(
|
||||
`INSERT INTO cards (id, requester, title, description, color, column_id, position, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
c.ID, c.Requester, c.Title, c.Description, c.Color, c.ColumnID, c.Position, c.CreatedAt, c.UpdatedAt,
|
||||
`INSERT INTO cards (id, requester, title, description, color, column_id, position, tags, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
c.ID, c.Requester, c.Title, c.Description, c.Color, c.ColumnID, c.Position, encodeTags(c.Tags), c.CreatedAt, c.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -402,6 +485,7 @@ type CardPatch struct {
|
||||
Locked *bool
|
||||
AssigneeID *string // empty string clears assignment
|
||||
HasAssignee bool // distinguishes "set to null" from "not provided"
|
||||
Tags *[]string
|
||||
}
|
||||
|
||||
func (db *DB) UpdateCard(id string, patch CardPatch) error {
|
||||
@@ -445,6 +529,11 @@ func (db *DB) UpdateCardWithActor(id string, patch CardPatch, actorID string) er
|
||||
}
|
||||
}
|
||||
}
|
||||
if patch.Tags != nil {
|
||||
if _, err := tx.Exec(`UPDATE cards SET tags=?, updated_at=? WHERE id=?`, encodeTags(*patch.Tags), nowRFC3339(), id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if patch.Locked != nil {
|
||||
var current int
|
||||
if err := tx.QueryRow(`SELECT locked FROM cards WHERE id=?`, id).Scan(¤t); err != nil {
|
||||
@@ -500,7 +589,7 @@ func (db *DB) PurgeCard(id string) error {
|
||||
// ListDeletedCards returns cards in the trash, newest first.
|
||||
func (db *DB) ListDeletedCards() ([]Card, error) {
|
||||
rows, err := db.conn.Query(`
|
||||
SELECT c.id, c.requester, c.title, c.description, c.color, c.column_id, c.position, c.locked, c.assignee_id, c.completed_at, c.deleted_at, c.created_at, c.updated_at
|
||||
SELECT c.id, c.requester, c.title, c.description, c.color, c.column_id, c.position, c.locked, c.assignee_id, c.completed_at, c.deleted_at, c.tags, c.created_at, c.updated_at
|
||||
FROM cards c
|
||||
WHERE c.deleted_at IS NOT NULL
|
||||
ORDER BY c.deleted_at DESC
|
||||
@@ -515,8 +604,9 @@ func (db *DB) ListDeletedCards() ([]Card, error) {
|
||||
var assignee sql.NullString
|
||||
var completed sql.NullString
|
||||
var deleted sql.NullString
|
||||
var tagsJSON string
|
||||
var locked int
|
||||
if err := rows.Scan(&c.ID, &c.Requester, &c.Title, &c.Description, &c.Color, &c.ColumnID, &c.Position, &locked, &assignee, &completed, &deleted, &c.CreatedAt, &c.UpdatedAt); err != nil {
|
||||
if err := rows.Scan(&c.ID, &c.Requester, &c.Title, &c.Description, &c.Color, &c.ColumnID, &c.Position, &locked, &assignee, &completed, &deleted, &tagsJSON, &c.CreatedAt, &c.UpdatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Locked = locked != 0
|
||||
@@ -532,6 +622,7 @@ func (db *DB) ListDeletedCards() ([]Card, error) {
|
||||
s := deleted.String
|
||||
c.DeletedAt = &s
|
||||
}
|
||||
c.Tags = parseTags(tagsJSON)
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, rows.Err()
|
||||
|
||||
Reference in New Issue
Block a user