Files
imap-copier/internal/store/tasks.go
T
vasyansk 0bb584fe10 feat: folder mapping UI on account add (probe + map modal)
ADD now probes both connections, lists folders on each side, and opens a
mapping modal to route source->destination folders (e.g. Спам -> Spam) so we
append into the existing folder instead of creating a duplicate.

- store: SetTaskFolderMapping (+ round-trip test)
- httpapi: POST /tasks/{id}/probe (test both, return folder lists),
  PUT /tasks/{id}/folder-mapping
- web: FolderMappingModal (reuses Modal, size=lg), submitAccount probes then
  opens the modal; confirm creates the account and saves the task mapping

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMHQTtnQtQqL8muAXHr9kd
2026-07-02 14:39:31 +07:00

83 lines
2.7 KiB
Go

package store
import "context"
type Task struct {
ID int64 `json:"id"`
Name string `json:"name"`
SrcEndpointID int64 `json:"src_endpoint_id"`
DstEndpointID int64 `json:"dst_endpoint_id"`
Status string `json:"status"`
FolderMapping map[string]string `json:"folder_mapping"`
}
func (s *Store) CreateTask(ctx context.Context, t Task) (int64, error) {
if t.FolderMapping == nil {
t.FolderMapping = map[string]string{}
}
var id int64
err := s.Pool.QueryRow(ctx,
`INSERT INTO tasks (name, src_endpoint_id, dst_endpoint_id, folder_mapping)
VALUES ($1,$2,$3,$4) RETURNING id`,
t.Name, t.SrcEndpointID, t.DstEndpointID, t.FolderMapping).Scan(&id)
return id, err
}
func (s *Store) GetTask(ctx context.Context, id int64) (Task, error) {
var t Task
err := s.Pool.QueryRow(ctx,
`SELECT id, name, src_endpoint_id, dst_endpoint_id, status, folder_mapping
FROM tasks WHERE id=$1`, id).
Scan(&t.ID, &t.Name, &t.SrcEndpointID, &t.DstEndpointID, &t.Status, &t.FolderMapping)
return t, err
}
// DeleteTask removes a task and its accounts/runs/migrated_messages via ON DELETE CASCADE.
func (s *Store) DeleteTask(ctx context.Context, id int64) error {
_, err := s.Pool.Exec(ctx, `DELETE FROM tasks WHERE id=$1`, id)
return err
}
func (s *Store) ListTasks(ctx context.Context) ([]Task, error) {
rows, err := s.Pool.Query(ctx,
`SELECT id, name, src_endpoint_id, dst_endpoint_id, status, folder_mapping
FROM tasks ORDER BY id DESC`)
if err != nil {
return nil, err
}
defer rows.Close()
out := []Task{}
for rows.Next() {
var t Task
if err := rows.Scan(&t.ID, &t.Name, &t.SrcEndpointID, &t.DstEndpointID, &t.Status, &t.FolderMapping); err != nil {
return nil, err
}
out = append(out, t)
}
return out, rows.Err()
}
func (s *Store) SetTaskFolderMapping(ctx context.Context, id int64, mapping map[string]string) error {
if mapping == nil {
mapping = map[string]string{}
}
_, err := s.Pool.Exec(ctx, `UPDATE tasks SET folder_mapping=$2 WHERE id=$1`, id, mapping)
return err
}
func (s *Store) SetTaskStatus(ctx context.Context, id int64, status string) error {
_, err := s.Pool.Exec(ctx, `UPDATE tasks SET status=$2 WHERE id=$1`, id, status)
return err
}
// TryMarkTaskRunning atomically sets status='running' only if the task is not already running.
// Returns true if this call acquired the run (status was not 'running' before), false otherwise.
func (s *Store) TryMarkTaskRunning(ctx context.Context, id int64) (bool, error) {
ct, err := s.Pool.Exec(ctx,
`UPDATE tasks SET status='running' WHERE id=$1 AND status<>'running'`, id)
if err != nil {
return false, err
}
return ct.RowsAffected() == 1, nil
}