chore(memory): consolidate scattered feedback/project/reference files

Compressed memory store 104 -> 71 files via four passes:

- Syncro: 19 scattered feedback_syncro_* files merged into 3 rule files
  (api/billing/workflow) + an on-demand feedback_syncro_history.md for
  incident detail, quotes, and tech/product ID tables.
- Four near-duplicate merges: Howard paste-safety, Pluto build server,
  Howard backend deferral, IX server access (ssh+tailscale).
- Per-cluster rule/state/history split applied to GuruConnect (2->1),
  Dataforth (3->2), Cascades (7->3), GuruRMM (13->3).
- New reference_resource_map.md: single auto-loaded cheatsheet for
  "do I have access to X and how do I connect from this machine?"
- MEMORY.md rewritten to match the new layout.

Health: broken backlinks 8->7, overlap clusters 12->5, orphans 17->0.
This commit is contained in:
2026-06-01 16:25:45 -07:00
parent 2a1ccfac73
commit 0c000109dc
75 changed files with 1473 additions and 1324 deletions

View File

@@ -0,0 +1,82 @@
---
name: Syncro API plumbing — headers, endpoints, response shapes, idempotency
description: Technical mechanics for talking to the Syncro API — required Content-Type header, the no-idempotency rule (always GET before retry), response wrappers, the add_line_item endpoint shape, HTML rendering, and the (now historical) timer_entry response shape.
metadata:
type: feedback
---
Rules only. Incident detail, verbatim quotes, ticket numbers, and dates live in [[feedback_syncro_history]] — read on-demand when judging an edge case. Billing/product rules: [[feedback_syncro_billing]]. Workflow rules: [[feedback_syncro_workflow]].
---
## 1. Content-Type header is required on every POST/PUT
Always include `-H "Content-Type: application/json"`. Without it, curl sends `application/x-www-form-urlencoded` and Syncro returns a **400 HTML page** (not JSON). Applies to comments, tickets, line items, estimates, updates.
Ticket comment payloads also need the `subject` field:
```json
{"subject":"...","body":"...","hidden":true,"do_not_email":true}
```
---
## 2. No idempotency — ALWAYS GET before retrying any POST
Syncro has **no idempotency on any endpoint**. One `POST` always creates one record, regardless of whether the client saw an error. A jq parse error, curl error, timeout, or weird-looking response does NOT mean the POST failed — verify first.
**Verification before retry:**
- Comments: `GET /tickets/{id}` and search `.ticket.comments[] | select(.subject == "…")`. Check ALL comments, not just `[-3:]`.
- Tickets: `GET /customers/{id}/tickets` before retrying.
- Line items: `GET /tickets/{id}``.ticket.line_items[]`.
**Response wrappers — CRITICAL for jq:**
- `POST /tickets``{"ticket": {...}}` → use `.ticket.id`.
- `POST /comment``{"comment": {...}}` → use `.comment.id`.
**Hardening:** Write payload JSON to a temp file (e.g. `tmp/syncro_comment.json`) before posting. Avoids shell quoting/encoding failures that masquerade as POST failures on requests that actually succeeded.
Comments cannot be deleted via API — duplicates require manual GUI removal.
---
## 3. HTML formatting in comments
Use `<br>` for line breaks. Do **NOT** use `<ul>` or `<li>` — Syncro's renderer collapses them into one line. For bulleted lists:
```html
<p>
- Item one<br>
- Item two<br>
- Item three
</p>
```
---
## 4. add_line_item endpoint
`POST /api/v1/tickets/{internal_ticket_id}/add_line_item`
- Path uses the **internal ticket ID** (e.g. `111387456`), NOT the ticket number (`32339`). Wrong-ID variants (`/line_item`, `/line_items`, `PUT line_items_attributes`) all 404.
- Required fields: `name`, `description`, `quantity`, `price`, `taxable` (and `product_id` for catalog items). Missing `name`/`description` → 422.
- Response is **flat** — parse `.id` directly (no wrapper).
Example:
```bash
curl -X POST "$BASE/tickets/111387456/add_line_item?api_key=$KEY" \
-H "Content-Type: application/json" \
-d '{"product_id":1049360,"name":"Labor- Warranty work","description":"…","quantity":1,"price":0.0,"taxable":false}'
```
For ad-hoc API testing, use the internal ACG account only (customer ID `15353550`).
---
## 5. Timer response — HISTORICAL / SUPERSEDED
> Timers are no longer part of the ACG Syncro billing workflow (as of 2026-05-21 — see [[feedback_syncro_billing]] and [[feedback_syncro_history]]). Kept here for the rare manual-timer case.
`POST /tickets/{id}/timer_entry` returns a **FLAT** object — parse `.id`, NOT `.timer.id // .timer_entry.id` (both resolve to `null` and break `charge_timer_entry`, which can trigger a retry → duplicate). `charge_timer_entry` response is also flat: use `.ticket_line_item_id`.
Authoritative timer list for a ticket: `GET /tickets/{id}``.ticket.ticket_timers[]`. The standalone `/ticket_timers?ticket_id=N` returns global history, not filtered.
Cleanup duplicates: `POST /tickets/{id}/delete_timer_entry` with `{"timer_entry_id": N}`.