# 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

```bash
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:

```bash
./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:

```bash
./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/find`
- `ocr://host/document/query/batch`
- `IFURI_FS_ROOT=/home/tom/Downloads/2026/5` for the filesystem sandbox

## Flatten invoice folders

After non-invoices are moved aside, flatten nested invoice folders into files
directly under each month folder:

```text
2026.03/2026.03.17-windsurf-Invoice-8WJKVN37-0003.pdf
```

Create a dry-run plan first:

```bash
./flatten_invoices.py \
  --node-url http://192.168.188.201:8765 \
  --input-csv .state/invoice_audit_files.csv \
  --output-dir .state
```

Then execute:

```bash
./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`:

```bash
./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:

```bash
./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/...`:

```bash
./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:

- `contractor`
- `buyer`
- `tax_id`
- `platform`
- `products_services`
- `country`
- `contractor_source`
- `product_source`

Run:

```bash
./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.

```bash
./run.sh
```

Defaults:

- `INVOICE_ROOT=/home/tom/Downloads/2026/5` when that folder exists, otherwise
  the current example folder
- `REPORT_DIR=./.state`
- `DELETE_MODE=trash`, which moves deleted files to
  `$INVOICE_ROOT/.deleted/...`

Useful overrides:

```bash
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:

```bash
./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:

```bash
./run_service_dashboard.sh
```

Defaults from `.env.example`:

- `LENOVO_NODE_URL=http://192.168.188.201:8765`
- `INVOICE_PANEL_ROOT=/home/tom/Downloads/2026/5`
- `INVOICE_PANEL_PORT=8100`
- `INVOICE_PANEL_URL=http://192.168.188.201:8100`
- `SERVICE_DASHBOARD_HOST=127.0.0.1`
- `SERVICE_DASHBOARD_PORT=8196`

The service dashboard itself is local by default:

```text
http://127.0.0.1:8196/
```

To add another controllable service, add a new item to `services.yaml` with:

- `node_url`
- `public_url`
- `actions.status.uri`
- `actions.start.uri`
- `actions.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:

```bash
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:

```bash
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:

```bash
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:

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