chore: auto-commit (21 archivos)
- app.md - backend/dist/assets/index-D_Kep7Fb.js - backend/dist/index.html - backend/handlers.go - backend/main.go - frontend/src/App.tsx - frontend/src/api.ts - frontend/src/components/CardChatPanel.tsx - frontend/src/components/LoginPage.tsx - frontend/src/types.ts - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+85
-34
@@ -74,8 +74,21 @@ func handleGetBoard(db *DB) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// publishInvalidated emits a board.invalidated event so connected clients
|
||||
// refetch /api/board. Best-effort: dropped events recover on next mutation
|
||||
// or via the periodic safety reload kept in the SPA.
|
||||
func publishInvalidated(hub *EventHub, cardID, columnID string) {
|
||||
if hub == nil {
|
||||
return
|
||||
}
|
||||
hub.PublishJSON("board.invalidated", cardID, "", map[string]string{
|
||||
"card_id": cardID,
|
||||
"column_id": columnID,
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/columns { name }
|
||||
func handleCreateColumn(db *DB) http.HandlerFunc {
|
||||
func handleCreateColumn(db *DB, hub *EventHub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct{ Name string `json:"name"` }
|
||||
if err := infra.HTTPParseBody(r, &body, maxBodyBytes); err != nil {
|
||||
@@ -91,12 +104,13 @@ func handleCreateColumn(db *DB) http.HandlerFunc {
|
||||
serverError(w, err)
|
||||
return
|
||||
}
|
||||
publishInvalidated(hub, "", c.ID)
|
||||
infra.HTTPJSONResponse(w, http.StatusCreated, c)
|
||||
}
|
||||
}
|
||||
|
||||
// PATCH /api/columns/{id} { name?, position?, location?, width? }
|
||||
func handleUpdateColumn(db *DB) http.HandlerFunc {
|
||||
func handleUpdateColumn(db *DB, hub *EventHub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
var body struct {
|
||||
@@ -116,24 +130,26 @@ func handleUpdateColumn(db *DB) http.HandlerFunc {
|
||||
serverError(w, err)
|
||||
return
|
||||
}
|
||||
publishInvalidated(hub, "", id)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/columns/{id}
|
||||
func handleDeleteColumn(db *DB) http.HandlerFunc {
|
||||
func handleDeleteColumn(db *DB, hub *EventHub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
if err := db.DeleteColumn(id); err != nil {
|
||||
serverError(w, err)
|
||||
return
|
||||
}
|
||||
publishInvalidated(hub, "", id)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/columns/reorder { ids: [...] }
|
||||
func handleReorderColumns(db *DB) http.HandlerFunc {
|
||||
func handleReorderColumns(db *DB, hub *EventHub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct{ IDs []string `json:"ids"` }
|
||||
if err := infra.HTTPParseBody(r, &body, maxBodyBytes); err != nil {
|
||||
@@ -144,12 +160,13 @@ func handleReorderColumns(db *DB) http.HandlerFunc {
|
||||
serverError(w, err)
|
||||
return
|
||||
}
|
||||
publishInvalidated(hub, "", "")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/cards { column_id, requester?, title, description? }
|
||||
func handleCreateCard(db *DB) http.HandlerFunc {
|
||||
func handleCreateCard(db *DB, hub *EventHub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
ColumnID string `json:"column_id"`
|
||||
@@ -186,12 +203,13 @@ func handleCreateCard(db *DB) http.HandlerFunc {
|
||||
serverError(w, err)
|
||||
return
|
||||
}
|
||||
publishInvalidated(hub, c.ID, body.ColumnID)
|
||||
infra.HTTPJSONResponse(w, http.StatusCreated, c)
|
||||
}
|
||||
}
|
||||
|
||||
// PATCH /api/cards/{id} { requester?, title?, description?, color? }
|
||||
func handleUpdateCard(db *DB) http.HandlerFunc {
|
||||
func handleUpdateCard(db *DB, hub *EventHub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
var raw map[string]any
|
||||
@@ -249,12 +267,13 @@ func handleUpdateCard(db *DB) http.HandlerFunc {
|
||||
serverError(w, err)
|
||||
return
|
||||
}
|
||||
publishInvalidated(hub, id, "")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/cards/{id}/stickers { stickers: [{emoji,x,y}, ...] }
|
||||
func handleUpdateCardStickers(db *DB) http.HandlerFunc {
|
||||
func handleUpdateCardStickers(db *DB, hub *EventHub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
var body struct {
|
||||
@@ -268,12 +287,13 @@ func handleUpdateCardStickers(db *DB) http.HandlerFunc {
|
||||
serverError(w, err)
|
||||
return
|
||||
}
|
||||
publishInvalidated(hub, id, "")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/cards/{id}
|
||||
func handleDeleteCard(db *DB) http.HandlerFunc {
|
||||
func handleDeleteCard(db *DB, hub *EventHub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
actor, _ := infra.UserIDFromContext(r.Context(), userCtxKey)
|
||||
@@ -281,12 +301,13 @@ func handleDeleteCard(db *DB) http.HandlerFunc {
|
||||
serverError(w, err)
|
||||
return
|
||||
}
|
||||
publishInvalidated(hub, id, "")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/cards/{id}/move { column_id, ordered_ids }
|
||||
func handleMoveCard(db *DB) http.HandlerFunc {
|
||||
func handleMoveCard(db *DB, hub *EventHub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
var body struct {
|
||||
@@ -310,6 +331,7 @@ func handleMoveCard(db *DB) http.HandlerFunc {
|
||||
serverError(w, err)
|
||||
return
|
||||
}
|
||||
publishInvalidated(hub, id, body.ColumnID)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
@@ -328,7 +350,10 @@ func handleListCardMessages(db *DB) http.HandlerFunc {
|
||||
}
|
||||
|
||||
// POST /api/cards/{id}/messages { body }
|
||||
func handleCreateCardMessage(db *DB) http.HandlerFunc {
|
||||
//
|
||||
// Parses @mentions, fans out notifications and publishes message.created via
|
||||
// the hub so SSE/WS subscribers see the message immediately.
|
||||
func handleCreateCardMessage(db *DB, hub *EventHub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
var body struct {
|
||||
@@ -347,7 +372,7 @@ func handleCreateCardMessage(db *DB) http.HandlerFunc {
|
||||
infra.HTTPErrorResponse(w, infra.HTTPError{Status: http.StatusUnauthorized, Code: "unauthorized", Message: "session required"})
|
||||
return
|
||||
}
|
||||
m, err := db.CreateCardMessage(id, actor, body.Body)
|
||||
m, _, _, err := db.CreateCardMessageAndNotify(id, actor, body.Body, hub)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
notFound(w, err.Error())
|
||||
@@ -361,8 +386,9 @@ func handleCreateCardMessage(db *DB) http.HandlerFunc {
|
||||
}
|
||||
|
||||
// DELETE /api/cards/{cid}/messages/{mid}
|
||||
func handleDeleteCardMessage(db *DB) http.HandlerFunc {
|
||||
func handleDeleteCardMessage(db *DB, hub *EventHub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
cid := r.PathValue("id")
|
||||
mid := r.PathValue("mid")
|
||||
actor, _ := infra.UserIDFromContext(r.Context(), userCtxKey)
|
||||
if actor == "" {
|
||||
@@ -377,12 +403,15 @@ func handleDeleteCardMessage(db *DB) http.HandlerFunc {
|
||||
serverError(w, err)
|
||||
return
|
||||
}
|
||||
if hub != nil {
|
||||
hub.PublishJSON("message.deleted", cid, "", map[string]string{"id": mid})
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/cards/{id}/duplicate
|
||||
func handleDuplicateCard(db *DB) http.HandlerFunc {
|
||||
func handleDuplicateCard(db *DB, hub *EventHub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
actor, _ := infra.UserIDFromContext(r.Context(), userCtxKey)
|
||||
@@ -395,6 +424,7 @@ func handleDuplicateCard(db *DB) http.HandlerFunc {
|
||||
serverError(w, err)
|
||||
return
|
||||
}
|
||||
publishInvalidated(hub, c.ID, c.ColumnID)
|
||||
infra.HTTPJSONResponse(w, http.StatusCreated, c)
|
||||
}
|
||||
}
|
||||
@@ -425,7 +455,7 @@ func handleListTrash(db *DB) http.HandlerFunc {
|
||||
}
|
||||
|
||||
// POST /api/cards/{id}/restore
|
||||
func handleRestoreCard(db *DB) http.HandlerFunc {
|
||||
func handleRestoreCard(db *DB, hub *EventHub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
actor, _ := infra.UserIDFromContext(r.Context(), userCtxKey)
|
||||
@@ -433,6 +463,7 @@ func handleRestoreCard(db *DB) http.HandlerFunc {
|
||||
serverError(w, err)
|
||||
return
|
||||
}
|
||||
publishInvalidated(hub, id, "")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
@@ -553,44 +584,48 @@ func handleListArchive(db *DB) http.HandlerFunc {
|
||||
}
|
||||
|
||||
// POST /api/cards/{id}/archive
|
||||
func handleArchiveCard(db *DB) http.HandlerFunc {
|
||||
func handleArchiveCard(db *DB, hub *EventHub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
if err := db.ArchiveCard(id); err != nil {
|
||||
serverError(w, err)
|
||||
return
|
||||
}
|
||||
publishInvalidated(hub, id, "")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/cards/{id}/unarchive
|
||||
func handleUnarchiveCard(db *DB) http.HandlerFunc {
|
||||
func handleUnarchiveCard(db *DB, hub *EventHub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
if err := db.UnarchiveCard(id); err != nil {
|
||||
serverError(w, err)
|
||||
return
|
||||
}
|
||||
publishInvalidated(hub, id, "")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/cards/{id}/purge
|
||||
func handlePurgeCard(db *DB) http.HandlerFunc {
|
||||
func handlePurgeCard(db *DB, hub *EventHub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
if err := db.PurgeCard(id); err != nil {
|
||||
serverError(w, err)
|
||||
return
|
||||
}
|
||||
publishInvalidated(hub, id, "")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
func apiRoutes(db *DB, chatWorkdir string, logger *ChatLogger, internalToken string, flags *FeatureFlags) []infra.Route {
|
||||
func apiRoutes(db *DB, chatWorkdir string, logger *ChatLogger, internalToken string, flags *FeatureFlags, hub *EventHub) []infra.Route {
|
||||
return []infra.Route{
|
||||
{Method: "GET", Path: "/api/flags", Handler: handleListFlags(flags)},
|
||||
{Method: "GET", Path: "/api/version", Handler: handleVersion()},
|
||||
{Method: "POST", Path: "/api/auth/register", Handler: handleRegister(db, flags)},
|
||||
{Method: "POST", Path: "/api/auth/login", Handler: handleLogin(db)},
|
||||
{Method: "POST", Path: "/api/auth/logout", Handler: handleLogout(db)},
|
||||
@@ -598,37 +633,53 @@ func apiRoutes(db *DB, chatWorkdir string, logger *ChatLogger, internalToken str
|
||||
{Method: "PATCH", Path: "/api/me", Handler: handlePatchMe(db)},
|
||||
{Method: "GET", Path: "/api/users", Handler: handleListUsers(db)},
|
||||
{Method: "GET", Path: "/api/board", Handler: handleGetBoard(db)},
|
||||
{Method: "POST", Path: "/api/columns", Handler: handleCreateColumn(db)},
|
||||
{Method: "POST", Path: "/api/columns/reorder", Handler: handleReorderColumns(db)},
|
||||
{Method: "PATCH", Path: "/api/columns/{id}", Handler: handleUpdateColumn(db)},
|
||||
{Method: "DELETE", Path: "/api/columns/{id}", Handler: handleDeleteColumn(db)},
|
||||
{Method: "POST", Path: "/api/cards", Handler: handleCreateCard(db)},
|
||||
{Method: "PATCH", Path: "/api/cards/{id}", Handler: handleUpdateCard(db)},
|
||||
{Method: "PUT", Path: "/api/cards/{id}/stickers", Handler: handleUpdateCardStickers(db)},
|
||||
{Method: "DELETE", Path: "/api/cards/{id}", Handler: handleDeleteCard(db)},
|
||||
{Method: "POST", Path: "/api/cards/{id}/move", Handler: handleMoveCard(db)},
|
||||
{Method: "POST", Path: "/api/cards/{id}/duplicate", Handler: handleDuplicateCard(db)},
|
||||
{Method: "POST", Path: "/api/columns", Handler: handleCreateColumn(db, hub)},
|
||||
{Method: "POST", Path: "/api/columns/reorder", Handler: handleReorderColumns(db, hub)},
|
||||
{Method: "PATCH", Path: "/api/columns/{id}", Handler: handleUpdateColumn(db, hub)},
|
||||
{Method: "DELETE", Path: "/api/columns/{id}", Handler: handleDeleteColumn(db, hub)},
|
||||
{Method: "POST", Path: "/api/cards", Handler: handleCreateCard(db, hub)},
|
||||
{Method: "PATCH", Path: "/api/cards/{id}", Handler: handleUpdateCard(db, hub)},
|
||||
{Method: "PUT", Path: "/api/cards/{id}/stickers", Handler: handleUpdateCardStickers(db, hub)},
|
||||
{Method: "DELETE", Path: "/api/cards/{id}", Handler: handleDeleteCard(db, hub)},
|
||||
{Method: "POST", Path: "/api/cards/{id}/move", Handler: handleMoveCard(db, hub)},
|
||||
{Method: "POST", Path: "/api/cards/{id}/duplicate", Handler: handleDuplicateCard(db, hub)},
|
||||
{Method: "GET", Path: "/api/cards/{id}/messages", Handler: handleListCardMessages(db)},
|
||||
{Method: "POST", Path: "/api/cards/{id}/messages", Handler: handleCreateCardMessage(db)},
|
||||
{Method: "DELETE", Path: "/api/cards/{id}/messages/{mid}", Handler: handleDeleteCardMessage(db)},
|
||||
{Method: "POST", Path: "/api/cards/{id}/messages", Handler: handleCreateCardMessage(db, hub)},
|
||||
{Method: "DELETE", Path: "/api/cards/{id}/messages/{mid}", Handler: handleDeleteCardMessage(db, hub)},
|
||||
{Method: "GET", Path: "/api/cards/{id}/history", Handler: handleCardHistory(db)},
|
||||
{Method: "GET", Path: "/api/trash", Handler: handleListTrash(db)},
|
||||
{Method: "POST", Path: "/api/cards/{id}/restore", Handler: handleRestoreCard(db)},
|
||||
{Method: "POST", Path: "/api/cards/{id}/restore", Handler: handleRestoreCard(db, hub)},
|
||||
{Method: "GET", Path: "/api/reports/daily", Handler: handleDailyReport(db)},
|
||||
{Method: "GET", Path: "/api/reports/daily/summary", Handler: handleGetDailySummary(db)},
|
||||
{Method: "POST", Path: "/api/reports/daily/summary", Handler: handleGenerateDailySummary(db)},
|
||||
{Method: "GET", Path: "/api/settings/{key}", Handler: handleGetSetting(db)},
|
||||
{Method: "PUT", Path: "/api/settings/{key}", Handler: handlePutSetting(db)},
|
||||
{Method: "GET", Path: "/api/archive", Handler: handleListArchive(db)},
|
||||
{Method: "POST", Path: "/api/cards/{id}/archive", Handler: handleArchiveCard(db)},
|
||||
{Method: "POST", Path: "/api/cards/{id}/unarchive", Handler: handleUnarchiveCard(db)},
|
||||
{Method: "DELETE", Path: "/api/cards/{id}/purge", Handler: handlePurgeCard(db)},
|
||||
{Method: "POST", Path: "/api/cards/{id}/archive", Handler: handleArchiveCard(db, hub)},
|
||||
{Method: "POST", Path: "/api/cards/{id}/unarchive", Handler: handleUnarchiveCard(db, hub)},
|
||||
{Method: "DELETE", Path: "/api/cards/{id}/purge", Handler: handlePurgeCard(db, hub)},
|
||||
{Method: "POST", Path: "/api/chat", Handler: handleChat(db, chatWorkdir, logger)},
|
||||
{Method: "GET", Path: "/api/chat/ws", Handler: handleChatWS(db, chatWorkdir, logger, internalToken)},
|
||||
{Method: "POST", Path: "/api/tool/{name}", Handler: handleInternalTool(db, internalToken, logger)},
|
||||
{Method: "GET", Path: "/api/metrics", Handler: handleMetrics(db)},
|
||||
{Method: "GET", Path: "/api/tags", Handler: handleListTags(db)},
|
||||
{Method: "GET", Path: "/api/requesters", Handler: handleListRequesters(db)},
|
||||
{Method: "GET", Path: "/api/events", Handler: handleEventStream(hub)},
|
||||
{Method: "GET", Path: "/api/cards/{id}/chat/ws", Handler: handleCardChatWS(db, hub)},
|
||||
{Method: "GET", Path: "/api/notifications", Handler: handleListNotifications(db)},
|
||||
{Method: "GET", Path: "/api/notifications/unread-count", Handler: handleUnreadCount(db)},
|
||||
{Method: "POST", Path: "/api/notifications/{id}/read", Handler: handleMarkNotificationRead(db, hub)},
|
||||
{Method: "POST", Path: "/api/notifications/read-all", Handler: handleMarkAllNotificationsRead(db, hub)},
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/version → {"version": "<semver>"}
|
||||
//
|
||||
// Public, no auth. Skipped from session middleware via skip list updated in
|
||||
// main.go to keep the SPA pre-login able to display the running build.
|
||||
func handleVersion() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
infra.HTTPJSONResponse(w, http.StatusOK, map[string]string{"version": Version})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user