Skip to content

Commit c474288

Browse files
committed
feat: startup credential gate, production-default endpoint, TUI freeze fix
- fix(tui): arrow keys on an empty table hard-locked the tview event loop at 100% CPU (Table's selectable scan never terminates with zero selectable rows); row selection now follows data-row presence, regression-tested - feat(aade): credentials read from YOURDATA_USERNAME / YOURDATA_SUBSCRIPTION_KEY; the binary refuses to start in every mode without both, naming the missing vars - feat: endpoint defaults to production; --dev forces the AADE test endpoint, YOURDATA_ENDPOINT is an explicit override (--dev wins) - build: make build emits bin/yourdata; tcell pinned to tview's declared v2.8.1 - docs: SPECIFICATIONS.md, README.md, CLAUDE.md, tui-rules.md updated to match
1 parent 63db3be commit c474288

19 files changed

Lines changed: 350 additions & 44 deletions

.claude/rules/tui-rules.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@ These rules apply when working in `internal/tui` — the terminal user-interface
1313

1414
- **Package:** `github.com/rivo/tview` — rich, interactive terminal widgets. Built on
1515
`github.com/gdamore/tcell/v2` (the screen/event backend).
16-
- **Version:** pin the latest stable `rivo/tview` and `gdamore/tcell/v2` in `go.mod`. tcell **v2**
17-
is mandatory — never import the v1 module path.
16+
- **Version:** pin the latest stable `rivo/tview` in `go.mod`, and pin `gdamore/tcell/v2` to
17+
**exactly the version tview's own `go.mod` declares** (v2.8.1 for tview v0.42.0) — never bump
18+
tcell independently of tview. A newer tcell than tview was built against has caused silent
19+
startup crashes in terminals that answer tcell's capability queries (e.g. VS Code's integrated
20+
terminal), while working fine in a bare PTY. tcell **v2** is mandatory — never import the v1
21+
module path.
1822
- **Imports:** no alias; use the canonical names:
1923
```go
2024
import (

CLAUDE.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ itself. Layer rules live in `.claude/rules/` (`db-rules.md`, `mcp-server.md`, `t
1010
One Go binary, no external runtime dependencies: no HTTP listener, no database server, no cgo, no
1111
daemon, no telemetry, no background network activity. Exactly **two** operations touch the network —
1212
submit an invoice and fetch a book snapshot — both explicit, synchronous and bounded, through
13-
`github.com/techthos/yourdata-sdk`. bbolt is the source of truth, not a cache. Credentials come from
14-
the environment (`AADE_USER_ID`, `AADE_SUB_KEY`, `AADE_ENDPOINT`) and are never persisted.
13+
`github.com/techthos/yourdata-sdk`. bbolt is the source of truth, not a cache. Credentials
14+
(`YOURDATA_USERNAME`, `YOURDATA_SUBSCRIPTION_KEY`) come from the environment, are **required at
15+
startup** in every mode, and are never persisted. The endpoint defaults to **production**; `--dev`
16+
forces the AADE test endpoint, `YOURDATA_ENDPOINT` is an explicit override (`--dev` wins).
1517

1618
## Layers & the dependency rule
1719

README.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ tview TUI for humans and an MCP stdio server for AI agents, over one embedded bb
1111
```sh
1212
yourdata # launch the TUI (sections: Drafts, Submitted, Counterparties, Catalog, Books, Settings)
1313
yourdata mcp # serve MCP over stdio for AI agents
14+
yourdata --dev # use the AADE development (test) endpoint instead of production
1415
yourdata -db /path/to/file.db # explicit database file
1516
```
1617

@@ -48,16 +49,16 @@ go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
4849

4950
AADE credentials are read from the environment and are **never persisted**:
5051

51-
| Variable | Purpose |
52-
|---|---|
53-
| `AADE_USER_ID` | myDATA user id |
54-
| `AADE_SUB_KEY` | myDATA subscription key |
55-
| `AADE_ENDPOINT` | Target environment (defaults to the AADE test endpoint) |
56-
| `YOURDATA_MCP_ALLOW_SUBMIT` | Set to `1` to expose the gated `submit_invoice` MCP tool (sandbox only — it always refuses on production) |
57-
| `YOURDATA_DB` | Database file path override |
52+
| Variable | Required | Purpose |
53+
|---|---|---|
54+
| `YOURDATA_USERNAME` | yes | myDATA user id |
55+
| `YOURDATA_SUBSCRIPTION_KEY` | yes | myDATA subscription key |
56+
| `YOURDATA_ENDPOINT` | no | Explicit endpoint override (default: **production**; `--dev` forces the test endpoint and wins over this) |
57+
| `YOURDATA_MCP_ALLOW_SUBMIT` | no | Set to `1` to expose the gated `submit_invoice` MCP tool (sandbox only — it always refuses on production) |
58+
| `YOURDATA_DB` | no | Database file path override |
5859

59-
Missing credentials are not a startup failure — the app runs normally and refuses only the two
60-
operations that touch the network (submit invoice, fetch book), with an explanation. The active
60+
The binary refuses to start — in every mode — when `YOURDATA_USERNAME` or
61+
`YOURDATA_SUBSCRIPTION_KEY` is unset, exiting with an error naming the missing variables. The active
6162
environment is rendered permanently in the TUI status bar as `[TEST]` or `[PROD]`.
6263

6364
## MCP

bin/yourdata

162 KB
Binary file not shown.

docs/SPECIFICATIONS.md

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ only protocol frames). The database file resolves as `-db` flag > `$YOURDATA_DB`
3232
`$XDG_DATA_HOME/yourdata/yourdata.db` (defaulting to `~/.local/share/yourdata/yourdata.db`); the
3333
directory is created on first run. Both surfaces may run **simultaneously** against the same file.
3434

35+
**Startup requirement.** The binary refuses to start — in **every** mode — unless both
36+
`YOURDATA_USERNAME` and `YOURDATA_SUBSCRIPTION_KEY` are set, exiting immediately with an error that
37+
names each missing variable. Credentials are read from the environment only and are never persisted.
38+
39+
**Endpoint selection.** The AADE endpoint defaults to **production**. It resolves as: `--dev` flag
40+
(forces the AADE development/test endpoint, overriding everything) > `YOURDATA_ENDPOINT` (explicit
41+
endpoint override) > production. Both surfaces honour the same resolution, and the PROD safeguards
42+
remain regardless of how production was selected: the TUI's typed-`SUBMIT` confirmation and the MCP
43+
`submit_invoice` refusal on production.
44+
3545
---
3646

3747
## Goals & Non-Goals
@@ -72,8 +82,9 @@ Greek tax authority's REST API. That dependency is contained as follows, and no
7282
pricing, previewing XML and reading history all work with the network down.
7383
- A network failure surfaces as a `[red]` message in the status bar (TUI) or a tool-level error
7484
result (MCP). It never crashes the app and never corrupts local state.
75-
- Missing credentials are **not** a startup failure. The app runs normally and refuses only those
76-
two actions, with an explanation.
85+
- Missing credentials **are a startup failure**: the binary exits with an error naming the missing
86+
variables (see the startup requirement above). As defence-in-depth, the two network actions also
87+
refuse with an explanation if they are ever reached without credentials.
7788

7889
Also out of scope for v1: multiple issuer entities, multi-branch series scoping, invoice PDF
7990
rendering, e-mailing invoices, payment tracking, and any AADE endpoint the SDK does not implement
@@ -93,7 +104,7 @@ product and are not defects to be worked around:
93104
| There is **no cancel/void endpoint**. | Submission is irreversible. Correction is only possible via a credit note (`5.1`/`5.2`) carrying `correlatedInvoices`. This drives the immutability rule and the submit confirmation. |
94105
| `InvoiceSummaryBuilder.Build()` hardcodes `TotalWithheldAmount`, `TotalStampDutyAmount` and `TotalDeductionsAmount` to `0`, and never populates `ExpensesClassifications` (deliberate TS parity — `summary.go:16-21`). | **We do not call `CalculateSummary()`.** `internal/models` computes all eight totals and both classification groupings itself. |
95106
| `RequestArgs` exposes `NextPartitionKey`/`NextRowKey` but the SDK does not loop — and its response envelope surfaces a continuation only via `BookInfo.ContinuationToken` (a doc-level token element is discarded). | The fetch use-case follows continuation tokens itself, bounded. A token it cannot split into partition/row keys ends the loop with the snapshot stored and flagged `Partial=true` — never silently truncated. |
96-
| `LoadConfig()` reads `AADE_USER_ID`, `AADE_SUB_KEY`, `AADE_ENDPOINT` from the environment. | Credentials are environment-only and never persisted. |
107+
| The SDK's own `LoadConfig()` reads `AADE_*` variables — this app does **not** use it. It builds `sdk.Config` itself from `YOURDATA_USERNAME`, `YOURDATA_SUBSCRIPTION_KEY`, `YOURDATA_ENDPOINT`. | Credentials are environment-only, app-prefixed, and never persisted. |
97108
| `sdk.ResponseDoc.InvoiceMark` is `*int64` (~15 digits). | Stored as `int64`; **serialised as a decimal string** in all MCP output, so JS-based MCP clients cannot lose precision. |
98109

99110
The SDK is used for what it is good at: AADE enum constants, XML marshalling (`MarshalInvoicesDoc`),
@@ -260,7 +271,7 @@ grouped too**.
260271
| Field | Type | Notes |
261272
|---|---|---|
262273
| `SubmittedAt` | `time.Time` | |
263-
| `Endpoint` | `string` | The `AADE_ENDPOINT` in force — records test vs. prod. |
274+
| `Endpoint` | `string` | The resolved endpoint in force — records test vs. prod. |
264275
| `StatusCode` | `string` | From `sdk.ResponseDoc`. |
265276
| `Mark` | `int64` | AADE MARK. **Serialised as a decimal string in MCP output.** |
266277
| `UID` | `string` | `invoiceUid`. |
@@ -390,7 +401,7 @@ counterparty names are resolved within the transaction so an invoice stays finda
390401
1. **Validate locally.** Reject if: no lines; `Series` or `AA` blank; no `InvoiceType`; no
391402
counterparty; a line has `VATCategory == "7"` without a `VATExemptionCategory`; `Currency != EUR`
392403
without an `ExchangeRate`; or the status is already `submitted`.
393-
2. **Check credentials.** Missing `AADE_USER_ID`/`AADE_SUB_KEY` → refuse with an explanatory
404+
2. **Check credentials.** Missing `YOURDATA_USERNAME`/`YOURDATA_SUBSCRIPTION_KEY` → refuse with an explanatory
394405
message. No network call attempted.
395406
3. **Freeze and marshal.** Resolve the counterparty into a `Party` copy, recompute the summary, map
396407
to `sdk.Invoice`, `sdk.MarshalInvoicesDoc`.
@@ -473,7 +484,7 @@ including `create_invoice_draft` and every other draft-building, reference-data
473484
Exactly one tool is gated:
474485

475486
- **`submit_invoice`** is registered **only** when `YOURDATA_MCP_ALLOW_SUBMIT=1`, and even then it
476-
**refuses at call time** when `AADE_ENDPOINT` resolves to production (`sdk.EnvProd`). An agent may
487+
**refuses at call time** when the resolved endpoint is production (`sdk.EnvProd`). An agent may
477488
rehearse a full submission against the AADE dev sandbox; filing a real tax document remains a
478489
human act at the TUI.
479490

@@ -644,7 +655,7 @@ check runs on field change and renders a **non-blocking `[yellow]` warning** —
644655

645656
**Submit confirmation.** A centered `Modal` over the body, **focus defaulting to `No`**, naming the
646657
series/AA, the counterparty, the gross total and the target environment, and stating that
647-
**submission cannot be undone**. When `AADE_ENDPOINT` is production, the modal additionally requires
658+
**submission cannot be undone**. When the resolved endpoint is production, the modal additionally requires
648659
typing `SUBMIT` before `Yes` is enabled — `tui-rules.md` reserves this for high-blast-radius actions,
649660
and filing a real tax document qualifies. During the call the status bar shows a spinner; the outcome
650661
lands there as `[green]✓ MARK 400001234567890` or `[red]✗ <error code> <message>`.
@@ -732,7 +743,7 @@ non-matching pair shows nothing.
732743
**UC-16 Snapshots.** A prefix seek on `income\x00` returns only income snapshots, in chronological
733744
order, without scanning expense records.
734745

735-
**UC-17 Environment.** The status bar always shows `[TEST]` or `[PROD]` matching `AADE_ENDPOINT`.
746+
**UC-17 Environment.** The status bar always shows `[TEST]` or `[PROD]` matching the resolved endpoint.
736747
Settings reports credential presence without ever displaying the subscription key.
737748

738749
**Envelope.** The binary opens no listening socket. With the network fully unavailable, the app
@@ -741,7 +752,7 @@ against the same file: a write in one is visible to the other on refresh, and ne
741752
lock while idle. The MCP server writes nothing to **stdout** except protocol frames.
742753

743754
**MCP gating.** Without `YOURDATA_MCP_ALLOW_SUBMIT=1`, `submit_invoice` does not appear in
744-
`tools/list`. With it set **and** `AADE_ENDPOINT` at production, the tool appears but every call
755+
`tools/list`. With it set **and** the resolved endpoint at production, the tool appears but every call
745756
returns a tool-level error and transmits nothing. **`create_invoice_draft` and every other
746757
non-submit tool appear in `tools/list` unconditionally.** MARK values are strings in all tool output.
747758

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/techthos/yourdata
33
go 1.26.4
44

55
require (
6-
github.com/gdamore/tcell/v2 v2.13.10
6+
github.com/gdamore/tcell/v2 v2.8.1
77
github.com/mark3labs/mcp-go v0.56.0
88
github.com/rivo/tview v0.42.0
99
github.com/techthos/yourdata-sdk v0.0.0-20260501220034-d8ac9a523e8a
@@ -15,6 +15,7 @@ require (
1515
github.com/google/jsonschema-go v0.4.2 // indirect
1616
github.com/google/uuid v1.6.0 // indirect
1717
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
18+
github.com/mattn/go-runewidth v0.0.16 // indirect
1819
github.com/rivo/uniseg v0.4.7 // indirect
1920
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
2021
github.com/spf13/cast v1.7.1 // indirect

0 commit comments

Comments
 (0)