diff --git a/SPEC.md b/SPEC.md index 6c2cfdc..f1eec6c 100644 --- a/SPEC.md +++ b/SPEC.md @@ -243,7 +243,7 @@ Decided on 2026-10-30 (`huismus-intern/besluiten.md`): Huismus works the same on Decided on 2026-10-28 and 2026-10-29; build choices by the product-owner agent on 2026-10-30 (`huismus-intern/besluiten.md`, "AI-koppeling (M16 stap 1): bouwkeuzes"). An AI assistant (the Claude app, Claude Code, ChatGPT, Gemini CLI) works with the administration through the Model Context Protocol. Everything the assistant does goes through the same code as the app, so numbering, totals and immutability stay where they are. Two steps. - **Step 1: an address with a key, everywhere.** `/mcp` speaks MCP over "Streamable HTTP" (stateless, JSON answers, no event stream), built on Django without the `mcp` package (that is an ASGI stack; Huismus is WSGI). It answers only requests with `Authorization: Bearer `, never a session cookie, and refuses an `Origin` of another host. The key lives in its own owner-only file in `data/`, left out of backups (section 9). Without a key `/mcp` refuses everything. In the desktop app the window token does not apply to `/mcp`; the key does, and the app tries port 8765 first so the address stays the same. - **What the assistant may do.** Read: an overview (revenue this year against the KOR limit, outstanding, overdue), searching and opening invoices, credit notes and quotes, searching contacts, expenses per year, and the receipts without an expense (M20, part 2); no settings, other attachments, PDFs, mail texts or mail log. Drafts: make and change invoice and quote drafts, and a credit-note draft from an issued invoice, through the editor's own forms and `huismus/documents/drafts.py`. Confirm: issuing an invoice or credit note, sending a quote and mailing a document are separate actions, marked destructive and never read-only, so the assistant's app asks the user before each; a read action before each gives the summary (customer, amount, the number it will get, and the KOR-limit line of the issue dialog, M12). Mailing goes only to the customer's address with the standard subject and text. Expenses (besluit 2026-11-02, `huismus-intern/besluiten.md`, "AI-koppeling: uitgaven toevoegen"): `add_expense` adds one with its lines (gross amount, optional VAT for information, an existing category by name or none), through the expense page's own forms and `huismus/expenses/lines.py`; not destructive, since it gets no number and the user can change or delete it; no categories made; a receipt only from the receipts without an expense (M20, part 2), else the answer says to add one in Huismus, with the path to the expense. Never: contacts, changing or deleting expenses, payments, a quote's answer, deleting, "Toch bewerken". Action titles are in the default language from Settings; descriptions for the assistant in English; no legal or tax claims in either. -- **Instellingen → AI-assistenten** (design D5, decision 2026-10-29: the page only connects): first one card **Adres en sleutel**, for every assistant (besluit 2026-10-30): "Niet gekoppeld" and "Koppelen", which makes the key; then the address (the one Huismus was opened at, plus `/mcp`), the key in a read-only field, hidden as dots until "Toon" (besluit 2026-11-02; without JS a folded block shows it), with "Kopiëren" when JS works, which copies it hidden or not, "Nieuwe sleutel", "Ontkoppelen" (both after a confirmation) and a link to the steps (the help page AI-assistenten, M17); the pill says "Wacht op verbinding" until a request with the key came in, then "Gekoppeld". Below it a card per assistant with one line on how it uses them: **Claude** (Claude Code), **ChatGPT** (through OpenAI's tunnel, for technical users) and **Gemini** ("Niet beschikbaar": the app cannot, Gemini CLI can). +- **Instellingen → AI-assistenten** (design D5, decision 2026-10-29: the page only connects): first one card **Adres en sleutel**, for every assistant (besluit 2026-10-30): "Niet gekoppeld" and "Koppelen", which makes the key; then the address (the one Huismus was opened at, plus `/mcp`), the key in a read-only field, hidden as dots until "Toon" (besluit 2026-11-02; without JS a folded block shows it), with "Kopiëren" when JS works, which copies it hidden or not, "Nieuwe sleutel", "Ontkoppelen" (both after a confirmation) and a link to the steps (the help page AI-assistenten, M17); the pill says "Wacht op verbinding" until a request with the key came in, then "Gekoppeld"; while it waits it asks again every few seconds, so it changes without a reload (the owner, 2026-11-13). Below it a card per assistant with one line on how it uses them: **Claude** (Claude Code), **ChatGPT** (through OpenAI's tunnel, for technical users) and **Gemini** ("Niet beschikbaar": the app cannot, Gemini CLI can). - **Step 2: the Claude app in the desktop app** (the owner and the product-owner agent, 2026-11-02, `huismus-intern/besluiten.md`). Only in the desktop app, the page shows a card **Claude** first: one line, the pill and a button **Koppel met Claude**, a plain POST. It makes the key if there is none (else uses it), writes `Huismus.mcpb`, an extension in the Claude app's own format ([MCPB](https://github.com/modelcontextprotocol/mcpb)), to an owner-only folder in the user's temporary folder, and opens it with the Claude app, which asks the user to install it. Huismus then says to confirm that there (the Claude app may be in English or Dutch, so no button name); without the Claude app it says where to download it. The file holds the key, the address, texts in the language at the moment of connecting, and a program without dependencies (`huismus/assistants/bridge/server.js`) that the Claude app runs with its own Node: it passes each message on to `/mcp`. The desktop app writes its address at every start to a file next to the administration (not in it, no key in it), which the program reads first, so another port breaks nothing; the app removes `Huismus.mcpb` at the next start. While Huismus is closed the program answers itself ("Huismus is niet open…") and tells the Claude app the tools changed once Huismus opens. "Nieuwe sleutel" and "Ontkoppelen" lock the Claude app out too. Below the card, folded in "Andere assistenten (voor technische gebruikers)", the step-1 cards. The web app's page does not change. *Done when (step 1):* tests cover the protocol, the key (none, wrong, renewed, removed), the Origin check, the desktop window token, every action and that it uses the app's code (totals, KOR 0 %, numbering through `issue()`, an issued document refused, the mail log), the summaries, and the page; a browser test connects and copies; the page is in `test_every_page.py`; strings exist in Dutch and English; `docs/hulp/ai-assistenten.*.md` explains ChatGPT's tunnel, Gemini CLI and Claude Code (M17). *Done when (step 2):* tests cover the bundle (key made or kept, address, address file, texts per language, owner-only), the button only in the desktop app, the missing Claude app, and the program itself under Node (passing on, the address file, Huismus closed and opening, a renewed key); the help page explains the Claude app; tried on a Mac with the Claude app. diff --git a/huismus/templates/business/_assistant_status.html b/huismus/templates/business/_assistant_status.html index 0ee4423..135010e 100644 --- a/huismus/templates/business/_assistant_status.html +++ b/huismus/templates/business/_assistant_status.html @@ -1 +1 @@ -{% load i18n %}{% if not key %}{% translate "Niet gekoppeld" %}{% elif used %}{% translate "Gekoppeld" %}{% else %}{% translate "Wacht op verbinding" %}{% endif %} \ No newline at end of file +{% load i18n %}{% comment %}While it waits for the first request with the key, the pill asks the page again every few seconds, so it turns "Gekoppeld" without a reload (the owner, 2026-11-13); once connected or without a key it stops asking. Without JS: reload the page.{% endcomment %}{% if not key %}{% translate "Niet gekoppeld" %}{% elif used %}{% translate "Gekoppeld" %}{% else %}{% translate "Wacht op verbinding" %}{% endif %} diff --git a/tests/e2e/test_browser.py b/tests/e2e/test_browser.py index 8d58975..303a153 100644 --- a/tests/e2e/test_browser.py +++ b/tests/e2e/test_browser.py @@ -1455,8 +1455,9 @@ def test_connecting_an_ai_assistant_and_copying_the_key(app, browser_name): headers={"Authorization": f"Bearer {secret}"}, ) assert answer.json() == {"jsonrpc": "2.0", "id": 1, "result": {}} - app.reload() - expect(app.locator("#adres-en-sleutel .pill")).to_have_text("Gekoppeld") + # Without a reload: the pill asks again while it waits (the owner + # connected Claude and the page went on saying "Wacht op verbinding"). + expect(app.locator("#adres-en-sleutel .pill")).to_have_text("Gekoppeld", timeout=10_000) # A new key asks first, then locks the old one out. app.get_by_role("button", name="Nieuwe sleutel").click()