{"ok":true,"protocol":"aimeat","version":"v1","node":"aimeat-finland-001-genesis","timestamp":"2026-11-07T20:14:27.814Z","request_id":"req-34042051","data":{"skill":{"ref":"user:happydude500001/paneeli","scope":"user","owner":"happydude500001","name":"paneeli","description":"Put an AIMEAT app on an embedded device as its user interface and keep it there safely: register the device's W3C Web of Things Thing Description, give an installer a one-time code, build an offline bundle from an app, sign and release it to devices, and answer what runs on which device and who put it there; check it with screenshots, accessibility and flows, group panels, release in a window under a budget, read alarms and remote screenshots, pair screens on the machine network, and take a release to a panel on a USB stick; share an interface template or a brand kit in the PANEELI market, and take one that somebody else shared. Use when someone talks about a device screen, an HMI, a touch panel, a kiosk, a Raspberry Pi or ESP32 with a display, a laitteen käyttöliittymä, PANEELI, or replacing Qt for an embedded interface.","version":"1.0.7","visibility":"public","files":[{"path":"SKILL.md","size":32209}],"updatedAt":"2026-10-31T18:09:08.366Z","license":"MIT","metadata":{"binding":"app:happydude500001/paneeli.html","audience":"agent"},"binding":"app:happydude500001/paneeli.html","aiProvenanceId":"a1b6cf27-6bba-4f68-9635-5f68487e95cd","fileContents":{"SKILL.md":"---\nname: paneeli\ndescription: >-\n  Put an AIMEAT app on an embedded device as its user interface and keep it there safely: register\n  the device's W3C Web of Things Thing Description, give an installer a one-time code, build an\n  offline bundle from an app, sign and release it to devices, and answer what runs on which device\n  and who put it there; check it with screenshots, accessibility and flows, group panels, release in a\n  window under a budget, read alarms and remote screenshots, pair screens on the machine network, and\n  take a release to a panel on a USB stick; share an interface template or a brand kit in the PANEELI\n  market, and take one that somebody else shared.\n  Use when someone talks about a device screen, an HMI, a touch panel, a kiosk, a Raspberry Pi or ESP32\n  with a display, a laitteen käyttöliittymä, PANEELI, or replacing Qt for an embedded interface.\nlicense: MIT\nmetadata:\n  binding: app:happydude500001/paneeli.html\n  audience: agent\n---\n\n# PANEELI: a web interface on a device, kept there safely\n\nThe device's interface is an ordinary AIMEAT app. A Linux panel shows it in a WPE WebKit kiosk; a\nmicrocontroller such as an ESP32 behind the panel does the control work. The device's Thing\nDescription (W3C Web of Things, TD 1.1) is the contract between the interface and the device. A small\ndevice agent on the panel installs only releases signed with the owner's key, and serves the page and\na bridge on 127.0.0.1:8090. The page can reach the bridge and nothing else.\n\nEverything is under the caller's own account, whoever the caller is. Call the actions with\n`aimeat_extension_invoke { extension_name: \"paneeli\", action_id: \"<tool>\", input }`; this works for every\nsigned-in account. (The same actions are app tools of `happydude500001/paneeli.html`, but a free app tool\nis offered only to its owner's own agents.) The first time a person opens the screen at\nhttps://paneeli.apps.aimeat.io/ they allow it once (\"Connect\"); after that it shows the same data.\nThe demo interface is https://paneeli-tuuletin.apps.aimeat.io/.\n\n## When the person starts here\n\nThe making happens in this chat; the PANEELI screen is for watching panels and operating them. When\nthe person opens with something like \"Aloitetaan PANEELI\" or \"I want a screen for my machine\", lead\nthe conversation instead of listing features:\n1. Ask what the device is and what a person at the screen needs to see and do (values, settings,\n   commands, alarms). A picture, a photo of an old panel or a datasheet answers most of it at once.\n2. Ask how the device talks (HTTP, MQTT, Modbus TCP or RTU, OPC UA, BACnet/IP, CAN, Bluetooth LE,\n   something else, or \"I don't know\": then ask for the\n   device's make and model, or its datasheet) and what the screen will run on (a Raspberry Pi with a\n   display, an industrial panel PC, an ESP32 with a phone or tablet, not decided yet).\n3. Say in two sentences what you will do next, then do it with the flow below: the device type, the\n   interface, the check with screenshots, and only then the panel and the release. Show the\n   screenshots before anything goes to a real panel.\n4. Tell them where to watch it: https://paneeli.apps.aimeat.io/ (panels, what each one runs, alarms).\nAsk one or two questions at a time, in their language.\n\n**A datasheet or a register map.** When the person gives a PDF, a table of Modbus registers, an MQTT\ntopic list or a vendor manual, write the Thing Description from it: one property per register or\ntopic the screen needs (name in the person's words, unit, scale, minimum, maximum, readOnly), with the\nbinding's forms (`modv:entity`, `modv:type`, address, `mqv:topic`). Name what you could not find in\nthe document and ask only that. Do not invent a register.\n\n**Their own interface from a template.** Read the template with `aimeat_app_get` (for example\n`happydude500001/paneeli-ohjauspaneeli.html`), change what the person wants (title, values, layout,\ntheme, brand colours) and publish it under their account with `aimeat_app_publish` and a new file\nname such as `paneeli-<name>.html`. For an A/B comparison publish two variants and check both; on real\npanels release A to some panels and B to others (`deviceIds`) and compare their reports.\n\n**A change to one spot.** The PANEELI Studio lets a person point at an element and copy a sentence\nthat names the app, the element (a CSS selector and its markup) and what they want changed. Change\nonly that element in the app source, publish a new version, build and check it, and show the\nscreenshot.\n\n## The flow\n\n1. **Describe the device.** `thingRegister { td }` with the Thing Description. A refusal lists the\n   problems; fix them and send again. If the person has no TD, write one with them: one property per\n   value the screen shows or sets (type, unit, minimum, maximum, enum, readOnly, observable), one action\n   per command, one event per alarm. Every affordance needs a form with an `href`; the scheme of the\n   href picks the protocol (\"Protocols\" below). A device that speaks HTTP serves its TD; for every other\n   protocol write the forms and give the panel the TD as a file (`--thing file:///etc/paneeli/x.td.json`).\n   Each protocol has a full example on GitHub, `contracts/demo-fan-<protocol>.td.json`.\n   Mark which events and property states are alarms with `paneeli:alarm` (add\n   `\"paneeli\": \"https://paneeli.aimeat.io/td#\"` to `@context`): an event `\"paneeli:alarm\": \"critical\"`,\n   a boolean property `\"paneeli:alarm\": true`, a number `{ \"above\": 60, \"severity\": \"critical\" }`, or\n   an allowed band `{ \"above\": 3, \"below\": 1 }` (past either limit is an alarm). The page, the ESP32's\n   own screen and the panel's alarms all read the same mark, so they show the same colours.\n2. **Write the interface** as an AIMEAT app. Start from a template and change it, or use components:\n   - Templates (apps on this node): `paneeli-ohjauspaneeli.html` (controls and a service tab),\n     `paneeli-mittaritaulu.html` (gauge, trend, values), `paneeli-valvonta.html` (state, alarms, alarm\n     log), `paneeli-auto.html` (`<p-auto>` draws the whole TD), `paneeli-kayttoohje-tabletti.html`\n     (instructions, controls and languages for a tablet), `paneeli-tehdas.html` (a production line with\n     tanks, a floor plan and an event log).\n   - Components: load `/v1/cortex/paneeli/libs/paneeli-thing.js`, then `paneeli-ui.js`, then\n     `paneeli-ui-extra.js` when you use an extra element (paths without a host). Inside `<p-thing>`:\n     core `<p-value>`, `<p-gauge>`, `<p-trend>`, `<p-slider>`, `<p-toggle>`, `<p-segmented>`,\n     `<p-keypad>`, `<p-action>`, `<p-alarm>`, `<p-tabs><p-tab>`, `<p-status>`, `<p-brand>`,\n     `<p-theme-switch>`, `<p-confirm>`, `<p-auto>`; extra `<p-tank>`, `<p-bar-gauge>`, `<p-seven>`,\n     `<p-setpoint>`, `<p-sparkline>`, `<p-chart>`, `<p-indicators>`, `<p-table>`, `<p-log>`,\n     `<p-camera-still>`, `<p-floorplan><p-spot>`, `<p-dial>`, `<p-stepper>`, `<p-schedule>`,\n     `<p-pin-lock>`, `<p-clock>`, `<p-language>`, `<p-qr>`. The PANEELI screen's Kirjasto shows every\n     one running, with its attributes and a snippet to copy.\n   - Look: themes `<html data-p-theme=\"studio|industrial|contrast|warm\">`, light or dark with\n     `data-theme`, density `--p-density: compact`. Ten ready brand looks are `AIMEAT.ui.brandPresets`.\n     Languages: `<meta name=\"paneeli-languages\" content=\"fi,en,sv\">`, `?lang=xx`, `data-t` keys.\n   - Put the TD in the page as `<script type=\"application/td+json\">`. On aimeat.io the page runs\n     against a simulator built from the TD; on the device the same code talks to the real thing. A\n     `simulate(state, api)` in `AIMEAT.ui.simulator` that models the device makes the try-out honest.\n   - Nothing from outside aimeat.io (the bundle refuses it), no `onclick=` attributes (the device\n     policy blocks them; use addEventListener), no AIMEAT sign-in or data libraries (there is no node\n     on the device). System fonts, inline SVG, CSS.\n   - Touch first: controls at least 48 px, large numbers, and a layout that holds at 800x480 and\n     1024x600, the common panel sizes. Light and dark both reach 4.5:1 text contrast; the check\n     measures it.\n   - Light and dark: follow `prefers-color-scheme`, allow a fixed choice with `data-theme` on `<html>`,\n     and read `?theme=light|dark` from the URL, because a panel's browser has no system setting (it\n     shows light) and the kiosk command is where the installer picks one (`.../ui/?theme=dark` in\n     `/etc/paneeli/agent.json`).\n   `paneeli-tuuletin.html` is a complete example for the demo fan controller, with a three-way theme\n   button.\n3. **Build the bundle and check it.** `bundleBuild { appFile, appOwner? }`. Read `warnings`; a refusal\n   lists the outside files. The app must be readable on the node (published, not private). Then `check { bundleId }` or, for a real target, `check { bundleId,\n   profile }` with a device profile from `profileList` (for example `rpi4-7in`): the profile brings the\n   panel's sizes and an **estimated** frame rate on that device (say \"estimate\" when you report it, and\n   name the profile). Read `check { id }` until `status` is `done` (about a minute). Then:\n   - open the screenshot links (sizes × states: as loaded, at maximums, an alarm, the device away);\n   - read `minFps`, `consoleErrors`, `a11yErrors` and `a11yWarnings`: each accessibility finding has a\n     kind (contrast, target, overflow), a selector and often a cropped picture; fix the errors;\n   - a page that declares languages is rendered in each; read the overflow counts;\n   - the saved flows of the app are replayed in every state; a failed step has its picture;\n   - `checkDiff { a, b }` compares two checks picture by picture (changed share per size and state, a\n     red picture of the changes): use it after a change to show what moved.\n   Never release a bundle you have not looked at.\n4. **Test flows.** `flowSave { bundleId, flow: { name, steps } }` saves steps the checks replay:\n   `{ do: \"click\", target }`, `{ do: \"press\", value: \"Enter\", target? }`, `{ do: \"set\", prop, value }`,\n   `{ do: \"wait\", ms }` or `{ do: \"wait\", target }`, `{ do: \"expect\", target, text? }` or\n   `{ do: \"expect\", prop, value }`. `target` is a CSS selector that reaches into shadow roots with\n   ` >> ` (`p-segmented[prop=\"mode\"] >> button:has-text(\"Käsiohjaus\")`). At most 50 steps, 20 flows per\n   app. `flowList { bundleId }`, `flowDelete { id }`. The person can also record one on the PANEELI\n   screen (\"Tallenna käyttöpolku\").\n5. **Enrol the panel.** `enrolCreate { thingId, name }` gives an eight-character code valid for 30\n   minutes. The installer runs one command on the panel (Debian or Raspberry Pi OS):\n   `curl -fsSL https://paneeli.aimeat.io/install.sh | sudo sh -s -- --code <CODE> --thing http://<device address>/`.\n   It installs the agent and the kiosk; the agent spends the code once and pins the owner's signing key.\n   Add `--allow-screenshot` if the owner wants remote screenshots, and `--lan <address:port>` for LAN\n   mode (below).\n   **Raspberry Pi 4 or 5 with nothing typed on it:** make the code with `validHours: 168` and send the\n   person to the PANEELI screen (the code window, \"Raspberry Pi: käynnistystiedosto\"), which makes the\n   `user-data` file in their browser, Wi-Fi included. Then, easiest first:\n   - **The ready SD image** (link, SHA-256 and size on the same screen, and in Kirjasto, Ohjeet): write\n     `paneeli-rpi-trixie-arm64-<date>.img.xz` with Raspberry Pi Imager, *Use custom*, no\n     customisation; take the card out and put it back; copy `user-data` to the drive `bootfs`,\n     replacing the file of that name; power on. The panel installs from the card, so the first boot\n     needs the network only to enrol. It has run in an emulator, not yet on a real Raspberry Pi: say so.\n   - **Stock Raspberry Pi OS Lite** (64-bit, Trixie): the same `user-data` file; the first boot\n     downloads the software, and about 5 minutes later the interface is on screen.\n   **ESP32 alone, no Linux:** the ESP32-S3 standalone firmware serves the signed interface itself to a\n   phone or tablet on the same network and enrols with the same code (firmware/esp32-standalone).\n6. **Group the panels.** `groupSave { name, kind: \"site\" }` makes a group, such as a customer's hall;\n   `deviceMove { deviceIds, groupId }` moves panels in (`groupId: null` takes them out). A panel is in\n   one group at a time. `groupList` shows the groups and their panel counts. `groupBrand { id, brand }`\n   gives a group its own brand: the same interface released to its panels wears that look.\n   `groupSave { id, holdAgentUpdates: true }` holds the panel software updates of a group.\n7. **Release safely.**\n   - `releaseCreate { bundleId, thingId, canaryDeviceIds: [one panel], note }` sends it to one panel\n     first; read that panel's report (`deviceList { id }`), then `releaseStep { id, step: \"promote\" }`.\n     Something wrong: `releaseStep { id, step: \"rollback\", note }` puts the bundle before back, signed.\n   - To groups: `releaseCreate { bundleId, groupIds, thingId?, note }`; panels moved into the group\n     later get it too.\n   - A time window: `window: { from: \"2026-11-01T02:00\", to: \"2026-11-01T04:00\", tz: \"Europe/Helsinki\",\n     repeat: \"weekly\" }`; panels install only inside it and the release shows as scheduled until then.\n   - A performance budget per device type: `budget { thingId }` reads it, `budget { thingId, budget:\n     { minFps, maxBundleBytes, maxConsoleErrors, maxA11yErrors, profile } }` sets it, `{}` removes it.\n     `releaseGates { bundleId, thingId }` says before releasing whether the newest check meets it, and\n     why not. A release over the budget is refused (`OVER_BUDGET`, with `reasons`); release anyway only\n     when the person says why, with `override: { note: \"<their reason>\" }`. The note goes into the\n     record and the report.\n   - Releasing changes what real screens show: say what will change and on which panels, and get the\n     person's yes first, unless they asked you to release. You are recorded as the approver; with\n     `settings { approvals: 2 }` your release waits (`status: pending`) until the person approves it\n     (`releaseStep { id, step: \"approve\" }` by them, not by you).\n8. **Watch.** `overview` names the panels that are offline or behind, and the open alarms.\n   `deviceList { id }` gives a panel's last reports: frame rate, memory, whether the device answers,\n   its group, its panel software version and whether that is current, held or went back.\n   - **Alarms:** `alarmList` (by default the ones nobody acknowledged; `state: \"all\"`, `deviceId`,\n     `groupId`), `alarmAck { id, note }` or `alarmAck { ids, note }`. Alarms reach the owner's phone\n     within a minute: the PANEELI extension tells the AIMEAT bell, and the phone shows it when\n     notifications for aimeat.io are on there (Profile, Notifications). `settings { alarmNotify: \"all\" |\n     \"critical\" | \"off\" }` picks which. An owner other than the one who installed the extension allows\n     it once: offer this the first time they ask about alarms, and with their yes call\n     `aimeat_consent_grant { data_pattern: \"ext:paneeli\", recipient: \"ext:paneeli\", purpose:\n     \"extension_notify\" }`. The sentence they may say: \"Salli PANEELIn kertoa paneelieni hälytykset\n     minulle\" / \"Allow PANEELI to tell me my panels' alarms\".\n   - **What a panel shows now:** `screenshot { deviceId }`, then `screenshot { id }` until `status` is\n     `taken` (the panel answers on its next round, about 20 seconds) and open `png` (15 minutes). A\n     panel installed without `--allow-screenshot` refuses; say so instead of retrying.\n   - `agentRelease` is the panel software version the service publishes; `settings { agentUpdates:\n     \"hold\" }` holds every panel's updates.\n9. **Answer for it.** `releaseRecord { id }` is the record of one release: the signed manifest, the\n   key, who approved it (and any override with its reason), every component with its hash, and when\n   each panel installed it. `releaseSbom { id }` gives the CycloneDX 1.6 bill of materials;\n   `releaseReport { id }` gives a printable report link that works for 15 minutes. These help with the\n   EU Cyber Resilience Act; they do not by themselves make a product compliant.\n\n## Protocols: the form of each\n\nThe panel's bridge reaches the device through the form of each affordance; the page in the kiosk\nknows no protocol. None of these has run against a real device yet, only against simulators (Modbus\nRTU also through `stty` on a virtual serial line and CAN through can-utils on `vcan0`, on Linux): say\nso when the person relies on one.\n\n| Protocol | href (base + form) | Notes |\n|---|---|---|\n| HTTP | `http://<host>/properties/temperature` | W3C HTTP binding |\n| MQTT 3.1.1 | `mqtt://<broker>:1883` with `mqv:filter` or `mqv:topic` | W3C MQTT binding |\n| Modbus TCP | `modbus+tcp://<host>:502/<unit>/<address>?quantity=<n>` | `modv:entity`, `modv:type`, `modv:pollingTime` |\n| Modbus RTU | `modbus+rtu://ttyUSB0/<unit>/<address>?quantity=<n>` | the `modv:` terms, and the line as `rtu:baud` (19200), `rtu:parity` (even), `rtu:dataBits`, `rtu:stopBits`, the same on every form of one device file; context `https://paneeli.aimeat.io/td/modbus-rtu#`; the user `paneeli` needs the group `dialout` |\n| OPC UA | `opc.tcp://<host>:4840/?id=ns=1;s=Name` (also `i=`, `g=`, `b=`, `nsu=<uri>;s=`) | **No security mode only**: SecurityMode None, policy None, anonymous. A TD asking for Sign, SignAndEncrypt or a user token is refused before connecting. A method's object: `uav:componentOf` or `uav:objectId` |\n| BACnet/IP | `bacnet://<device instance>/<object type>,<instance>[/<property>]`, e.g. `bacnet://1234/0,1/85` | `bacv:hasDataType`, `commandPriority`, COV with `covIncrement`; device addresses in `bindings.bacnet.devices` or found by Who-Is; no segmentation, routing or BBMD |\n| CAN | `can://can0/0x101` | one signal per form: `can:byteOffset`, `can:length` or `can:bit`, `can:byteOrder`, `can:signed`, `can:float`, `can:scale`, `can:offset`; context `https://paneeli.aimeat.io/td/can#`; the interface must be up |\n| Bluetooth LE | `gatt://AA-BB-CC-DD-EE-FF/<service>/<characteristic>` (dashes in the address; 16-bit UUIDs allowed) | `gatt:format` (uint8 to float64, utf8, hex), `gatt:scale`, `gatt:offset`, `gatt:writeType`; context `https://paneeli.aimeat.io/td/gatt#`; the device paired or known to BlueZ |\n\nWhere a protocol has no events PANEELI reads, an event form names a value, and the event fires when\nit changes to something other than false or 0. The bridge checks every write against the TD before it\nleaves the panel. The PANEELI screen shows this list with one example each (Laitetyypit, and Kirjasto,\nOhjeet).\n\n## A USB stick: a panel without the network\n\n`releaseFile { id }` gives an **active** release as one `.paneeli` file: `{ name, size, base64 }` of a\ntar with the signed manifest and the bundle. `content: false` answers the name and size only: use it\nto check first, and fetch the file only when you can write it to disk for the person. The easier road\nfor a person is the release record on the PANEELI screen, \"Lataa USB-tikulle (.paneeli)\". Tell them\nthree things: copy the file to the root of a USB stick (or its `paneeli/` folder) and do not unpack\nit; put the stick in the panel, which installs it only if the signature matches the owner key it\npinned at enrolment, the release is meant for that panel and it is not older than the installed one;\nthe panel reports the result the next time it has network, and `deviceList { id }` shows it in the\nreports' `usb.events` (time, file, installed or refused with the reason). A pending release answers\n`NOT_APPROVED`, a canary `NOT_ACTIVE` (promote it, or release to the named panels), and a file over\n12 MB `TOO_LARGE`.\n\n## The market: share a template or a brand kit, take one into use\n\nA PANEELI template (an interface of the person's own) or a brand kit (name, logo, colours, theme) is an\nAIMEAT **package** with `category: \"paneeli\"` and one memory record, the listing, that the market card\nshows. Everybody on the node sees the market: Kirjasto, Kauppa on the PANEELI screen\n(https://paneeli.apps.aimeat.io/), or `aimeat_package_list { category: \"paneeli\" }`. Read one with\n`aimeat_package_get { group_id }`: its `listing` component holds the title, the description, up to\nthree screenshots, the device type it was made for and the price.\n\n**Share a brand kit.** `listingDraft { kind: \"brand\", title, description, groupId? }` (the person's\nsaved brand, or a group's), then publish its `package` as it is:\n`aimeat_invoke { capability: \"aimeat_package_publish\", input: <package> }`.\n\n**Share a template** (an interface the person published on the node). The app goes into its package\non the node, so its source never passes through the chat:\n1. `aimeat_package_compose { name: \"paneeli-pohja-<short name>-app\", apps: [\"<file>.html\"], category:\n   \"paneeli-content\", tags: [\"paneeli\", \"paneeli-template\", \"paneeli-content\"], visibility: \"public\",\n   include_cortex: false, include_skills: false, allow_expectations: true }`; take `group_id` from the answer.\n2. `listingDraft { appFile, contentGroup: <group_id>, thingId?, title, description, screenshots }`\n   (0 to 3 screenshots from the newest check; 0 keeps the answer small).\n3. `aimeat_invoke { capability: \"aimeat_package_publish\", input: <listingPackage> }`.\n\nThe PANEELI screen does the same in one press: Studio, an own interface, \"Julkaise pohjana\"; and\nAsetukset, Brändi, \"Julkaise brändipakettina\". Say what becomes public before you publish: every\naccount on the node sees it. When the answer carries `publish_request`, the package stays private until\nthe person approves it at its `approve_url`; give them that address.\n\n**A price: selling.** A priced listing (`price` in euros) puts the content in a private package\n`<name>-sisalto` (its group id is the listing's `contentGroup`); the public listing says \"Tulossa:\nmyynti\" (coming) until the maker opens the sale. The node sells it, not PANEELI:\n- **The maker opens the sale** with `aimeat_package_price_set { group_id: <contentGroup>, price: { amount,\n  currency } }` (EUR or USD in micro-units, 1 EUR = 1000000), or on the AIMEAT Packages page\n  (https://aimeat.io/v1/profile?tab=packages&package=<contentGroup>), where the PANEELI screen sends them.\n  It needs their own Stripe account connected; the money goes to it. `aimeat_package_sales` shows\n  the sales, and a refund is made there.\n- **A buyer pays on the AIMEAT Packages page** (the same address), or you buy it for them with\n  `aimeat_package_buy { group_id: <contentGroup> }` (first `action: \"offer\"` to show what it is and the\n  price, and buy only on their yes, within your daily purchase limit). Once paid, they install it as\n  below, and every later version too.\nA card's sale state is in `aimeat_package_get { group_id: <contentGroup> }`: `sale.state` on_sale or\npaused, and `entitled` true once bought.\n\n**Take one into use.** `aimeat_package_install { group_id: <the listing's contentGroup, or the\npackage itself> }` gives the person their own editable copy.\n- A template brings an app. Without the permission `packages:install-code` the install waits for\n  the person (`status: \"awaiting_owner\"`): tell them to approve it under Notifications\n  (https://aimeat.io/v1/profile?tab=notifications). Then build its bundle: `bundleBuild { appFile: <the\n  app component's registeredAs> }`, and it is in Käyttöliittymät, ready for the Studio.\n- A brand kit writes one memory record. Use it with `brand { brand: <the kit> }` (it replaces the\n  person's brand, so ask first) or `groupBrand { id, brand: <the kit> }` for one group.\n  The kit is the `kit` component's entry value `{ name, logo, tokens, theme }`.\n\nInstalling twice makes two copies; read `aimeat_package_instances` first. Removing an install\n(`DELETE /v1/instances/{id}`) leaves its memory records; the brand stays as it was set.\n\n## LAN mode: a screen beside the box\n\nA Linux box without a screen (a Siemens IOT2050, a Raspberry Pi in a cabinet) can serve the interface\nto a web panel or tablet on the machine network. The installer adds `--kiosk none --lan\n192.168.0.20:8091` (a static address of the box; `--lan-tls` for https with a self-signed\ncertificate). At the screen the person enters `http://192.168.0.20:8091/`; the screen shows a six-digit\ncode.\n- `lanStatus { deviceId }` gives the address, the TLS fingerprint if any, the screens waiting with the\n  code each one shows, and the paired screens.\n- `lanPair { deviceId, code, label? }` approves a screen. Ask the person to read the code on the screen\n  and approve only that code; the panel pairs it on its next round (up to about 40 seconds).\n- `lanUnpair { deviceId, browserId }` removes a paired screen; it goes back to the pairing page.\n- A Siemens Unified Comfort Panel: the Web control's URL is the address. If it asks for a code at every\n  start, the box pairs it by its IP address at the console (`sudo paneeli-agent pair --address <IP>`),\n  which only the box can do. A WAGO Touch Panel 600: the address in Favorites as the start page.\n- LAN mode ran on a PC, not yet on a device or a web panel: say so. Plain http belongs on a closed\n  machine network only.\n\n## From a picture to a running screen\n\nThe person pastes a picture into the chat (a sketch, a photo of a whiteboard, a screenshot of another\nproduct) and says what the device is. Do the whole thing yourself with these tools:\n1. Read the picture: which values it shows, which it sets, which commands and alarms it has.\n2. Write the Thing Description from that and the person's words; `thingRegister`. Ask only what the\n   picture cannot tell (units, ranges, the device's address).\n3. Write the interface with the components, matching the picture's layout, and publish it as an app\n   with `aimeat_app_publish`.\n4. `bundleBuild`, `check`, open the screenshots and compare them with the picture; repeat until they\n   match. Then show the person the screenshots and ask before releasing.\nThe brand (`brand`) is added at build time, so the same interface wears each customer's look.\n\n## \"Osoita ja muuta\": a sentence from the Studio\n\nOn the PANEELI screen the person can point at an element in the preview and copy a sentence for you.\nIt has this shape:\n\n> Muuta PANEELI-käyttöliittymässäni `<file>` kohtaa `<selector>` (`<markup>`): `<wish>`. Julkaise uusi\n> versio, rakenna paketti, aja tarkistus ja näytä kuva.\n\n(in English: \"In my PANEELI interface `<file>`, change `<selector>` (`<markup>`): `<wish>`. Publish a\nnew version, build a bundle, run the check and show me the picture.\") Read the app's source\n(`aimeat_app_get`), find the element by the selector and the markup, change only that element, publish\nwith `aimeat_app_publish`, then `bundleBuild`, `check`, and show the screenshots of the changed spot,\nwith `checkDiff` against the check before the change.\n\n## Actions\n\n| Action | Input | What it does |\n|---|---|---|\n| `overview` | none | Counts, newest releases, panels offline or behind, open alarms |\n| `thingRegister` | `td` | Store and check a Thing Description (a device type) |\n| `thingList` | `id?` | Device types; with id, one and its TD |\n| `thingDelete` | `id` | Refused while a panel uses it |\n| `enrolCreate` | `thingId`, `name`, `validHours?` (0.5 to 168) | One-time code for one panel |\n| `deviceList` | `id?`, `groupId?` | Panels; with id, the last 20 reports |\n| `deviceRemove` | `id` | The panel's token stops working at once |\n| `deviceMove` | `deviceIds`, `groupId` (null for none) | Move panels into a group or out |\n| `groupList` | `id?` | Groups and sites; with id, one and its panels |\n| `groupSave` | `id?`, `name`, `kind?`, `note?`, `holdAgentUpdates?` | Make or change a group |\n| `groupDelete` | `id` | Its panels stay, in no group |\n| `groupBrand` | `id`, `brand` or `remove: true` | A group's own brand |\n| `bundleBuild` | `appFile`, `appOwner?` | Offline bundle from an app on the node |\n| `bundleList` | `id?`, `html?` | Bundles; with id and html, the bundle itself |\n| `check` | `bundleId` (start; `profile?`, `sizes?`, `states?`; with `list: true` its checks) or `id` (read) | Screenshots, frame rate and device estimate, errors, accessibility, languages, flows |\n| `checkDiff` | `a`, `b`, `threshold?` | Two checks compared picture by picture |\n| `profileList` | none | Device profiles for a check |\n| `profileSave` | `id`, `sizes`, `cpuFactor`, `name?`, `dpr?`, `factorSource?`, `touch?` | Your own profile |\n| `profileDelete` | `id` | Remove your own profile |\n| `flowSave` | `bundleId` or `appOwner` and `appFile`, `flow` | Save a test flow the checks replay |\n| `flowList` | `id?`, `bundleId?`, `appOwner?`, `appFile?` | Saved flows |\n| `flowDelete` | `id` | Later checks no longer replay it |\n| `budget` | `thingId`, `budget?` | Read or set a device type's performance budget |\n| `releaseGates` | `bundleId`, `thingId` or `deviceIds` | Would a release meet the budget, and why not |\n| `releaseCreate` | `bundleId`, `thingId` or `groupIds` or `deviceIds`, `canaryDeviceIds?`, `window?`, `note?`, `override?` | Sign and send |\n| `releaseStep` | `id`, `step` (promote, rollback, approve), `note?` | Widen, go back, approve |\n| `releaseList` | none | Releases and their installs |\n| `releaseRecord` | `id` | The release record |\n| `releaseSbom` | `id` | CycloneDX 1.6 bill of materials |\n| `releaseReport` | `id` | Printable report link (15 minutes) |\n| `releaseFile` | `id`, `content?` | An active release as a `.paneeli` file for a USB stick |\n| `listingDraft` | `kind?`, `appFile` or `groupId?`, `contentGroup?`, `thingId?`, `screenshots?`, `includeBrand?`, `title`, `description`, `price?` | The market listing and package bodies of a template or a brand kit |\n| `alarmList` | `state?`, `deviceId?`, `groupId?`, `limit?` | Alarms, newest first |\n| `alarmAck` | `id` or `ids`, `note?` | Acknowledge with what was done |\n| `screenshot` | `deviceId` (ask) or `id` (read) | What a panel shows now |\n| `lanStatus` | `deviceId` | LAN address, screens waiting with their codes, paired screens |\n| `lanPair` | `deviceId`, `code`, `label?` | Approve a screen by the code it shows |\n| `lanUnpair` | `deviceId`, `browserId` | Remove a paired screen |\n| `agentRelease` | none | The panel software version the service publishes |\n| `brand` | `brand?` | Read or set name, logo, tokens (`--p-accent`, `--p-radius`, `--p-density`...), theme |\n| `settings` | `approvals?`, `alarmNotify?`, `agentUpdates?` | Two-person approval, alarms to the phone, panel updates |\n| `signingKey` | none | The owner's Ed25519 public key |\n\nThe action names keep the service's words: `thing*` is a device type, `device*` is a panel. The\nextension also has `alarmDeliver`, which the node runs every minute; do not call it.\n\n## When something does not work\n\nWhen an action fails in a way its error does not explain, a panel does not behave as its record\nsays, or the person reports a fault, send it to the people who run the node: `aimeat_dm_send\n{ to: \"support@operators\", subject: \"PANEELI: <what failed>\", body }`. Say what you were doing, the\naction and its input (without secrets), what happened instead, and the panel or release involved.\nThe answer comes in the same conversation. A person without an AI at hand uses \"Kirjoita\nylläpidolle\" in PANEELI, which opens the same message on AIMEAT. Tell the person that you sent it\nand that the operators will answer there.\n\n## Speak to the person in their words\n\nSay \"paneeli\" for the screen computer, \"laite\" for what it controls, \"laitetyyppi\" for its\ndescription, \"käyttöliittymä\", \"julkaisu\", \"liittymiskoodi\", \"ryhmä\", \"hälytys\", \"käyttöpolku\",\n\"suorituskykyraja\", \"lähiverkon näyttö\", \"USB-tikku\", \"levykuva\", \"protokolla\", \"kauppa\", \"pohja\",\n\"brändipaketti\", \"ottaa käyttöön\" (panel, device, device type, interface, release, enrolment code,\ngroup, alarm, flow, performance budget, screen on the local network, USB stick, SD image, protocol,\nmarket, template, brand kit, take into use). The ids (thg_, dev_, bnd_,\nrel_, grp_, alm_, flw_, chk_, brw_) are for your calls, not for the person, unless they ask. A frame\nrate from a device profile is an estimate; say so.\n"},"versions":[{"version":"1.0.0","publishedAt":"2026-10-30T13:06:39.517Z"},{"version":"1.0.1","publishedAt":"2026-10-30T14:42:11.444Z"},{"version":"1.0.2","publishedAt":"2026-10-30T15:44:02.638Z"},{"version":"1.0.3","publishedAt":"2026-10-31T00:12:27.796Z"},{"version":"1.0.4","publishedAt":"2026-10-31T01:39:33.394Z"},{"version":"1.0.5","publishedAt":"2026-10-31T12:49:41.963Z"},{"version":"1.0.6","publishedAt":"2026-10-31T15:31:21.677Z"},{"version":"1.0.7","publishedAt":"2026-10-31T18:09:08.366Z"}]}}}