fix: transaction-safe AcceptSuggestionWith + safe eventIds fallback + debug logging

Root cause: s.eventIds may be undefined in JavaScript even when s.events
has data (Wails v2 marshalling of []string in nested struct response).
On calling AcceptSuggestionWith(eventIDs []string), empty array reached Go,
no INSERTs executed, events silently lost.

Changes:
- Frontend: extractEventIds() fallback — s.eventIds || s.events[].id || []
- Frontend: console.log debug for eventIds/events in accept handler
- Backend: AcceptSuggestionWith wrapped in tx (Begin/Commit/Rollback) so
  entry creation + event linking is atomic
- Backend: AddWithSourceTx method for transaction-aware insert
- Backend: buildEntry helper extracted
- Backend: fmt.Printf debug logging for received eventIDs + link count
- Backend: verification query after commit
- Cleanup: removed stale frontend-dist assets, .gitignore build.log
This commit is contained in:
mirivlad 2026-06-03 15:10:25 +08:00
parent 7076980954
commit 21a595c3ce
26 changed files with 100 additions and 66 deletions

1
.gitignore vendored
View File

@ -48,3 +48,4 @@ server-data/
# Build output
build/
build.log

View File

@ -116,25 +116,57 @@ func (a *App) AcceptSuggestion(nodeID, summary string, minutes int, date string,
return a.AcceptSuggestionWith(nodeID, summary, minutes, date, eventIDs)
}
// AcceptSuggestionWith creates a worklog entry and links events. Uses flat fields to avoid Wails marshalling issues.
// AcceptSuggestionWith creates a worklog entry and links events in a single transaction.
// Uses flat fields to avoid Wails marshalling issues.
func (a *App) AcceptSuggestionWith(nodeID, summary string, minutes int, date string, eventIDs []string) (*WorklogDTO, error) {
d := date
if d == "" {
d = time.Now().Format("2006-01-02")
}
entry, err := a.worklog.AddWithSource(nodeID, summary, "", d, minutes, true, false, worklog.SourceSuggestion)
// Log what we received from the frontend
fmt.Printf("DEBUG AcceptSuggestionWith: nodeID=%q summary=%q minutes=%d date=%q eventIDs=%v (len=%d)\n",
nodeID, summary, minutes, d, eventIDs, len(eventIDs))
// Use a transaction to atomically create entry + link events
tx, err := a.db.Begin()
if err != nil {
return nil, err
return nil, fmt.Errorf("begin tx: %w", err)
}
// Link activity events to this worklog entry.
defer tx.Rollback()
entry, err := a.worklog.AddWithSourceTx(tx, nodeID, summary, "", d, minutes, true, false, worklog.SourceSuggestion)
if err != nil {
return nil, fmt.Errorf("create entry: %w", err)
}
fmt.Printf("DEBUG AcceptSuggestionWith: entry created id=%s\n", entry.ID)
linked := 0
for _, eid := range eventIDs {
_, err := a.db.Exec(
res, err := tx.Exec(
`INSERT OR IGNORE INTO worklog_entry_events (entry_id, event_id) VALUES (?,?)`,
entry.ID, eid)
if err != nil {
return nil, fmt.Errorf("link event %s: %w", eid, err)
}
n, _ := res.RowsAffected()
linked += int(n)
}
fmt.Printf("DEBUG AcceptSuggestionWith: linked %d events (out of %d eventIDs)\n", linked, len(eventIDs))
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("commit tx: %w", err)
}
// Verify the links were stored
if len(eventIDs) > 0 {
var count int
a.db.QueryRow("SELECT COUNT(*) FROM worklog_entry_events WHERE entry_id = ?", entry.ID).Scan(&count)
fmt.Printf("DEBUG AcceptSuggestionWith: verification COUNT(*) = %d\n", count)
}
_ = a.sync.RecordOp(syncsvc.EntityWorklog, entry.ID, syncsvc.OpCreate, worklogPayload(entry))
mins := 0
if entry.Minutes != nil {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -16,7 +16,7 @@
background: #13131f;
}
</style>
<script type="module" crossorigin src="/assets/main-cq32hqy7.js"></script>
<script type="module" crossorigin src="/assets/main-DQ318Oic.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-BafVhx43.css">
</head>
<body>

View File

@ -970,16 +970,26 @@
}
}
function extractEventIds(s) {
if (s.eventIds && s.eventIds.length) return s.eventIds
if (s.events && s.events.length) return s.events.map(ev => ev.id).filter(Boolean)
return []
}
async function acceptTodaySuggestion(s) {
try {
await wailsCall('AcceptSuggestionWith', s.nodeId, s.summary, s.suggestedMin, '', s.eventIds || [])
const eventIds = extractEventIds(s)
console.log('DEBUG acceptTodaySuggestion:', { nodeId: s.nodeId, eventIdsLen: eventIds.length, eventIds, eventsCount: s.events?.length })
await wailsCall('AcceptSuggestionWith', s.nodeId, s.summary, s.suggestedMin, '', eventIds)
await refreshAfterSuggestion()
} catch (e) { console.error(e) }
}
async function acceptJournalSuggestion(s) {
try {
await wailsCall('AcceptSuggestionWith', s.nodeId, s.summary, s.suggestedMin, '', s.eventIds || [])
const eventIds = extractEventIds(s)
console.log('DEBUG acceptJournalSuggestion:', { nodeId: s.nodeId, eventIdsLen: eventIds.length, eventIds, eventsCount: s.events?.length })
await wailsCall('AcceptSuggestionWith', s.nodeId, s.summary, s.suggestedMin, '', eventIds)
await refreshAfterSuggestion()
} catch (e) { console.error(e) }
}

View File

@ -50,7 +50,7 @@ func (s *Service) Add(nodeID, summary, details string, minutes int, approximate,
return s.AddWithSource(nodeID, summary, details, date, minutes, approximate, billable, SourceManual)
}
// Add inserts a new worklog entry.
// AddWithSource inserts a new worklog entry with an explicit source.
func (s *Service) AddWithSource(nodeID, summary, details, date string, minutes int, approximate, billable bool, source string) (*Entry, error) {
if nodeID == "" {
return nil, fmt.Errorf("node_id required")
@ -62,19 +62,7 @@ func (s *Service) AddWithSource(nodeID, summary, details, date string, minutes i
date = time.Now().Format("2006-01-02")
}
e := &Entry{
ID: util.UUID7(),
NodeID: nodeID,
Summary: summary,
Details: details,
Date: date,
Minutes: &minutes,
Approximate: approximate,
Billable: billable,
Source: source,
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
e := buildEntry(nodeID, summary, details, date, minutes, approximate, billable, source)
_, err := s.db.Exec(
`INSERT INTO worklog_entries (id,node_id,date,minutes,approximate,billable,
@ -90,6 +78,50 @@ func (s *Service) AddWithSource(nodeID, summary, details, date string, minutes i
return e, nil
}
// AddWithSourceTx inserts a new worklog entry within an existing transaction.
func (s *Service) AddWithSourceTx(tx *sql.Tx, nodeID, summary, details, date string, minutes int, approximate, billable bool, source string) (*Entry, error) {
if nodeID == "" {
return nil, fmt.Errorf("node_id required")
}
if summary == "" {
return nil, fmt.Errorf("summary required")
}
if date == "" {
date = time.Now().Format("2006-01-02")
}
e := buildEntry(nodeID, summary, details, date, minutes, approximate, billable, source)
_, err := tx.Exec(
`INSERT INTO worklog_entries (id,node_id,date,minutes,approximate,billable,
summary,details,source,created_at,updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?)`,
e.ID, e.NodeID, e.Date, e.Minutes, boolInt(e.Approximate),
boolInt(e.Billable), e.Summary, e.Details, e.Source,
e.CreatedAt.Format(time.RFC3339), e.UpdatedAt.Format(time.RFC3339),
)
if err != nil {
return nil, err
}
return e, nil
}
func buildEntry(nodeID, summary, details, date string, minutes int, approximate, billable bool, source string) *Entry {
return &Entry{
ID: util.UUID7(),
NodeID: nodeID,
Summary: summary,
Details: details,
Date: date,
Minutes: &minutes,
Approximate: approximate,
Billable: billable,
Source: source,
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
}
// Update modifies an existing entry.
func (s *Service) Update(id, summary, details string, minutes int, approximate, billable bool) error {
t := time.Now().UTC().Format(time.RFC3339)