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:
2026-05-08 15:55:35 +02:00
parent 9290a0b2d0
commit 2a727eb7c1
10 changed files with 583 additions and 52 deletions
+112 -21
View File
@@ -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(&current); 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()