Organized
This commit is contained in:
@@ -0,0 +1,824 @@
|
||||
# Ticket-Centric Spam Cleanup Handoff — 2026-07-09
|
||||
|
||||
## Purpose
|
||||
|
||||
This handoff captures the current state of the new ticket-centric Redmine maintenance substrate and the spam cleanup workflow for the `customer-service` project.
|
||||
|
||||
The immediate business goal is to reduce visible spam clutter in `customer-service` without disrupting existing NMI production scripts or doing unsafe bulk Redmine writes.
|
||||
|
||||
## Important Safety / Operating Rules
|
||||
|
||||
1. **Do not modify NMI scripts right now.** Existing NMI cron scripts are reference material only.
|
||||
2. **Dry-run first.** Redmine mutations require explicit `--apply`.
|
||||
3. **Reads do not impersonate.** Ordinary sync/search/dry-run planning should use normal API access.
|
||||
4. **Writes impersonate `rebot`.** Spam cleanup / ticket maintenance writes should use `X-Redmine-Switch-User: rebot` (Harry Botter), because Redmine activity logs should show automated cleanup under that account, not Jason.
|
||||
5. **Treat impersonation like sudo.** Use it only where the write is supposed to happen under that agent account.
|
||||
6. **LAN first.** The validated target so far is the LAN Redmine copy at `http://192.168.50.170`, project `customer-service`.
|
||||
7. **Confirm status IDs before applying.** Existing evidence suggests `status_id=5` is closed, but verify before first real apply if uncertain.
|
||||
|
||||
## Relevant Files
|
||||
|
||||
- Tool: `/home/jason/projects/redmine/redmine_tickets.py`
|
||||
- Tests: `/home/jason/projects/redmine/tests/test_redmine_tickets.py`
|
||||
- Plan: `/home/jason/projects/redmine/docs/ticket_sync_tool_plan.md`
|
||||
- General project handoff: `/home/jason/projects/redmine/HANDOFF.md`
|
||||
- Helpdesk API include docs: `/home/jason/projects/redmine/docs/redmine_issue_api_helpdesk_include.md`
|
||||
- Existing contact tool: `/home/jason/projects/redmine/redmine_contacts.py`
|
||||
|
||||
## Why `redmine_tickets.py` Exists
|
||||
|
||||
The earlier `redmine_contacts.py` contact conveyor is useful for contact enrichment and contact-level routing, but it is not the best first tool for clearing visible spam from `customer-service` because the UI pain is ticket-level: the top/open tickets themselves are clutter.
|
||||
|
||||
`redmine_tickets.py` is the new ticket-centric substrate. It syncs enriched issue payloads, searches cached tickets, and runs idempotent ticket actions.
|
||||
|
||||
## Modern Helpdesk Access
|
||||
|
||||
Do **not** recreate RebotSlick `PullHelpdeskInfo` behavior. The old separate helpdesk pull/cache pattern is obsolete because the local Redmine Helpdesk plugin now supports:
|
||||
|
||||
```text
|
||||
GET /issues/:id.json?include=journals,helpdesk
|
||||
```
|
||||
|
||||
This returns normal Redmine issue data plus embedded `helpdesk_ticket` metadata, including contact/email/source fields.
|
||||
|
||||
## Current Architecture
|
||||
|
||||
### Ticket payload cache
|
||||
|
||||
Per-ticket JSON files are stored under a configurable cache directory, e.g.:
|
||||
|
||||
```text
|
||||
.cache/redmine_tickets_lan/customer-service/41788.json
|
||||
.cache/redmine_tickets_lan/customer-service/41789.json
|
||||
```
|
||||
|
||||
### Ticket/action state
|
||||
|
||||
State is currently manifest-backed:
|
||||
|
||||
```text
|
||||
.cache/redmine_tickets_lan/manifest.json
|
||||
```
|
||||
|
||||
But the code now separates payload cache from state storage:
|
||||
|
||||
- `TicketCache` handles ticket JSON cache + delegates state operations.
|
||||
- `ManifestTicketStateStore` is the current state backend.
|
||||
- The abstraction is intentionally small so SQLite can replace the manifest later for thousands of tickets.
|
||||
|
||||
Required state-store methods:
|
||||
|
||||
```python
|
||||
get_ticket_state(issue_id)
|
||||
upsert_ticket_state(issue_id, fields)
|
||||
has_action(issue_id, action_name)
|
||||
record_action(issue_id, action_name, result, metadata=None)
|
||||
```
|
||||
|
||||
## Implemented Commands
|
||||
|
||||
### Sync enriched tickets
|
||||
|
||||
```bash
|
||||
cd /home/jason/projects/redmine
|
||||
|
||||
./redmine_tickets.py \
|
||||
--base-url http://192.168.50.170 \
|
||||
--project customer-service \
|
||||
--cache-dir .cache/redmine_tickets_lan \
|
||||
sync \
|
||||
--status-id open \
|
||||
--max-tickets 10 \
|
||||
--order newest-updated-first
|
||||
```
|
||||
|
||||
Behavior:
|
||||
- fetches issue stubs/pages from Redmine
|
||||
- fetches full issue payloads with `include=journals,helpdesk`
|
||||
- writes only missing/updated tickets to cache
|
||||
- second identical run skips unchanged tickets
|
||||
|
||||
Verified output:
|
||||
|
||||
```text
|
||||
Sync complete: seen=10 fetched=10 skipped_cached=0 written=10
|
||||
Sync complete: seen=10 fetched=0 skipped_cached=10 written=0
|
||||
```
|
||||
|
||||
### Search cached tickets
|
||||
|
||||
```bash
|
||||
./redmine_tickets.py \
|
||||
--cache-dir .cache/redmine_tickets_lan \
|
||||
--project customer-service \
|
||||
search \
|
||||
--max-tickets 10 \
|
||||
--order newest-updated-first
|
||||
```
|
||||
|
||||
Validated that this matches the UI top-open-ticket pattern and includes spam-looking tickets such as:
|
||||
|
||||
```text
|
||||
#41788 Up to 20% Off Select Fans & Blowers
|
||||
#41789 Up to 20% Off Select Fans & Blowers
|
||||
```
|
||||
|
||||
### Spam check cached tickets
|
||||
|
||||
```bash
|
||||
./redmine_tickets.py \
|
||||
--cache-dir .cache/redmine_tickets_lan \
|
||||
--project customer-service \
|
||||
spam-check
|
||||
```
|
||||
|
||||
Validated result:
|
||||
|
||||
```text
|
||||
#41788 SPAM score=2 from=reply@globalindustrial.com Up to 20% Off Select Fans & Blowers
|
||||
#41789 SPAM score=2 from=reply@globalindustrial.com Up to 20% Off Select Fans & Blowers
|
||||
```
|
||||
|
||||
Record local idempotency only (no Redmine mutation):
|
||||
|
||||
```bash
|
||||
./redmine_tickets.py \
|
||||
--cache-dir .cache/redmine_tickets_lan \
|
||||
--project customer-service \
|
||||
spam-check \
|
||||
--apply
|
||||
```
|
||||
|
||||
After this, a second `spam-check` checks 0 tickets because `spam-check` action markers are recorded.
|
||||
|
||||
### Plan closure for spam tickets
|
||||
|
||||
Dry-run:
|
||||
|
||||
```bash
|
||||
./redmine_tickets.py \
|
||||
--base-url http://192.168.50.170 \
|
||||
--project customer-service \
|
||||
--cache-dir .cache/redmine_tickets_lan \
|
||||
close-if-spam \
|
||||
--close-status-id 5 \
|
||||
--contact-ticket-limit 10
|
||||
```
|
||||
|
||||
Validated output:
|
||||
|
||||
```text
|
||||
#41788 close_payload={"notes": "Closed as likely spam by redmine_tickets.py close-if-spam.", "status_id": 5} contact_id=621 contact_spam=4/10 likely_spammer=True Up to 20% Off Select Fans & Blowers
|
||||
#41789 close_payload={"notes": "Closed as likely spam by redmine_tickets.py close-if-spam.", "status_id": 5} contact_id=621 contact_spam=4/10 likely_spammer=True Up to 20% Off Select Fans & Blowers
|
||||
Close-if-spam complete: planned=2 apply=False
|
||||
Dry run only. Re-run with --apply to close Redmine issues and record idempotency markers.
|
||||
```
|
||||
|
||||
No Redmine mutation has been applied yet.
|
||||
|
||||
## Actual Closure Command (Pending Approval)
|
||||
|
||||
After confirming `status_id=5` is the correct closed status on the LAN copy, this is the command that would close the two planned tickets and record idempotency markers:
|
||||
|
||||
```bash
|
||||
cd /home/jason/projects/redmine
|
||||
|
||||
./redmine_tickets.py \
|
||||
--base-url http://192.168.50.170 \
|
||||
--project customer-service \
|
||||
--cache-dir .cache/redmine_tickets_lan \
|
||||
close-if-spam \
|
||||
--close-status-id 5 \
|
||||
--contact-ticket-limit 10 \
|
||||
--impersonate-user rebot \
|
||||
--apply
|
||||
```
|
||||
|
||||
Expected write behavior:
|
||||
|
||||
- sends `PUT /issues/:id.json`
|
||||
- payload:
|
||||
```json
|
||||
{
|
||||
"issue": {
|
||||
"status_id": 5,
|
||||
"notes": "Closed as likely spam by redmine_tickets.py close-if-spam."
|
||||
}
|
||||
}
|
||||
```
|
||||
- includes header:
|
||||
```text
|
||||
X-Redmine-Switch-User: rebot
|
||||
```
|
||||
- records local actions:
|
||||
- `close-if-spam = closed`
|
||||
- `contact-spam-review = likely-spammer` or `single-ticket-spam`
|
||||
|
||||
## Tests / Verification
|
||||
|
||||
Current test command:
|
||||
|
||||
```bash
|
||||
python3 -m unittest discover -s tests -p 'test_redmine_tickets.py' -v
|
||||
```
|
||||
|
||||
Last verified:
|
||||
|
||||
```text
|
||||
Ran 11 tests
|
||||
OK
|
||||
```
|
||||
|
||||
Compile command:
|
||||
|
||||
```bash
|
||||
python3 -m py_compile redmine_tickets.py tests/test_redmine_tickets.py redmine_contacts.py
|
||||
```
|
||||
|
||||
Last verified: OK.
|
||||
|
||||
## 25-Ticket LAN Evaluation — 2026-07-09
|
||||
|
||||
Jason confirmed `status_id=5` is Closed. A fresh throwaway cache was used so old action markers would not hide results:
|
||||
|
||||
```text
|
||||
.cache/redmine_tickets_lan_25_eval
|
||||
```
|
||||
|
||||
Commands run:
|
||||
|
||||
```bash
|
||||
./redmine_tickets.py \
|
||||
--base-url http://192.168.50.170 \
|
||||
--project customer-service \
|
||||
--cache-dir .cache/redmine_tickets_lan_25_eval \
|
||||
sync \
|
||||
--status-id open \
|
||||
--max-tickets 25 \
|
||||
--order newest-updated-first
|
||||
|
||||
./redmine_tickets.py \
|
||||
--cache-dir .cache/redmine_tickets_lan_25_eval \
|
||||
--project customer-service \
|
||||
spam-check \
|
||||
--apply
|
||||
|
||||
./redmine_tickets.py \
|
||||
--base-url http://192.168.50.170 \
|
||||
--project customer-service \
|
||||
--cache-dir .cache/redmine_tickets_lan_25_eval \
|
||||
close-if-spam \
|
||||
--close-status-id 5 \
|
||||
--contact-ticket-limit 10
|
||||
```
|
||||
|
||||
Verified output summary:
|
||||
|
||||
```text
|
||||
Sync complete: seen=25 fetched=25 skipped_cached=0 written=25
|
||||
Spam check complete: checked=25 apply=True
|
||||
Close-if-spam complete: planned=5 apply=False
|
||||
SUMMARY cached_tickets=25
|
||||
SUMMARY spam=5 not_spam=20
|
||||
```
|
||||
|
||||
Closure candidates produced by dry-run:
|
||||
|
||||
```text
|
||||
#41753 Automated unfulfillable FBA inventory removal notification — from fba-noreply@amazon.com — contact_id=51 — contact_spam=10/10 — likely_spammer=True
|
||||
#41760 Celebrate America's 250th: Save Up To 40% Off — from reply@globalindustrial.com — contact_id=621 — contact_spam=4/10 — likely_spammer=True
|
||||
#41761 Celebrate America's 250th: Save Up To 40% Off — from reply@globalindustrial.com — contact_id=621 — contact_spam=4/10 — likely_spammer=True
|
||||
#41767 — — from enews-noreply@qnap.com — contact_id=3162 — contact_spam=10/10 — likely_spammer=True
|
||||
#41776 Your favorite sale ENDS THURSDAY ⏳ — from noreply@mail.vistaprint.com — contact_id=3795 — contact_spam=10/10 — likely_spammer=True
|
||||
```
|
||||
|
||||
Important finding from first run: the scorer conflated **spam/promotional clutter** with **automated operational notifications**. Example: `#41753` from `fba-noreply@amazon.com` is likely an automated Amazon/FBA operational notification, not normal spam.
|
||||
|
||||
No Redmine close mutation was applied during this 25-ticket evaluation.
|
||||
|
||||
## Refined Promotional-Junk Evaluation — 2026-07-09
|
||||
|
||||
The scorer was refined to target Jason's immediate goal: quickly and reliably close **obvious unsolicited promotional/sales junk**, such as Walmart/Uline/GlobalIndustrial/Vistaprint/QNAP-style promotional mail and lead-gen/sales outreach from people trying to sell LDR something without an established relationship.
|
||||
|
||||
False-positive protections added:
|
||||
|
||||
- `automated-operational`: Amazon/FBA/order/API/payment-style notifications are excluded from the default auto-close class.
|
||||
- Customer/vendor operational threads are protected from staff-reply contamination: LDR staff-authored journal notes are no longer scored as spam evidence.
|
||||
- Standalone `SEO` in customer operational context is not treated as a sales trigger.
|
||||
|
||||
Regression tests now cover:
|
||||
|
||||
- promotional sale email => `spam-promotional`
|
||||
- cleaning-service sales pitch => `spam-promotional`
|
||||
- Amazon/FBA notification => `automated-operational`, not spam
|
||||
- real FBA returns inquiry with LDR pricing reply => not spam
|
||||
- customer operational context mentioning SEO => not spam
|
||||
|
||||
Fresh final cache:
|
||||
|
||||
```text
|
||||
.cache/redmine_tickets_lan_25_promotional_eval_final_1783600119
|
||||
```
|
||||
|
||||
Final verified summary:
|
||||
|
||||
```text
|
||||
Sync complete: seen=25 fetched=25 skipped_cached=0 written=25
|
||||
Close-if-spam complete: planned=6 apply=False
|
||||
SUMMARY categories={'unknown-review': 17, 'automated-operational': 2, 'spam-promotional': 6}
|
||||
```
|
||||
|
||||
Current safe close candidates from the final dry-run:
|
||||
|
||||
```text
|
||||
#41767 — from enews-noreply@qnap.com — QNAP newsletter/promo
|
||||
#41776 Your favorite sale ENDS THURSDAY ⏳ — from noreply@mail.vistaprint.com
|
||||
#41788 Up to 20% Off Select Fans & Blowers — from reply@globalindustrial.com
|
||||
#41789 Up to 20% Off Select Fans & Blowers — from reply@globalindustrial.com
|
||||
#41804 Do you serve e-commerce brands? — from d.elliott@fulfillmentviewpro.com — 3PL lead-gen/sales pitch
|
||||
#41834 Japan’s “JC-STAR Level 1” — from enews-noreply@qnap.com — QNAP promotional/newsletter item
|
||||
```
|
||||
|
||||
No Redmine close mutation was applied during this final evaluation.
|
||||
|
||||
### LAN apply verification — 2026-07-09
|
||||
|
||||
Jason approved moving toward closing the obvious junk. The following command was run against the final 25-ticket cache:
|
||||
|
||||
```bash
|
||||
./redmine_tickets.py \
|
||||
--base-url http://192.168.50.170 \
|
||||
--project customer-service \
|
||||
--cache-dir .cache/redmine_tickets_lan_25_promotional_eval_final_1783600119 \
|
||||
close-if-spam \
|
||||
--close-status-id 5 \
|
||||
--contact-ticket-limit 10 \
|
||||
--impersonate-user rebot \
|
||||
--apply
|
||||
```
|
||||
|
||||
Applied closures:
|
||||
|
||||
```text
|
||||
#41767
|
||||
#41776
|
||||
#41788
|
||||
#41789
|
||||
#41804
|
||||
#41834
|
||||
```
|
||||
|
||||
Idempotency check:
|
||||
|
||||
```text
|
||||
Close-if-spam complete: planned=0 apply=True
|
||||
```
|
||||
|
||||
API verification after apply:
|
||||
|
||||
```text
|
||||
41767 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41776 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41788 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41789 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41804 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41834 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
```
|
||||
|
||||
## Current Limitations / Known Next Improvements
|
||||
|
||||
1. `manifest.json` is acceptable for the current tracer, but SQLite should be considered once caches/actions cover thousands of tickets.
|
||||
2. Spam scoring is still intentionally simple and reviewable. It now distinguishes `spam-promotional`, `automated-operational`, and `unknown-review`; future classes can be added as patterns emerge.
|
||||
3. Contact-level spammer handling is only advisory right now. The tool reports `likely_spammer=True`, but does not yet tag the contact.
|
||||
4. First real close apply for each newly expanded batch should happen on LAN only, then immediately re-sync/verify status and activity author.
|
||||
5. The production Redmine path should remain out of scope until LAN write behavior is proven and reviewed.
|
||||
|
||||
## 50-Ticket Refined Promotional-Junk Dry Run — 2026-07-09
|
||||
|
||||
Fresh cache:
|
||||
|
||||
```text
|
||||
.cache/redmine_tickets_lan_50_promotional_eval_refined_1783600628
|
||||
```
|
||||
|
||||
Verified summary:
|
||||
|
||||
```text
|
||||
Sync complete: seen=50 fetched=50 skipped_cached=0 written=50
|
||||
Close-if-spam complete: planned=9 apply=False
|
||||
SUMMARY categories={'unknown-review': 32, 'spam-promotional': 9, 'automated-operational': 9}
|
||||
```
|
||||
|
||||
Promotional close candidates:
|
||||
|
||||
```text
|
||||
#41685 Samsung promo — noreply_samsung@sea.samsung.com — "Jason, we’ve missed you. We just can't stop innovating."
|
||||
#41687 QNAP promo — enews-noreply@qnap.com — "Qsync Now Supports File Backup on Mac"
|
||||
#41698 Vistaprint promo — noreply@mail.vistaprint.com — "Life’s best moments, treasured forever"
|
||||
#41705 Popmenu/restaurant promo — noreply@popmenu.com — "Take a break from cooking!"
|
||||
#41722 3PL lead-gen/sales pitch — hello@offer.search3pl.com — "Live fulfilment leads waiting for you to connect"
|
||||
#41738 Vistaprint promo — noreply@mail.vistaprint.com — "Up to 15% off top business products that’ll draw them in"
|
||||
#41746 Business Journals events/nominations promo — replycincinnati@engaged.bizjournals.com — "Hi LDR, here are our upcoming events and nominations"
|
||||
#41760 Global Industrial sale — reply@globalindustrial.com — "Celebrate America's 250th: Save Up To 40% Off"
|
||||
#41761 Global Industrial sale — reply@globalindustrial.com — "Celebrate America's 250th: Save Up To 40% Off"
|
||||
```
|
||||
|
||||
Operational/system notifications excluded from auto-close:
|
||||
|
||||
```text
|
||||
#41694 USPS eReceipt
|
||||
#41704 2024 UNPAID INVOICES $3,413.55
|
||||
#41714 Undelivered Mail Returned to Sender
|
||||
#41716 Critical security alert
|
||||
#41726 UPS Invoice Now Available
|
||||
#41753 Automated unfulfillable FBA inventory removal notification
|
||||
#41765 Amazon Listings API changes
|
||||
#41772 Cintas Payment Status
|
||||
#41823 Amazon order confirmation
|
||||
```
|
||||
|
||||
No Redmine mutation was applied during this 50-ticket dry run.
|
||||
|
||||
### 50-ticket LAN apply verification — 2026-07-09
|
||||
|
||||
Jason approved applying the 9-candidate promotional set. The following command was run against the exact reviewed cache:
|
||||
|
||||
```bash
|
||||
./redmine_tickets.py \
|
||||
--base-url http://192.168.50.170 \
|
||||
--project customer-service \
|
||||
--cache-dir .cache/redmine_tickets_lan_50_promotional_eval_refined_1783600628 \
|
||||
close-if-spam \
|
||||
--close-status-id 5 \
|
||||
--contact-ticket-limit 10 \
|
||||
--impersonate-user rebot \
|
||||
--apply
|
||||
```
|
||||
|
||||
Applied closures:
|
||||
|
||||
```text
|
||||
#41685
|
||||
#41687
|
||||
#41698
|
||||
#41705
|
||||
#41722
|
||||
#41738
|
||||
#41746
|
||||
#41760
|
||||
#41761
|
||||
```
|
||||
|
||||
Idempotency check:
|
||||
|
||||
```text
|
||||
Close-if-spam complete: planned=0 apply=True
|
||||
```
|
||||
|
||||
API verification after apply:
|
||||
|
||||
```text
|
||||
41685 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41687 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41698 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41705 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41722 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41738 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41746 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41760 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41761 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
```
|
||||
|
||||
## Recommended Next Session Steps
|
||||
|
||||
1. If Jason approves the current 27-candidate production 200-ticket set, apply closures on production using `--impersonate-user rebot` and the exact cache `.cache/redmine_tickets_prod_200_promotional_eval_1783612885`.
|
||||
2. Optionally apply the 3 `maybe-customer` contact tags from the same cache.
|
||||
3. Re-sync/inspect via Redmine API/UI and verify closed status, close-note author `Harry B.`, second-apply idempotency, and local action markers.
|
||||
4. Continue with another bounded production dry-run batch.
|
||||
|
||||
## Customer identification update — 2026-07-09
|
||||
|
||||
Jason confirmed the following contacts/patterns are customers and should be tagged `Customer`:
|
||||
|
||||
```text
|
||||
koki-takeda@shinkiya-inc.com
|
||||
hello@bohoflow.co
|
||||
hi5@bavariatrachten.com
|
||||
maia@solride.org
|
||||
mike@solride.org
|
||||
info@u2wear.com
|
||||
sales@u2wear.com
|
||||
any Cyberstorm email
|
||||
Esire-family emails
|
||||
```
|
||||
|
||||
Important correction: do **not** treat incidental substrings such as `desiree` in Indeed/applicant email aliases as Esire-family customer matches.
|
||||
|
||||
Production contact tagging was applied with `impersonate_user='rebot'`:
|
||||
|
||||
```text
|
||||
MATCHED_COUNT 31
|
||||
UPDATED_COUNT 25
|
||||
SKIPPED_ALREADY_CUSTOMER 6
|
||||
```
|
||||
|
||||
Two accidental applicant matches were corrected immediately:
|
||||
|
||||
```text
|
||||
4253 conversation-desireethrasher-customerreturnsinspect-0n3@indeedemail.com: removed Customer, left applicant
|
||||
3934 conversation-desireethrasher-customerreturnsinspect-9nv@indeedemail.com: removed Customer, left applicant
|
||||
```
|
||||
|
||||
After customer tagging, re-running the production 200 close plan still produced 27 candidates and kept `#41238` removed from the plan.
|
||||
|
||||
## Customer-tag safety gate — 2026-07-09
|
||||
|
||||
New hard rule: before closing any ticket as spam, `redmine_tickets.py` fetches the Helpdesk contact and checks `tag_list`. If the contact has tag `customer`, the ticket is **not** closed as spam. Absence of the `customer` tag does **not** imply spam.
|
||||
|
||||
Jason also clarified the closure risk model:
|
||||
|
||||
- Closed tickets are not deleted.
|
||||
- If the other party replies to a closed Helpdesk ticket, Redmine reopens it as `New Message`.
|
||||
- Therefore, stale 30+ day tickets that match a verified `spam-promotional` profile are reasonable fast-close candidates once contact safety gates pass.
|
||||
- However, if a ticket was previously closed and then reopened because the contact messaged again, do **not** auto-close it again. Escalate instead: bump priority and optionally set a due date 1–3 days out.
|
||||
|
||||
A separate action now exists to tag contacts needing manual/customer review:
|
||||
|
||||
```bash
|
||||
./redmine_tickets.py \
|
||||
--base-url https://rm.ldrprep.com \
|
||||
--project customer-service \
|
||||
--cache-dir <cache> \
|
||||
tag-maybe-customer [--apply]
|
||||
```
|
||||
|
||||
This adds `maybe-customer` to contacts with inbound/established-business inquiry evidence, using `--impersonate-user rebot` for writes.
|
||||
|
||||
Tests now cover 20 cases, including the customer-tag skip and `maybe-customer` tag action.
|
||||
|
||||
## Production 200-Ticket Dry Run With Customer Gate — 2026-07-09
|
||||
|
||||
Production cache:
|
||||
|
||||
```text
|
||||
.cache/redmine_tickets_prod_200_promotional_eval_1783612885
|
||||
```
|
||||
|
||||
Re-running the close plan after adding the contact `customer` safety gate produced:
|
||||
|
||||
```text
|
||||
Close-if-spam complete: planned=27 apply=False
|
||||
```
|
||||
|
||||
The customer-tag gate removed `#41238` from the close plan, which was the suspicious `manager@sonisha.com` / `Account Queries` candidate.
|
||||
|
||||
Current 27 close candidates:
|
||||
|
||||
```text
|
||||
#41098
|
||||
#41114
|
||||
#41121
|
||||
#41122
|
||||
#41127
|
||||
#41128
|
||||
#41153
|
||||
#41164
|
||||
#41167
|
||||
#41168
|
||||
#41201
|
||||
#41222
|
||||
#41226
|
||||
#41227
|
||||
#41242
|
||||
#41248
|
||||
#41269
|
||||
#41293
|
||||
#41297
|
||||
#41315
|
||||
#41331
|
||||
#41340
|
||||
#41341
|
||||
#41347
|
||||
#41368
|
||||
#41373
|
||||
#41850
|
||||
```
|
||||
|
||||
Maybe-customer dry-run produced 3 contact tag candidates:
|
||||
|
||||
```text
|
||||
contact_id=4470 source_issue_id=41424 tag_list=["maybe-customer"]
|
||||
contact_id=3919 source_issue_id=41448 tag_list=["maybe-customer"]
|
||||
contact_id=4476 source_issue_id=41504 tag_list=["maybe-customer"]
|
||||
```
|
||||
|
||||
No production mutation was applied after adding this guard.
|
||||
|
||||
## Production 100-Ticket Promotional-Junk Dry Run — 2026-07-09
|
||||
|
||||
Production URL:
|
||||
|
||||
```text
|
||||
https://rm.ldrprep.com
|
||||
```
|
||||
|
||||
Fresh cache:
|
||||
|
||||
```text
|
||||
.cache/redmine_tickets_prod_100_promotional_eval_1783609390
|
||||
```
|
||||
|
||||
Verified summary:
|
||||
|
||||
```text
|
||||
Sync complete: seen=100 fetched=100 skipped_cached=0 written=100
|
||||
Close-if-spam complete: planned=20 apply=False
|
||||
SUMMARY categories={'unknown-review': 59, 'automated-operational': 21, 'spam-promotional': 20}
|
||||
```
|
||||
|
||||
Promotional close candidates:
|
||||
|
||||
```text
|
||||
#41397 Business Journals events/nominations promo — replycincinnati@engaged.bizjournals.com
|
||||
#41430 Vistaprint sale — noreply@mail.vistaprint.com
|
||||
#41431 Business Summit promo — replycincinnati@partner.bizjournals.com
|
||||
#41657 Global Industrial Kee Safety promo — reply@globalindustrial.com
|
||||
#41658 Global Industrial Kee Safety promo — reply@globalindustrial.com
|
||||
#41685 Samsung promo — noreply_samsung@sea.samsung.com
|
||||
#41687 QNAP promo — enews-noreply@qnap.com
|
||||
#41698 Vistaprint promo — noreply@mail.vistaprint.com
|
||||
#41705 Popmenu restaurant promo — noreply@popmenu.com
|
||||
#41722 3PL lead-gen sales pitch — hello@offer.search3pl.com
|
||||
#41738 Vistaprint promo — noreply@mail.vistaprint.com
|
||||
#41746 Business Journals events/nominations promo — replycincinnati@engaged.bizjournals.com
|
||||
#41760 Global Industrial sale — reply@globalindustrial.com
|
||||
#41761 Global Industrial sale — reply@globalindustrial.com
|
||||
#41767 QNAP newsletter/promo — enews-noreply@qnap.com
|
||||
#41776 Vistaprint sale — noreply@mail.vistaprint.com
|
||||
#41788 Global Industrial sale — reply@globalindustrial.com
|
||||
#41789 Global Industrial sale — reply@globalindustrial.com
|
||||
#41804 3PL lead-gen sales pitch — d.elliott@fulfillmentviewpro.com
|
||||
#41834 QNAP promotional/newsletter item — enews-noreply@qnap.com
|
||||
```
|
||||
|
||||
No production mutation was applied during this dry run.
|
||||
|
||||
### Production apply verification — 2026-07-09
|
||||
|
||||
Jason approved applying the 20-candidate promotional set. The following command was run against the exact reviewed production cache:
|
||||
|
||||
```bash
|
||||
./redmine_tickets.py \
|
||||
--base-url https://rm.ldrprep.com \
|
||||
--project customer-service \
|
||||
--cache-dir .cache/redmine_tickets_prod_100_promotional_eval_1783609390 \
|
||||
close-if-spam \
|
||||
--close-status-id 5 \
|
||||
--contact-ticket-limit 10 \
|
||||
--impersonate-user rebot \
|
||||
--apply
|
||||
```
|
||||
|
||||
Applied closures:
|
||||
|
||||
```text
|
||||
#41397
|
||||
#41430
|
||||
#41431
|
||||
#41657
|
||||
#41658
|
||||
#41685
|
||||
#41687
|
||||
#41698
|
||||
#41705
|
||||
#41722
|
||||
#41738
|
||||
#41746
|
||||
#41760
|
||||
#41761
|
||||
#41767
|
||||
#41776
|
||||
#41788
|
||||
#41789
|
||||
#41804
|
||||
#41834
|
||||
```
|
||||
|
||||
Idempotency check:
|
||||
|
||||
```text
|
||||
Close-if-spam complete: planned=0 apply=True
|
||||
```
|
||||
|
||||
API verification after apply:
|
||||
|
||||
```text
|
||||
41397 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41430 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41431 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41657 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41658 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41685 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41687 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41698 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41705 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41722 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41738 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41746 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41760 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41761 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41767 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41776 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41788 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41789 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41804 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41834 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
```
|
||||
|
||||
## 100-Ticket Refined Promotional-Junk Dry Run — 2026-07-09
|
||||
|
||||
Fresh cache:
|
||||
|
||||
```text
|
||||
.cache/redmine_tickets_lan_100_promotional_eval_refined_1783601115
|
||||
```
|
||||
|
||||
Verified summary:
|
||||
|
||||
```text
|
||||
Sync complete: seen=100 fetched=100 skipped_cached=0 written=100
|
||||
Close-if-spam complete: planned=9 apply=False
|
||||
SUMMARY categories={'unknown-review': 68, 'spam-promotional': 9, 'automated-operational': 23}
|
||||
```
|
||||
|
||||
Promotional close candidates:
|
||||
|
||||
```text
|
||||
#41227 Global Industrial clearance — reply@globalindustrial.com — "Clearance: Bestsellers At Best Prices"
|
||||
#41297 Vistaprint sale — noreply@mail.vistaprint.com — "Up to 30% off select customer faves, like mugs, postcards & more"
|
||||
#41340 Global Industrial AC/fans — reply@globalindustrial.com — "Advanced AC, Wall Fans, And Cooling Units"
|
||||
#41373 TikTok Shop promo — taylor@shop.tiktok.com — "The simplest way to start selling this June"
|
||||
#41397 Business Journals events/nominations promo — replycincinnati@engaged.bizjournals.com — "Hi LDR, here are our upcoming events and nominations"
|
||||
#41430 Vistaprint sale — noreply@mail.vistaprint.com — "Email exclusive! Up to 40% off select top products ENDS TOMORROW"
|
||||
#41431 Business Summit promo — replycincinnati@partner.bizjournals.com — "Regional Business Summit Returns August 5"
|
||||
#41657 Global Industrial Kee Safety promo — reply@globalindustrial.com — "'Kee Safety' Modular & Durable Fall Prevention Equipment"
|
||||
#41658 Global Industrial Kee Safety promo — reply@globalindustrial.com — "'Kee Safety' Modular & Durable Fall Prevention Equipment"
|
||||
```
|
||||
|
||||
False-positive protections added/verified during this 100-ticket run:
|
||||
|
||||
- VoIP voicemail / call transcription tickets are `automated-operational`, not promotional spam.
|
||||
- Inbound buying/relationship inquiries like RePackify Gaylord purchase interest and u2wear returns-provider follow-up remain `unknown-review`, not auto-close.
|
||||
- Receipts, bounces, security alerts, invoices, Amazon/FBA/order notices remain `automated-operational`, not auto-close.
|
||||
|
||||
No Redmine mutation was applied during this 100-ticket dry run.
|
||||
|
||||
### 100-ticket LAN apply verification — 2026-07-09
|
||||
|
||||
Jason approved applying the 9-candidate promotional set. The following command was run against the exact reviewed cache:
|
||||
|
||||
```bash
|
||||
./redmine_tickets.py \
|
||||
--base-url http://192.168.50.170 \
|
||||
--project customer-service \
|
||||
--cache-dir .cache/redmine_tickets_lan_100_promotional_eval_refined_1783601115 \
|
||||
close-if-spam \
|
||||
--close-status-id 5 \
|
||||
--contact-ticket-limit 10 \
|
||||
--impersonate-user rebot \
|
||||
--apply
|
||||
```
|
||||
|
||||
Applied closures:
|
||||
|
||||
```text
|
||||
#41227
|
||||
#41297
|
||||
#41340
|
||||
#41373
|
||||
#41397
|
||||
#41430
|
||||
#41431
|
||||
#41657
|
||||
#41658
|
||||
```
|
||||
|
||||
Idempotency check:
|
||||
|
||||
```text
|
||||
Close-if-spam complete: planned=0 apply=True
|
||||
```
|
||||
|
||||
API verification after apply:
|
||||
|
||||
```text
|
||||
41227 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41297 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41340 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41373 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41397 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41430 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41431 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41657 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
41658 status=Closed close_note_author=Harry B. close_note_found=True
|
||||
```
|
||||
Reference in New Issue
Block a user