ifURI examples
← all examples

41 — invoice audit flow

Invoice audit and filesystem cleanup over a urirun node.

The flow uses existing URI routes:

  1. fs://host/duplicates/query/find
  2. ocr://host/document/query/batch
  3. 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:

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:

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:

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:

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:

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:

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:

the current example folder

$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:

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:

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://**'

Files

.env.exampleREADME.mdcontractor_metadata.pyflatten_invoices.pyflatten_tree_files.pyindex.phpinvoice-audit.flow.yamlinvoice_audit.pymove_no_invoice.pypanel_server.pypreviewer.pyrun.shrun_service_dashboard.shservice_dashboard.pyservices.yamltest_contractor_metadata.pytest_flatten_invoices.pytest_flatten_tree_files.pytest_invoice_audit.pytest_move_no_invoice.pytest_previewer.pytest_service_dashboard.pytest_verify_invoice_files.pyverify_invoice_files.py.state/

View on GitHub →