41 — invoice audit flow
Invoice audit and filesystem cleanup over a urirun node.
The flow uses existing URI routes:
fs://host/duplicates/query/findocr://host/document/query/batch- local CSV/JSON/Markdown report generation
The initial audit is read-only. Follow-up cleanup steps use explicit fs:// command routes and default to dry-run.
Run
cd /home/tom/github/if-uri/examples/41-invoice-audit-flow
./invoice_audit.py \
--node-url http://192.168.188.201:8765 \
--root /home/tom/Downloads/2026/5 \
--output-dir .state
Outputs:
.state/invoice_audit_categories.csv.state/invoice_audit_files.csv.state/invoice_audit_duplicates.csv.state/invoice_audit_raw.json.state/invoice_audit_report.md
Move confirmed non-invoices
First create a dry-run move plan:
./move_no_invoice.py \
--node-url http://192.168.188.201:8765 \
--input-csv .state/invoice_audit_files.csv \
--output-dir .state
Then execute only if the plan is acceptable:
./move_no_invoice.py \
--node-url http://192.168.188.201:8765 \
--input-csv .state/invoice_audit_files.csv \
--output-dir .state \
--execute
The script moves only conservative matches: OCR must be OK, invoice markers must be absent, and a non-invoice marker such as regulamin, cennik, formularz, warunki or instrukcja must be present.
The node should have:
fs://host/duplicates/query/findocr://host/document/query/batchIFURI_FS_ROOT=/home/tom/Downloads/2026/5for the filesystem sandbox
Flatten invoice folders
After non-invoices are moved aside, flatten nested invoice folders into files directly under each month folder:
2026.03/2026.03.17-windsurf-Invoice-8WJKVN37-0003.pdf
Create a dry-run plan first:
./flatten_invoices.py \
--node-url http://192.168.188.201:8765 \
--input-csv .state/invoice_audit_files.csv \
--output-dir .state
Then execute:
./flatten_invoices.py \
--node-url http://192.168.188.201:8765 \
--input-csv .state/invoice_audit_files.csv \
--output-dir .state \
--execute
The script skips files classified as no_invoice, extracts the document date from OCR/filename where possible, uses the audit category as the supplier/category label, and appends -2, -3, ... when flat names collide.
Additional node route required for this step:
fs://host/file/command/move
If there are files outside the OCR audit extensions, flatten the remaining node tree directly through fs://host/dir/query/list:
./flatten_tree_files.py \
--node-url http://192.168.188.201:8765 \
--output-dir .state
./flatten_tree_files.py \
--node-url http://192.168.188.201:8765 \
--output-dir .state \
--execute
This scans month folders on the node, skips already-flat files and no_invoice, and moves remaining nested files to the same YYYY.MM/YYYY.MM.DD-label-original shape.
Optional cleanup after moves:
fs://host/dir/command/prune_empty
Re-check current flat files
After flattening, run a second pass over the current node state to catch files that are still not invoices. This pass uses current OCR for PDF/images and reads simple metadata from spreadsheet/zip/text files.
Dry-run:
./verify_invoice_files.py \
--node-url http://192.168.188.201:8765 \
--absolute-root /home/tom/Downloads/2026/5 \
--output-dir .state
Move confident non-invoices to no_invoice/YYYY.MM/...:
./verify_invoice_files.py \
--node-url http://192.168.188.201:8765 \
--absolute-root /home/tom/Downloads/2026/5 \
--output-dir .state \
--execute
Reports:
.state/invoice_recheck_plan.csv.state/invoice_recheck_plan_moves.csv.state/invoice_recheck_manifest.csv.state/invoice_recheck_manifest_moves.csv
Contractor metadata
contractor_metadata.py builds .state/contractor_metadata.csv/json from OCR previews and move manifests. The panel reads this manifest and shows contractor metadata in the Metadata column:
contractorbuyertax_idplatformproducts_servicescountrycontractor_sourceproduct_source
Run:
./contractor_metadata.py --state-dir .state
The parser uses known brand/platform rules first, then conservative OCR markers such as Sprzedawca: / Seller:. Product/service metadata is inferred from known SaaS, marketplace, hosting, payment, logistics, and retail providers, with a conservative fallback for explicit OCR item markers. Generic categories like bank-finance or email-assets stay in platform; they are not shown as contractors.
Local review/delete UI
index.php provides a small local file review panel for the flattened month folders. It scans INVOICE_ROOT, reads .state manifests when available, shows structure/metadata, opens files inline, and lets you delete one file at a time. Generated grouping views such as _by_supplier, _by_category, and _by_platform are skipped so the review table does not show the same physical attachment through old intermediate folder layouts.
./run.sh
Defaults:
INVOICE_ROOT=/home/tom/Downloads/2026/5when that folder exists, otherwise
the current example folder
REPORT_DIR=./.stateDELETE_MODE=trash, which moves deleted files to
$INVOICE_ROOT/.deleted/...
Useful overrides:
INVOICE_ROOT=/home/tom/Downloads/2026/5 REPORT_DIR="$PWD/.state" ./run.sh
HOST=0.0.0.0 PORT=8099 ./run.sh
DELETE_MODE=unlink ./run.sh # permanent deletion
Host service dashboard
service_dashboard.py is the host-side control plane for services exposed by a node over URI. It reads services.yaml, expands host/node variables from .env or .env.example, and allows only the registered status, start, stop, and restart actions. This is the small surface intended for chat-driven service control.
Chat-style command from the host:
./service_dashboard.py chat "uruchom panel faktur"
./service_dashboard.py chat "czy działa panel faktur?"
./service_dashboard.py stop invoice-panel
Dashboard on the host:
./run_service_dashboard.sh
Defaults from .env.example:
LENOVO_NODE_URL=http://192.168.188.201:8765INVOICE_PANEL_ROOT=/home/tom/Downloads/2026/5INVOICE_PANEL_PORT=8100INVOICE_PANEL_URL=http://192.168.188.201:8100SERVICE_DASHBOARD_HOST=127.0.0.1SERVICE_DASHBOARD_PORT=8196
The service dashboard itself is local by default:
http://127.0.0.1:8196/
To add another controllable service, add a new item to services.yaml with:
node_urlpublic_urlactions.status.uriactions.start.uriactions.stop.uri
The NL prompt never selects arbitrary URIs; it only chooses one declared service and one declared action.
Document previewer URI
previewer.py exposes a lightweight URI connector for document preview cards and cached thumbnails. Images still use native browser thumbnails. PDF files are rendered from the first page with pdftoppm; TXT/CSV/JSON/MD/XML/HTML files are rendered as SVG thumbnails containing the first visible lines. XLS/DOC/ZIP/EML and unknown files keep a stable SVG-style type card until a richer renderer is added. Expensive text/image analysis remains in ocr://; file bytes remain in fs://.
Generate bindings:
python3 - <<'PY' > .state/previewer.bindings.json
import json
import previewer
print(json.dumps(previewer.urirun_bindings(), ensure_ascii=False, indent=2, sort_keys=True))
PY
Deploy to a node:
urirun host deploy http://192.168.188.201:8765 \
--identity ~/.ssh/id_ed25519 \
--bindings .state/previewer.bindings.json \
--code previewer.py \
--merge \
--allow 'ocr://**' \
--allow 'fs://**' \
--allow 'panel://**' \
--allow 'preview://**'
Query one preview card:
urirun run 'preview://host/document/query/card' .state/previewer.bindings.json \
--payload '{"root":"/home/tom/Downloads/2026/5","path":"2026.03/example.pdf"}' \
--execute \
--allow 'preview://**'
Generate one cached thumbnail:
urirun run 'preview://host/document/command/thumbnail' .state/previewer.bindings.json \
--payload '{"root":"/home/tom/Downloads/2026/5","path":"2026.03/example.pdf"}' \
--execute \
--allow 'preview://**'