From a187c33854c433d08c1020f805600086091b76c2 Mon Sep 17 00:00:00 2001 From: Josh Mabry Date: Sat, 12 Sep 2026 23:28:35 -0700 Subject: [PATCH] =?UTF-8?q?feat(view):=20copy=20as=20a=20for-sale=20chat?= =?UTF-8?q?=20post=20=E2=80=94=20condition=20next=20to=20the=20name,=20sys?= =?UTF-8?q?tem=20headers,=20pending=20marker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Josh pasted how he actually reshaped the copied list for his game group chat, and asked for the condition next to the name before the price. The copied text now IS that post: Warhammer 40K (all NoS) Wartrakk — $20 Land Speeder — $40 (Pending) Intercessor Squad — $30 Warhammer AoS Helsmiths of Hashut Army Box (NIB) — $170 - Plain system header (no `##`), no bullets, no lot line, no category — chat apps do not render Markdown and he removed all of it by hand. - `Name (condition) — $price`; when every item in a system shares one condition it collapses to `(all )` under the header, as he wrote it. - Items already pending / listed / sold are gathered after a `(Pending)` / `(Listed)` / `(Sold)` marker inside their system, as he grouped them. - Prices print as whole dollars when whole ($20), with cents otherwise ($57.69); an unpriced item prints without a price. - Condition is the existing `condition` field; the item dialog and inline edit now suggest the usual codes (NoS, NIB, Sealed, Assembled, Primed, Painted, Used) plus whatever is already in use (`GET /conditions`). The button reads "Copy list". - Node harness pins the exact document; 128 tests. 0.4.1. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011UQ33xXfygwEkzbbJx3YwZ --- README.md | 2 +- api.py | 4 +++ protoagent.plugin.yaml | 2 +- pyproject.toml | 2 +- store.py | 6 ++++ tests/test_system.py | 3 ++ tests/test_view.py | 7 +++-- view.py | 63 +++++++++++++++++++++++++++--------------- 8 files changed, 61 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index fb3784c..85de2ac 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ inventory_summary() | | | |---|---| | **Tools** | `inventory_summary` · `inventory_list` · `inventory_get` · `inventory_upsert_lot` · `inventory_upsert_item` · `inventory_delete_item` · `inventory_set_price` · `inventory_mark_sold` · `inventory_listing` · `inventory_import_csv` · `inventory_export_csv` · `inventory_reprice_plan` · `inventory_stale` | -| **View** | a rail panel: the item grid (double-click to edit name/category/condition/qty; status select; click a price for the Price dialog), Price / Sold / Listing / Edit / Delete per row, an optional **game system** per item (filter, ordering, inline edit with suggestions), multi-select with **Copy as Markdown** grouped `## system` → `### lot` → `- Name — Category — $price` (also ⌘C), lots with their P&L, sales, an activity log, CSV import (file or paste, with the mapping report) and export | +| **View** | a rail panel: the item grid (double-click to edit name/category/condition/qty; status select; click a price for the Price dialog), Price / Sold / Listing / Edit / Delete per row, an optional **game system** per item (filter, ordering, inline edit with suggestions), multi-select with **Copy list** (also ⌘C) — a for-sale post per game system: the system as a header, `(all NoS)` when the group shares a condition, `Name (condition) — $price` per line, pending/listed items under their own marker, lots with their P&L, sales, an activity log, CSV import (file or paste, with the mapping report) and export | | **API** | bearer-gated JSON under `/api/plugins/inventory` — `summary`, `lots`, `items`, `items/{id}/price`, `items/{id}/sold`, `items/{id}/listings`, `listings/{id}/end`, `sales`, `stale`, `audit`, `import`, `export` | | **Automations** | `weekly_review: true` arms a plugin-owned recurring turn (`weekly_review_cron`, default Monday 09:00 in `review_timezone`) that re-prices stale evidence from eBay sold comps through the same tools, reports stale listings with a recommendation (it never changes a listing itself), and posts the per-lot P&L. Cancelled when the plugin is disabled. | | **Events** | `inventory.item.changed`, `inventory.lot.changed`, `inventory.sale.recorded`, `inventory.imported` | diff --git a/api.py b/api.py index 8400813..242fa0d 100644 --- a/api.py +++ b/api.py @@ -40,6 +40,10 @@ async def _lots() -> dict: async def _systems() -> dict: return {"systems": store.systems()} + @r.get("/conditions") + async def _conditions() -> dict: + return {"conditions": store.conditions()} + @r.put("/lots/{lot_id}") async def _put_lot(lot_id: str, body: dict) -> dict: try: diff --git a/protoagent.plugin.yaml b/protoagent.plugin.yaml index ded8f90..46903e7 100644 --- a/protoagent.plugin.yaml +++ b/protoagent.plugin.yaml @@ -1,6 +1,6 @@ id: inventory name: Inventory (resale source of truth) -version: 0.4.0 +version: 0.4.1 description: >- The resale inventory as a source of truth: lots (what you bought and for how much), items (what each lot split into, its condition and status), listings (where each item is diff --git a/pyproject.toml b/pyproject.toml index 1824a8f..7887139 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "inventory-plugin" -version = "0.4.0" +version = "0.4.1" description = "Resale inventory as a source of truth for protoAgent — lots, items, listings, sales, price evidence, audit" requires-python = ">=3.11" diff --git a/store.py b/store.py index 9a945e0..f8fe086 100644 --- a/store.py +++ b/store.py @@ -534,6 +534,12 @@ def systems(self) -> list[str]: rows = con.execute("SELECT DISTINCT system FROM items WHERE system!='' ORDER BY system").fetchall() return [r[0] for r in rows] + def conditions(self) -> list[str]: + """The distinct condition labels in use (NoS, NIB, sealed, painted …) for suggestions.""" + with self._read() as con: + rows = con.execute("SELECT DISTINCT condition FROM items WHERE condition!='' ORDER BY condition").fetchall() + return [r[0] for r in rows] + def find_item(self, *, lot_id: str, name: str) -> dict | None: """The item with this lot + name (case-insensitive) — how a sheet without an id column is matched on re-import instead of minting duplicates.""" diff --git a/tests/test_system.py b/tests/test_system.py index 5569274..0778539 100644 --- a/tests/test_system.py +++ b/tests/test_system.py @@ -63,6 +63,9 @@ def test_api_lists_systems_and_filters(registry): c.post("/api/plugins/inventory/items", json={"id": "B", "name": "b", "system": "Warhammer 40K"}) c.post("/api/plugins/inventory/items", json={"id": "C", "name": "c"}) assert c.get("/api/plugins/inventory/systems").json() == {"systems": ["Blood Bowl", "Warhammer 40K"]} + c.put("/api/plugins/inventory/items/A", json={"condition": "NoS"}) + c.put("/api/plugins/inventory/items/B", json={"condition": "NIB"}) + assert c.get("/api/plugins/inventory/conditions").json() == {"conditions": ["NIB", "NoS"]} assert [ i["id"] for i in c.get("/api/plugins/inventory/items", params={"system": "Blood Bowl"}).json()["items"] ] == ["A"] diff --git a/tests/test_view.py b/tests/test_view.py index cbb5ff8..e1cfab9 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -110,9 +110,10 @@ def test_the_pure_helpers_under_node(): "\nconsole.log(JSON.stringify({ neg: fmt(-12.5), pos: fmt(1234.5), nul: fmt(null), zero: fmt(0), sneg: signed(-3), spos: signed(3), snul: signed(null)," " esc: esc('&\\''), field: field('name', 'Name', '\">'), opt: field('lot_id', 'Lot', '', { type: 'select', options: [['\">', 'x