sync: auto-sync from GURU-BEAST-ROG at 2026-05-22 13:13:08
Author: Mike Swanson Machine: GURU-BEAST-ROG Timestamp: 2026-05-22 13:13:08
This commit is contained in:
@@ -584,14 +584,21 @@ POST `/invoices` pulls all current line items from the ticket into the invoice a
|
||||
|
||||
#### Estimates
|
||||
|
||||
Estimates (quotes) are standalone or ticket-linked. Verified 2026-05-22 against ACG internal account.
|
||||
Estimates (quotes) always get an associated ticket with a private note containing links. This is a hard workflow requirement — never create an estimate without the ticket and private note.
|
||||
|
||||
**Required fields for POST /estimates:** `customer_id`, `date` (ISO date string `"YYYY-MM-DD"`)
|
||||
**Optional:** `name` (estimate title), `ticket_id` (link to ticket), `location_id`
|
||||
**Statuses:** `Fresh` (default), `Approved`, `Declined`
|
||||
|
||||
**MANDATORY estimate workflow (4 steps, always in this order):**
|
||||
|
||||
1. Create estimate
|
||||
2. Add line items (with price fix — see below)
|
||||
3. Create ticket (`do_not_email: true`, `hidden: true` note) with private note containing estimate link + product/source links
|
||||
4. Link estimate to ticket via PUT, then post a single bot alert with both links
|
||||
|
||||
```bash
|
||||
# Create estimate
|
||||
# Step 1 — Create estimate
|
||||
EST_RESP=$(curl -s -X POST "${BASE}/estimates?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @- <<JSON
|
||||
@@ -605,8 +612,9 @@ JSON
|
||||
ESTIMATE_ID=$(echo "$EST_RESP" | jq -r '.estimate.id')
|
||||
ESTIMATE_NUM=$(echo "$EST_RESP" | jq -r '.estimate.number')
|
||||
|
||||
# Add line item — endpoint is /line_items NOT /add_line_item (that 404s)
|
||||
# Response: {"estimate": {...}, "line_item": {"id": N, "item": name, "price": rate, ...}}
|
||||
# Step 2 — Add line item, then fix price via PUT
|
||||
# NOTE: POST /line_items ignores price_retail for hardware (product 32252) — price stays $0.
|
||||
# Always follow up with PUT /line_items/{id} to set the price. Verified 2026-05-22.
|
||||
LI_RESP=$(curl -s -X POST "${BASE}/estimates/${ESTIMATE_ID}/line_items?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @- <<JSON
|
||||
@@ -616,13 +624,57 @@ LI_RESP=$(curl -s -X POST "${BASE}/estimates/${ESTIMATE_ID}/line_items?api_key=$
|
||||
"description": "<one-line description>",
|
||||
"quantity": ${QTY},
|
||||
"price_retail": ${RATE},
|
||||
"taxable": false
|
||||
"taxable": true
|
||||
}
|
||||
JSON
|
||||
)
|
||||
LI_ID=$(echo "$LI_RESP" | jq -r '.line_item.id')
|
||||
|
||||
# GET estimate (verify)
|
||||
# Fix price via PUT (required — POST does not apply price_retail for hardware)
|
||||
curl -s -X PUT "${BASE}/estimates/${ESTIMATE_ID}/line_items/${LI_ID}?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @- <<JSON
|
||||
{"price": ${RATE}, "price_retail": ${RATE}}
|
||||
JSON
|
||||
|
||||
# Step 3 — Create ticket + private note
|
||||
TICKET_RESP=$(curl -s -X POST "${BASE}/tickets?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @- <<JSON
|
||||
{
|
||||
"customer_id": ${CUST_ID},
|
||||
"subject": "<subject matching estimate name>",
|
||||
"problem_type": "Hardware",
|
||||
"status": "New",
|
||||
"priority": "2 Normal",
|
||||
"user_id": ${TECH_ID},
|
||||
"do_not_email": true
|
||||
}
|
||||
JSON
|
||||
)
|
||||
TICKET_ID=$(echo "$TICKET_RESP" | jq -r '.ticket.id')
|
||||
TICKET_NUM=$(echo "$TICKET_RESP" | jq -r '.ticket.number')
|
||||
|
||||
# Private note — hidden, with estimate link + product/source links
|
||||
curl -s -X POST "${BASE}/tickets/${TICKET_ID}/comment?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @- <<JSON
|
||||
{
|
||||
"subject": "Estimate Links",
|
||||
"body": "Estimate #${ESTIMATE_NUM}: https://computerguru.syncromsp.com/estimates/${ESTIMATE_ID}<br><source link description>: <URL><br><cost breakdown>",
|
||||
"hidden": true,
|
||||
"do_not_email": true
|
||||
}
|
||||
JSON
|
||||
|
||||
# Step 4 — Link estimate to ticket + touch to recalculate total
|
||||
curl -s -X PUT "${BASE}/estimates/${ESTIMATE_ID}?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @- <<JSON
|
||||
{"ticket_id": ${TICKET_ID}}
|
||||
JSON
|
||||
|
||||
# GET estimate (verify total recalculated)
|
||||
curl -s "${BASE}/estimates/${ESTIMATE_ID}?api_key=${API_KEY}" | \
|
||||
jq '{id: .estimate.id, number: .estimate.number, total: .estimate.total,
|
||||
lines: [.estimate.line_items[]? | {id, item, name, quantity, price}]}'
|
||||
@@ -636,11 +688,15 @@ curl -s -X DELETE "${BASE}/estimates/${ESTIMATE_ID}?api_key=${API_KEY}"
|
||||
|
||||
**GET /estimates line_items vs POST response:** GET returns `line_items` as an array on `.estimate.line_items[]`. POST `/line_items` returns the line item under `.line_item` (singular, not nested under estimate).
|
||||
|
||||
**Estimate total recalculation:** The `total` field on GET /estimates does not update automatically after line item changes. Always do a PUT on the estimate (even a no-op name update) to trigger recalculation, then re-fetch to verify.
|
||||
|
||||
**Hardware on estimates:** All hardware line items use a single generic product — `product_id: 32252` ("Hardware", `price_retail: 0.0`). The specific item name and price are set per-line-item via the `name` and `price_retail` fields on each line. Never look up a separate product ID for hardware items on estimates — always use `32252` and vary the description and price per item.
|
||||
|
||||
**Hardware line item price bug (verified 2026-05-22):** POST `/estimates/{id}/line_items` ignores `price_retail` for product 32252 — the line item is created at $0. Always follow POST with a PUT to `/estimates/{id}/line_items/{li_id}` passing both `price` and `price_retail`. The PUT succeeds and sets the price correctly.
|
||||
|
||||
```bash
|
||||
# Example hardware line item on an estimate
|
||||
curl -s -X POST "${BASE}/estimates/${ESTIMATE_ID}/line_items?api_key=${API_KEY}" \
|
||||
# Example hardware line item on an estimate (POST + required price fix PUT)
|
||||
LI_RESP=$(curl -s -X POST "${BASE}/estimates/${ESTIMATE_ID}/line_items?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @- <<JSON
|
||||
{
|
||||
@@ -652,6 +708,15 @@ curl -s -X POST "${BASE}/estimates/${ESTIMATE_ID}/line_items?api_key=${API_KEY}"
|
||||
"taxable": true
|
||||
}
|
||||
JSON
|
||||
)
|
||||
LI_ID=$(echo "$LI_RESP" | jq -r '.line_item.id')
|
||||
|
||||
# Required: fix price via PUT
|
||||
curl -s -X PUT "${BASE}/estimates/${ESTIMATE_ID}/line_items/${LI_ID}?api_key=${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary @- <<JSON
|
||||
{"price": 649.00, "price_retail": 649.00}
|
||||
JSON
|
||||
```
|
||||
|
||||
### Display formatting
|
||||
@@ -803,6 +868,9 @@ echo "$ALERT_OUT"
|
||||
|---|---|
|
||||
| Ticket (create / update / close / comment / bill) | `https://computerguru.syncromsp.com/tickets/<ticket.id>` |
|
||||
| Customer (create) | `https://computerguru.syncromsp.com/customers/<customer.id>` |
|
||||
| Estimate (create) | `https://computerguru.syncromsp.com/estimates/<estimate.id>` |
|
||||
|
||||
**Estimate alert — single post with both links:** When creating an estimate, send ONE alert after all four steps complete (estimate + line items + ticket + link). Include both the ticket link and estimate link in a single message.
|
||||
|
||||
**Examples:**
|
||||
|
||||
@@ -812,6 +880,10 @@ bash "$CLAUDETOOLS_ROOT/.claude/scripts/post-bot-alert.sh" \
|
||||
"[SYNCRO] Howard created #32301 (Desert Auto Tech) - Server won't boot -> https://computerguru.syncromsp.com/tickets/110736645"
|
||||
# Success output: [OK] post-bot-alert: posted to #bot-alerts (message_id=1507055781780918404)
|
||||
|
||||
# Estimate created (single alert, both links)
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/post-bot-alert.sh" \
|
||||
"[SYNCRO] Mike created estimate #7188 (Arizona Computer Guru) - ASUS V500 i7 Workstation \$849.99 | ticket #32316 -> https://computerguru.syncromsp.com/tickets/110843061 | https://computerguru.syncromsp.com/estimates/23967407"
|
||||
|
||||
# Billed + invoiced
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/post-bot-alert.sh" \
|
||||
"[SYNCRO] Mike billed #32164 (Jerry Burger) - 1.0h remote, \$150.00 -> https://computerguru.syncromsp.com/tickets/110169036"
|
||||
|
||||
99
projects/discord-bot/scripts/web-fetch-chrome.py
Normal file
99
projects/discord-bot/scripts/web-fetch-chrome.py
Normal file
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python
|
||||
"""Fetch a page with real (headless) Chrome when plain HTTP/WebFetch is bot-blocked.
|
||||
|
||||
Drives the installed Chrome 148 via Playwright's channel="chrome" (no bundled
|
||||
Chromium download). Runs headless in an isolated temp profile, so it never touches
|
||||
the interactive Chrome session a human may have open on BEAST.
|
||||
|
||||
Usage (always invoke with the bot venv's python):
|
||||
projects/discord-bot/.venv/Scripts/python.exe \
|
||||
projects/discord-bot/scripts/web-fetch-chrome.py "<url>" [options]
|
||||
|
||||
Options:
|
||||
--html Output raw rendered HTML instead of readable body text (default: text)
|
||||
--selector CSS Wait for and extract only this element's text/HTML
|
||||
--max-chars N Truncate output to N chars (default 8000; 0 = no limit)
|
||||
--settle-ms N Extra wait after load for JS to render (default 1500)
|
||||
--timeout-ms N Navigation timeout (default 25000)
|
||||
--wait-until STATE domcontentloaded | load | networkidle (default: load)
|
||||
|
||||
Exit codes: 0 ok, 2 navigation/render error, 3 bad usage.
|
||||
Errors go to stderr; page content goes to stdout.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(add_help=True)
|
||||
ap.add_argument("url")
|
||||
ap.add_argument("--html", action="store_true")
|
||||
ap.add_argument("--selector", default=None)
|
||||
ap.add_argument("--max-chars", type=int, default=8000)
|
||||
ap.add_argument("--settle-ms", type=int, default=1500)
|
||||
ap.add_argument("--timeout-ms", type=int, default=25000)
|
||||
ap.add_argument("--wait-until", default="load",
|
||||
choices=["domcontentloaded", "load", "networkidle"])
|
||||
args = ap.parse_args()
|
||||
|
||||
if not args.url.lower().startswith(("http://", "https://")):
|
||||
print("[ERROR] url must start with http:// or https://", file=sys.stderr)
|
||||
return 3
|
||||
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout
|
||||
except ImportError:
|
||||
print("[ERROR] playwright not installed in this interpreter — "
|
||||
"use the bot venv: projects/discord-bot/.venv/Scripts/python.exe", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
with sync_playwright() as p:
|
||||
# Drive the installed Chrome (not bundled Chromium). Strip the automation
|
||||
# flags so navigator.webdriver isn't a dead giveaway to bot detectors.
|
||||
browser = p.chromium.launch(
|
||||
channel="chrome",
|
||||
headless=True,
|
||||
args=["--disable-blink-features=AutomationControlled", "--disable-gpu"],
|
||||
ignore_default_args=["--enable-automation"],
|
||||
)
|
||||
# Strip the "HeadlessChrome" token from the UA (a common bot-detection tell),
|
||||
# derived from the live UA so it tracks the installed Chrome version.
|
||||
tmp = browser.new_context()
|
||||
ua = tmp.new_page().evaluate("() => navigator.userAgent").replace("HeadlessChrome", "Chrome")
|
||||
tmp.close()
|
||||
ctx = browser.new_context(
|
||||
viewport={"width": 1366, "height": 900},
|
||||
locale="en-US",
|
||||
user_agent=ua,
|
||||
)
|
||||
page = ctx.new_page()
|
||||
try:
|
||||
page.goto(args.url, wait_until=args.wait_until, timeout=args.timeout_ms)
|
||||
if args.settle_ms > 0:
|
||||
page.wait_for_timeout(args.settle_ms)
|
||||
if args.selector:
|
||||
page.wait_for_selector(args.selector, timeout=args.timeout_ms)
|
||||
target = page.query_selector(args.selector)
|
||||
out = (target.inner_html() if args.html else target.inner_text()) if target else ""
|
||||
else:
|
||||
out = page.content() if args.html else page.inner_text("body")
|
||||
except PWTimeout:
|
||||
print(f"[ERROR] timed out loading {args.url}", file=sys.stderr)
|
||||
return 2
|
||||
except Exception as e: # navigation, DNS, TLS, blocked, etc.
|
||||
print(f"[ERROR] {type(e).__name__}: {e}", file=sys.stderr)
|
||||
return 2
|
||||
finally:
|
||||
browser.close()
|
||||
|
||||
out = out or ""
|
||||
if args.max_chars > 0 and len(out) > args.max_chars:
|
||||
out = out[: args.max_chars] + f"\n...[truncated at {args.max_chars} chars]"
|
||||
sys.stdout.write(out)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -207,3 +207,94 @@ full Syncro including billing.
|
||||
- Related DESKTOP commits (Mike, parallel): Syncro overhaul `64a0ba7`/`90748d0`/`ce38304`;
|
||||
Rob Limited-Operator buildout `67dd7a4`/`063b209`/`8e8a18c`.
|
||||
- Helper: `.claude/scripts/post-bot-alert.sh`. Bot rules: `projects/discord-bot/DISCORD_CLAUDE.md`.
|
||||
|
||||
---
|
||||
|
||||
## Update: 13:10 PT — Syncro estimate workflow + skill update
|
||||
|
||||
### User
|
||||
- **User:** Mike Swanson (mike) — via Discord Bot
|
||||
- **Machine:** GURU-BEAST-ROG (Discord Bot service)
|
||||
- **Role:** admin
|
||||
- **Session Span:** ~13:00–13:10 PT (Discord thread: #Create new test estimate for a i7 workstation from Bestbuy)
|
||||
|
||||
### Session Summary
|
||||
|
||||
Mike requested a test estimate in Syncro for an i7 workstation sourced from Best Buy with a $200 markup. The bot first searched Best Buy for current i7 desktop listings (web scraping was blocked by Best Buy, so product data was gathered via web search), producing a list of seven candidates across ASUS, Lenovo, and HP covering prices from $649.99 to ~$1,099. Mike selected option #1: ASUS V500 Desktop (V500MVC-I71TB), Intel Core i7, 16GB RAM, 1TB SSD, $649.99 Best Buy price, $849.99 sell price with the $200 markup.
|
||||
|
||||
The estimate was created in Syncro under Arizona Computer Guru (internal test account). During line item creation, a Syncro API bug was encountered: POST `/estimates/{id}/line_items` silently ignores `price_retail` for the generic hardware product (32252), creating the line at $0. This was resolved by following the POST with a PUT to the line item endpoint, which correctly set the price to $849.99. A separate total-recalculation issue was also encountered: the estimate's `total` field does not update after line item changes until the estimate itself is touched via a PUT. After the PUT touch, the total correctly showed $849.99 + $73.95 tax = $923.94.
|
||||
|
||||
After the estimate was complete, Mike noted that the standard workflow requires every estimate to have an associated ticket with a private (hidden) note containing links. A ticket (#32316) was created under Arizona Computer Guru, a hidden comment was posted with the estimate link, Best Buy product link, and cost breakdown, and the estimate was linked to the ticket via PUT. The skill file was then updated to encode this as a hard workflow requirement, document the hardware line item price bug and fix pattern, update the bot alert format (single post with both ticket and estimate links), and add the estimate URL to the bot-alerts link table.
|
||||
|
||||
### Key Decisions
|
||||
|
||||
- Selected ASUS V500 (V500MVC-I71TB) as the test unit — lowest price point ($649.99) with current 14th-gen i7, making it the cleanest baseline for estimate testing.
|
||||
- Used Arizona Computer Guru (customer ID 15353550) as the internal test account rather than creating a dummy customer.
|
||||
- Followed POST line item with PUT price fix rather than retrying POST with different field names — per hard rules, alternative payload formats are not to be tried; PUT is the documented update path.
|
||||
- Bot alert for estimates now sends a single message with both ticket and estimate links, per Mike's direction.
|
||||
|
||||
### Problems Encountered
|
||||
|
||||
- **Best Buy scraping blocked:** WebFetch timed out or socket-closed on all Best Buy product URLs. Resolved by using web search to gather product names, SKUs, and prices from search result snippets and cached review pages.
|
||||
- **Estimate line item price $0 on POST:** Syncro's POST `/estimates/{id}/line_items` does not apply `price_retail` for hardware product 32252. Resolved with a follow-up PUT to `/estimates/{id}/line_items/{id}` passing both `price` and `price_retail`. Documented in skill as a hard rule with example.
|
||||
- **Estimate total not recalculating:** After the PUT price fix, GET /estimates still showed total $0.0. Resolved by doing a no-op PUT on the estimate itself (sending the same name) to trigger server-side recalculation. Total correctly updated to $923.94.
|
||||
- **Ticket + private note not created initially:** The estimate workflow was executed without a ticket, which Mike flagged as missing. Ticket and private note were created after the fact and the skill was updated to make this mandatory going forward.
|
||||
|
||||
### Configuration Changes
|
||||
|
||||
- **Modified:** `.claude/commands/syncro.md`
|
||||
- Estimate section rewritten as a mandatory 4-step workflow (estimate → line items + price fix → ticket + private note → link + bot alert)
|
||||
- Documented hardware line item price bug (POST ignores price_retail, requires PUT fix)
|
||||
- Documented estimate total recalculation requirement (PUT touch)
|
||||
- Bot-alerts link table updated to include estimate URL format
|
||||
- Added estimate bot alert example (single post, both ticket and estimate links)
|
||||
|
||||
### Credentials & Secrets
|
||||
|
||||
- No new credentials. Mike's Syncro API key used (hardcoded in skill): `T259810e5c9917386b-52c2aeea7cdb5ff41c6685a73cebbeb3`
|
||||
|
||||
### Infrastructure & Servers
|
||||
|
||||
- Syncro PSA: `https://computerguru.syncromsp.com/api/v1`
|
||||
|
||||
### Commands & Outputs
|
||||
|
||||
```bash
|
||||
# Estimate created
|
||||
POST /estimates → estimate ID 23967407, number #7188
|
||||
|
||||
# Line item created at $0 (bug)
|
||||
POST /estimates/23967407/line_items → line_item ID 124969387, price: "0.0"
|
||||
|
||||
# Line item deleted and re-added — still $0
|
||||
DELETE /estimates/23967407/line_items/124969387
|
||||
POST /estimates/23967407/line_items → line_item ID 124969416, price: "0.0"
|
||||
|
||||
# Price fixed via PUT
|
||||
PUT /estimates/23967407/line_items/124969416 {"price": 849.99, "price_retail": 849.99}
|
||||
→ price: "849.99" [OK]
|
||||
|
||||
# Estimate total recalculated via touch PUT
|
||||
PUT /estimates/23967407 {"name": "..."} → subtotal: "849.99", total: "923.94", tax: "73.95"
|
||||
|
||||
# Ticket created
|
||||
POST /tickets → ticket ID 110843061, number #32316
|
||||
|
||||
# Private note posted
|
||||
POST /tickets/110843061/comment {"hidden": true} → comment ID 412479047
|
||||
|
||||
# Estimate linked to ticket
|
||||
PUT /estimates/23967407 {"ticket_id": 110843061} → ticket_id: 110843061 [OK]
|
||||
```
|
||||
|
||||
### Pending / Incomplete Tasks
|
||||
|
||||
- None. Estimate and ticket both complete and linked.
|
||||
|
||||
### Reference Information
|
||||
|
||||
- Syncro Estimate #7188: https://computerguru.syncromsp.com/estimates/23967407
|
||||
- Syncro Ticket #32316: https://computerguru.syncromsp.com/tickets/110843061
|
||||
- Best Buy product: https://www.bestbuy.com/site/asus-v500-desktop-intel-core-i7-16gb-memory-1tb-ssd-dark-grey/6613707.p
|
||||
- Customer: Arizona Computer Guru (ID 15353550)
|
||||
- Skill updated: `.claude/commands/syncro.md`
|
||||
|
||||
Reference in New Issue
Block a user