Commit Graph

5274 Commits

Author SHA1 Message Date
Maurice 5f49c87f4a fix(admin): map the retired integration group for managed tabs
The settings redesign folded the integration sidebar group into config,
but ManagedAdminTab still accepts group 'integration'. Indexing the label
map with it was an implicit any under the strict check and would have put
such a tab under an empty heading. It now lands under config.
2026-11-13 23:30:35 +00:00
Maurice 01653f81e3 test(settings): follow the redesigned settings and admin pages
The tests now look for what the pages show after the redesign: the admin
Settings tab is Security and Services, MCP Access and User Defaults are MCP
and Defaults, notification channels and the 2FA rule are rows with a named
switch, hints sit behind the info mark, each addon type card counts its own
addons, OAuth scopes and the AI sharing note are shown in full, and the 2FA
disable password is the field beside the code. The phone's MCP admin header
keeps its own title, MCP Access. The mail pill test renders with
translations, so it finds From mail rather than the key.
2026-11-13 23:10:05 +00:00
Maurice a0764b9c96 fix(settings): the checks the settings redesign left red
The phone's MCP card still read the config fold state that the shared hook
no longer holds, so the client failed to typecheck. The phone keeps its
folded config, so it now holds that state itself.

SettingsColumns takes start and end instead of left and right: the prop
types counted as two physical sides for lint:rtl, and in a right-to-left
language the first column is the right one anyway.

Also: an unused icon import, seven files outside the format baseline run
through Prettier, Routing (de) and Services (fr) confirmed as the same word
as in English, and the size and warning baselines lowered to what the
redesign left (AdminPluginsPanel one wrapped line shorter, two promise
warnings, one long function and two non-null assertions in tests fewer).
2026-11-13 23:10:05 +00:00
Maurice 94ac7673a5 feat(settings): one look for every user and admin settings page
The settings pages had grown into a mix of styles: folds, dialogs behind
rows, full-width rows next to boxed ones, pills for every figure and buttons
in four sizes. Every page now uses the same kit.

- Cards keep their tinted head band; every part of a card is a rounded block
  with its own small head (SettingsBlock), and rows sit in rounded boxes.
- Nothing folds and nothing hides behind a dialog: the MCP config, OAuth
  scopes, mail how-to, AI privacy note and addon sub-options are all shown.
- Each page is split into two columns balanced by length (SettingsColumns).
- Long explanations moved into an info mark next to the label (HintMark).
- One button size everywhere (text-control); tab names are one word.
- Admin: the old Settings tab is split into Security and Services
  (?tab=settings still opens Security, and the phone keeps its single
  Settings section for both); the Integration group is gone, live flights
  and mail import sit next to the addons they configure.
- Addons: one card per type with rows, no search, filter or fold.
- Quota, TREK API facts, mail import status and Google's daily limit are
  compact lines and strips instead of tiles and pills.
- Language is a select; routing has its own card under the defaults;
  storage saves from the foot of its categories card.
- Unused keys, FoldSummary and the page-width wrapper are removed.
2026-11-13 23:18:28 +01:00
Maurice 5fe3dea4cc feat(settings): collapsible settings cards, first on the integrations tab
The settings pages list everything at once, so they read as a wall of
forms. SettingsCard and Section take 'collapsible': the card starts shut
and its badge says its state ('Connected', 'Not set up'); its title is the
button, stretched over the head, the head's action stays its own button.
The body stays mounted while shut. A link to #id opens the card and
scrolls to it; the browser remembers a card left open.

Every integrations card is collapsible now; AI parsing shows its saved
model and MCP the apps signed in. The assistant's settings link opens the
AI connection card. Tests that read into a card open it first
(tests/helpers/settingsCards.ts).
2026-11-13 21:57:20 +01:00
Maurice 608f76f32c feat(settings): two columns for the integrations tab
From xl up, as on the admin settings: what connects your trips with other
services (booking mails, photo servers, AirTrail, Dawarich, Wanderer) on
the left, AI and developer access (AI connection, AI parsing, API keys,
MCP) on the right. A column with nothing to show drops out and the other
takes the whole width.
2026-11-13 21:35:04 +01:00
Maurice a2cc7af695 fix(bookings): keep 'From mail' out of the card and list head
Behind the title stood the review mark, the 'From mail' pill and the two
actions, and the title was cut short. Where a booking came from is looked
up in its detail dialog, so the pill stays there only. The AirTrail note
sat above the mail pill; it is back above AirTrailPill.
2026-11-13 21:21:49 +01:00
Maurice dfbe23e5b8 fix: one status tile list for both admin cards; clear the copied timer on unmount
- The mail import's status tiles (which, labels, tones) were written out
  in the desktop and the mobile card alike; statTiles in the shared model
  holds them, each card keeps its own look.
- The journey share link set 'copied' back two seconds after a copy even
  when the dialog had closed, which also broke a client test run (window
  gone at teardown). useCopied, the trip share's hook, moves to hooks/ and
  serves both; it clears its timer on unmount.
- PlanLlmConfig is not exported: only the union is used outside.
2026-11-13 21:17:28 +01:00
Maurice 6ac7bd2d96 fix(mail-ingest): read a mail as soon as a transport takes it in
Only an upload started a processing run; a forwarded mail waited for the
next two-minute tick, and one that came in right after a run waited for two.
The IMAP and relay runs start one run once they took anything in, the
webhook once per mail it accepted.
2026-11-13 21:05:54 +01:00
Maurice a67361c6bd fix(mail-ingest): read on to To when a delivery header names no address
Stalwart writes Delivered-To as the mailbox itself, its sub-address dropped,
so a forwarded mail to trek+<token>@ was refused as unknown_address: the
first delivery header won even though it carried no token. The headers are
now read in their order of trust, and the first that names a live address
decides.
2026-11-13 20:57:55 +01:00
Maurice 7a40225852 feat(llm-parse): read bookings on a ChatGPT plan
A ChatGPT sign-in only ever ran the assistant: parsing's clients call an
endpoint with a key, and a plan has none. AI connections now lends the plan
as a reader. Each document is one Responses call with the schema as the one
tool the model has to call (tool_choice required). The token is read and
refreshed per call by ChatgptAuthService, and a sign-in that has to be
renewed lends nothing.

- ResolvedLlmConfig is a union: ApiLlmConfig (what an admin configures) or
  PlanLlmConfig (provider chatgpt, a read function, never stored).
- ConnectionReadClient sends the text plus photo or scanned pages, and reads
  the filled shape, or the shape written as text; prose is unreadable.
- AiChatClientService.chatgptPlan is the one place that builds the plan's
  client, for the assistant and for parsing.
- Wiki: a ChatGPT sign-in reads bookings on its plan.
2026-11-13 20:49:08 +01:00
Maurice 29816f81a9 fix(mail-ingest): send an uploaded .eml as multipart; publish the booking origin as a contract
- The upload posted its FormData through the client's JSON default, so the
  form went as JSON and the server saw no file (400). It goes through
  postMultipart now, like every other upload.
- reservation-origin.ts becomes reservation-origin.contract.ts: the booking
  import sets an origin, and a contract is what another domain may import.
- CI: the client's strict type check holds the whole program with strict
  checks and outgrew the default ~4 GB heap; it gets 8 GB like the server's.
2026-11-13 20:49:08 +01:00
Maurice 4337969c2c fix(mail-ingest): the settings card makes the person's own address on first look
The own address was made only when asked for, and the card had no way to ask
before one existed, so it showed none. Once a template is set and the
person's import is on, the first look provisions it (idempotent; read after).
2026-11-13 20:32:31 +01:00
Maurice 6789ee1cb6 refactor(server): keep the grown files at their size baselines; centre the inbox dialog
- auth.service: how an admin form value is stored (validated, encrypted, a
  masked echo kept) moves to the app settings registry as storedAdminValue
  and isMaskEcho.
- reservations.service: a booking's origin (withoutOrigin, originColumns)
  moves to reservation-origin.ts, its input shapes to reservation-inputs.ts,
  exported on from the service so importers stay as they were.
- Reservations.repository: the joined projection, the origin lookup and the
  origin columns move to _shared/, like the Wallet pass context before.
- test-sql: MailIngestApplyService.prepare is marked as no SQL statement.
- The pending bookings dialog opens centred, and says 'nothing waiting' once.
2026-11-13 20:28:17 +01:00
Maurice 7ed15a77b4 style(mail-ingest): a more compact admin status 2026-11-13 19:55:13 +01:00
Maurice 4806c62976 feat(mail-ingest): the admin status as tiles with a health dot
The last read sits in the head line with a green dot, amber while the last
run failed; the day's numbers are tiles with their icon in a soft tone
(mails, added on their own, waiting, failed turning red above zero); the
last error is its own amber note. Desktop and phone.
2026-11-13 19:53:30 +01:00
Maurice 3957b292be feat(mail-ingest): a blue note under each way of taking mail in
Upload only, IMAP and webhook each explain themselves in the admin card,
desktop and phone: what the way means, what it needs (a mailbox of its own
taking plus addresses, a signing secret), with its icon centred beside the
title and the sentence. All 28 locales.
2026-11-13 19:50:47 +01:00
Maurice 783f679124 chore(server): lower the eslint baseline to what the code holds now 2026-11-13 19:38:57 +01:00
Maurice 7e1de86f0b test: no non-null assertions or awaits in a loop in the new mail import tests 2026-11-13 19:31:14 +01:00
Maurice c13c288f00 test(broadcast-parity): cases for the mail import's write tools
add, move, file, discard and undo of a mail booking and the import
settings each get a case with their REST twin. A trip expectation may
name its own room, for the move that announces in two trips.
2026-11-13 19:31:14 +01:00
Maurice 89b67a791f fix(mail-ingest): Outlook HTML forwards, the default AI limit, secret env vars, and the pipeline tests
- mime-walker: trim each line of a stripped HTML body, so the space a tag
  leaves is not read as a folded header line that swallows the booking
  under an Outlook forward's header block.
- settings: an unset daily AI limit is the default of ten, not zero
  (Number(null) is 0), which kept the AI from ever reading a mail.
- env: MAIL_INGEST_IMAP_PASS and MAIL_INGEST_WEBHOOK_SECRET are secrets
  and never printed in the boot report.
- pipeline test: the subject is kept without its forward prefixes, the
  demo move adds its booking before the second trip exists, the typed
  notification mock, and AI reading stays offered while the person
  switched it off.
- e2e: the pipeline tick runs in a request context of its own, as the
  cron registrar runs it, and the 401 checks run without awaiting in a loop.
2026-11-13 19:31:13 +01:00
Maurice 2ee413368b fix(mail-ingest): AI reading can be switched back on, and its hint names the addon
ai_available folded the person's own switch in, so switching AI reading off
greyed the switch out and it never came back. It now says only whether AI
reading works on the instance (the AI Parsing addon and a connection). When
it does not, the settings card says it needs the AI Parsing addon, by its
localized name, in all 28 locales; the wiki says the same.
2026-11-13 19:29:48 +01:00
Maurice 1aa586d829 test: type fixes the CI typecheck found, and the lowered test-new-service baseline
- mailInboxModel.test: actionsOf takes the entry alone since the AI reread left.
- booking-origin.test: its endpoint helper gives the sequence an endpoint has.
- test-new-service baseline follows booking-import's single service factory.
2026-11-13 19:20:09 +01:00
Maurice 53833fd212 fix(mail-ingest): server gates after joining dev, and smaller admin buttons
- lint:tx: the mail transports and the intake document why their writes stand
  alone (@txIndependent, network I/O between them, one copy per address); the
  JSDoc of NotificationsService.respond is back above it after the response
  actions were registered in between.
- lint:boundaries: booking-import asks McpToolGuardsService whether the person
  may edit the trip's bookings instead of reading the Users table; its unit
  test builds the service through one named factory.
- lint:query-api: findIdByExternal reads through Kysely.
- lint:test-mocks: the mail e2e hands its own snapshot db to the ORM instead
  of mocking the database module.
- Entities regenerated (index order only).
- Client: the review row reads the live booking, the shells share
  useMailInboxShell, and the admin card's buttons are one size down.
2026-11-13 18:58:55 +01:00
Maurice a7adef5353 feat(mail-ingest): admin card, review mark, inbox events and links, translations
- Admin settings (desktop and phone, self-hosted only): the instance switch,
  how mail arrives (IMAP or a signed webhook) with its fields, the address
  template, the AI allowance per person and day, a connection test and the
  status in counts. Changed fields only are saved; secrets stay masked.
- The review mark of an imported booking takes its mark off: the pill in the
  desktop dialog and a row in the phone's transport sheet ('Looks right'),
  through the ordinary booking update. Its hint no longer speaks of airports.
- The inbox follows mailInbox:changed and mailInbox:settings, and
  ?inbox=mail (the 'bookings waiting' notification) opens it.
- mailIngestRepo: the online-only path the views take, answers parsed
  against the shared contracts.
- Dropped 'read with AI' and the unused mail-review contract: the server
  reads a passing AI failure again on its own.
- mailInbox, mailAdmin and the new reservation, settings and notification
  strings in all 28 locales; the pending notice is a plural group.
- The live flight line on the booking cards is one of the card's fields,
  full width on the phone; the phone's live card keeps its map to the middle.
- Admin flight and mail cards sit in the left column.
2026-11-13 18:31:07 +01:00
Maurice bd11cd347d fix(mail-ingest): type errors and formatting after joining the flight work
The cron callbacks hand back nothing; the cursor's lookup is findCursor so it
no longer shadows the base repository's find(); the draft's suggested trip
uses the shared contract's type; endpoints spell out their optional columns;
the relay page's running count is typed. Prettier over every new file outside
the generated entities.
2026-11-13 18:03:14 +01:00
Maurice b19bd8e709 build: lock imapflow for the mail import's IMAP transport 2026-11-13 17:49:07 +01:00
Maurice 8dedab0375 feat(mail-ingest): inbox, settings and trip address UI on desktop and phone (wip) 2026-11-13 17:46:19 +01:00
Maurice cbd1800976 test(mail-ingest): name the control plane's fixture file the relay page is copied from
The fixture moved out of the control plane's sources so its tests read it
from disk; the comment now names where it lives.
2026-11-13 17:44:57 +01:00
Maurice 44f669cd0e feat(mail-ingest): tell every address holder once when the import stops
A plan without the import switches it off from outside the server, and an
admin can switch it off too; the plan promises a "mail import paused" note.
An hourly job sends mail_ingest_paused to each holder of a live address with
their own import on, notes the pause first, and forgets it once it is back.
2026-11-13 17:44:55 +01:00
Maurice 1d052f725f fix(mail-ingest): send mailInbox:settings, and give both events their client place
The settings event was registered with no sender; renewing or revoking an
address and changing the switches now send it. The client lists both events
as not yet shown, which keeps the registry-parity test green until the mail
screens move them to their listener.
2026-11-13 17:43:55 +01:00
Maurice f7bad3c5b3 docs(wiki): the mail import's deferred folder, recipients, limits and sender hint
Describes TREK/deferred, which header names the address, the webhook's 401
and 429 answers and where the sender hint can show at all. The variables
table leaves an empty default blank instead of a dash.
2026-11-13 17:43:54 +01:00
Maurice 264f352502 fix(mail-ingest): guard the public webhook and the settings that open it
The webhook refuses headers that cannot be signed right before reading a
byte, limits each address to 120 requests a minute and reads four bodies at
once. The admin save refuses a webhook secret under 32 characters, and the
MCP settings tool registers its input strict like every other tool.
2026-11-13 17:43:54 +01:00
Maurice 32291ce8b4 fix(mail-ingest): trust a sender verdict only from the IMAP box's own server
Anyone could add an Authentication-Results header with dmarc=pass. The hint
now needs a DKIM pass signed by the From domain or a DMARC pass naming it,
and reads it only for IMAP, where the receiving server's header is topmost;
the Cloud relay, the webhook and uploads never show a sender as verified.
2026-11-13 17:43:53 +01:00
Maurice c35f39add1 fix(mail-ingest): only the forwarder moves a booking, and filing claims the entry first
A co-member could move the forwarder's booking into a private trip, carrying
their mail along; undo stays open to editors. Move and filing refuse demo
accounts, and filing claims the entry before writing documents, putting it
back into the inbox when nothing could be filed.
2026-11-13 17:43:52 +01:00
Maurice 993ed4b472 fix(mail-ingest): refuse an upload while the person's import is off, and start no run in tests
An upload made an address and stored a mail the pipeline then put aside;
it now answers 409 instead. The run an upload asks for at once starts only
where the crons run, so it can no longer outlive a test's database.
2026-11-13 17:43:51 +01:00
Maurice e7db110417 fix(mail-ingest): read the Cloud inbox as it is sent, and keep mail refused for now
The relay job reads the control plane's { messages, more, next } page, checks
each mail on its own, pages on while more wait and is idle on a 404. A mail
for several addresses is stored for each, and a passing refusal (inbox full,
daily share taken) leaves it with the relay or in IMAP's TREK/deferred.
2026-11-13 17:43:51 +01:00
Maurice 011cc9f11f fix(mail-ingest): an empty IMAP folder is not asked for a uid range
Some servers refuse a FETCH range on a folder with no mails, which would turn
every quiet run into a failure on the status card.
2026-11-13 17:43:50 +01:00
Maurice 844d03a9cd feat(mail-ingest): REST routes and MCP tools for the settings card, the inbox and trip addresses
/api/mail-ingest serves the settings card, the inbox, the mail preview, the
.eml upload and every action on an entry; trip addresses and the admin status
and connection test have routes of their own. The MCP tools act through the
same services and never hand out an address, and the trip assistant is not offered them.
2026-11-13 17:43:50 +01:00
Maurice 1bfaf4ea98 feat(mail-ingest): read forwarded mails from an IMAP box, a signed webhook and the TREK Cloud relay
The IMAP job reads the box every two minutes through the SSRF guard, keeps a
uid cursor and moves read mails to TREK/processed. The webhook takes raw MIME
signed with HMAC-SHA256 over a timestamp, the recipient and the mail, and is a
404 unless it is the transport. The relay job collects a managed install's mails.
2026-11-13 17:43:49 +01:00
Maurice 6309064f40 feat(mail-ingest): the inbox a person reads, and moving an added booking to another trip
The inbox pages a person's entries with the mail each came from and the trip
it suggests (named only when the reader can see it), shows a mail as plain
text, and takes an uploaded .eml, reading it at once. Move writes the booking
anew in the other trip and removes it from the first in one transaction.
2026-11-13 17:43:49 +01:00
Maurice 7d3ed21a03 feat(mail-ingest): the transport settings in the admin form, each overridable from the environment
The IMAP box, the webhook secret and the address template join the admin
settings form (masked, encrypted, held by the operator on a managed install),
and MAIL_INGEST_* variables override them through the mailIngestConfig token.
The audit row masks every masked key, and key rotation covers the new secrets.
2026-11-13 17:43:48 +01:00
Maurice f319b3610a feat(mail-ingest): tell open clients when the inbox changes
Every new waiting entry and every decision sends mailInbox:changed with the count, so a badge follows without a reload.
2026-11-13 17:43:27 +01:00
Maurice 81d27d8e8d refactor(mail-ingest): keep the types nothing else imports private to their file
Exported types without a reader outside their file only add to the unused-export count.
2026-11-13 17:43:26 +01:00
Maurice de7de4b8de feat(mail-ingest): read forwarded booking mails and add what passes the checks to its trip
Addresses per person and per trip (random base32 token, stored hashed and
encrypted), an intake every transport shares, and a job that reads each
mail once under a lease: filters without AI, machine-readable data always,
one AI request with the person's trips, the server's checks G1 to G9, then
one transaction per mail that adds what passed, keeps the rest in the inbox
with a reason, and notifies the forwarder and co-travellers. Undo within
7 days, manual apply, filing as documents, a prefilled form and a nightly
retention complete it; on a managed install the operator's switch decides.
2026-11-13 17:43:26 +01:00
Maurice 340454cfc7 feat(notifications): events for bookings added from mail, and actions a domain registers
mail_booking_added tells the forwarder (with keep or undo) and co-travellers
of a shared trip; mail_booking_pending points at the inbox. Both are listed
only while the mail import is on, with texts in all 28 locales.
registerResponseAction lets a domain name what a yes/no tap does without
this module importing it.
2026-11-13 17:43:25 +01:00
Maurice 5f600cf263 style(mail-ingest): wrap the repository lines the way prettier does
The new repositories and their tests held a few lines past the 120-column print width.
2026-11-13 17:42:44 +01:00
Maurice aa84d71302 feat(llm-parse): read a forwarded booking mail and name its trip in one request
MailReadService sends one mail as one document: its subject, body and up to
three attachments inside data tags the prompt calls untrusted, with the
person's trips under references made up for the call. Answers keep only
offered trip references and known field names. BookingImportService reads
a mail's machine-readable parts and maps an AI reading to booking items.
2026-11-13 17:42:44 +01:00
Maurice baa906fb6b feat(mail-ingest): tables for forwarded booking mails and their bookings
Five tables carry the mail import: forwarding addresses (token hash plus the
encrypted token), a person's switches, the readers' cursor and lease, every
mail as it arrived, and each booking read from it with its checks and fate.
Repositories read the trips a booking may go to by column, so the import
weighs a booking without reaching into five other domains.
2026-11-13 17:42:43 +01:00
Maurice 53ebc9d632 fix(llm-parse): the local router reports how its reading ended
The Ollama router catches a failed model call and answered an empty list, which
the outcome read as a document without bookings. It now returns the outcome with
the list; failureOutcome and foundOutcome move to llm-outcome.ts so the router and
the service share them without importing each other.
2026-11-13 17:40:53 +01:00