chore: auto-commit (23 archivos)

- app.md
- backend/auth.go
- backend/db.go
- backend/dist/assets/index-CPqSy0gZ.js
- backend/dist/index.html
- backend/handlers.go
- backend/main.go
- frontend/src/App.tsx
- frontend/src/api.ts
- frontend/src/components/KanbanCard.tsx
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 18:40:22 +02:00
parent f1ee116d3b
commit a34a8142cc
23 changed files with 2034 additions and 1184 deletions
+168
View File
@@ -1031,3 +1031,171 @@ func (db *DB) CardHistory(cardID string) (*CardHistoryResponse, error) {
CurrentlyLock: currently,
}, nil
}
type CardMessage struct {
ID string `json:"id"`
CardID string `json:"card_id"`
AuthorID *string `json:"author_id"`
Body string `json:"body"`
CreatedAt string `json:"created_at"`
}
func (db *DB) ListCardMessages(cardID string) ([]CardMessage, error) {
rows, err := db.conn.Query(
`SELECT id, card_id, author_id, body, created_at FROM card_messages WHERE card_id=? ORDER BY created_at`,
cardID,
)
if err != nil {
return nil, err
}
defer rows.Close()
out := []CardMessage{}
for rows.Next() {
var m CardMessage
var author sql.NullString
if err := rows.Scan(&m.ID, &m.CardID, &author, &m.Body, &m.CreatedAt); err != nil {
return nil, err
}
if author.Valid && author.String != "" {
s := author.String
m.AuthorID = &s
}
out = append(out, m)
}
return out, rows.Err()
}
func (db *DB) CreateCardMessage(cardID, authorID, body string) (*CardMessage, error) {
body = strings.TrimSpace(body)
if body == "" {
return nil, fmt.Errorf("body required")
}
if authorID == "" {
return nil, fmt.Errorf("author required")
}
var exists int
if err := db.conn.QueryRow(`SELECT 1 FROM cards WHERE id=?`, cardID).Scan(&exists); err != nil {
return nil, fmt.Errorf("card not found: %w", err)
}
s := authorID
m := &CardMessage{ID: newID(), CardID: cardID, AuthorID: &s, Body: body, CreatedAt: nowRFC3339()}
if _, err := db.conn.Exec(
`INSERT INTO card_messages (id, card_id, author_id, body, created_at) VALUES (?, ?, ?, ?, ?)`,
m.ID, m.CardID, authorID, m.Body, m.CreatedAt,
); err != nil {
return nil, err
}
return m, nil
}
func (db *DB) DeleteCardMessage(id, requesterID string) error {
if requesterID == "" {
return fmt.Errorf("session required")
}
res, err := db.conn.Exec(`DELETE FROM card_messages WHERE id=? AND author_id=?`, id, requesterID)
if err != nil {
return err
}
n, _ := res.RowsAffected()
if n == 0 {
return fmt.Errorf("not found or not author")
}
return nil
}
// DuplicateCard clones a card into the same column at the end of the list.
// Copies title, description, color, requester, assignee, tags, deadline, stickers.
// Does NOT copy card_column_history, card_lock_history, card_events, card_messages.
// Title gets " (copia)" suffix.
func (db *DB) DuplicateCard(srcID, actorID string) (*Card, error) {
tx, err := db.conn.Begin()
if err != nil {
return nil, err
}
defer tx.Rollback()
var src Card
var assignee sql.NullString
var deadline sql.NullString
var tagsJSON, stickersJSON string
if err := tx.QueryRow(
`SELECT requester, title, description, color, column_id, assignee_id, tags, stickers, deadline
FROM cards WHERE id=? AND deleted_at IS NULL`, srcID,
).Scan(&src.Requester, &src.Title, &src.Description, &src.Color, &src.ColumnID, &assignee, &tagsJSON, &stickersJSON, &deadline); err != nil {
return nil, fmt.Errorf("card not found: %w", err)
}
if assignee.Valid && assignee.String != "" {
s := assignee.String
src.AssigneeID = &s
}
if deadline.Valid && deadline.String != "" {
s := deadline.String
src.Deadline = &s
}
src.Tags = parseTags(tagsJSON)
src.Stickers = parseStickers(stickersJSON)
var maxPos sql.NullInt64
if err := tx.QueryRow(`SELECT MAX(position) FROM cards WHERE column_id=?`, src.ColumnID).Scan(&maxPos); err != nil {
return nil, err
}
pos := 0
if maxPos.Valid {
pos = int(maxPos.Int64) + 1
}
var maxSeq sql.NullInt64
if err := tx.QueryRow(`SELECT MAX(seq_num) FROM cards`).Scan(&maxSeq); err != nil {
return nil, err
}
seqNum := 1
if maxSeq.Valid {
seqNum = int(maxSeq.Int64) + 1
}
now := nowRFC3339()
newTitle := src.Title + " (copia)"
c := Card{
ID: newID(), SeqNum: seqNum, Requester: src.Requester, Title: newTitle,
Description: src.Description, Color: src.Color, ColumnID: src.ColumnID, Position: pos,
AssigneeID: src.AssigneeID, Tags: src.Tags, Stickers: src.Stickers, Deadline: src.Deadline,
CreatedAt: now, UpdatedAt: now, EnteredAt: now,
}
var assigneeVal any
if c.AssigneeID != nil && *c.AssigneeID != "" {
assigneeVal = *c.AssigneeID
}
var deadlineVal any
if c.Deadline != nil && *c.Deadline != "" {
deadlineVal = *c.Deadline
}
if _, err := tx.Exec(
`INSERT INTO cards (id, seq_num, requester, title, description, color, column_id, position, assignee_id, tags, stickers, deadline, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
c.ID, c.SeqNum, c.Requester, c.Title, c.Description, c.Color, c.ColumnID, c.Position,
assigneeVal, encodeTags(c.Tags), encodeStickers(c.Stickers), deadlineVal, c.CreatedAt, c.UpdatedAt,
); err != nil {
return nil, err
}
if _, err := tx.Exec(
`INSERT INTO card_column_history (id, card_id, column_id, entered_at, actor_id) VALUES (?, ?, ?, ?, ?)`,
newID(), c.ID, c.ColumnID, now, nullableActor(actorID),
); err != nil {
return nil, err
}
var destDone int
if err := tx.QueryRow(`SELECT is_done FROM columns WHERE id=?`, c.ColumnID).Scan(&destDone); err != nil {
return nil, err
}
if destDone == 1 {
if _, err := tx.Exec(`UPDATE cards SET completed_at=? WHERE id=?`, now, c.ID); err != nil {
return nil, err
}
c.CompletedAt = &now
}
if err := insertCardEvent(tx, c.ID, "created", actorID, map[string]any{"title": newTitle, "column_id": c.ColumnID, "duplicated_from": srcID}); err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
return &c, nil
}