9.5 KiB
Ticket Sync Tool — Architecture & Implementation Plan
Status: Phase 1–3 tracer implemented and LAN-smoke-tested (2026-07-09)
Goal: Build a reliable, incremental ticket sync + query foundation (redmine_tickets.py) that replaces the obsolete RebotSlick PullHelpdeskInfo pattern with the modern include=helpdesk capability. This becomes the core data layer for all future agentic ticket workflows (spam cleanup, enrichment, dropped-ball detection, etc.).
Primary target: customer-service project (currently >1700 tickets). Tool must work for any project.
Key constraints
- Do not modify any NMI components.
- Reuse existing
RedmineClientand helpdesk methods fromredmine_contacts.py. - Leverage the
include=helpdesk(andinclude=journals,helpdesk) patch already deployed. - Cache must be idempotent and cheap to re-run.
- Tool must support “do nothing but sync what’s new” when run with no action.
1. Core Requirements (from user spec)
-
Incremental sync
- Recursively fetch tickets with optional criteria (
--updated-since,--category,--status,--project,--with-tag, etc.). - Store full enriched payload (issue + journals + helpdesk_ticket + messages) as local JSON.
- Only re-fetch when the ticket’s
updated_onis newer than the cached file mtime (or use a small manifest).
- Recursively fetch tickets with optional criteria (
-
No-op default behavior
- Running with no filters/action does a safe, incremental sync of everything (or a sensible default window).
-
Filter/search mode
- When filters are supplied but no action is chosen, behave as a powerful search/report tool (list matching tickets, optionally write JSONL).
-
Idempotent action layer (future)
- Higher-level workers (spam-check, close, enrich, etc.) must be able to mark a ticket as “already processed” so repeated runs skip it.
- Suggested mechanism: sidecar manifest or lightweight SQLite table (
ticket_id,action_name,last_run,result).
-
Helpdesk enrichment
- Always request
include=journals,helpdesk(or configurable) so every cached ticket already contains the rich contact/source metadata.
- Always request
2. Proposed Architecture
2.1 Tool location & reuse
- New script:
/home/jason/projects/redmine/redmine_tickets.py - Reuses the existing
RedmineClientclass (especiallyissue_with_helpdesk,get_issue, list methods). - Same argument patterns, dry-run safety,
--apply, error handling, and completion scripts asredmine_contacts.py.
2.2 Cache layout (local filesystem)
Recommended structure (under a configurable --cache-dir, default /home/jason/projects/redmine/cache/tickets/ or /tmp/redmine-tickets-cache/):
cache/tickets/
├── manifest.json # { "ticket_id": { "updated_on": "...", "last_synced": "...", "actions": {...} } }
├── customer-service/
│ ├── 12345.json # full enriched payload
│ ├── 12346.json
│ └── ...
└── other-project/
└── ...
- One JSON file per ticket (stable, human-inspectable, easy to diff).
manifest.jsontracksupdated_on+ per-action idempotency state.- Simple mtime +
updated_oncomparison is sufficient for v1; manifest makes it robust. - Implementation note: ticket payload storage and ticket/action state storage are separated in code.
ManifestTicketStateStoreis the current implementation, butTicketCacheonly depends on a small state-store method set (get_ticket_state,upsert_ticket_state,has_action,record_action). If the manifest becomes cumbersome with thousands of tickets, replace it with SQLite or another backend without rewriting sync/search/action logic.
2.3 CLI structure (v1 scope)
./redmine_tickets.py \
--base-url http://192.168.50.170 \
--project customer-service \
[sync|search] \
[--updated-since YYYY-MM-DD] \
[--updated-before YYYY-MM-DD] \
[--status open|closed|*] \
[--category "Spam"] \
[--max-tickets N] \
[--order newest-updated-first] \
[--cache-dir ...] \
[--dry-run] [--apply] \
[--json] [--jsonl]
Commands
sync(default if omitted): Perform incremental fetch + cache update.search: Same selection logic, but only output matching tickets (no cache write unless--write-cache).
Safety
- Dry-run by default for any write.
--applyrequired to persist cache or perform mutations.
2.4 Idempotency & action hooks (v1 + v2)
- Manifest stores per-ticket action state:
"actions": { "spam-check": { "last_run": "2026-07-09", "result": "not-spam" }, "close-if-spam": { "last_run": "...", "result": "skipped" } } - Future workers read the manifest before acting and update it after.
3. Implementation Phases
Phase 1 — Core sync + cache (implemented tracer)
- Implement
RedmineClientenhancements if needed (list issues withinclude=journals,helpdesk, pagination). - Basic
synccommand that:- Accepts date/status/project filters.
- Fetches pages of issues.
- For each issue, writes
cache/<project>/<id>.jsononly if newer than manifest entry. - Updates manifest.
--max-ticketssafety limit.- Dry-run shows what would be fetched/updated.
Verified 2026-07-09: redmine_tickets.py sync pulled 10 open customer-service tickets from LAN Redmine (192.168.50.170), wrote enriched per-ticket JSON, and a second identical run fetched 0 / skipped 10 unchanged cached tickets.
Phase 2 — Search / filter-only mode (implemented tracer)
searchsubcommand that reuses the same selection engine.- Output options: human table,
--json,--jsonl(for downstream agents). - Ability to pipe results into later tools.
Verified 2026-07-09: cached search over the 10-ticket LAN cache listed the same newest open tickets seen in the UI, including promo candidates like #41788 and #41789.
Phase 3 — Idempotency manifest + action skeleton (implemented tracer)
- Add manifest read/write helpers.
- Skeleton
--actioninterface (initially just records “action X run on ticket Y”). - Example action:
spam-check(calls the logic we started inspam_promo_scorer.pybut at ticket level).
Verified 2026-07-09: spam-check identified #41788 and #41789 as SPAM based on promotional subject language, recorded local manifest idempotency with --apply, and a second spam-check run checked 0 tickets. close-if-spam dry-run planned closures for those two tickets and reviewed sender contact_id=621, finding 4/10 recent tickets scored as spam (likely_spammer=True). No Redmine close mutation has been applied yet.
Phase 4 — Spam cleanup worker (next)
- Concrete
spam-check+close-if-spamactions. - Rules based on sender patterns, subject language, lack of human replies, existing contact tags, etc.
- Produces a report + optional
--applyto close tickets and addspamtag to contact.
Preferred spam policy:
- When a ticket is high-confidence spam, close that ticket.
- Use
helpdesk_ticket.contact.idto fetch/analyze the last 5–10 tickets from the same contact. - Decide whether the sender/contact appears to be a recurring spammer or whether only the specific ticket/email is spam.
- Only mark/tag the contact as a spammer when the contact-level lookback supports that conclusion.
- Record both
close-if-spamandcontact-spam-reviewidempotency markers so repeated runs do not re-close/re-review the same work. - Treat Redmine impersonation like
sudo: do not use it for ordinary reads or dry-run planning, but all spam-cleanup write calls should impersonaterebot/ “Harry Botter” so Redmine activity logs clearly attribute automated cleanup work.
4. Non-Goals (for this phase)
- Do not replace or modify any NMI cron scripts.
- Do not build full agent orchestration yet (this is the data layer).
- Do not implement complex NLP classifiers in v1 (keep scoring simple and reviewable).
- Cache does not need to be a database (filesystem + manifest is sufficient and inspectable).
5. Success Criteria
- Running the tool twice in a row with the same filters produces zero API calls on the second run (everything is cached and up-to-date).
- A filter-only search for “tickets updated in last 30 days with no human reply” returns results in < 5 seconds after initial sync.
- The same tool can be pointed at any project and produce usable enriched ticket JSON.
- Higher-level workers can be written as thin Python scripts that consume the cache/manifest without needing their own Redmine client logic.
6. Next Immediate Action
The implementation tracer is complete through dry-run close-if-spam.
Next safe execution sequence:
- Confirm LAN issue
status_id=5is the intended closed status. - Run
close-if-spam --applyagainst the LAN copy only, impersonatingrebot(the CLI default). - Re-sync or inspect the affected LAN tickets.
- Verify:
#41788and#41789are closed,- Redmine activity author is
rebot/ Harry Botter, - local state records
close-if-spamandcontact-spam-review, - a second
close-if-spam --applydoes nothing.
- Only then expand the batch size beyond the initial 10-ticket cache.
Detailed handoff: docs/ticket_spam_cleanup_handoff_2026-07-09.md.
References
docs/redmine_issue_api_helpdesk_include.md(the key enabler)redmine_contacts.py(client patterns, filtering, dry-run UX)docs/contact_triage_loop_*(overall workflow philosophy)- RebotSlick
RebotSlick.php(historical cache idea — modernized)
This plan deliberately keeps scope tight while giving us a clean, extensible foundation for the spam-reduction work that is currently blocking the customer-service UI.