825 lines
27 KiB
Markdown
825 lines
27 KiB
Markdown
# 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
|
||
```
|