Organized

This commit is contained in:
Jason Thistlethwaite
2026-10-17 20:50:35 -04:00
parent bd26c8894f
commit 9ee9eb3ec6
130 changed files with 24172 additions and 140 deletions
+28
View File
@@ -0,0 +1,28 @@
# Feature detail: Ticket attachments
**Parent roadmap:** [[../features-for-redmine_tickets-sync]]
**Status:** MVP 2026-07-15
**Module:** `redmine_ticket_attachments.py`
## Commands
```bash
./bin/rtl attachments list --issue-id 39305 --refresh
./bin/rtl attachments download --issue-id 39305 --attachment-id 55917
./bin/rtl attachments download --issue-id 39305 --all
./bin/rtd attachments upload --issue-id 39305 --file ./scan.pdf # dry-run
./bin/rtd attachments upload --issue-id 39305 --file ./scan.pdf --apply --notes 'scan attached'
```
| Op | Emails customer? | Notes |
|----|------------------|--------|
| list | no | cache-first; `--refresh` pulls `include=attachments` |
| download | no | needs API key; default dir `<cache>/<project>/attachments/<issue_id>/` |
| upload | **no** | Redmine `POST /uploads.json` + issue `uploads` token; prod needs `--allow-prod-writes` |
Sync now also requests `attachments` on full issue fetch so metadata lands in cache over time.
## Still open
- OCR rename workflow (see [[../ocr-ticket-attachment-to-rewrite-update-ticket]])
- Draft-queue binary uploads (paths) if desired
+44
View File
@@ -0,0 +1,44 @@
# Feature detail: Interactive browse loop
**Parent roadmap:** [[../features-for-redmine_tickets-sync]]
**Status:** MVP+ polish 2026-07-15
**Module:** `redmine_ticket_browse.py`
## Flow
1. Filtered ticket list via **fzf** (preview pane) or numbered pick
2. **Enter** → Markdown pager (`glow` / `bat` / `batcat` / `cat`)
3. After pager → **action menu**
4. Back to list until Esc/q
## Actions (queue-safe)
| Key | Behavior |
|-----|----------|
| v / Enter | View again |
| n / p | Queue internal / private note |
| f | Queue field update (names) |
| t | Local control-tag |
| c | ticket-contact handoff |
| e | Pattern entities |
| **m** | NMI cs-brief → confirm → **private** note draft |
| i | Print id |
| l / q | List / quit |
## Flags
- `--where`, `--status`, `--control-tag`, `--has-attachments`, **`--open-only`**
- `--max-tickets`, `--order`, `--pager`, `--once`, **`--no-preview`**
## Done polish
- [x] fzf header lists actions
- [x] fzf preview pane
- [x] loud draft banner
- [x] NMI menu action
## Non-goals
- Full curses app (separate spike)
- Customer-email from browse (CLI-only by design)
- Applying prod writes from browse without draft step
+41
View File
@@ -0,0 +1,41 @@
# Config files / short aliases (minimal)
**Parent:** [[../features-for-redmine_tickets-sync]]
**Status:** minimal MVP (global defaults + bin wrappers). Nested per-command defaults deferred.
## Runtime config
```bash
./redmine_tickets.py --config configs/live.json search --max-tickets 10
./redmine_tickets.py --config configs/dev.json note --issue-id 39305 --body 'x' --apply
```
JSON (always works) or YAML if PyYAML is installed.
Supported top-level keys (also under `redmine_tickets:`):
- `base-url` / `base_url`
- `project`
- `cache-dir` / `cache_dir`
- `allow-prod-writes` (bool)
CLI flags already present win over config.
## Short wrappers (`bin/`)
| Command | Maps to |
|---------|---------|
| `bin/rtl` | live/prod CS (`configs/live.json`) |
| `bin/rtd` | LAN/dev CS (`configs/dev.json`) |
| `bin/qaa` | alias → `rtl` (muscle-memory example) |
```bash
./bin/rtl search --where 'assigned_to IS NULL' --max-tickets 5
./bin/rtl --allow-prod-writes draft apply <id>
./bin/rtd note --issue-id 39305 --body 'LAN note' --apply
```
## Not yet
- Nested `search.max-tickets` / command-specific defaults from YAML namespaces
- Auto-discovery of many named profiles beyond live/dev
@@ -0,0 +1,5 @@
## Contact Triage / Fixing
- [ ] Properly link "call transcript" and "voicemail" tickets to the correct person by searching the contact database with the phone number involved
- [ ] Ingest call transcripts into our timeline
- [ ] Fix contact company is set to their email address, when the name of their business is easily found through context in tickets
@@ -0,0 +1,38 @@
# Feature detail: Contacts sync + mutation (tie-in)
**Parent roadmap:** [[../features-for-redmine_tickets-sync]]
**Status:** Sync/search/show + draftable `contact-set` + `ticket-contact` bridge (2026-07-15)
**Module:** `redmine_ticket_contacts.py`
**Still separate from:** full `redmine_contacts.py` CRM CLI (batch/loops/custom-field tooling)
## Why not one tool
- Tickets workbench wants co-located per-id JSON + draft/apply safety next to tickets.
- `redmine_contacts.py` already owns heavy CRM workflows and a different cache layout.
- Do not rewrite/delete the CRM CLI; add bridges so a ticket-started workflow can end on contacts.
## Implemented
```bash
./bin/rtl contacts-sync --max-contacts 100
./bin/rtl contacts-search --query ada
./bin/rtl contact-show --contact-id 14 --fetch
# Bridge ticket → contact id
./bin/rtl ticket-contact --issue-id 41934
./bin/rtl ticket-contact --issue-id 41934 --show --fetch
# CRM mutation (draft by default; same prod/LAN gates as ticket writes)
./bin/rtl contact-set --contact-id 4515 --add-tag maybe-customer
./bin/rtd contact-set --contact-id 2284 --add-tag maybe-customer --apply --refresh-cache
```
- Durable path: `<cache-dir>/contacts/<id>.json` + `manifest.json`
- Draft kind: `contact_fields` (apply via `draft apply`)
- Handoff hints print `redmine_contacts.py show …` for full CRM when needed
## Still open
- Richer custom-field helpers (use `redmine_contacts.py` or extend `--set`)
- Optional cron for contacts next to ticket sync
- Shared library / single on-disk layout if dual-cache friction becomes painful
+31
View File
@@ -0,0 +1,31 @@
# Feature detail: Local control tags (filter-assist labeling)
**Parent roadmap:** [[../features-for-redmine_tickets-sync]]
**Status:** MVP implemented 2026-07-15 (`control-tag` CLI + `search --control-tag`)
**Module:** `redmine_ticket_control_tags.py`
**Critical distinction:** these tags are **local-only**. They are **not** Redmine issue tags, **not** contact CRM tags.
## Storage
`<cache-dir>/control-tags/tags.json` — wipe without touching Redmine or ticket JSON.
## CLI
```bash
./redmine_tickets.py ... control-tag add --tag bavaria --issue-id 41934 \
--property subject --property helpdesk_ticket.contact.company \
--annotation 'BavariaTrachten shape'
./redmine_tickets.py ... control-tag list
./redmine_tickets.py ... control-tag show --tag bavaria
./redmine_tickets.py ... control-tag for-issue --issue-id 41934
./redmine_tickets.py ... control-tag remove --tag bavaria --issue-id 41934
./redmine_tickets.py ... control-tag wipe --i-understand-this-wipes-local-control-tags
./redmine_tickets.py ... search --control-tag bavaria
```
## Still open
- Suggest/refine loop (LLM or rules + human correct/incorrect)
- Training-export format for classifier experiments
@@ -0,0 +1,25 @@
# Feature detail: External entity type mapper / identifier
**Parent roadmap:** [[../features-for-redmine_tickets-sync]]
**Status:** pattern MVP + NMI CLI bridge + browse `m` (2026-07-15)
**Modules:** `redmine_ticket_entities.py`, `redmine_ticket_nmi.py`
**NMI env docs:** [[nmi-bridge]]
## Commands
```bash
./bin/rtl entities show --issue-id 41394
./bin/rtl entities scan --where 'subject.includes("1Z")' --max-tickets 20
./bin/rtl entities nmi --issue-id 41394
```
## Done
- Pattern types: UPS/USPS, FedEx candidates, Amazon order, order_id, item_or_sku, **item_no** (LDR 9-digit)
- Markdown view section: **Detected entities**
- NMI bridge + browse **`m`** → private note draft
## Open
- [ ] Optional membership index of recent NMI order/item ids
- [ ] Postal addresses / phones / social (much later)
@@ -0,0 +1,39 @@
# Feature detail: Local search + CLI Redmine client
**Parent roadmap:** [[../features-for-redmine_tickets-sync]]
**Status:** MVP+ (2026-07-14 … 2026-07-15) — ops-usable
**Cookbook:** `docs/cli_power_user_client.md`
## Modules
| Module | Role |
|--------|------|
| `redmine_ticket_query.py` | Property AST (`--where`) |
| `redmine_ticket_view.py` | Markdown + pager |
| `redmine_ticket_browse.py` | fzf → pager → action menu loop |
| `redmine_ticket_filters.py` | Saved filters + symlink queues |
| `redmine_ticket_drafts.py` | Draft queue + host safety |
| `redmine_ticket_meta.py` | Name→id for set-fields |
| `redmine_ticket_ocr.py` | Attachment text extract + subject suggest |
## Done
- [x] Local search / `@saved` filters / queues
- [x] view / walk (`--open-only` on search/browse/walk)
- [x] note vs customer-email vs set-fields (draft/apply)
- [x] `--body-file` + `--to-textile` (pandoc)
- [x] browse **loop** with action menu (queue-only mutations + NMI `m`)
- [x] configs: `bin/rtl`, `bin/rtd`, `--config`
- [x] `ocr-rename` + `@scanned-document`
- [x] `categories list`
## Open
- [ ] Nested config defaults
- [ ] Full curses TUI (optional later)
- [ ] categories create/update
## Non-goals
- Replacing Redmine web UI for everything
- Silent customer email from browse
+41
View File
@@ -0,0 +1,41 @@
# NMI bridge env + usage
**Parent:** [[entity-type-mapper]] · module `redmine_ticket_nmi.py`
## Environment
| Variable | Default | Purpose |
|----------|---------|---------|
| `NMI_CLI` | `/home/jason/projects/mrscClient-php/bin/nmi` | Path to NMI PHP CLI |
| `NMI_ENV_FILE` | `/home/jason/projects/mrscClient-php/examples/modern/.env` | Env file for credentials/endpoint |
| `NMI_MODE` | `dev` | CLI `--mode` (dev/prod as supported by nmi) |
Requires `php` on PATH.
## CLI
```bash
# Pattern extract only
./bin/rtl entities show --issue-id 41394
./bin/rtl entities scan --type tracking_ups --max-tickets 20
# Resolve / cs-brief via NMI CLI
./bin/rtl entities nmi --issue-id 41394
./bin/rtl entities nmi --issue-id 41394 --nmi-mode dev
./bin/rtl entities nmi --issue-id 41394 --as-private-note-draft # if wired; else use browse `m`
```
## Browse
After opening a ticket in `browse`, press **`m`**:
1. Runs NMI brief from extracted entities
2. Shows brief text
3. Confirms → queues **private** internal note draft
Never emails the customer.
## Safety
- NMI is read-side ops context, not customer mail
- Private note drafts still need `draft apply` (+ prod flags) to land in Redmine
+46
View File
@@ -0,0 +1,46 @@
# Feature detail: OCR attachment → subject rename
**Parent roadmap:** [[../features-for-redmine_tickets-sync]]
**Status:** MVP 2026-07-15
**Module:** `redmine_ticket_ocr.py` · CLI: `ocr-rename`
## Intent
Scanned docs land in Helpdesk as tickets titled “Scanned document…”.
Ops need: open attachment → understand contents → rename subject (+ optional notes).
## Commands
```bash
# One ticket: download OCR-able attachments, propose subject, queue private note (+ subject draft)
./bin/rtl ocr-rename --issue-id 12345
# Scan cache for generic scan subjects
./bin/rtl ocr-rename --where '@scanned-document' --only-generic-subjects --max-tickets 20
# Apply subject now (LAN/prod safety gates); still queues private OCR note by default
./bin/rtd ocr-rename --issue-id 12345 --apply-subject
# Extract only (no drafts)
./bin/rtl ocr-rename --issue-id 12345 --no-queue-subject --no-queue-note --json
```
## Pipeline
1. Load ticket + attachments (`include=attachments` refresh if needed)
2. Download PDF/image candidates (multi-URL fallback)
3. Extract text: `pdftotext` → else `pdftoppm` + `tesseract`
4. Heuristic `suggest_subject_from_text` (doc type, person/company, date)
5. Queue drafts:
- `fields` subject change (unless `--no-queue-subject`)
- private `internal_note` with OCR snippet (unless `--no-queue-note`)
## Saved filter
`saved-filters/scanned-document.expr` → `@scanned-document`
## Still open
- [ ] Stronger LLM subject summarization (optional; keep heuristics default)
- [ ] Cron wrapper that only processes new generic-scan tickets
- [ ] HR-project default project config example
@@ -0,0 +1,3 @@
- [ ] Download voicemails and call recordings from voip.ms automatically
- [ ] helper PHP client library https://git.int.ldrprep.com/dusk/php-voip.ms
@@ -0,0 +1,91 @@
# Feature detail: Ticket sync foundation
**Parent roadmap:** [[../features-for-redmine_tickets-sync]]
**Status:** largely implemented (2026-07-14); cron wiring still open
**Primary script:** `redmine_tickets.py`
**Target project (first):** `customer-service`
## Intent
Durable, incremental local mirror of Redmine issues so humans and agents can work offline-first, cron-refresh frequently, and not re-pull full payloads for unchanged tickets.
## Requirements
- Sync tickets updated within a window (e.g. past 30 days) into a **stable** cache path (not timestamped per run).
- Skip full fetch when cached `updated_on` still matches the list stub.
- Support frequent refresh (every few minutes / couple times per hour) using a tight remote window after the initial warm-up.
- Never delete old ticket JSON unless explicitly asked.
- Optional small run-stats ring buffer for cron observability.
## Cache locations
| Environment | Path |
|-------------|------|
| Production (`https://rm.ldrprep.com`) | `/home/jason/projects/redmine/cache/redmine_tickets` |
| Dev / LAN (`http://192.168.50.170`) | `/home/jason/projects/redmine/cache-dev/redmine_tickets` |
Layout:
```text
<cache-dir>/
manifest.json
run-stats.json # optional, with --run-log
<project>/
<issue-id>.json
```
## Server-side vs local filters (sync)
| Flag | Side | Meaning |
|------|------|---------|
| `--status-id` / `--remote-status-id` | **remote** | Redmine API `status_id` (`*`, `open`, `closed`, numeric) |
| `--remote-updated-on` | **remote** | Raw Redmine syntax: `>=2026-06-14`, `>=2014-01-02T08:12:32Z`, `><a\|b` |
| `--remote-updated-since` | **remote** | Convenience → `>=...` (YYYY-MM-DD, ISO, `30d`, `2 days ago`, …) |
| `--remote-updated-before` | **remote** | Convenience → `<=...`; with since → `><since\|before` |
| `--updated-since` / `--updated-before` / `--status` / `--category` | **local** | Post-filters after API list only |
Implementation notes:
- List stubs intentionally omit heavy `include=journals,helpdesk`; full include is only on per-ticket fetch when `should_fetch` is true.
- Progress on by default; `--quiet` for cron.
- Unit tests: `tests/test_redmine_tickets_remote_filters.py`
- Run log: `--run-log` → default `<cache-dir>/run-stats.json`, keep last N (`--run-log-keep`, default 3).
## Recommended commands
```bash
cd /home/jason/projects/redmine
# Initial 30-day window (server-side)
./redmine_tickets.py \
--base-url https://rm.ldrprep.com \
--project customer-service \
--cache-dir /home/jason/projects/redmine/cache/redmine_tickets \
sync \
--remote-status-id '*' \
--remote-updated-since 30d \
--run-log
# Frequent refresh
./redmine_tickets.py \
--base-url https://rm.ldrprep.com \
--project customer-service \
--cache-dir /home/jason/projects/redmine/cache/redmine_tickets \
sync \
--remote-status-id '*' \
--remote-updated-since 2h \
--run-log \
--quiet
```
Raw Redmine form:
```bash
./redmine_tickets.py ... sync --remote-updated-on '>=2026-06-14' --status-id '*'
```
## Open
- [x] Scripts under `cron-scripts/` installed + crontab running (live + dev)
- [ ] **ADJUST LATER (do not forget):** cron still uses `--remote-updated-since 30d` on a few-minute cadence. Fine while validating (~3–4s/run after warm cache) but should tighten to `2h`/`6h` (or marker-based since-last-success) so list pages stay cheap long-term. `run-log-keep 20` is already set.
- [ ] Optional: master storage + symlink organization of JSON (see parent roadmap)