Conversation
…devices Adds a right-click 'Remote Proxy' menu item that opens a popup mini-browser (protocol/address/port bar + iframe) which reaches a device's web UI on the agent's network, proxied through the agent by the backend.
Popup terminal (connection bar + xterm.js) that opens an SSH or Telnet session to a device on the agent's network, bridged through the backend over the agent.
Merge the separate Remote Terminal view into Remote Proxy. One menu item opens one window where the protocol dropdown (HTTPS/HTTP/SSH/Telnet) switches between the web iframe (http/https) and the xterm terminal (ssh/telnet).
New 'Network Devices' tab in the dashboard (next to Servers/Workstations/Mixed) showing a per-scope devices table with Add/Edit/Delete and a Connect action that opens the unified Remote Proxy popup via the resolved agent. Add/Edit dialog includes name/protocol/ip/port/description and an ordered preferred-agent picker (up/down reorder).
When adding a device while on a Site, that site is preselected; while on a Client, the client's first site is preselected. Only on All Clients must the user choose a site.
Previously the popup ran on the frontend domain while the proxied device loaded from the API domain, making it a cross-site (third-party) iframe. Browsers drop third-party cookies, so apps that set an auth cookie (Proxmox/PBS, pfSense) logged in then immediately kicked the user out. Load the iframe from the popup's own origin (also served at /agentproxy/ on the frontend vhost) so the device gets first-party cookies and sessions persist.
|
Pi Agent seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account. You have signed the CLA already but the status is still pending? Let us recheck it. |
The dashboard tab is bound to defaultAgentTblTab, so selecting the Network Devices tab sets it to netdevice. A tree (client/site) selection then dispatches refreshDashboard -> loadAgents, which appended monitoring_type=netdevice to GET /agents/. The agents API only accepts server/workstation, so it returned 400 (monitoring type does not exist). Network Devices are served by the /netdevices/ endpoint and the agents table is not rendered on that tab, so skip the /agents/ query entirely when defaultAgentTblTab is netdevice.
Adds a VNC protocol to the unified Remote Proxy window. Connects to a device's VNC server on the agent's LAN by reusing MeshCentral's bundled noVNC viewer (relayed through the agent), so no VNC client is bundled. fetchWebProxyVNCUrl calls the backend WebVNC endpoint with the device address; the viewer URL is shown in the same iframe used for HTTP/HTTPS.
Summary of what this PR delivers (web UI)Frontend for the Remote Proxy and Network Devices features (backend: amidaware/tacticalrmm#2485). Remote Proxy window (
|
… bulk AI commands, role-based model access, dynamic helpdesk ticketing - Device chat popup (PiChat.vue) with tool approval cards, steering, resume - AI History tab per device; AI Tasks tab + overview; Bulk AI Commands - Global Settings -> Pi.dev AI: providers, models, defaults, toggles - Helpdesk Ticket Policy: admin-defined prompt + generic authenticated ticketing API tool (helpdesk_api_request); API key substituted server-side, never exposed to the model - Role-based access: can_use_ai, model allow-lists, mutate/autoapprove rights
…s code box) below providers/models
…olicy + helpdesk.js, apply-to-box)
… review dialog with Machine column
…hen a client/site is selected; remove right-click popup
New schedule kind in AIReportSchedules.vue with its own options (all-teams, "AI writes a review per technician", "AI audits the data each run"), plus sendTechProductivityNow() in api/core.ts for an ad-hoc run. The options caption states plainly that every figure is computed in code and the AI cannot change any of them - the audit only flags what should not be concluded.
…the window
The Global Settings dialog is sized `min-width: 60vw` with no maximum, so any
child wider than the viewport drags the card out past the edge of the browser
window - the frame ends up bigger than the popup and the right-hand side becomes
unreachable. The scheduled-reports table was that child: it carries report names,
the report-type caption under each one, recipient lists and the whole "last
result" sentence ("sent to chris@..., fred@... - 5 techs, 323 closed, 344h 44m
tech time, 939 calls ..."), and none of it was allowed to wrap, so the table
demanded the width. Adding a third report type with a long label made it obvious.
- The table gets `wrap-cells` and per-column min/max widths, so long text wraps
instead of setting the table width, and the "last result" sentence wraps too.
- It sits in a `max-width: 100%; overflow-x: auto` box, so even an unbreakable
token scrolls inside the table rather than moving the dialog.
- The dialog itself is capped at `max-width: 95vw`. That is the general fix: no
future tab can push the frame outside the window either.
Paste a log extract, a command, an indented block or a list into the device chat or the ai-decision window and it looked right in the textarea, then collapsed into one unreadable paragraph the instant you hit enter. The assistant's bubble renders through an inner `.pi-text`, which carries `white-space: pre-wrap`. The user's own bubble did not, so the browser collapsed every run of whitespace and dropped the newlines. Same for the system/info line. Both now use `.pi-text`. Display only: send() does `input.value.trim()`, which strips the ends and leaves internal newlines alone, and the same string goes to the bubble and to the websocket - so the model was always receiving the formatting correctly. Only the transcript you were reading was wrong. Both surfaces are this one component: /ai-decision/:token routes to PiChat too.
Search for a model that does not exist in Global Settings > Pi.dev AI > add model and the search box was gone for good - you were left with the manual "type it yourself" input and could not get back without closing and reopening the dialog. The select's own existence was bound to the FILTERED list: <q-select v-if="availableForProvider.length > 0" ... @filter="filterAvailable"> availableForProvider applies the type-ahead filter, so a term matching nothing emptied it, which flipped the v-if false and unmounted the select mid-search, swapping in the v-else manual input. The filter string survived the unmount, so the computed stayed empty and the select could never re-mount. It also meant the #no-option slot ("No models found for this key") was unreachable - the component was destroyed before it could ever render that message. The select now keys off providerHasModels - whether this provider's key exposes ANY models at all, before filtering - so a fruitless search leaves it mounted and simply shows the empty-result message, which now distinguishes "nothing matches your search" (naming the term, and how many models are there to see) from "this key exposes no models". The filter is also cleared when the provider changes or the dialog is opened, so a stale term cannot hide the next provider's models.
Twelve modified files, 831 insertions, that existed only in the working tree of this box. Missed in this morning's sync, which covered the API and the bridge but not the web repo -- so the AI user interface was the one part of the stack still unbacked. Contents: PiChat.vue and the AI history/tasks tabs, the AI settings and report-schedule modals, AIProcedures and AITicketConsole views, aiCompletionAlerts, RolesForm (AI permissions), FileBar, and the api/agents + api/core clients they call. Committed on top of 0.101.59, which is what this checkout is. Note the deployed frontend is 0.101.64, a prebuilt bundle downloaded by the updater, so this source tree is BEHIND what is actually serving. Reconciling that is a separate step and should not be conflated with getting these files into git.
The brand is now src/branding/ -- product name, both logos, the square logo for PDFs,
the favicon master and the Quasar palette -- and upstream files reference it.
The first version of this did the obvious thing and edited upstream in place:
quasar.variables.sass got our colours, MainLayout.vue and LoginView.vue got our name
inline, and src/assets/{logo,trmm_256}.png plus public/favicon.ico were OVERWRITTEN.
That works and it is the wrong shape. Every one of those files belongs to amidaware, so
every future release becomes a merge conflict about our logo, and an overwritten binary
asset is a change git cannot show meaningfully -- the upstream file is simply gone. Those
three assets are restored to upstream's versions here and ours live in the module.
Footprint in upstream files is now 28 lines across five files: a sass @import, two
component imports, two element swaps, productName, and the favicon copy.
Colours came from the compiled stylesheet on www.blueuc.com rather than being matched by
eye -- primary #00C4FF, dark #1B1319. $secondary is the darker cyan #007294 the site uses
for hover states, because Quasar's default teal made every button darken AWAY from the
brand when hovered.
Two logos, deliberately: the toolbar is dark and the login card and generated PDFs are
light, so a single logo is invisible in one place or the other.
Status colours are left as Quasar's defaults on purpose. Red means broken and green means
fine in every other tool a technician has open, and a house style is not worth making an
alert ambiguous.
public/favicon.ico is a copy rather than an import because Quasar copies public/ verbatim
and does not resolve imports there; `npm run brand:sync` refreshes it from the module so
the master stays in one place.
…toring Reporting I broke Reporting. Branding was applied at source level and the resulting build was deployed over /var/www/rmm/dist, which discarded the official bundle -- and the official bundle is the only thing that has Reporting in it. The public source ships src/boot/integrations.ts with empty arrays, so a source build has no Reporting Manager and falls back to the "requires a Tier 2 or higher sponsorship" notice. The Tier 2 sponsorship was never the problem: the token is valid and in use, and `manage.py get_webtar_url` authenticates with it to fetch a bundle that does contain the EE reporting UI. My build simply lacked it. The backend was never gated at all -- ee/reporting/permissions.py checks ordinary role permissions, and the database still held 20 report templates and 11 HTML templates throughout. Fix: the official webtar is deployed, and branding is applied ON TOP as a post-build pass (src/branding/apply-to-dist.py) -- product name, palette, logo and favicon, with images matched by CONTENT because the bundler renames assets to content hashes. Deliberately NOT done: populating integrations.ts to switch Reporting on from source. That would be working around the licence-key mechanism the EE licence explicitly prohibits, and it would misrepresent what the sponsorship covers. If a feature is not in the bundle Amidaware served, this tooling leaves it absent. The script must be re-run after every update.sh, since the updater replaces dist wholesale. It warns when pointed at a bundle with no Reporting UI, because that means a source build was deployed and branding would otherwise hide the real fault behind a cosmetic success.
Network Devices, PiChat, AI Ticket Console and AI Procedures disappeared because I deployed Amidaware's official webtar to get the EE Reporting UI back. That bundle is their build and knows nothing about our views -- they exist only in this repo. Straight regression, caused this morning, on a tab used daily. Rebuilt from source and redeployed, so the custom views are back and the branding is back with them. Reporting is absent again, which is what was asked for once the trade-off was visible. Worth recording that Reporting was NOT in the Aug 4 build either: it had our views and no reporting UI, so choosing this side loses nothing that was working. Report data is untouched regardless -- 20 templates and 6 AI report schedules sit in the database and the schedules run server-side. What I did NOT do, again: register EE reporting by hand in src/boot/integrations.ts. The EE licence prohibits working around that mechanism, and a valid Tier 2 token does not change what the licence says about the method. README now states which bundle is deployed, the trade-off table, and the exact deploy steps -- including `env-config.js`, which the build does not produce and which I dropped earlier today, leaving the frontend with no API URL.
The old favicon was the website logo scaled down. That logo is WHITE on transparent, so
in a light tab strip it was very nearly invisible -- which is also the likely reason the
login-card logo "disappeared": it was rendering, in white, on a white card. Both problems
have the same root cause and neither was a missing file.
Now generated rather than resized (src/branding/make-favicon.py):
* baby blue discs, #7DDFFF with a #00AEE3 rim, so there is a solid high-contrast
silhouette on light AND dark browser themes, and a defined edge against white;
* three overlapping discs, echoing the cloud built from arcs in the website mark rather
than a shape invented for the icon;
* a white T drawn from RECTANGLES, not set in a font -- at 16px a glyph turns to mush
while a geometric T stays legible. Chunky proportions for the same reason;
* drawn at 512px and downsampled, which antialiases far better than drawing at 16;
* six sizes in the ICO, 16 through 256.
Verified at tab size before shipping, not after: rendered to a 22x22 ASCII preview to
confirm the T still reads and sits inside the circles rather than hanging off the bottom,
which the first attempt did.
Also replaced the hashed copy the bundler emitted (50b17c72.ico), not just the one at the
dist root -- otherwise the browser keeps serving the old icon from the hashed path.
…yeballed
The login-card logo was white on a white card. It was rendering perfectly and could not
be seen, which is why it read as "you removed the logo". The toolbar had the same fault
waiting: with $primary now brand cyan, a white mark on it measures 2.03:1, under the 3:1
WCAG minimum for large graphics.
Contrast ratios, computed not guessed:
white on cyan toolbar 2.03:1 FAIL
white on white card 1.00:1 FAIL <- the reported bug
dark on cyan toolbar 8.96:1 pass
dark on white card 18.20:1 pass
white on dark card 16.67:1 pass
So both surfaces follow one rule: dark mode gets the white mark, light mode gets the
near-black one. #1B1319 rather than plain black, since that is the brand's own dark
(--o-color-5 on the website) and keeps the mark on-brand on a light surface.
logo-dark.png and logo-cyan.png are generated from the white original by keeping the
alpha channel and replacing RGB. The source mark is pure white on transparent, so its
shape lives entirely in alpha -- no thresholding, no halo, antialiased edges intact.
Source builds switch with $q.dark.isActive. The dist post-processor does the same for the
official bundle via .body--dark, so both paths behave identically instead of drifting.
Replaces the white T on baby-blue circles. Blue field, black B, so it is identifiable as
ours at a glance in a tab strip.
* solid brand-cyan #00C4FF rounded square rather than a circle -- it fills more of the
tile at 16px, so the icon is easier to pick out among other tabs;
* B set in DejaVu Sans Bold rather than drawn from primitives. The previous T was drawn
from rectangles because a T is trivial and stays crisp; a B is not, and curves and
counters built from primitives look wrong long before they look crisp;
* glyph sized by measurement, growing until it fills ~60% of the tile width, so it lands
the same way regardless of the font's own metrics rather than depending on a guessed
point size;
* black is #1B1319, the brand's near-black, not plain #000. On brand cyan that measures
8.96:1, well past the 3:1 WCAG minimum for large graphics;
* drawn at 512px and downsampled, six sizes in the ICO.
Checked at tab size before shipping by rendering a 22x22 ASCII preview, the same way the
T was checked -- the counters stay open and the B is still a B.
The hashed copy the bundler emits (f6f730a1.ico) was replaced along with the root one.
Only doing the root leaves the browser serving the old icon from the hashed path.
A toggle beside Auto-approve, shown only when the role carries can_use_ai_autocredential, that lets Pi use a stored IT Notebook login without stopping to ask for each retrieval. Deliberately its own switch rather than a side effect of Auto-approve: approving a device change and handing over a live password are different decisions, and the person making them should see them separately. The server stays authoritative - autocredential_state comes back from the socket, so a role without the permission sees the switch snap back rather than being told something untrue. Remembered per user like Auto-approve, so a refresh does not silently revert it. Privileged rows still prompt every time, and the chat says so when the switch goes on.
A box at the top of the chat window for the technician's own name for the conversation, and a sortable Label column in AI History placed ahead of Summary - because that is the column you actually scan when hunting for a past session, and the last-message snippet rarely tells you what the work was. Saved on blur/Enter rather than per keystroke, so typing does not chatter over the socket, and only when the value actually changed (blur fires on every click away). The label is also mirrored into the window title, so a desktop full of Pi popups becomes navigable. Task/bulk/scheduled rows have nobody to type a label and render a dash.
Toolbar button in Pi Chat and AI Decision (shown only when the server says the role, the global switch and a configured relay all allow it), a QR pairing dialog, and the Remote (mobile) section in Global Settings -> Pi.dev AI plus the role checkbox. Three states, not two: 'Remote' / 'Remote: waiting' / 'Remote: <device>'. On and 'a phone is actually attached' are different facts, and conflating them is how someone walks away believing they are reachable when nothing is listening. Every state change comes from a remote_state frame - the button never assumes. Messages typed on the phone appear in this transcript tagged with the device, and an approval answered from the phone clears the on-screen banner. Two surfaces driving one session must not be able to hide turns from each other. Relay URL validation mirrors the extension: http(s) only, ws(s) refused with a targeted message rather than silently coerced.
…es the rule The switch was decision-window only because Pi Chat had no credential path at all. It has one now (same permission, same gate, same audit line), so the toggle is no longer gated on isDecision. The tooltip said privileged rows "still ask you every time", which is no longer the whole truth and was the sentence that made the feature look broken: it asks when the AI wants a privileged row on its own initiative, and stops asking once you have asked for the admin login yourself in the chat.
… the phone can see The bridge now owns the window's switches as typed commands so a paired phone can reach them (the Remote Pi app renders a chat and nothing else). This is the browser half. - A slash-command menu above the input, fed by `ready.commands` - the SERVER decides what is offered, so the list can never present a switch this role does not carry as if it would work. A refused command is still listed, greyed with a lock and the reason: hiding it makes a permission look like a missing feature and the technician goes hunting for a bug. - Up/Down to move, Tab or Enter to complete, Esc to dismiss, click to pick. The badge shows live ON/OFF from the toolbar refs, not the server's snapshot, so it cannot go stale after a toggle. - `system_note` renders in the transcript. That is how a switch flipped ON THE PHONE appears at the desk - two people driving one machine on different assumptions about Write mode is the failure this prevents. - A window command answers in a millisecond and never reaches the model, so it no longer raises the streaming spinner (with `system_note` clearing it as a backstop for an aliased spelling). - Dropped the local system lines for autocredential_state and allow_email_state: the bridge narrates those once now, for both surfaces. - The Remote pairing dialog's first click works, so the placeholder can finally advertise "/ for commands". FIX: `allow_email_state` printed "Switched to READ-ONLY mode" / "Switched to WRITE mode" - a copy-paste from the readonly branch. Toggling customer email told you the opposite thing about a different switch entirely.
The bridge decides which model a window opens on - it reopens on whatever the conversation was last using, which is not necessarily the global default the session endpoint handed the browser. The picker was set from the mint response only, so it sat on the default while the session ran on something else. It now takes the model from the ready frame, and puts a line in the transcript when the model was remembered, or when the chat's last model is not available to this role and the default was used instead.
The ready frame now carries switches_restored and switches_denied. Both are put in the transcript: a restored Write mode is worth knowing, and a DENIED one is worth knowing more - a technician who thinks Write mode is still on will not understand why their next instruction is refused.
UI half of work running in production since 2026-08-26, never committed. - PiChat.vue: every switch and session action moves into a ☰ Options menu; the bar keeps only what you read at a glance (label, cost, model, alerts, connection). Agent group / model picker as one "Model / group" select. "Summarize & clear history" with a confirm dialog. Reopens on what this person last picked. - AISettings.vue + api/core.ts: manage AI agent groups (orchestrator + specialists). - AIAgentGroups.vue: the editor dialog.
…to the tooltip
Backend half is tacticalrmm a4e512aa ("the cost meter is per conversation, not per
device"). The chip now starts at $0.00 in a new chat instead of showing every dollar
every chat had spent on the machine. The lifetime figure is not lost: the tooltip
gains "This device, all chats: $X - N turns" (or "This ticket" on the ticket chat),
driven by window_scope / window_cost / window_turns on cost_update. null = the server
could not read the ledger, so the line is omitted rather than showing a wrong $0.00.
A "Queue" button in the bar opens a panel to the right of the chat (over it on a narrow window) with every queue control: Auto-Next, add (with "Compact & clear first"), Run next, Pause/Resume, Clear done/all, and the list - edit (double-click or pencil, Ctrl+Enter saves), move up/down, toggle compact-first, skip / queue again, delete. Pending count on the button; colour says running / paused / idle. The bridge owns the state (queue_state frames, tacticalrmm 8934f2e9); everything here is a request to it. A queued prompt shows as the tech's own bubble tagged "from the queue". A new pause is written into the transcript once, with the reason; when it is the assistant asking, the panel opens so the question is in view. Only the panel's open/closed state lives in this browser (localStorage).
Waiting items (orange) show the assistant's question and an answer box; the exchange is kept on the item as a thread. Replies show in the transcript as "answered in the queue".
Drag the grip to widen or narrow it (300px up to the window minus 360px of chat; anything when floating). Double-click resets to 400px. Width is remembered in this browser like open/closed.
The action row had four tiny buttons fighting for 400px, with "Clear all" as small red text on dark grey. Now: destructive things (Clear finished / Clear everything) and Reset width live in a header menu at normal size; Run next and Pause/Resume are two full-width buttons; the two switches share one row with short labels (detail in tooltips); one status line says what the queue is doing; per-item tools are move up/down plus one menu (Edit, Compact first, Skip/Queue again, Remove). Helper paragraphs cut to a sentence, thread turns labelled ASKED / YOU, status icon has a plain-English tooltip.
The Answer box under a waiting item had a labelled button below it that was easy to miss; now it looks like the main chat input: textarea with a round send button beside it, Enter sends, Shift+Enter is a newline. Same shape for the Add box (round +).
- Questions from the assistant are cards at the top of the panel (asked text, which item it is about, answer box with a send triangle, dismiss). Items no longer carry an answer box; a waiting item points up. An item's past Q&A is folded behind "show N Q&A". - History button opens a dialog listing everything the queue did, newest first, with the answers given and where; Clear history with confirm. Never sent to the AI. - The panel opens by itself when a chat comes up with anything queued, and whenever a new question arrives. Closing it by hand sticks for that page load.
Remote Proxy & Network Devices (frontend)
Companion UI for the backend PR in
amidaware/tacticalrmm.Reuses the existing
@xterm/xtermdependency. Developed against v0.101.59 (master); happy to rebase ontodevelop(maintainer_can_modifyenabled).