ConnectWise PSA skills and tools under ops/connectwise (hub-sanitized 2026-11-01: client intel, internal workflow/credential/member/company ids, archived-page apiToken/sessionId stripped to placeholders)
This commit is contained in:
commit
9c96092034
36 files changed
+310875
No files matched your search
@@ -0,0 +1,34 @@
|
||||
# ConnectWise PSA Production Query Patterns
|
||||
|
||||
## Legacy Mirror Bypass
|
||||
If the `/service/tickets` endpoint returns tickets from 2011, the request is hitting a legacy mirror.
|
||||
|
||||
**Solution:** Use the `conditions` parameter to force a production database hit.
|
||||
- **Query**: `conditions=id >= 110000`
|
||||
- **Result**: This bypasses the mirror and retrieves current production records.
|
||||
|
||||
## Secrets File Parsing & 401 Triage
|
||||
`~/.secrets/connectwise` uses `KEY=value` lines with prefixed keys (`CW_COMPANY_ID` / `CW_PUBLIC_KEY` / `CW_PRIVATE_KEY`). Proven auth build:
|
||||
```python
|
||||
import base64
|
||||
d = {}
|
||||
for line in open(path).read().splitlines():
|
||||
if "=" in line:
|
||||
k, v = line.split("=", 1); d[k.strip()] = v
|
||||
preencode = d["CW_COMPANY_ID"] + d["CW_PUBLIC_KEY"] + ":" + d["CW_PRIVATE_KEY"]
|
||||
auth = "Basic " + base64.b64encode(preencode.encode()).decode()
|
||||
```
|
||||
The PublicKey already starts with `+`, so plain concatenation is correct (no extra separator).
|
||||
|
||||
On 401 Unauthorized: print parsed key names and value lengths BEFORE editing code. A key-name mismatch (lookup found nothing) and a genuinely bad credential emit the same errors — `Incorrect username/password` or `Incorrect Authorization header format` — and only the parsed shape tells which fix applies. Patch the lookup; do not rewrite the whole script around it.
|
||||
|
||||
## Data Structures
|
||||
The API can return tickets in two primary formats:
|
||||
1. **Raw List**: `[{"id": 123, ...}, {...}]`
|
||||
2. **Wrapped Object**: `{"tickets": [{"id": 123, ...}, {...}]}`
|
||||
|
||||
Always check for both in Python:
|
||||
```python
|
||||
raw_data = json.loads(response)
|
||||
tickets = raw_data['tickets'] if isinstance(raw_data, dict) and 'tickets' in raw_data else raw_data
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
# ConnectWise PSA Technical Reference
|
||||
|
||||
## Authentication
|
||||
- **Auth Format**: `Basic base64(companyid+publickey:privatekey)`
|
||||
- **Client ID**: `<CLIENT_ID>`
|
||||
- **Base URL**: `https://cw.vistanetinc.com/v4_6_release/apis/3.0`
|
||||
|
||||
## Reporting Conventions
|
||||
|
||||
### "Open Tickets" Report
|
||||
- **Board**: VistaNet Help Desk
|
||||
- **Filters**:
|
||||
- `closedFlag = false`
|
||||
- Status does NOT start with `>` (e.g., exclude `>Completed`)
|
||||
- **Ordering**: Newest to Oldest (`dateEntered`)
|
||||
- **Delivery**: Professional HTML email to `sbrown@vninc.com`
|
||||
- **Fields**: Ticket #, Company, Owner, Status, Summary
|
||||
|
||||
## API Property Mappings
|
||||
- **Tickets**: Use `status` (Object: contains `id`, `name`, `Sort`)
|
||||
- **Agreements**: Use `agreementStatus` (String) instead of `status`
|
||||
|
||||
## Common Endpoints
|
||||
- Tickets: `/service/tickets`
|
||||
- Agreements: `/finance/agreements`
|
||||
- Companies: `/company/companies`
|
||||
@@ -0,0 +1,75 @@
|
||||
# Ticket Scheduling Runbook (Schedule Entries)
|
||||
|
||||
End-to-end flow for "schedule my tickets on the calendar" requests, validated end-to-end on the VistaNet instance. All timestamps UTC.
|
||||
|
||||
## 1. Resolve the member
|
||||
`/system/myMember` 404s for API members. Use `GET /system/members?conditions=identifier='SBrown'` (URL-encode quotes) → id. Reuse the id for every `owner/id` and `member/id` (SBrown = 147).
|
||||
|
||||
## 2. Pull open tickets
|
||||
`GET /service/tickets?conditions=owner/id=<memberId> AND closedFlag=false&pageSize=1000`; filter client-side for truly-open (status starting `>`, name `Closed`, or `dateResolved` present ⇒ exclude). Handle both list and `{"tickets": [...]}` shapes.
|
||||
|
||||
## 3. Check SLA reality before doing SLA math
|
||||
`GET /service/SLAs/{id}` tells you the plan exists. If `/service/SLAs/{id}/plans` returns EMPTY and `/service/slaPlans` returns 404, the SLA carries NO hour plans — Standard SLA (id 2) here has none, so every Priority 3–Medium ticket is SLA-identical and SLA contributes nothing to urgency. Do not invent response-time tiers from the SLA name. `/service/boards/{id}/slaVersions` and its `/detail` subpath also 404 on this instance — treat both as dead ends, not as a reason to stop ranking.
|
||||
|
||||
## 4. Rank by what changes a deadline
|
||||
When priorities are uniform (all P3-Medium), rank from signals in the ticket data itself, in order:
|
||||
1. Hard expiration dates in the summary (cert renewals: "Expiring on YYYY-MM-DD") — expired or expiring this week first.
|
||||
2. Active security incidents and outages (summary words: intrusion, confirmed, outage, unreachable, failing) with status In Progress.
|
||||
3. Client-flagged urgency ("Urgent per <name>").
|
||||
4. Security hygiene (patching, key setup, directory sync, mail filtering).
|
||||
5. User-facing breakage (Tier 3 Assigned).
|
||||
6. Small tasks (new user setup) swept into slack blocks.
|
||||
|
||||
Do NOT cram project/coordination tickets (status `Must Schedule`) into gaps — they need client scheduling in long blocks; propose them for next week instead.
|
||||
|
||||
Unschedulable (list, don't book): On Hold, Waiting Client Response, Waiting parts/repair.
|
||||
|
||||
Status nuance: ticket status `Scheduled` only means SOME entry exists — often an all-day 0.0h placeholder months out. Check the entry's date against the summary expiry before trusting it.
|
||||
|
||||
## 5. Check existing commitments first
|
||||
Fetch the week UNFILTERED — `dateStart > [Monday-local ISO] AND dateStart < [NextMonday-local ISO]`, pageSize 500 — and filter by `member.id`/`member.identifier` client-side. Member-scoped conditions on this endpoint intermittently drop or mutate entries. Retry until two consecutive fetches agree (a stability gate), never gate on an absolute entry count — entries legitimately vanish mid-session when other people edit the calendar. Compose around firm entries; anchor back-to-back rather than overlapping (overlaps ⇒ `400 InvalidObject` SystemConflict on dateStart). Note objectId-0 blocks (internal placeholders with no ticket behind them) and re-check commitments immediately before POSTing — the calendar can move between your fetch and your write.
|
||||
|
||||
## 6. Slot composition and dispositions
|
||||
- Local (PDT = UTC-7) to UTC conversion: +7h (2:30p PDT → 21:30Z).
|
||||
- Same-day urgent work goes TODAY in the first free block — an expiring cert waits for no one; start from the next half hour.
|
||||
- Batch small tasks (≤1h each) contiguously on the lightest afternoon.
|
||||
- Tickets whose start is gated externally (client confirmation pending, vendor scheduling unknown, still in evaluation) get NO entry — list them as dispositions with the blocking reason so nothing silently drops. Book when the gate clears. An ALREADY-BOOKED ticket whose gate moves later keeps its entry: reschedule via PATCH (§9), never delete+repost. A scheduling gate is recorded as an INTERNAL TICKET NOTE (§9), never in the entry `name` — a ticket-linked entry's display name auto-regenerates from `Company/Summary` and silently discards custom label text.
|
||||
- Recurring unavailability: team/tech meeting every Friday 8:00–9:30 AM PDT — never schedule SBrown in that window, and never let a Friday block start before 9:30.
|
||||
- After-hours work (evening server/RDS upgrades needing users logged off) gets a full evening block (e.g. 6–9p), not an afternoon gap fill, and needs a coordination runway — book the earliest evening the client could plausibly agree to, not tonight.
|
||||
- Booking requests are literal id lists: book exactly the named tickets; a duplicated id means one booking plus a note, not two entries.
|
||||
|
||||
## 7. POST each entry
|
||||
`POST /schedule/entries` with a plain JSON object (NOT JSON Patch):
|
||||
```json
|
||||
{
|
||||
"name": "Company / Ticket# 12345 - short label",
|
||||
"member": {"id": 147},
|
||||
"type": {"id": 4},
|
||||
"span": {"id": 1},
|
||||
"status": {"id": 2},
|
||||
"objectId": 12345,
|
||||
"ticketType": "S",
|
||||
"where": {"id": 4},
|
||||
"dateStart": "2026-10-06T16:00:00Z",
|
||||
"dateEnd": "2026-10-06T18:00:00Z",
|
||||
"hours": 2.0,
|
||||
"doneFlag": false,
|
||||
"meetingFlag": false
|
||||
}
|
||||
```
|
||||
Observed ids: span 1 = `N`, status 2 = `Firm`, type 4 = `S`, where 4 = `Remote`. `name` is the free-form calendar label.
|
||||
|
||||
## 8. Verify, then audit coverage
|
||||
Re-GET the unfiltered week window and confirm each new entry by objectId AND intended start time; a 200 on the POST is not proof the calendar shows it. If a freshly posted entry is missing from the refetch, direct-GET the entry id: 404 ⇒ deleted/rolled back; present ⇒ transient list unreliability. Then audit the whole picture in the same pass: every open ticket with a summary expiration date vs its calendar coverage, every Must Schedule ticket vs entries, my timed entries pair-wise for overlaps, and next week's window for duplicate bookings of the same ticket. A vanished block usually means the ticket closed (`>Completed`), was reassigned (owner changed), or a duplicate was cleaned up — check ticket status/owner before treating absence as a scheduling failure.
|
||||
|
||||
Present results with local times, then a disposition list — booked / blocked-status (Waiting Client, On Hold, Waiting parts) / pending-external-gate / intentionally unbooked — plus flags: expired-but-waiting-on-client warnings, objectId-0 blocks with no ticket, and Must Schedule candidates for next week.
|
||||
|
||||
## 9. Modifying existing entries and recording gates
|
||||
- **PATCH `/schedule/entries/{id}` requires a JSON Patch ARRAY** (same as tickets): `[{'op':'replace','path':'/dateStart','value':'...Z'},{'op':'replace','path':'/dateEnd','value':'...Z'},{'op':'replace','path':'/hours','value':3.0}]`. A plain JSON object 400s. A 200 does NOT prove every field took — re-GET and check each patched field.
|
||||
- **Do not patch `/name` on a ticket-linked entry**: the write is accepted but the display name regenerates from `Company/Summary`, so any PENDING/DO-NOT-START marker in the label silently disappears. Skip it up front.
|
||||
- **Scheduling gates live as internal ticket notes**: `POST /service/tickets/{id}/notes` with a PLAIN JSON object (notes are not JSON Patch): `{'text': '...', 'internalAnalysisFlag': true, 'processNotifications': false}`. State the window, the entry id, who must confirm, and DO-NOT-START until then. `customerFlag` is not a valid ServiceNote member on this version (400 if included).
|
||||
- **Owner reassignment** (ticket leaves your queue): `[{'op':'replace','path':'/owner/id','value':<memberId>}]` on `/service/tickets/{id}`, then fresh-GET to verify owner identifier. Resolve the new member's id first with `GET /system/members?conditions=identifier='X'`. Remove the ticket from your scheduling plans the same turn — don't book/keep entries for tickets you no longer own.
|
||||
- **Ownership hygiene check (standing rule)**: for a tech's open tickets whose most recent note is from another tech (a likely transfer), vet before PATCH: automation members (the API/service-account members) NEVER receive ownership — their notes are integration/alert noise; time entries count as "me being there" (older intake note beats nothing, loses to a newer TE by the current owner); read the note's intent — intake/directive notes ("please order and drop ship", "get you guys involved") keep the ticket with the current owner; when the current owner has NO note and NO TE and the other tech's note is newer, that's a legitimate transfer. Verify each PATCH with a fresh GET of owner.identifier; drop transferred tickets from any staged plan same turn.
|
||||
|
||||
## Next-week candidate review
|
||||
When asked what could be scheduled next week: fetch next + far-week windows (stability-gated), subtract every open ticket that has a booking in them, list the remainder grouped as schedulable (with concrete proposed slots from actual gaps), conditional-on-gate, and parked — naming the gate per ticket. Next-week Mon–Wed is typically wide open; propose day+time anchors, not vague availability.
|
||||
Reference in new issue
Block a user