mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
feat(pikvm): let the owner skip the confirmation of power actions and keys
power-action and send-key are now approvalOverridable. The add-on page has an "Ask before acting" section with Ask each time (default) or Don't ask for each of them, with a plain warning that a chat can then switch the machine off at once. type-text has no such choice and always asks. The connection row lists which tools run without asking. The page strings are translated to Italian and the shared connector pages show the same list in the access view and the access preview. Assisted-by: Claude:claude-sonnet-5-5
This commit is contained in:
36 files changed
+943
-113
No files matched your search
@@ -5,6 +5,7 @@ import { useCallback, useEffect, useState } from 'react'
|
||||
import { Notice } from './controls'
|
||||
import { connectorsApi } from './api'
|
||||
import { pageLocale } from './connector'
|
||||
import ApprovalsView from './ApprovalsView'
|
||||
|
||||
export default function AccessView({ binding, capabilities, s, version }) {
|
||||
const [view, setView] = useState(null)
|
||||
@@ -27,6 +28,7 @@ export default function AccessView({ binding, capabilities, s, version }) {
|
||||
<button type="button" className="dk-btn dk-btn--secondary dk-btn--sm" aria-busy={loading ? 'true' : undefined} disabled={loading} onClick={load}>{s('access.refresh')}</button>
|
||||
</div>
|
||||
{view && (<>
|
||||
<ApprovalsView approvals={view.approvals} s={s} testId="connector-access-approvals" />
|
||||
<p className="dk-hint">{s('access.recentHelp')}</p>
|
||||
{(view.recent || []).length === 0 ? <p className="cn-muted">{s('access.recentNone')}</p> : (
|
||||
<ul className="cn-runs" aria-label={s('access.recentTitle')} data-testid="connector-audit">
|
||||
@@ -37,7 +39,7 @@ export default function AccessView({ binding, capabilities, s, version }) {
|
||||
<span className="cn-muted">{new Date(e.at).toLocaleString(pageLocale(), { dateStyle: 'short', timeStyle: 'medium' })}</span>
|
||||
{(e.senders || []).length > 0 && <span className="cn-muted cn-mono">{s('access.who', { senders: e.senders.join(', ') })}</span>}
|
||||
{(e.roles || []).length > 0 && <span className="cn-muted">{s('access.roles', { roles: e.roles.join(', ') })}</span>}
|
||||
{e.via && <span className="cn-muted">{s('access.via', { via: e.via })}</span>}
|
||||
{e.via && <span className="cn-muted">{e.via === 'no-confirmation' ? s('access.viaNoConfirm') : s('access.via', { via: e.via })}</span>}
|
||||
{e.reason && <span className="cn-muted">{e.reason}</span>}
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/* eslint-disable no-unused-vars -- components used only inside JSX look unused to this config, which has no eslint-plugin-react */
|
||||
// The tools of the add-on whose confirmation the owner can relax, and who they
|
||||
// run for without asking right now. It draws what the host reports in the
|
||||
// access view (saved or preview); the owner's choice itself is made on the
|
||||
// settings page of the add-on, never here.
|
||||
import { approvalState } from './approvals.js'
|
||||
|
||||
export default function ApprovalsView({ approvals, s, sender = '', testId = 'connector-approvals' }) {
|
||||
const list = Array.isArray(approvals) ? approvals : []
|
||||
if (list.length === 0) return null
|
||||
return (
|
||||
<div className="cn-approvals" data-testid={testId}>
|
||||
<h4 className="cn-approvals__title">{s('approvals.title')}</h4>
|
||||
<p className="dk-hint">{s('approvals.help')}</p>
|
||||
<ul className="cn-approvals__list" aria-label={s('approvals.title')}>
|
||||
{list.map((a) => {
|
||||
const state = approvalState(a, sender)
|
||||
return (
|
||||
<li key={a.tool} className="cn-approvals__row" data-testid={`connector-approval-${a.tool}`} data-state={state}>
|
||||
<code className="cn-approvals__tool">{a.tool}</code>
|
||||
<span className={`cn-approvals__state cn-approvals__state--${state}`}>{s(`approvals.state.${state}`)}</span>
|
||||
<span className="cn-muted">{s(`approvals.line.${state}`)}</span>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import Icon from '../../host-compat/src/components/Icon'
|
||||
import { basePath } from '../../host-compat/src/utils/basePath.js'
|
||||
import { FixNotice, Select } from './controls'
|
||||
import { connectorsApi } from './api'
|
||||
import ApprovalsView from './ApprovalsView'
|
||||
import { fixAction, remainingChecks, sentenceCase, fixLabelKey, pickSubject, previewBody, previewId, previewKey, senderOptions } from './access'
|
||||
|
||||
const FLASH_MS = 1600
|
||||
@@ -152,6 +153,7 @@ export default function CapabilitiesPanel({ form, kindInfo, capabilities, lists,
|
||||
<ul className="cn-can__list" aria-label={s('can.title')} data-dim={loading ? 'true' : undefined}>
|
||||
{decisions.map((d) => <Row key={d.capability} d={d} title={title} s={s} ctx={ctx} />)}
|
||||
</ul>
|
||||
<ApprovalsView approvals={preview.view.approvals} s={s} sender={selected} testId="connector-can-approvals" />
|
||||
</>)}
|
||||
</fieldset>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// The model of the approvals list. No React, so a node test drives it.
|
||||
|
||||
// The state of one tool as the host reported it:
|
||||
// "asks" the owner did not relax it: every call waits for a card;
|
||||
// "runs" it runs without asking for the owner and for paired senders;
|
||||
// "idle" the owner relaxed it, but the write mode of the connection is Read
|
||||
// only, so nothing runs at all.
|
||||
// "other" the owner relaxed it, but the kind of sender shown here is not the
|
||||
// owner or a paired sender, so this sender is still asked.
|
||||
// sender is the kind of sender the view is for ("listed", "anyone", "paired",
|
||||
// "role:<id>"), or empty for the owner's own view.
|
||||
export function approvalState(a, sender = '') {
|
||||
if (!a || a.skip !== true) return 'asks'
|
||||
const who = Array.isArray(a.runs_without_asking) ? a.runs_without_asking : []
|
||||
if (who.length === 0) return 'idle'
|
||||
// Only the owner and a paired sender skip the card; the host reports both
|
||||
// together, so a paired sender is the one kind of sender that is not asked.
|
||||
if (sender && !who.includes(sender)) return 'other'
|
||||
return 'runs'
|
||||
}
|
||||
|
||||
// The tools that run without asking, in the order the host gave them.
|
||||
export const runningUnasked = (list) => (Array.isArray(list) ? list.filter((a) => approvalState(a) === 'runs').map((a) => a.tool) : [])
|
||||
@@ -248,3 +248,11 @@ fieldset.cn-group {
|
||||
/* The host kit writes " (optional)" in English after a label marked data-optional.
|
||||
The page puts the word of its language in the attribute, and this draws it. */
|
||||
.cn-page .dk-label[data-optional]::after { content: " (" attr(data-optional) ")"; }
|
||||
|
||||
/* The tools whose confirmation the owner can relax. */
|
||||
.cn-approvals { display: flex; flex-direction: column; gap: var(--dk-space-2); margin-top: var(--dk-space-3); }
|
||||
.cn-approvals__title { margin: 0; font-size: var(--fs-body); font-weight: var(--font-weight-semibold); }
|
||||
.cn-approvals__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--dk-space-2); }
|
||||
.cn-approvals__row { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--dk-space-2); min-width: 0; overflow-wrap: anywhere; }
|
||||
.cn-approvals__state { font-size: var(--fs-meta); padding: 0 var(--dk-space-2); border-radius: var(--r-sm); background: var(--color-bg-secondary); color: var(--color-text-secondary); }
|
||||
.cn-approvals__state--runs { color: var(--dk-warn-text, var(--color-text-secondary)); background: var(--dk-warn-wash, var(--color-bg-secondary)); }
|
||||
@@ -286,5 +286,16 @@
|
||||
"sender.line_other": "{{name}} · {{count}} Nachrichten · {{id}}",
|
||||
"ready.blocker.nobodyLetIn": "Noch ist niemand zugelassen.",
|
||||
"ready.blocker.nobodyPaired": "Dafür braucht es eine mit dem Bot gekoppelte Person, und noch ist niemand gekoppelt.",
|
||||
"optional": "optional"
|
||||
"optional": "optional",
|
||||
"approvals.title": "Runs without asking",
|
||||
"approvals.help": "Some tools of this add-on can run without a confirmation card, but only when the owner chose that on the page of the add-on. Every other tool and every other sender is still asked.",
|
||||
"approvals.state.asks": "Asks every time",
|
||||
"approvals.state.runs": "Runs without asking",
|
||||
"approvals.state.idle": "Set to run without asking, does nothing now",
|
||||
"approvals.state.other": "Still asks this sender",
|
||||
"approvals.line.asks": "A card waits for the owner's OK.",
|
||||
"approvals.line.runs": "For the owner's own chats and for senders paired with the bot. Everyone else is still asked.",
|
||||
"approvals.line.idle": "The write mode of this connection is Read only, so no write tool runs.",
|
||||
"approvals.line.other": "Only the owner and a paired sender skip the card. This kind of sender is asked.",
|
||||
"access.viaNoConfirm": "ran without asking"
|
||||
}
|
||||
@@ -286,5 +286,16 @@
|
||||
"sender.line_other": "{{name}} · {{count}} messages · {{id}}",
|
||||
"ready.blocker.nobodyLetIn": "Nobody is let in yet.",
|
||||
"ready.blocker.nobodyPaired": "This needs a person you paired with the bot, and nobody is paired yet.",
|
||||
"optional": "optional"
|
||||
"optional": "optional",
|
||||
"approvals.title": "Runs without asking",
|
||||
"approvals.help": "Some tools of this add-on can run without a confirmation card, but only when the owner chose that on the page of the add-on. Every other tool and every other sender is still asked.",
|
||||
"approvals.state.asks": "Asks every time",
|
||||
"approvals.state.runs": "Runs without asking",
|
||||
"approvals.state.idle": "Set to run without asking, does nothing now",
|
||||
"approvals.state.other": "Still asks this sender",
|
||||
"approvals.line.asks": "A card waits for the owner's OK.",
|
||||
"approvals.line.runs": "For the owner's own chats and for senders paired with the bot. Everyone else is still asked.",
|
||||
"approvals.line.idle": "The write mode of this connection is Read only, so no write tool runs.",
|
||||
"approvals.line.other": "Only the owner and a paired sender skip the card. This kind of sender is asked.",
|
||||
"access.viaNoConfirm": "ran without asking"
|
||||
}
|
||||
@@ -286,5 +286,16 @@
|
||||
"sender.line_other": "{{name}} · {{count}} mensajes · {{id}}",
|
||||
"ready.blocker.nobodyLetIn": "Aún no se deja entrar a nadie.",
|
||||
"ready.blocker.nobodyPaired": "Esto necesita una persona emparejada con el bot, y aún no hay nadie emparejado.",
|
||||
"optional": "opcional"
|
||||
"optional": "opcional",
|
||||
"approvals.title": "Runs without asking",
|
||||
"approvals.help": "Some tools of this add-on can run without a confirmation card, but only when the owner chose that on the page of the add-on. Every other tool and every other sender is still asked.",
|
||||
"approvals.state.asks": "Asks every time",
|
||||
"approvals.state.runs": "Runs without asking",
|
||||
"approvals.state.idle": "Set to run without asking, does nothing now",
|
||||
"approvals.state.other": "Still asks this sender",
|
||||
"approvals.line.asks": "A card waits for the owner's OK.",
|
||||
"approvals.line.runs": "For the owner's own chats and for senders paired with the bot. Everyone else is still asked.",
|
||||
"approvals.line.idle": "The write mode of this connection is Read only, so no write tool runs.",
|
||||
"approvals.line.other": "Only the owner and a paired sender skip the card. This kind of sender is asked.",
|
||||
"access.viaNoConfirm": "ran without asking"
|
||||
}
|
||||
@@ -286,5 +286,16 @@
|
||||
"sender.line_other": "{{name}} · {{count}} messages · {{id}}",
|
||||
"ready.blocker.nobodyLetIn": "Personne n’est encore admis.",
|
||||
"ready.blocker.nobodyPaired": "Il faut une personne associée au bot, et personne n’est encore associé.",
|
||||
"optional": "facultatif"
|
||||
"optional": "facultatif",
|
||||
"approvals.title": "Runs without asking",
|
||||
"approvals.help": "Some tools of this add-on can run without a confirmation card, but only when the owner chose that on the page of the add-on. Every other tool and every other sender is still asked.",
|
||||
"approvals.state.asks": "Asks every time",
|
||||
"approvals.state.runs": "Runs without asking",
|
||||
"approvals.state.idle": "Set to run without asking, does nothing now",
|
||||
"approvals.state.other": "Still asks this sender",
|
||||
"approvals.line.asks": "A card waits for the owner's OK.",
|
||||
"approvals.line.runs": "For the owner's own chats and for senders paired with the bot. Everyone else is still asked.",
|
||||
"approvals.line.idle": "The write mode of this connection is Read only, so no write tool runs.",
|
||||
"approvals.line.other": "Only the owner and a paired sender skip the card. This kind of sender is asked.",
|
||||
"access.viaNoConfirm": "ran without asking"
|
||||
}
|
||||
@@ -286,5 +286,16 @@
|
||||
"sender.line_other": "{{name}} · {{count}} pesan · {{id}}",
|
||||
"ready.blocker.nobodyLetIn": "Belum ada yang diizinkan masuk.",
|
||||
"ready.blocker.nobodyPaired": "Ini memerlukan orang yang Anda pasangkan dengan bot, dan belum ada yang dipasangkan.",
|
||||
"optional": "opsional"
|
||||
"optional": "opsional",
|
||||
"approvals.title": "Runs without asking",
|
||||
"approvals.help": "Some tools of this add-on can run without a confirmation card, but only when the owner chose that on the page of the add-on. Every other tool and every other sender is still asked.",
|
||||
"approvals.state.asks": "Asks every time",
|
||||
"approvals.state.runs": "Runs without asking",
|
||||
"approvals.state.idle": "Set to run without asking, does nothing now",
|
||||
"approvals.state.other": "Still asks this sender",
|
||||
"approvals.line.asks": "A card waits for the owner's OK.",
|
||||
"approvals.line.runs": "For the owner's own chats and for senders paired with the bot. Everyone else is still asked.",
|
||||
"approvals.line.idle": "The write mode of this connection is Read only, so no write tool runs.",
|
||||
"approvals.line.other": "Only the owner and a paired sender skip the card. This kind of sender is asked.",
|
||||
"access.viaNoConfirm": "ran without asking"
|
||||
}
|
||||
@@ -286,5 +286,16 @@
|
||||
"sender.line_other": "{{name}} · {{count}} messaggi · {{id}}",
|
||||
"ready.blocker.nobodyLetIn": "Nessuno è ancora ammesso.",
|
||||
"ready.blocker.nobodyPaired": "Serve una persona abbinata al bot, e nessuno è ancora abbinato.",
|
||||
"optional": "facoltativo"
|
||||
"optional": "facoltativo",
|
||||
"approvals.title": "Parte senza chiedere",
|
||||
"approvals.help": "Alcuni strumenti di questo add-on possono partire senza una scheda di conferma, ma solo se il proprietario lo ha scelto nella pagina dell'add-on. Ogni altro strumento e ogni altro mittente viene comunque interpellato.",
|
||||
"approvals.state.asks": "Chiede ogni volta",
|
||||
"approvals.state.runs": "Parte senza chiedere",
|
||||
"approvals.state.idle": "Impostato per partire senza chiedere, ora non fa nulla",
|
||||
"approvals.state.other": "Chiede comunque a questo mittente",
|
||||
"approvals.line.asks": "Una scheda attende il consenso del proprietario.",
|
||||
"approvals.line.runs": "Per le chat del proprietario e per i mittenti abbinati al bot. Tutti gli altri vengono comunque interpellati.",
|
||||
"approvals.line.idle": "La modalità di scrittura di questa connessione è Sola lettura, quindi nessuno strumento di scrittura parte.",
|
||||
"approvals.line.other": "Solo il proprietario e un mittente abbinato saltano la scheda. Questo tipo di mittente viene interpellato.",
|
||||
"access.viaNoConfirm": "eseguito senza chiedere"
|
||||
}
|
||||
@@ -286,5 +286,16 @@
|
||||
"sender.line_other": "{{name}} · 메시지 {{count}}개 · {{id}}",
|
||||
"ready.blocker.nobodyLetIn": "아직 아무도 허용되지 않았습니다.",
|
||||
"ready.blocker.nobodyPaired": "봇과 페어링한 사람이 필요한데, 아직 페어링된 사람이 없습니다.",
|
||||
"optional": "선택"
|
||||
"optional": "선택",
|
||||
"approvals.title": "Runs without asking",
|
||||
"approvals.help": "Some tools of this add-on can run without a confirmation card, but only when the owner chose that on the page of the add-on. Every other tool and every other sender is still asked.",
|
||||
"approvals.state.asks": "Asks every time",
|
||||
"approvals.state.runs": "Runs without asking",
|
||||
"approvals.state.idle": "Set to run without asking, does nothing now",
|
||||
"approvals.state.other": "Still asks this sender",
|
||||
"approvals.line.asks": "A card waits for the owner's OK.",
|
||||
"approvals.line.runs": "For the owner's own chats and for senders paired with the bot. Everyone else is still asked.",
|
||||
"approvals.line.idle": "The write mode of this connection is Read only, so no write tool runs.",
|
||||
"approvals.line.other": "Only the owner and a paired sender skip the card. This kind of sender is asked.",
|
||||
"access.viaNoConfirm": "ran without asking"
|
||||
}
|
||||
@@ -286,5 +286,16 @@
|
||||
"sender.line_other": "{{name}} · {{count}} mensagens · {{id}}",
|
||||
"ready.blocker.nobodyLetIn": "Ninguém foi liberado ainda.",
|
||||
"ready.blocker.nobodyPaired": "Isto exige uma pessoa pareada com o bot, e ninguém está pareado ainda.",
|
||||
"optional": "opcional"
|
||||
"optional": "opcional",
|
||||
"approvals.title": "Runs without asking",
|
||||
"approvals.help": "Some tools of this add-on can run without a confirmation card, but only when the owner chose that on the page of the add-on. Every other tool and every other sender is still asked.",
|
||||
"approvals.state.asks": "Asks every time",
|
||||
"approvals.state.runs": "Runs without asking",
|
||||
"approvals.state.idle": "Set to run without asking, does nothing now",
|
||||
"approvals.state.other": "Still asks this sender",
|
||||
"approvals.line.asks": "A card waits for the owner's OK.",
|
||||
"approvals.line.runs": "For the owner's own chats and for senders paired with the bot. Everyone else is still asked.",
|
||||
"approvals.line.idle": "The write mode of this connection is Read only, so no write tool runs.",
|
||||
"approvals.line.other": "Only the owner and a paired sender skip the card. This kind of sender is asked.",
|
||||
"access.viaNoConfirm": "ran without asking"
|
||||
}
|
||||
@@ -286,5 +286,16 @@
|
||||
"sender.line_other": "{{name}} · {{count}} 条消息 · {{id}}",
|
||||
"ready.blocker.nobodyLetIn": "目前没有人被放行。",
|
||||
"ready.blocker.nobodyPaired": "这需要一个与机器人配对过的人,而目前没有人配对。",
|
||||
"optional": "可选"
|
||||
"optional": "可选",
|
||||
"approvals.title": "Runs without asking",
|
||||
"approvals.help": "Some tools of this add-on can run without a confirmation card, but only when the owner chose that on the page of the add-on. Every other tool and every other sender is still asked.",
|
||||
"approvals.state.asks": "Asks every time",
|
||||
"approvals.state.runs": "Runs without asking",
|
||||
"approvals.state.idle": "Set to run without asking, does nothing now",
|
||||
"approvals.state.other": "Still asks this sender",
|
||||
"approvals.line.asks": "A card waits for the owner's OK.",
|
||||
"approvals.line.runs": "For the owner's own chats and for senders paired with the bot. Everyone else is still asked.",
|
||||
"approvals.line.idle": "The write mode of this connection is Read only, so no write tool runs.",
|
||||
"approvals.line.other": "Only the owner and a paired sender skip the card. This kind of sender is asked.",
|
||||
"access.viaNoConfirm": "ran without asking"
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
//
|
||||
// What the device returns is data. Every result says so. The write tools have
|
||||
// physical effects: all of them wait for a person's confirmation on every
|
||||
// call, and the typing and hard power actions are off until an administrator
|
||||
// call (the owner can relax that for power-action and send-key, in the host), and the typing and hard power actions are off until an administrator
|
||||
// switches them on for the device (package settings).
|
||||
package tools
|
||||
|
||||
@@ -48,7 +48,8 @@ const (
|
||||
// ReadTools lists the tools any chat may call to read.
|
||||
func ReadTools() []string { return []string{ToolGetInfo, ToolPowerState, ToolKeyboard} }
|
||||
|
||||
// WriteTools lists the write tools. Every one needs a confirmation each time.
|
||||
// WriteTools lists the write tools. Each one needs a confirmation each time,
|
||||
// unless the owner relaxed it for a tool the manifest marks overridable.
|
||||
func WriteTools() []string { return []string{ToolPowerAction, ToolTypeText, ToolSendKey} }
|
||||
|
||||
// PageTools lists the tools only the add-on's page calls.
|
||||
|
||||
@@ -20,7 +20,10 @@ import (
|
||||
|
||||
// The three write tools have physical effects on a real machine. The manifest
|
||||
// declares approval "always" for each: a person confirms every call, whatever
|
||||
// the write mode of the connection is. The kvmd API has no idempotency key, so
|
||||
// the write mode of the connection is. power-action and send-key are also
|
||||
// approvalOverridable: the owner can choose "Don't ask" for them, and only the
|
||||
// host honors that, for the owner's own chat and for paired senders. type-text
|
||||
// is never overridable. The kvmd API has no idempotency key, so
|
||||
// the add-on notes an operation before it sends it and never sends one twice.
|
||||
|
||||
const (
|
||||
|
||||
+17
-11
@@ -92,9 +92,10 @@
|
||||
},
|
||||
{
|
||||
"name": "power-action",
|
||||
"description": "Press a power button of the machine the PiKVM controls: on, off (short press, soft power off) or reset. off_hard, reset_hard and long_press force the machine and are refused unless an administrator allowed hard power actions for this device. A person confirms every call and sees the power state. Do not say it was done until the result says so.",
|
||||
"description": "Press a power button of the machine the PiKVM controls: on, off (short press, soft power off) or reset. off_hard, reset_hard and long_press force the machine and are refused unless an administrator allowed hard power actions for this device. A person confirms each call and sees the power state, unless the owner chose to skip that for this tool. Do not say it was done until the result says so.",
|
||||
"kind": "write",
|
||||
"approval": "always",
|
||||
"approvalOverridable": true,
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -152,9 +153,10 @@
|
||||
},
|
||||
{
|
||||
"name": "send-key",
|
||||
"description": "Press single keys or the chords ctrl+alt+delete, ctrl+c and ctrl+z on the machine the PiKVM controls, in order. Allowed keys: a-z, 0-9, up, down, left, right, enter, escape, tab, f1 to f12. Nothing else, no Windows or Meta chords. Off until an administrator switches on typing for this device. A person confirms every call.",
|
||||
"description": "Press single keys or the chords ctrl+alt+delete, ctrl+c and ctrl+z on the machine the PiKVM controls, in order. Allowed keys: a-z, 0-9, up, down, left, right, enter, escape, tab, f1 to f12. Nothing else, no Windows or Meta chords. Off until an administrator switches on typing for this device. A person confirms each call, unless the owner chose to skip that for this tool.",
|
||||
"kind": "write",
|
||||
"approval": "always",
|
||||
"approvalOverridable": true,
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -227,6 +229,10 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"userSettings": [
|
||||
{ "key": "approval-power-action", "values": ["ask", "skip"], "default": "ask" },
|
||||
{ "key": "approval-send-key", "values": ["ask", "skip"], "default": "ask" }
|
||||
],
|
||||
"credentials": [
|
||||
{
|
||||
"id": "credential",
|
||||
@@ -301,15 +307,15 @@
|
||||
"id": "PiKVM"
|
||||
},
|
||||
"description": {
|
||||
"en": "Let your chats and agents check a PiKVM and, if you allow it, press power buttons and type on the machine it controls, with a confirmation every time.",
|
||||
"it": "Lascia che le tue chat e i tuoi agenti controllino un PiKVM e, se lo permetti, premano i pulsanti di accensione e scrivano sulla macchina che governa, con una conferma ogni volta.",
|
||||
"de": "Lassen Sie Ihre Chats und Agenten einen PiKVM prüfen und, wenn Sie es erlauben, Einschalttasten drücken und auf dem gesteuerten Rechner tippen, jedes Mal mit einer Bestätigung.",
|
||||
"es": "Permite que tus chats y agentes revisen un PiKVM y, si lo autorizas, pulsen los botones de encendido y escriban en la máquina que controla, con una confirmación cada vez.",
|
||||
"fr": "Laissez vos conversations et vos agents consulter un PiKVM et, si vous l'autorisez, appuyer sur les boutons d'alimentation et taper sur la machine qu'il pilote, avec une confirmation à chaque fois.",
|
||||
"pt-BR": "Deixe seus chats e agentes verificarem um PiKVM e, se você permitir, pressionarem os botões de energia e digitarem na máquina que ele controla, com uma confirmação a cada vez.",
|
||||
"zh-CN": "让你的聊天和智能体查看 PiKVM,并在你允许时按下电源按钮、在它所控制的机器上输入内容,每次都需要确认。",
|
||||
"ko": "채팅과 에이전트가 PiKVM을 확인하고, 허용하면 전원 버튼을 누르고 PiKVM이 제어하는 기기에 입력할 수 있게 합니다. 매번 확인을 거칩니다.",
|
||||
"id": "Izinkan obrolan dan agen Anda memeriksa PiKVM dan, jika Anda mengizinkan, menekan tombol daya dan mengetik di mesin yang dikendalikannya, dengan konfirmasi setiap kali."
|
||||
"en": "Let your chats and agents check a PiKVM and, if you allow it, press power buttons and type on the machine it controls, asking you to confirm each time unless you say otherwise.",
|
||||
"it": "Lascia che le tue chat e i tuoi agenti controllino un PiKVM e, se lo permetti, premano i pulsanti di accensione e scrivano sulla macchina che governa, chiedendo conferma ogni volta, salvo che tu decida diversamente.",
|
||||
"de": "Lassen Sie Ihre Chats und Agenten einen PiKVM prüfen und, wenn Sie es erlauben, Einschalttasten drücken und auf dem gesteuerten Rechner tippen, mit einer Bestätigung bei jedem Mal, sofern Sie nichts anderes festlegen.",
|
||||
"es": "Permite que tus chats y agentes revisen un PiKVM y, si lo autorizas, pulsen los botones de encendido y escriban en la máquina que controla, pidiendo confirmación cada vez, salvo que decidas otra cosa.",
|
||||
"fr": "Laissez vos conversations et vos agents consulter un PiKVM et, si vous l'autorisez, appuyer sur les boutons d'alimentation et taper sur la machine qu'il pilote, en demandant une confirmation à chaque fois, sauf si vous en décidez autrement.",
|
||||
"pt-BR": "Deixe seus chats e agentes verificarem um PiKVM e, se você permitir, pressionarem os botões de energia e digitarem na máquina que ele controla, pedindo confirmação a cada vez, a menos que você decida o contrário.",
|
||||
"zh-CN": "让你的聊天和智能体查看 PiKVM,并在你允许时按下电源按钮、在它所控制的机器上输入内容,每次都需要确认,除非你另行设置。",
|
||||
"ko": "채팅과 에이전트가 PiKVM을 확인하고, 허용하면 전원 버튼을 누르고 PiKVM이 제어하는 기기에 입력할 수 있게 합니다. 별도로 설정하지 않으면 매번 확인을 거칩니다.",
|
||||
"id": "Izinkan obrolan dan agen Anda memeriksa PiKVM dan, jika Anda mengizinkan, menekan tombol daya dan mengetik di mesin yang dikendalikannya, dengan meminta konfirmasi setiap kali kecuali Anda menentukan lain."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -27,7 +27,7 @@ Before `power-action`, `type-text` or `send-key`:
|
||||
|
||||
1. Ask the person, unless they have just told you to do exactly this. A general wish ("fix the server") is not a request to press a button.
|
||||
2. Say in one line what will happen, with the real effect: "Short press of the power button on the test box. It is on now, so the operating system will probably shut it down." For text, show the exact text. For keys, list them in order.
|
||||
3. Call the tool. A person then confirms in a card that shows the same facts. Every call is confirmed, whatever the connection's write mode is.
|
||||
3. Call the tool. A person then confirms in a card that shows the same facts. The connection's write mode does not change that. The one exception is an owner who chose "Don't ask" for `power-action` or `send-key` on the PiKVM page: if the result says the action was done, no card was shown, so say plainly what you did. `type-text` is always confirmed.
|
||||
|
||||
Choose the gentlest action that does the job: `off` before `off_hard`, `reset` before `reset_hard`. The hard actions and `long_press` are refused unless an administrator switched them on for the device on the PiKVM page. If refused, tell the person; do not try another route to the same effect.
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/* eslint-disable no-unused-vars -- components used only inside JSX look unused to this config, which has no eslint-plugin-react */
|
||||
// What the owner is asked before a chat acts on a PiKVM. Power actions and key
|
||||
// presses can be set to run without a card; typing text cannot. The answer is
|
||||
// the signed-in person's own (a user setting kept by the host, written from
|
||||
// the person's browser session only), so no chat, agent or model can change it.
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { getSdk } from '../../../internal/host-compat/src/runtime.js'
|
||||
import { Notice, RadioCards } from '../../../internal/connector-ui/src/controls'
|
||||
import { ASK, CHOICES, TOOLS, approvalsChanged, declared, keyOf, valuesOf } from './approval'
|
||||
|
||||
function Tool({ tool, value, busy, onChoose, t }) {
|
||||
return (
|
||||
<fieldset className="pk-fieldset" data-testid={`pikvm-approval-${tool}`} disabled={busy}>
|
||||
<legend className="pk-approval__legend">{t(`approval.tool.${tool}`)}</legend>
|
||||
<RadioCards name={`approval-${tool}`} legend={t(`approval.tool.${tool}`)} value={value} onChange={(v) => onChoose(tool, v)}
|
||||
options={CHOICES.map((c) => ({
|
||||
value: c,
|
||||
label: (<span className="pk-modecard"><span className="pk-modecard__title">{t(`approval.${c}`)}</span><span className="pk-modecard__line">{t(`approval.line.${c}`)}</span></span>),
|
||||
}))} />
|
||||
<Notice tone="warn" role="note" testId={`pikvm-approval-warn-${tool}`}>
|
||||
<strong>{t('approval.warnTitle')}</strong> {t(`approval.warn.${tool}`)}
|
||||
</Notice>
|
||||
</fieldset>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ApprovalSettings() {
|
||||
const { t } = useTranslation('pikvm')
|
||||
const [state, setState] = useState({ phase: 'loading', values: {}, declared: true })
|
||||
const [busy, setBusy] = useState('')
|
||||
const [notice, setNotice] = useState('')
|
||||
const [failed, setFailed] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let live = true
|
||||
getSdk().userSettings.get().then((out) => {
|
||||
if (live) setState({ phase: 'ready', values: valuesOf(out), declared: declared(out) })
|
||||
}).catch(() => { if (live) setState({ phase: 'error', values: {}, declared: true }) })
|
||||
return () => { live = false }
|
||||
}, [])
|
||||
|
||||
const choose = useCallback((tool, value) => {
|
||||
if (busy || state.values[tool] === value) return
|
||||
setBusy(tool)
|
||||
setFailed(false)
|
||||
setNotice('')
|
||||
getSdk().userSettings.set(keyOf(tool), value).then((out) => {
|
||||
setState((s) => ({ ...s, values: valuesOf(out) }))
|
||||
setNotice(t('approval.saved'))
|
||||
approvalsChanged()
|
||||
}).catch(() => setFailed(true)).finally(() => setBusy(''))
|
||||
}, [busy, state.values, t])
|
||||
|
||||
if (state.phase === 'loading') return <p className="cn-muted" role="status" data-testid="pikvm-approval-loading">{t('approval.loading')}</p>
|
||||
if (state.phase === 'error') return <Notice tone="error" role="alert" testId="pikvm-approval-unreadable">{t('approval.loadFailed')}</Notice>
|
||||
if (!state.declared) return null
|
||||
return (
|
||||
<section className="pk-settings pk-approval" data-testid="pikvm-approval" aria-labelledby="pk-approval-title">
|
||||
<h2 className="pk-settings__title" id="pk-approval-title">{t('approval.title')}</h2>
|
||||
<p className="cn-muted">{t('approval.intro')}</p>
|
||||
{TOOLS.map((tool) => <Tool key={tool} tool={tool} value={state.values[tool] || ASK} busy={busy === tool} onChoose={choose} t={t} />)}
|
||||
<Notice tone="info" role="note" testId="pikvm-approval-text">{t('approval.textAlways')}</Notice>
|
||||
<p className="cn-muted" data-testid="pikvm-approval-who">{t('approval.who')}</p>
|
||||
<p className="cn-muted" data-testid="pikvm-approval-writemode">{t('approval.needsWrite')}</p>
|
||||
<p className="cn-muted" data-testid="pikvm-approval-audit">{t('approval.audit')}</p>
|
||||
<p className="cn-muted" role="status" aria-live="polite" data-testid="pikvm-approval-status">{notice}</p>
|
||||
{failed && <Notice tone="error" role="alert" testId="pikvm-approval-failed">{t('approval.saveFailed')}</Notice>}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import { getSdk } from '../../../internal/host-compat/src/runtime.js'
|
||||
import { registerLocales } from '../../../internal/host-compat/src/i18n.js'
|
||||
import Icon from '../../../internal/host-compat/src/components/Icon'
|
||||
import ConnectorPage from '../../../internal/connector-ui/src/ConnectorPage'
|
||||
import ApprovalsView from '../../../internal/connector-ui/src/ApprovalsView'
|
||||
import { answerT } from '../../../internal/connector-ui/src/AnswerFields'
|
||||
import { connectorsApi } from '../../../internal/connector-ui/src/api'
|
||||
import { Field, Notice, RadioCards, TextInput } from '../../../internal/connector-ui/src/controls'
|
||||
import { pageLocale } from '../../../internal/connector-ui/src/connector'
|
||||
@@ -18,6 +20,8 @@ import id from './i18n/pikvm.id.json'
|
||||
import ko from './i18n/pikvm.ko.json'
|
||||
import pt from './i18n/pikvm.pt-BR.json'
|
||||
import SettingsEditor from './SettingsEditor'
|
||||
import ApprovalSettings from './ApprovalSettings'
|
||||
import { approvalRevision, onApprovalsChanged } from './approval'
|
||||
import ProposalCard from './ProposalCard'
|
||||
import { addressProblem, checkFailure, checkItem, credentialProblem, extraBody, extraForm, extraValid, followName, needsPinHint, pikvmSecret, WRITE_MODES, writeModeOf } from './pikvm'
|
||||
import { finishTest, resultOf, startTest, subscribe } from './results'
|
||||
@@ -136,7 +140,22 @@ function Checks({ c, t }) {
|
||||
)
|
||||
}
|
||||
|
||||
// The effective view of a connection: which of its tools run without asking.
|
||||
// The host reports it; the choice is made in the settings below.
|
||||
function useApprovals(id, version) {
|
||||
const [approvals, setApprovals] = useState([])
|
||||
const [seen, setSeen] = useState(approvalRevision())
|
||||
useEffect(() => onApprovalsChanged(() => setSeen(approvalRevision())), [])
|
||||
useEffect(() => {
|
||||
let live = true
|
||||
connectorsApi.access(id).then((v) => { if (live) setApprovals(v?.approvals || []) }).catch(() => { if (live) setApprovals([]) })
|
||||
return () => { live = false }
|
||||
}, [id, version, seen])
|
||||
return approvals
|
||||
}
|
||||
|
||||
function RowInfo({ c, t }) {
|
||||
const approvals = useApprovals(c.id, c.write_mode)
|
||||
const address = (c.resources || [])[0]
|
||||
const shown = Boolean(resultOf(c.id)) && Boolean(address)
|
||||
const mode = writeModeOf(c.write_mode)
|
||||
@@ -144,6 +163,7 @@ function RowInfo({ c, t }) {
|
||||
<>
|
||||
{!shown && <span className="cn-muted" data-testid="pikvm-row-address">{address ? t('addressOf', { address: address.split('#')[0] }) : t('noAddress')}</span>}
|
||||
<span><span className={`pk-chip pk-chip--${mode}`} data-testid="pikvm-row-write">{t(`writeMode.row.${mode}`)}</span></span>
|
||||
<ApprovalsView approvals={approvals} s={answerT(t)} testId="pikvm-row-approvals" />
|
||||
<Checks c={c} t={t} />
|
||||
</>
|
||||
)
|
||||
@@ -181,6 +201,7 @@ export function createPikvm() {
|
||||
<ConnectorPage kind="pikvm" ns="pikvm" testId="pikvm-page" TokenFields={PikvmCredential} tokenOf={pikvmSecret} messaging={false}
|
||||
ExtraFields={PikvmFields} extraBody={extraBody} extraForm={extraForm} extraValid={extraValid} testExtra={testExtra}
|
||||
Help={Help} RowInfo={RowInfo} />
|
||||
<ApprovalSettings />
|
||||
<SettingsEditor />
|
||||
</RouteProvider>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// The model of the approval settings. No React, so a node test drives it. The
|
||||
// host keeps the answers (user settings of the add-on) and decides every
|
||||
// call; these helpers only name the keys and read an answer safely.
|
||||
|
||||
// The tools the owner may stop being asked for. type-text is not here on
|
||||
// purpose: it types arbitrary text, so the manifest gives it no such setting
|
||||
// and the host would not honor one.
|
||||
export const TOOLS = ['power-action', 'send-key']
|
||||
|
||||
export const ASK = 'ask'
|
||||
export const SKIP = 'skip'
|
||||
export const CHOICES = [ASK, SKIP]
|
||||
|
||||
// The key of the setting for a tool, as the manifest declares it.
|
||||
export const keyOf = (tool) => `approval-${tool}`
|
||||
|
||||
// What the host reports for the signed-in person: { tool: 'ask' | 'skip' }.
|
||||
// A tool the host does not report, or an answer it does not know, is ask.
|
||||
export function valuesOf(out) {
|
||||
const list = Array.isArray(out?.settings) ? out.settings : []
|
||||
const result = {}
|
||||
for (const tool of TOOLS) {
|
||||
const s = list.find((x) => x?.key === keyOf(tool))
|
||||
result[tool] = s?.value === SKIP ? SKIP : ASK
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Whether the host declares the settings at all. An older package declares
|
||||
// none, and the page then shows nothing about them.
|
||||
export const declared = (out) => TOOLS.some((tool) => (Array.isArray(out?.settings) ? out.settings : []).some((x) => x?.key === keyOf(tool)))
|
||||
|
||||
// The rows of the connections show which tools run without asking. They read
|
||||
// it from the host, so a save below must tell them to read it again.
|
||||
let revision = 0
|
||||
const listeners = new Set()
|
||||
export const approvalRevision = () => revision
|
||||
export function approvalsChanged() { revision += 1; for (const f of [...listeners]) f() }
|
||||
export function onApprovalsChanged(f) { listeners.add(f); return () => listeners.delete(f) }
|
||||
@@ -14,8 +14,8 @@
|
||||
"agentOf": "Agent: {{name}}",
|
||||
"inactive": "Läuft nicht: {{reason}}.",
|
||||
"title": "PiKVM",
|
||||
"intro": "Lass deine Chats und Agenten einen PiKVM und den Rechner prüfen, den er steuert. Wenn du es erlaubst, können sie auch seine Power-Tasten drücken und seine Tastatur nutzen, und eine Person bestätigt jede dieser Aktionen.",
|
||||
"limits": "Ein Agent, der an einem echten Rechner eine Power-Taste drücken oder tippen kann, kann Arbeit verlieren oder Befehle ausführen. Jede Aktion wartet auf deine Bestätigung. Tippen und die harten Power-Aktionen sind aus, bis ein Administrator sie unten einschaltet. Ein PiKVM kennt keine Nutzerrechte: Jeder Nutzer, den er kennt, kann alles, daher sind die Limits dieses Add-ons die einzigen.",
|
||||
"intro": "Lass deine Chats und Agenten einen PiKVM und den Rechner prüfen, den er steuert. Wenn du es erlaubst, können sie auch seine Power-Tasten drücken und seine Tastatur nutzen, und eine Person bestätigt diese Aktionen, sofern du unten nichts anderes einstellst.",
|
||||
"limits": "Ein Agent, der an einem echten Rechner eine Power-Taste drücken oder tippen kann, kann Arbeit verlieren oder Befehle ausführen. Standardmäßig wartet jede Aktion auf deine Bestätigung. Für Power-Aktionen und Tastendrücke kannst du das unten ausschalten, nie für getippten Text. Tippen und die harten Power-Aktionen sind aus, bis ein Administrator sie unten einschaltet. Ein PiKVM kennt keine Nutzerrechte: Jeder Nutzer, den er kennt, kann alles, daher sind die Limits dieses Add-ons die einzigen.",
|
||||
"new": "Neue PiKVM-Verbindung",
|
||||
"edit": "PiKVM-Verbindung bearbeiten",
|
||||
"user": "PiKVM-Benutzername",
|
||||
@@ -67,16 +67,16 @@
|
||||
},
|
||||
"writeMode": {
|
||||
"label": "Was der Agent mit diesem Gerät tun darf",
|
||||
"hint": "Den Zustand lesen ist immer erlaubt. Jede Aktion bestätigst du, egal was du wählst.",
|
||||
"hint": "Den Zustand lesen ist immer erlaubt. Jede Aktion bestätigst du, sofern du im Abschnitt unten für ein Werkzeug nichts anderes einstellst.",
|
||||
"off": "Nur lesen",
|
||||
"propose": "Fragt dich zuerst",
|
||||
"line": {
|
||||
"off": "Er kann nur lesen.",
|
||||
"propose": "Jede Aktion wartet auf dein OK."
|
||||
"propose": "Jede Aktion wartet auf dein OK, sofern du das nicht ausschaltest."
|
||||
},
|
||||
"note": {
|
||||
"off": "Schreibzugriff ist aus: Diese Verbindung liest nur den Zustand.",
|
||||
"propose": "Jede Power-Aktion, jeder Text und jeder Tastendruck wartet in deinen Freigaben, bis du ihn bestätigst. Tippen und harte Power-Aktionen brauchen zusätzlich den Schalter unten."
|
||||
"propose": "Jede Power-Aktion, jeder Text und jeder Tastendruck wartet in deinen Freigaben, bis du ihn bestätigst, sofern du das für dieses Werkzeug nicht ausgeschaltet hast. Tippen und harte Power-Aktionen brauchen zusätzlich den Schalter unten."
|
||||
},
|
||||
"row": {
|
||||
"off": "Nur lesen",
|
||||
@@ -90,17 +90,17 @@
|
||||
"saveFailed": "Die Schalter konnten nicht gespeichert werden.",
|
||||
"intro": "Diese Schalter gelten pro Gerät und für jeden Nutzer dieses Servers. Sie sind aus, bis ein Administrator sie ändert.",
|
||||
"physicalTitle": "Physische Wirkung.",
|
||||
"physical": "Eine Power-Aktion kann ungespeicherte Arbeit verlieren. Tippen und Tastendrücke gehen an das, was auf dem Rechner den Fokus hat: eine Shell, ein BIOS-Menü, ein Anmeldebildschirm. Eine Person bestätigt jede Aktion und sieht den genauen Text oder die genauen Tasten.",
|
||||
"physical": "Eine Power-Aktion kann ungespeicherte Arbeit verlieren. Tippen und Tastendrücke gehen an das, was auf dem Rechner den Fokus hat: eine Shell, ein BIOS-Menü, ein Anmeldebildschirm. Eine Person bestätigt jede Aktion und sieht den genauen Text oder die genauen Tasten, sofern du das für Power-Aktionen oder Tastendrücke nicht ausgeschaltet hast.",
|
||||
"readOnly": "Nur ein Administrator kann diese Schalter ändern.",
|
||||
"broken": "Die gespeicherten Schalter konnten nicht gelesen werden, daher gilt alles als aus. Speichere sie erneut, um sie zu reparieren.",
|
||||
"none": "Füge eine PiKVM-Verbindung hinzu, um ihre Schalter zu setzen.",
|
||||
"typing": "Tippen und Tastendrücke erlauben",
|
||||
"typingHint": "Erlaubt einem Chat, darum zu bitten, Text zu tippen und Tasten zu drücken. Jede Anfrage wartet weiterhin auf deine Bestätigung.",
|
||||
"typingHint": "Erlaubt einem Chat, darum zu bitten, Text zu tippen und Tasten zu drücken. Getippter Text wartet immer auf deine Bestätigung. Tastendrücke auch, sofern du das nicht ausgeschaltet hast.",
|
||||
"maxChars": "Längster Text",
|
||||
"maxCharsHint": "Zeichen in einem getippten Text. Standard {{default}}, höchstens 1000.",
|
||||
"maxCharsShape": "Gib eine ganze Zahl von 1 bis 1000 ein.",
|
||||
"hard": "Harte Power-Aktionen erlauben",
|
||||
"hardHint": "Erlaubt einem Chat, ein erzwungenes Ausschalten, einen Hardware-Reset oder ein langes Drücken der Power-Taste anzufordern. Jede Anfrage wartet weiterhin auf deine Bestätigung.",
|
||||
"hardHint": "Erlaubt einem Chat, ein erzwungenes Ausschalten, einen Hardware-Reset oder ein langes Drücken der Power-Taste anzufordern. Jede Anfrage wartet auf deine Bestätigung, sofern du das für Power-Aktionen nicht ausgeschaltet hast.",
|
||||
"save": "Schalter speichern",
|
||||
"saved": "Gespeichert. Der nächste Aufruf nutzt die neuen Schalter.",
|
||||
"neverTitle": "Nie verfügbar",
|
||||
@@ -129,6 +129,31 @@
|
||||
"counts": "{{chars}} Zeichen, {{lines}} Zeile(n)",
|
||||
"typeEffect": "Der Text wird auf der Tastatur des Rechners getippt, in das, was gerade den Fokus hat. Ein Zeilenumbruch drückt Enter.",
|
||||
"keys": "Tasten, der Reihe nach",
|
||||
"keysEffect": "Die Tasten werden in dieser Reihenfolge auf der Tastatur des Rechners gedrückt."
|
||||
"keysEffect": "Die Tasten werden in dieser Reihenfolge auf der Tastatur des Rechners gedrückt.",
|
||||
"title": "Ask before acting",
|
||||
"intro": "By default a power action, a key press or a typed text waits for your confirmation. For the two kinds of action below you can choose not to be asked. The choice is yours alone, it applies to all your PiKVM connections, and only this page can change it.",
|
||||
"tool": {
|
||||
"power-action": "Power actions (on, off, reset)",
|
||||
"send-key": "Key presses"
|
||||
},
|
||||
"ask": "Ask each time",
|
||||
"skip": "Don't ask",
|
||||
"line": {
|
||||
"ask": "A card waits for your OK.",
|
||||
"skip": "It runs at once."
|
||||
},
|
||||
"warnTitle": "Read this before you choose Don't ask.",
|
||||
"warn": {
|
||||
"power-action": "A chat can then switch this machine off or reset it at once, with no card to confirm, and unsaved work on it is lost. If an administrator allowed hard power actions, those run without asking too.",
|
||||
"send-key": "A chat can then press keys such as Ctrl+Alt+Delete, Escape or Enter on this machine at once, with no card to confirm."
|
||||
},
|
||||
"textAlways": "Typed text always asks. It can enter a password or a command, so it has no Don't ask.",
|
||||
"who": "Don't ask holds for your own chats and for messaging senders you paired with the bot. Everyone else is still asked: senders only on the allowlist, senders let in because the bot accepts anyone, scheduled runs and sub-agents.",
|
||||
"needsWrite": "A connection set to Read only runs nothing, whatever you choose here.",
|
||||
"audit": "Every run without asking is written to the log with the tool, the connection and the result. The typed text and the keys are not.",
|
||||
"loading": "Reading your choice...",
|
||||
"loadFailed": "Your choice could not be read. Reload the page to try again.",
|
||||
"saveFailed": "Your choice could not be saved. The earlier choice is still in force.",
|
||||
"saved": "Saved. The next call follows it."
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,8 @@
|
||||
"agentOf": "Agent: {{name}}",
|
||||
"inactive": "Not running: {{reason}}.",
|
||||
"title": "PiKVM",
|
||||
"intro": "Let your chats and agents check a PiKVM and the machine it controls. If you allow it, they can also press its power buttons and use its keyboard, and a person confirms every one of those actions.",
|
||||
"limits": "An agent that can press a power button or type on a real machine can lose work or run commands. Every action waits for your confirmation. Typing and the hard power actions are off until an administrator switches them on, below. A PiKVM has no user rights: any user it knows can do everything, so the limits of this add-on are the only ones.",
|
||||
"intro": "Let your chats and agents check a PiKVM and the machine it controls. If you allow it, they can also press its power buttons and use its keyboard, and a person confirms those actions unless you choose otherwise below.",
|
||||
"limits": "An agent that can press a power button or type on a real machine can lose work or run commands. By default every action waits for your confirmation. For power actions and key presses you can turn that off below, never for typed text. Typing and the hard power actions are off until an administrator switches them on, below. A PiKVM has no user rights: any user it knows can do everything, so the limits of this add-on are the only ones.",
|
||||
"new": "New PiKVM connection",
|
||||
"edit": "Edit PiKVM connection",
|
||||
"user": "PiKVM user name",
|
||||
@@ -67,16 +67,16 @@
|
||||
},
|
||||
"writeMode": {
|
||||
"label": "What the agent may do with this device",
|
||||
"hint": "Reading the state is always allowed. Every action is confirmed by you, whatever you choose.",
|
||||
"hint": "Reading the state is always allowed. You confirm every action, unless you choose Don't ask for a tool in the section below.",
|
||||
"off": "Read only",
|
||||
"propose": "Asks you first",
|
||||
"line": {
|
||||
"off": "It can only read.",
|
||||
"propose": "Each action waits for your OK."
|
||||
"propose": "Each action waits for your OK, unless you turn that off for it."
|
||||
},
|
||||
"note": {
|
||||
"off": "Write access is off: this connection only reads the state.",
|
||||
"propose": "Each power action, text and key press waits in your approvals until you confirm it. Typing and hard power actions also need the switch below."
|
||||
"propose": "Each power action, text and key press waits in your approvals until you confirm it, unless you chose Don't ask for that tool. Typing and hard power actions also need the switch below."
|
||||
},
|
||||
"row": {
|
||||
"off": "Read only",
|
||||
@@ -90,17 +90,17 @@
|
||||
"saveFailed": "The switches could not be saved.",
|
||||
"intro": "These switches are kept for each device and apply to every user of this server. They are off until an administrator changes them.",
|
||||
"physicalTitle": "Physical effects.",
|
||||
"physical": "A power action can lose unsaved work. Typing and key presses go to whatever has the focus on the machine: a shell, a BIOS menu, a login screen. A person confirms each one and sees the exact text or keys.",
|
||||
"physical": "A power action can lose unsaved work. Typing and key presses go to whatever has the focus on the machine: a shell, a BIOS menu, a login screen. A person confirms each one and sees the exact text or keys, unless you chose Don't ask for power actions or key presses.",
|
||||
"readOnly": "Only an administrator can change these switches.",
|
||||
"broken": "The saved switches could not be read, so everything is treated as off. Save them again to repair them.",
|
||||
"none": "Add a PiKVM connection to set its switches.",
|
||||
"typing": "Allow typing and key presses",
|
||||
"typingHint": "Lets a chat ask to type text and press keys. Each request still waits for your confirmation.",
|
||||
"typingHint": "Lets a chat ask to type text and press keys. Typed text always waits for your confirmation. Key presses wait too, unless you chose Don't ask for them.",
|
||||
"maxChars": "Longest text",
|
||||
"maxCharsHint": "Characters in one typed text. Default {{default}}, at most 1000.",
|
||||
"maxCharsShape": "Enter a whole number from 1 to 1000.",
|
||||
"hard": "Allow hard power actions",
|
||||
"hardHint": "Lets a chat ask for a forced power off, a hardware reset or a long press of the power button. Each request still waits for your confirmation.",
|
||||
"hardHint": "Lets a chat ask for a forced power off, a hardware reset or a long press of the power button. Each request waits for your confirmation, unless you chose Don't ask for power actions.",
|
||||
"save": "Save switches",
|
||||
"saved": "Saved. The next call uses the new switches.",
|
||||
"neverTitle": "Never available",
|
||||
@@ -129,6 +129,31 @@
|
||||
"counts": "{{chars}} characters, {{lines}} line(s)",
|
||||
"typeEffect": "The text is typed on the keyboard of the machine, into whatever has the focus. A newline presses Enter.",
|
||||
"keys": "Keys, in order",
|
||||
"keysEffect": "The keys are pressed in this order on the keyboard of the machine."
|
||||
"keysEffect": "The keys are pressed in this order on the keyboard of the machine.",
|
||||
"title": "Ask before acting",
|
||||
"intro": "By default a power action, a key press or a typed text waits for your confirmation. For the two kinds of action below you can choose not to be asked. The choice is yours alone, it applies to all your PiKVM connections, and only this page can change it.",
|
||||
"tool": {
|
||||
"power-action": "Power actions (on, off, reset)",
|
||||
"send-key": "Key presses"
|
||||
},
|
||||
"ask": "Ask each time",
|
||||
"skip": "Don't ask",
|
||||
"line": {
|
||||
"ask": "A card waits for your OK.",
|
||||
"skip": "It runs at once."
|
||||
},
|
||||
"warnTitle": "Read this before you choose Don't ask.",
|
||||
"warn": {
|
||||
"power-action": "A chat can then switch this machine off or reset it at once, with no card to confirm, and unsaved work on it is lost. If an administrator allowed hard power actions, those run without asking too.",
|
||||
"send-key": "A chat can then press keys such as Ctrl+Alt+Delete, Escape or Enter on this machine at once, with no card to confirm."
|
||||
},
|
||||
"textAlways": "Typed text always asks. It can enter a password or a command, so it has no Don't ask.",
|
||||
"who": "Don't ask holds for your own chats and for messaging senders you paired with the bot. Everyone else is still asked: senders only on the allowlist, senders let in because the bot accepts anyone, scheduled runs and sub-agents.",
|
||||
"needsWrite": "A connection set to Read only runs nothing, whatever you choose here.",
|
||||
"audit": "Every run without asking is written to the log with the tool, the connection and the result. The typed text and the keys are not.",
|
||||
"loading": "Reading your choice...",
|
||||
"loadFailed": "Your choice could not be read. Reload the page to try again.",
|
||||
"saveFailed": "Your choice could not be saved. The earlier choice is still in force.",
|
||||
"saved": "Saved. The next call follows it."
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,8 @@
|
||||
"agentOf": "Agente: {{name}}",
|
||||
"inactive": "No se está ejecutando: {{reason}}.",
|
||||
"title": "PiKVM",
|
||||
"intro": "Deja que tus chats y agentes revisen un PiKVM y la máquina que controla. Si lo permites, también pueden pulsar sus botones de encendido y usar su teclado, y una persona confirma cada una de esas acciones.",
|
||||
"limits": "Un agente que puede pulsar un botón de encendido o escribir en una máquina real puede perder trabajo o ejecutar comandos. Cada acción espera tu confirmación. Escribir y las acciones de apagado forzado están desactivadas hasta que un administrador las active, abajo. Un PiKVM no tiene derechos por usuario: cualquier usuario que conozca puede hacerlo todo, así que los límites de este complemento son los únicos.",
|
||||
"intro": "Deja que tus chats y agentes revisen un PiKVM y la máquina que controla. Si lo permites, también pueden pulsar sus botones de encendido y usar su teclado, y una persona confirma esas acciones, salvo que elijas otra cosa abajo.",
|
||||
"limits": "Un agente que puede pulsar un botón de encendido o escribir en una máquina real puede perder trabajo o ejecutar comandos. De forma predeterminada, cada acción espera tu confirmación. Para las acciones de encendido y las pulsaciones de teclas puedes desactivarlo abajo, nunca para el texto escrito. Escribir y las acciones de apagado forzado están desactivadas hasta que un administrador las active, abajo. Un PiKVM no tiene derechos por usuario: cualquier usuario que conozca puede hacerlo todo, así que los límites de este complemento son los únicos.",
|
||||
"new": "Nueva conexión de PiKVM",
|
||||
"edit": "Editar conexión de PiKVM",
|
||||
"user": "Nombre de usuario de PiKVM",
|
||||
@@ -67,16 +67,16 @@
|
||||
},
|
||||
"writeMode": {
|
||||
"label": "Qué puede hacer el agente con este dispositivo",
|
||||
"hint": "Leer el estado siempre está permitido. Cada acción la confirmas tú, elijas lo que elijas.",
|
||||
"hint": "Leer el estado siempre está permitido. Cada acción la confirmas tú, salvo que elijas otra cosa para una herramienta en la sección de abajo.",
|
||||
"off": "Solo lectura",
|
||||
"propose": "Te pregunta antes",
|
||||
"line": {
|
||||
"off": "Solo puede leer.",
|
||||
"propose": "Cada acción espera tu visto bueno."
|
||||
"propose": "Cada acción espera tu visto bueno, salvo que lo desactives."
|
||||
},
|
||||
"note": {
|
||||
"off": "El acceso de escritura está desactivado: esta conexión solo lee el estado.",
|
||||
"propose": "Cada acción de encendido, texto y pulsación de tecla espera en tus aprobaciones hasta que la confirmes. Escribir y las acciones de apagado forzado también necesitan el interruptor de abajo."
|
||||
"propose": "Cada acción de encendido, texto y pulsación de tecla espera en tus aprobaciones hasta que la confirmes, salvo que lo hayas desactivado para esa herramienta. Escribir y las acciones de apagado forzado también necesitan el interruptor de abajo."
|
||||
},
|
||||
"row": {
|
||||
"off": "Solo lectura",
|
||||
@@ -90,17 +90,17 @@
|
||||
"saveFailed": "No se pudieron guardar los interruptores.",
|
||||
"intro": "Estos interruptores se guardan por dispositivo y se aplican a todos los usuarios de este servidor. Están desactivados hasta que un administrador los cambie.",
|
||||
"physicalTitle": "Efectos físicos.",
|
||||
"physical": "Una acción de encendido puede hacer perder trabajo sin guardar. Lo que se escribe y las teclas pulsadas van a lo que tenga el foco en la máquina: un shell, un menú de BIOS, una pantalla de inicio de sesión. Una persona confirma cada una y ve el texto o las teclas exactos.",
|
||||
"physical": "Una acción de encendido puede hacer perder trabajo sin guardar. Lo que se escribe y las teclas pulsadas van a lo que tenga el foco en la máquina: un shell, un menú de BIOS, una pantalla de inicio de sesión. Una persona confirma cada una y ve el texto o las teclas exactos, salvo que lo hayas desactivado para las acciones de encendido o las teclas.",
|
||||
"readOnly": "Solo un administrador puede cambiar estos interruptores.",
|
||||
"broken": "No se pudieron leer los interruptores guardados, así que todo se trata como desactivado. Guárdalos de nuevo para repararlos.",
|
||||
"none": "Añade una conexión de PiKVM para configurar sus interruptores.",
|
||||
"typing": "Permitir escribir y pulsar teclas",
|
||||
"typingHint": "Permite que un chat pida escribir texto y pulsar teclas. Cada petición sigue esperando tu confirmación.",
|
||||
"typingHint": "Permite que un chat pida escribir texto y pulsar teclas. El texto escrito siempre espera tu confirmación. Las teclas también, salvo que lo hayas desactivado.",
|
||||
"maxChars": "Texto más largo",
|
||||
"maxCharsHint": "Caracteres en un texto escrito. Por defecto {{default}}, como máximo 1000.",
|
||||
"maxCharsShape": "Introduce un número entero del 1 al 1000.",
|
||||
"hard": "Permitir acciones de apagado forzado",
|
||||
"hardHint": "Permite que un chat pida un apagado forzado, un reinicio por hardware o una pulsación larga del botón de encendido. Cada petición sigue esperando tu confirmación.",
|
||||
"hardHint": "Permite que un chat pida un apagado forzado, un reinicio por hardware o una pulsación larga del botón de encendido. Cada petición espera tu confirmación, salvo que lo hayas desactivado para las acciones de encendido.",
|
||||
"save": "Guardar interruptores",
|
||||
"saved": "Guardado. La próxima llamada usa los interruptores nuevos.",
|
||||
"neverTitle": "Nunca disponible",
|
||||
@@ -129,6 +129,31 @@
|
||||
"counts": "{{chars}} caracteres, {{lines}} línea(s)",
|
||||
"typeEffect": "El texto se escribe en el teclado de la máquina, en lo que tenga el foco. Un salto de línea pulsa Enter.",
|
||||
"keys": "Teclas, en orden",
|
||||
"keysEffect": "Las teclas se pulsan en este orden en el teclado de la máquina."
|
||||
"keysEffect": "Las teclas se pulsan en este orden en el teclado de la máquina.",
|
||||
"title": "Ask before acting",
|
||||
"intro": "By default a power action, a key press or a typed text waits for your confirmation. For the two kinds of action below you can choose not to be asked. The choice is yours alone, it applies to all your PiKVM connections, and only this page can change it.",
|
||||
"tool": {
|
||||
"power-action": "Power actions (on, off, reset)",
|
||||
"send-key": "Key presses"
|
||||
},
|
||||
"ask": "Ask each time",
|
||||
"skip": "Don't ask",
|
||||
"line": {
|
||||
"ask": "A card waits for your OK.",
|
||||
"skip": "It runs at once."
|
||||
},
|
||||
"warnTitle": "Read this before you choose Don't ask.",
|
||||
"warn": {
|
||||
"power-action": "A chat can then switch this machine off or reset it at once, with no card to confirm, and unsaved work on it is lost. If an administrator allowed hard power actions, those run without asking too.",
|
||||
"send-key": "A chat can then press keys such as Ctrl+Alt+Delete, Escape or Enter on this machine at once, with no card to confirm."
|
||||
},
|
||||
"textAlways": "Typed text always asks. It can enter a password or a command, so it has no Don't ask.",
|
||||
"who": "Don't ask holds for your own chats and for messaging senders you paired with the bot. Everyone else is still asked: senders only on the allowlist, senders let in because the bot accepts anyone, scheduled runs and sub-agents.",
|
||||
"needsWrite": "A connection set to Read only runs nothing, whatever you choose here.",
|
||||
"audit": "Every run without asking is written to the log with the tool, the connection and the result. The typed text and the keys are not.",
|
||||
"loading": "Reading your choice...",
|
||||
"loadFailed": "Your choice could not be read. Reload the page to try again.",
|
||||
"saveFailed": "Your choice could not be saved. The earlier choice is still in force.",
|
||||
"saved": "Saved. The next call follows it."
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,8 @@
|
||||
"agentOf": "Agent : {{name}}",
|
||||
"inactive": "À l’arrêt : {{reason}}.",
|
||||
"title": "PiKVM",
|
||||
"intro": "Laissez vos chats et vos agents examiner un PiKVM et la machine qu’il pilote. Si vous l’autorisez, ils peuvent aussi appuyer sur ses boutons d’alimentation et utiliser son clavier, et une personne confirme chacune de ces actions.",
|
||||
"limits": "Un agent qui peut appuyer sur un bouton d’alimentation ou taper sur une vraie machine peut faire perdre du travail ou lancer des commandes. Chaque action attend votre confirmation. La saisie et les actions d’alimentation forcées sont désactivées tant qu’un administrateur ne les active pas, plus bas. Un PiKVM n’a pas de droits par utilisateur : tout utilisateur qu’il connaît peut tout faire, donc les limites de cet add-on sont les seules.",
|
||||
"intro": "Laissez vos chats et vos agents examiner un PiKVM et la machine qu’il pilote. Si vous l’autorisez, ils peuvent aussi appuyer sur ses boutons d’alimentation et utiliser son clavier, et une personne confirme ces actions, sauf si vous choisissez autrement plus bas.",
|
||||
"limits": "Un agent qui peut appuyer sur un bouton d’alimentation ou taper sur une vraie machine peut faire perdre du travail ou lancer des commandes. Par défaut, chaque action attend votre confirmation. Pour les actions d’alimentation et les appuis de touches, vous pouvez le désactiver plus bas, jamais pour le texte saisi. La saisie et les actions d’alimentation forcées sont désactivées tant qu’un administrateur ne les active pas, plus bas. Un PiKVM n’a pas de droits par utilisateur : tout utilisateur qu’il connaît peut tout faire, donc les limites de cet add-on sont les seules.",
|
||||
"new": "Nouvelle connexion PiKVM",
|
||||
"edit": "Modifier la connexion PiKVM",
|
||||
"user": "Nom d’utilisateur PiKVM",
|
||||
@@ -67,16 +67,16 @@
|
||||
},
|
||||
"writeMode": {
|
||||
"label": "Ce que l’agent peut faire avec cet appareil",
|
||||
"hint": "La lecture de l’état est toujours autorisée. Chaque action est confirmée par vous, quel que soit votre choix.",
|
||||
"hint": "La lecture de l’état est toujours autorisée. Chaque action est confirmée par vous, sauf si vous choisissez autrement pour un outil dans la section ci-dessous.",
|
||||
"off": "Lecture seule",
|
||||
"propose": "Vous demande d’abord",
|
||||
"line": {
|
||||
"off": "Il peut seulement lire.",
|
||||
"propose": "Chaque action attend votre accord."
|
||||
"propose": "Chaque action attend votre accord, sauf si vous le désactivez."
|
||||
},
|
||||
"note": {
|
||||
"off": "L’accès en écriture est désactivé : cette connexion ne fait que lire l’état.",
|
||||
"propose": "Chaque action d’alimentation, saisie de texte et appui de touche attend dans vos approbations jusqu’à votre confirmation. La saisie et les actions d’alimentation forcées exigent aussi l’interrupteur ci-dessous."
|
||||
"propose": "Chaque action d’alimentation, saisie de texte et appui de touche attend dans vos approbations jusqu’à votre confirmation, sauf si vous l’avez désactivé pour cet outil. La saisie et les actions d’alimentation forcées exigent aussi l’interrupteur ci-dessous."
|
||||
},
|
||||
"row": {
|
||||
"off": "Lecture seule",
|
||||
@@ -90,17 +90,17 @@
|
||||
"saveFailed": "Impossible d’enregistrer les interrupteurs.",
|
||||
"intro": "Ces interrupteurs sont conservés pour chaque appareil et s’appliquent à tous les utilisateurs de ce serveur. Ils sont désactivés tant qu’un administrateur ne les modifie pas.",
|
||||
"physicalTitle": "Effets physiques.",
|
||||
"physical": "Une action d’alimentation peut faire perdre du travail non enregistré. La saisie et les appuis de touches vont à ce qui a le focus sur la machine : un shell, un menu du BIOS, un écran de connexion. Une personne confirme chacune et voit le texte ou les touches exacts.",
|
||||
"physical": "Une action d’alimentation peut faire perdre du travail non enregistré. La saisie et les appuis de touches vont à ce qui a le focus sur la machine : un shell, un menu du BIOS, un écran de connexion. Une personne confirme chacune et voit le texte ou les touches exacts, sauf si vous l’avez désactivé pour les actions d’alimentation ou les touches.",
|
||||
"readOnly": "Seul un administrateur peut modifier ces interrupteurs.",
|
||||
"broken": "Les interrupteurs enregistrés n’ont pas pu être lus, donc tout est considéré comme désactivé. Enregistrez-les de nouveau pour les réparer.",
|
||||
"none": "Ajoutez une connexion PiKVM pour régler ses interrupteurs.",
|
||||
"typing": "Autoriser la saisie et les appuis de touches",
|
||||
"typingHint": "Permet à un chat de demander de taper du texte et d’appuyer sur des touches. Chaque demande attend toujours votre confirmation.",
|
||||
"typingHint": "Permet à un chat de demander de taper du texte et d’appuyer sur des touches. Le texte saisi attend toujours votre confirmation. Les touches aussi, sauf si vous l’avez désactivé.",
|
||||
"maxChars": "Texte le plus long",
|
||||
"maxCharsHint": "Caractères dans un texte saisi. Par défaut {{default}}, au plus 1000.",
|
||||
"maxCharsShape": "Saisissez un nombre entier de 1 à 1000.",
|
||||
"hard": "Autoriser les actions d’alimentation forcées",
|
||||
"hardHint": "Permet à un chat de demander un arrêt forcé, une réinitialisation matérielle ou un appui long sur le bouton d’alimentation. Chaque demande attend toujours votre confirmation.",
|
||||
"hardHint": "Permet à un chat de demander un arrêt forcé, une réinitialisation matérielle ou un appui long sur le bouton d’alimentation. Chaque demande attend votre confirmation, sauf si vous l’avez désactivé pour les actions d’alimentation.",
|
||||
"save": "Enregistrer les interrupteurs",
|
||||
"saved": "Enregistré. Le prochain appel utilise les nouveaux interrupteurs.",
|
||||
"neverTitle": "Jamais disponible",
|
||||
@@ -129,6 +129,31 @@
|
||||
"counts": "{{chars}} caractères, {{lines}} ligne(s)",
|
||||
"typeEffect": "Le texte est tapé sur le clavier de la machine, dans ce qui a le focus. Un retour à la ligne appuie sur Enter.",
|
||||
"keys": "Touches, dans l’ordre",
|
||||
"keysEffect": "Les touches sont pressées dans cet ordre sur le clavier de la machine."
|
||||
"keysEffect": "Les touches sont pressées dans cet ordre sur le clavier de la machine.",
|
||||
"title": "Ask before acting",
|
||||
"intro": "By default a power action, a key press or a typed text waits for your confirmation. For the two kinds of action below you can choose not to be asked. The choice is yours alone, it applies to all your PiKVM connections, and only this page can change it.",
|
||||
"tool": {
|
||||
"power-action": "Power actions (on, off, reset)",
|
||||
"send-key": "Key presses"
|
||||
},
|
||||
"ask": "Ask each time",
|
||||
"skip": "Don't ask",
|
||||
"line": {
|
||||
"ask": "A card waits for your OK.",
|
||||
"skip": "It runs at once."
|
||||
},
|
||||
"warnTitle": "Read this before you choose Don't ask.",
|
||||
"warn": {
|
||||
"power-action": "A chat can then switch this machine off or reset it at once, with no card to confirm, and unsaved work on it is lost. If an administrator allowed hard power actions, those run without asking too.",
|
||||
"send-key": "A chat can then press keys such as Ctrl+Alt+Delete, Escape or Enter on this machine at once, with no card to confirm."
|
||||
},
|
||||
"textAlways": "Typed text always asks. It can enter a password or a command, so it has no Don't ask.",
|
||||
"who": "Don't ask holds for your own chats and for messaging senders you paired with the bot. Everyone else is still asked: senders only on the allowlist, senders let in because the bot accepts anyone, scheduled runs and sub-agents.",
|
||||
"needsWrite": "A connection set to Read only runs nothing, whatever you choose here.",
|
||||
"audit": "Every run without asking is written to the log with the tool, the connection and the result. The typed text and the keys are not.",
|
||||
"loading": "Reading your choice...",
|
||||
"loadFailed": "Your choice could not be read. Reload the page to try again.",
|
||||
"saveFailed": "Your choice could not be saved. The earlier choice is still in force.",
|
||||
"saved": "Saved. The next call follows it."
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,8 @@
|
||||
"agentOf": "Agen: {{name}}",
|
||||
"inactive": "Tidak berjalan: {{reason}}.",
|
||||
"title": "PiKVM",
|
||||
"intro": "Biarkan chat dan agen Anda memeriksa sebuah PiKVM dan mesin yang dikendalikannya. Jika Anda mengizinkan, mereka juga dapat menekan tombol daya dan memakai keyboard-nya, dan seseorang mengonfirmasi setiap tindakan itu.",
|
||||
"limits": "Agen yang dapat menekan tombol daya atau mengetik di mesin sungguhan dapat menghilangkan pekerjaan atau menjalankan perintah. Setiap tindakan menunggu konfirmasi Anda. Mengetik dan tindakan daya paksa nonaktif sampai administrator mengaktifkannya, di bawah. PiKVM tidak punya hak pengguna: setiap pengguna yang dikenalnya dapat melakukan apa saja, jadi batas add-on ini adalah satu-satunya batas.",
|
||||
"intro": "Biarkan chat dan agen Anda memeriksa sebuah PiKVM dan mesin yang dikendalikannya. Jika Anda mengizinkan, mereka juga dapat menekan tombol daya dan memakai keyboard-nya, dan seseorang mengonfirmasi tindakan itu, kecuali Anda memilih lain di bawah.",
|
||||
"limits": "Agen yang dapat menekan tombol daya atau mengetik di mesin sungguhan dapat menghilangkan pekerjaan atau menjalankan perintah. Secara bawaan, setiap tindakan menunggu konfirmasi Anda. Untuk tindakan daya dan penekanan tombol, Anda dapat mematikannya di bawah, tidak pernah untuk teks yang diketik. Mengetik dan tindakan daya paksa nonaktif sampai administrator mengaktifkannya, di bawah. PiKVM tidak punya hak pengguna: setiap pengguna yang dikenalnya dapat melakukan apa saja, jadi batas add-on ini adalah satu-satunya batas.",
|
||||
"new": "Koneksi PiKVM baru",
|
||||
"edit": "Ubah koneksi PiKVM",
|
||||
"user": "Nama pengguna PiKVM",
|
||||
@@ -67,16 +67,16 @@
|
||||
},
|
||||
"writeMode": {
|
||||
"label": "Apa yang boleh dilakukan agen pada perangkat ini",
|
||||
"hint": "Membaca status selalu diizinkan. Setiap tindakan Anda konfirmasi, apa pun pilihan Anda.",
|
||||
"hint": "Membaca status selalu diizinkan. Setiap tindakan Anda konfirmasi, kecuali Anda memilih lain untuk sebuah alat di bagian bawah.",
|
||||
"off": "Hanya baca",
|
||||
"propose": "Tanya Anda dulu",
|
||||
"line": {
|
||||
"off": "Agen hanya dapat membaca.",
|
||||
"propose": "Setiap tindakan menunggu persetujuan Anda."
|
||||
"propose": "Setiap tindakan menunggu persetujuan Anda, kecuali Anda mematikannya."
|
||||
},
|
||||
"note": {
|
||||
"off": "Akses tulis nonaktif: koneksi ini hanya membaca status.",
|
||||
"propose": "Setiap tindakan daya, teks, dan penekanan tombol menunggu di persetujuan Anda sampai Anda mengonfirmasinya. Mengetik dan tindakan daya paksa juga memerlukan sakelar di bawah."
|
||||
"propose": "Setiap tindakan daya, teks, dan penekanan tombol menunggu di persetujuan Anda sampai Anda mengonfirmasinya, kecuali Anda mematikannya untuk alat itu. Mengetik dan tindakan daya paksa juga memerlukan sakelar di bawah."
|
||||
},
|
||||
"row": {
|
||||
"off": "Hanya baca",
|
||||
@@ -90,17 +90,17 @@
|
||||
"saveFailed": "Sakelar tidak dapat disimpan.",
|
||||
"intro": "Sakelar ini disimpan per perangkat dan berlaku untuk setiap pengguna server ini. Sakelar nonaktif sampai administrator mengubahnya.",
|
||||
"physicalTitle": "Efek fisik.",
|
||||
"physical": "Tindakan daya dapat menghilangkan pekerjaan yang belum disimpan. Ketikan dan penekanan tombol masuk ke apa pun yang sedang fokus di mesin: shell, menu BIOS, layar login. Seseorang mengonfirmasi masing-masing dan melihat teks atau tombol yang persis.",
|
||||
"physical": "Tindakan daya dapat menghilangkan pekerjaan yang belum disimpan. Ketikan dan penekanan tombol masuk ke apa pun yang sedang fokus di mesin: shell, menu BIOS, layar login. Seseorang mengonfirmasi masing-masing dan melihat teks atau tombol yang persis, kecuali Anda mematikannya untuk tindakan daya atau penekanan tombol.",
|
||||
"readOnly": "Hanya administrator yang dapat mengubah sakelar ini.",
|
||||
"broken": "Sakelar yang tersimpan tidak dapat dibaca, jadi semuanya dianggap nonaktif. Simpan lagi untuk memperbaikinya.",
|
||||
"none": "Tambahkan koneksi PiKVM untuk mengatur sakelarnya.",
|
||||
"typing": "Izinkan mengetik dan menekan tombol",
|
||||
"typingHint": "Memungkinkan chat meminta untuk mengetik teks dan menekan tombol. Setiap permintaan tetap menunggu konfirmasi Anda.",
|
||||
"typingHint": "Memungkinkan chat meminta untuk mengetik teks dan menekan tombol. Teks yang diketik selalu menunggu konfirmasi Anda. Penekanan tombol juga, kecuali Anda mematikannya.",
|
||||
"maxChars": "Teks terpanjang",
|
||||
"maxCharsHint": "Karakter dalam satu teks yang diketik. Bawaan {{default}}, paling banyak 1000.",
|
||||
"maxCharsShape": "Masukkan bilangan bulat dari 1 sampai 1000.",
|
||||
"hard": "Izinkan tindakan daya paksa",
|
||||
"hardHint": "Memungkinkan chat meminta pemadaman paksa, reset perangkat keras, atau penekanan lama tombol daya. Setiap permintaan tetap menunggu konfirmasi Anda.",
|
||||
"hardHint": "Memungkinkan chat meminta pemadaman paksa, reset perangkat keras, atau penekanan lama tombol daya. Setiap permintaan menunggu konfirmasi Anda, kecuali Anda mematikannya untuk tindakan daya.",
|
||||
"save": "Simpan sakelar",
|
||||
"saved": "Tersimpan. Panggilan berikutnya memakai sakelar baru.",
|
||||
"neverTitle": "Tidak pernah tersedia",
|
||||
@@ -129,6 +129,31 @@
|
||||
"counts": "{{chars}} karakter, {{lines}} baris",
|
||||
"typeEffect": "Teks diketik di keyboard mesin, ke apa pun yang sedang fokus. Baris baru menekan Enter.",
|
||||
"keys": "Tombol, berurutan",
|
||||
"keysEffect": "Tombol ditekan dalam urutan ini di keyboard mesin."
|
||||
"keysEffect": "Tombol ditekan dalam urutan ini di keyboard mesin.",
|
||||
"title": "Ask before acting",
|
||||
"intro": "By default a power action, a key press or a typed text waits for your confirmation. For the two kinds of action below you can choose not to be asked. The choice is yours alone, it applies to all your PiKVM connections, and only this page can change it.",
|
||||
"tool": {
|
||||
"power-action": "Power actions (on, off, reset)",
|
||||
"send-key": "Key presses"
|
||||
},
|
||||
"ask": "Ask each time",
|
||||
"skip": "Don't ask",
|
||||
"line": {
|
||||
"ask": "A card waits for your OK.",
|
||||
"skip": "It runs at once."
|
||||
},
|
||||
"warnTitle": "Read this before you choose Don't ask.",
|
||||
"warn": {
|
||||
"power-action": "A chat can then switch this machine off or reset it at once, with no card to confirm, and unsaved work on it is lost. If an administrator allowed hard power actions, those run without asking too.",
|
||||
"send-key": "A chat can then press keys such as Ctrl+Alt+Delete, Escape or Enter on this machine at once, with no card to confirm."
|
||||
},
|
||||
"textAlways": "Typed text always asks. It can enter a password or a command, so it has no Don't ask.",
|
||||
"who": "Don't ask holds for your own chats and for messaging senders you paired with the bot. Everyone else is still asked: senders only on the allowlist, senders let in because the bot accepts anyone, scheduled runs and sub-agents.",
|
||||
"needsWrite": "A connection set to Read only runs nothing, whatever you choose here.",
|
||||
"audit": "Every run without asking is written to the log with the tool, the connection and the result. The typed text and the keys are not.",
|
||||
"loading": "Reading your choice...",
|
||||
"loadFailed": "Your choice could not be read. Reload the page to try again.",
|
||||
"saveFailed": "Your choice could not be saved. The earlier choice is still in force.",
|
||||
"saved": "Saved. The next call follows it."
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,8 @@
|
||||
"agentOf": "Agente: {{name}}",
|
||||
"inactive": "Non in esecuzione: {{reason}}.",
|
||||
"title": "PiKVM",
|
||||
"intro": "Permetti alle tue chat e ai tuoi agenti di controllare un PiKVM e la macchina che governa. Se lo consenti, possono anche premere i suoi pulsanti di accensione e usare la sua tastiera, e una persona conferma ognuna di queste azioni.",
|
||||
"limits": "Un agente che può premere un pulsante di accensione o scrivere su una macchina reale può far perdere lavoro o eseguire comandi. Ogni azione aspetta la tua conferma. La digitazione e le azioni di alimentazione forzata sono disattivate finché un amministratore non le attiva, qui sotto. Un PiKVM non ha diritti per utente: qualsiasi utente che conosce può fare tutto, quindi i limiti di questo componente aggiuntivo sono gli unici.",
|
||||
"intro": "Permetti alle tue chat e ai tuoi agenti di controllare un PiKVM e la macchina che governa. Se lo consenti, possono anche premere i suoi pulsanti di accensione e usare la sua tastiera, e una persona conferma queste azioni, salvo che tu scelga diversamente qui sotto.",
|
||||
"limits": "Un agente che può premere un pulsante di accensione o scrivere su una macchina reale può far perdere lavoro o eseguire comandi. Di norma ogni azione aspetta la tua conferma. Per le azioni di accensione e i tasti premuti puoi disattivarla qui sotto, mai per il testo digitato. La digitazione e le azioni di alimentazione forzata sono disattivate finché un amministratore non le attiva, qui sotto. Un PiKVM non ha diritti per utente: qualsiasi utente che conosce può fare tutto, quindi i limiti di questo componente aggiuntivo sono gli unici.",
|
||||
"new": "Nuova connessione PiKVM",
|
||||
"edit": "Modifica la connessione PiKVM",
|
||||
"user": "Nome utente del PiKVM",
|
||||
@@ -67,16 +67,16 @@
|
||||
},
|
||||
"writeMode": {
|
||||
"label": "Cosa può fare l'agente con questo dispositivo",
|
||||
"hint": "Leggere lo stato è sempre consentito. Ogni azione la confermi tu, qualunque cosa scelga.",
|
||||
"hint": "Leggere lo stato è sempre consentito. Ogni azione la confermi tu, a meno che tu scelga Non chiedere per uno strumento nella sezione qui sotto.",
|
||||
"off": "Sola lettura",
|
||||
"propose": "Chiede prima a te",
|
||||
"line": {
|
||||
"off": "Può solo leggere.",
|
||||
"propose": "Ogni azione aspetta il tuo OK."
|
||||
"propose": "Ogni azione aspetta il tuo OK, a meno che tu lo disattivi."
|
||||
},
|
||||
"note": {
|
||||
"off": "L'accesso in scrittura è disattivato: questa connessione legge soltanto lo stato.",
|
||||
"propose": "Ogni azione di alimentazione, ogni testo e ogni tasto premuto aspetta tra le tue approvazioni finché non lo confermi. La digitazione e le azioni di alimentazione forzata richiedono anche l'interruttore qui sotto."
|
||||
"propose": "Ogni azione di alimentazione, ogni testo e ogni tasto premuto aspetta tra le tue approvazioni finché non lo confermi, a meno che tu abbia scelto Non chiedere per quello strumento. La digitazione e le azioni di alimentazione forzata richiedono anche l'interruttore qui sotto."
|
||||
},
|
||||
"row": {
|
||||
"off": "Sola lettura",
|
||||
@@ -90,17 +90,17 @@
|
||||
"saveFailed": "Non è stato possibile salvare gli interruttori.",
|
||||
"intro": "Questi interruttori sono tenuti per ogni dispositivo e valgono per tutti gli utenti di questo server. Sono disattivati finché un amministratore non li cambia.",
|
||||
"physicalTitle": "Effetti fisici.",
|
||||
"physical": "Un'azione di alimentazione può far perdere il lavoro non salvato. La digitazione e i tasti premuti vanno a ciò che ha il focus sulla macchina: una shell, un menu del BIOS, una schermata di accesso. Una persona conferma ogni azione e vede il testo o i tasti esatti.",
|
||||
"physical": "Un'azione di alimentazione può far perdere il lavoro non salvato. La digitazione e i tasti premuti vanno a ciò che ha il focus sulla macchina: una shell, un menu del BIOS, una schermata di accesso. Una persona conferma ogni azione e vede il testo o i tasti esatti, a meno che tu abbia scelto Non chiedere per le azioni di accensione o per i tasti.",
|
||||
"readOnly": "Solo un amministratore può cambiare questi interruttori.",
|
||||
"broken": "Gli interruttori salvati non si sono potuti leggere, quindi tutto è trattato come disattivato. Salvali di nuovo per ripararli.",
|
||||
"none": "Aggiungi una connessione PiKVM per impostare i suoi interruttori.",
|
||||
"typing": "Consenti digitazione e tasti premuti",
|
||||
"typingHint": "Permette a una chat di chiedere di scrivere testo e premere tasti. Ogni richiesta aspetta comunque la tua conferma.",
|
||||
"typingHint": "Permette a una chat di chiedere di scrivere testo e premere tasti. Il testo digitato aspetta sempre la tua conferma. Anche i tasti, a meno che tu abbia scelto Non chiedere.",
|
||||
"maxChars": "Testo più lungo",
|
||||
"maxCharsHint": "Caratteri in un testo digitato. Predefinito {{default}}, al massimo 1000.",
|
||||
"maxCharsShape": "Inserisci un numero intero da 1 a 1000.",
|
||||
"hard": "Consenti azioni di alimentazione forzata",
|
||||
"hardHint": "Permette a una chat di chiedere uno spegnimento forzato, un reset hardware o una pressione lunga del pulsante di accensione. Ogni richiesta aspetta comunque la tua conferma.",
|
||||
"hardHint": "Permette a una chat di chiedere uno spegnimento forzato, un reset hardware o una pressione lunga del pulsante di accensione. Ogni richiesta aspetta la tua conferma, a meno che tu abbia scelto Non chiedere per le azioni di accensione.",
|
||||
"save": "Salva gli interruttori",
|
||||
"saved": "Salvato. La prossima chiamata usa i nuovi interruttori.",
|
||||
"neverTitle": "Mai disponibili",
|
||||
@@ -129,6 +129,31 @@
|
||||
"counts": "{{chars}} caratteri, {{lines}} righe",
|
||||
"typeEffect": "Il testo viene digitato sulla tastiera della macchina, in ciò che ha il focus. Un a capo preme Invio.",
|
||||
"keys": "Tasti, in ordine",
|
||||
"keysEffect": "I tasti vengono premuti in quest'ordine sulla tastiera della macchina."
|
||||
"keysEffect": "I tasti vengono premuti in quest'ordine sulla tastiera della macchina.",
|
||||
"title": "Chiedere prima di agire",
|
||||
"intro": "Di norma un'azione di accensione, un tasto premuto o un testo digitato aspetta la tua conferma. Per i due tipi di azione qui sotto puoi scegliere di non essere interpellato. La scelta è solo tua, vale per tutte le tue connessioni PiKVM e solo questa pagina può cambiarla.",
|
||||
"tool": {
|
||||
"power-action": "Azioni di accensione (accendi, spegni, reset)",
|
||||
"send-key": "Tasti premuti"
|
||||
},
|
||||
"ask": "Chiedi ogni volta",
|
||||
"skip": "Non chiedere",
|
||||
"line": {
|
||||
"ask": "Una scheda aspetta il tuo OK.",
|
||||
"skip": "Parte subito."
|
||||
},
|
||||
"warnTitle": "Leggi prima di scegliere Non chiedere.",
|
||||
"warn": {
|
||||
"power-action": "Una chat potrà allora spegnere questa macchina o riavviarla subito, senza una scheda da confermare, e il lavoro non salvato andrà perso. Se un amministratore ha consentito le azioni di alimentazione forzata, anche queste partono senza chiedere.",
|
||||
"send-key": "Una chat potrà allora premere subito sulla macchina tasti come Ctrl+Alt+Canc, Esc o Invio, senza una scheda da confermare."
|
||||
},
|
||||
"textAlways": "Il testo digitato chiede sempre. Può inserire una password o un comando, quindi non ha Non chiedere.",
|
||||
"who": "Non chiedere vale per le tue chat e per i mittenti di messaggistica che hai abbinato al bot. Tutti gli altri vengono comunque interpellati: mittenti solo in lista consentiti, mittenti ammessi perché il bot accetta chiunque, esecuzioni programmate e sotto-agenti.",
|
||||
"needsWrite": "Una connessione in sola lettura non esegue nulla, qualunque cosa scelga qui.",
|
||||
"audit": "Ogni esecuzione senza chiedere viene scritta nel registro con lo strumento, la connessione e l'esito. Il testo digitato e i tasti no.",
|
||||
"loading": "Lettura della tua scelta...",
|
||||
"loadFailed": "Non è stato possibile leggere la tua scelta. Ricarica la pagina per riprovare.",
|
||||
"saveFailed": "Non è stato possibile salvare la tua scelta. Resta in vigore la scelta precedente.",
|
||||
"saved": "Salvato. La prossima chiamata la segue."
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,8 @@
|
||||
"agentOf": "에이전트: {{name}}",
|
||||
"inactive": "실행 중이 아님: {{reason}}.",
|
||||
"title": "PiKVM",
|
||||
"intro": "채팅과 에이전트가 PiKVM과 그것이 제어하는 기기를 확인할 수 있게 합니다. 허용하면 전원 버튼을 누르고 키보드를 사용할 수도 있으며, 그런 동작은 모두 사람이 확인해야 실행됩니다.",
|
||||
"limits": "실제 기기의 전원 버튼을 누르거나 입력할 수 있는 에이전트는 작업을 잃거나 명령을 실행할 수 있습니다. 모든 동작은 내 확인을 기다립니다. 입력과 강제 전원 동작은 관리자가 아래에서 켜기 전까지 꺼져 있습니다. PiKVM에는 사용자 권한이 없습니다. 아는 사용자는 누구나 모든 것을 할 수 있으므로, 이 애드온의 한도가 유일한 한도입니다.",
|
||||
"intro": "채팅과 에이전트가 PiKVM과 그것이 제어하는 기기를 확인할 수 있게 합니다. 허용하면 전원 버튼을 누르고 키보드를 사용할 수도 있으며, 아래에서 달리 정하지 않는 한 그런 동작은 사람이 확인해야 실행됩니다.",
|
||||
"limits": "실제 기기의 전원 버튼을 누르거나 입력할 수 있는 에이전트는 작업을 잃거나 명령을 실행할 수 있습니다. 기본적으로 모든 동작은 내 확인을 기다립니다. 전원 동작과 키 입력은 아래에서 확인을 끌 수 있지만, 입력하는 텍스트는 끌 수 없습니다. 입력과 강제 전원 동작은 관리자가 아래에서 켜기 전까지 꺼져 있습니다. PiKVM에는 사용자 권한이 없습니다. 아는 사용자는 누구나 모든 것을 할 수 있으므로, 이 애드온의 한도가 유일한 한도입니다.",
|
||||
"new": "새 PiKVM 연결",
|
||||
"edit": "PiKVM 연결 편집",
|
||||
"user": "PiKVM 사용자 이름",
|
||||
@@ -67,16 +67,16 @@
|
||||
},
|
||||
"writeMode": {
|
||||
"label": "에이전트가 이 기기에서 할 수 있는 것",
|
||||
"hint": "상태 읽기는 항상 허용됩니다. 어떤 것을 고르든 모든 동작은 내가 확인합니다.",
|
||||
"hint": "상태 읽기는 항상 허용됩니다. 아래 섹션에서 도구별로 달리 정하지 않는 한 모든 동작은 내가 확인합니다.",
|
||||
"off": "읽기 전용",
|
||||
"propose": "먼저 묻기",
|
||||
"line": {
|
||||
"off": "읽기만 할 수 있습니다.",
|
||||
"propose": "동작마다 내 승인을 기다립니다."
|
||||
"propose": "동작마다 내 승인을 기다립니다. 끄지 않는 한 그렇습니다."
|
||||
},
|
||||
"note": {
|
||||
"off": "쓰기 접근이 꺼져 있습니다. 이 연결은 상태만 읽습니다.",
|
||||
"propose": "전원 동작, 텍스트, 키 입력은 내가 확인할 때까지 승인 대기열에서 기다립니다. 입력과 강제 전원 동작에는 아래 스위치도 필요합니다."
|
||||
"propose": "전원 동작, 텍스트, 키 입력은 내가 확인할 때까지 승인 대기열에서 기다립니다. 해당 도구의 확인을 끄지 않은 경우입니다. 입력과 강제 전원 동작에는 아래 스위치도 필요합니다."
|
||||
},
|
||||
"row": {
|
||||
"off": "읽기 전용",
|
||||
@@ -90,17 +90,17 @@
|
||||
"saveFailed": "스위치를 저장할 수 없습니다.",
|
||||
"intro": "이 스위치는 기기별로 보관되며 이 서버의 모든 사용자에게 적용됩니다. 관리자가 바꾸기 전까지 꺼져 있습니다.",
|
||||
"physicalTitle": "물리적 영향.",
|
||||
"physical": "전원 동작은 저장하지 않은 작업을 잃게 할 수 있습니다. 입력과 키 입력은 기기에서 포커스를 가진 곳으로 전달됩니다. 셸, BIOS 메뉴, 로그인 화면 등이 해당합니다. 각 동작은 사람이 확인하며 정확한 텍스트나 키를 볼 수 있습니다.",
|
||||
"physical": "전원 동작은 저장하지 않은 작업을 잃게 할 수 있습니다. 입력과 키 입력은 기기에서 포커스를 가진 곳으로 전달됩니다. 셸, BIOS 메뉴, 로그인 화면 등이 해당합니다. 전원 동작이나 키 입력의 확인을 끄지 않은 한, 각 동작은 사람이 확인하며 정확한 텍스트나 키를 볼 수 있습니다.",
|
||||
"readOnly": "관리자만 이 스위치를 바꿀 수 있습니다.",
|
||||
"broken": "저장된 스위치를 읽을 수 없어서 모두 꺼진 것으로 처리합니다. 스위치를 다시 저장해 복구하세요.",
|
||||
"none": "스위치를 설정하려면 PiKVM 연결을 추가하세요.",
|
||||
"typing": "입력과 키 입력 허용",
|
||||
"typingHint": "채팅이 텍스트 입력과 키 누르기를 요청할 수 있게 합니다. 각 요청은 여전히 내 확인을 기다립니다.",
|
||||
"typingHint": "채팅이 텍스트 입력과 키 누르기를 요청할 수 있게 합니다. 입력하는 텍스트는 항상 내 확인을 기다립니다. 키 입력도 확인을 끄지 않은 한 기다립니다.",
|
||||
"maxChars": "최대 텍스트 길이",
|
||||
"maxCharsHint": "한 번에 입력하는 텍스트의 글자 수입니다. 기본값 {{default}}, 최대 1000.",
|
||||
"maxCharsShape": "1부터 1000까지의 정수를 입력하세요.",
|
||||
"hard": "강제 전원 동작 허용",
|
||||
"hardHint": "채팅이 강제 전원 끄기, 하드웨어 리셋, 전원 버튼 길게 누르기를 요청할 수 있게 합니다. 각 요청은 여전히 내 확인을 기다립니다.",
|
||||
"hardHint": "채팅이 강제 전원 끄기, 하드웨어 리셋, 전원 버튼 길게 누르기를 요청할 수 있게 합니다. 전원 동작의 확인을 끄지 않은 한, 각 요청은 내 확인을 기다립니다.",
|
||||
"save": "스위치 저장",
|
||||
"saved": "저장했습니다. 다음 호출부터 새 스위치가 적용됩니다.",
|
||||
"neverTitle": "사용할 수 없는 항목",
|
||||
@@ -129,6 +129,31 @@
|
||||
"counts": "{{chars}}자, {{lines}}줄",
|
||||
"typeEffect": "텍스트가 기기의 키보드로, 포커스를 가진 곳에 입력됩니다. 줄바꿈은 Enter를 누릅니다.",
|
||||
"keys": "키 (누르는 순서)",
|
||||
"keysEffect": "키가 기기의 키보드에서 이 순서대로 눌립니다."
|
||||
"keysEffect": "키가 기기의 키보드에서 이 순서대로 눌립니다.",
|
||||
"title": "Ask before acting",
|
||||
"intro": "By default a power action, a key press or a typed text waits for your confirmation. For the two kinds of action below you can choose not to be asked. The choice is yours alone, it applies to all your PiKVM connections, and only this page can change it.",
|
||||
"tool": {
|
||||
"power-action": "Power actions (on, off, reset)",
|
||||
"send-key": "Key presses"
|
||||
},
|
||||
"ask": "Ask each time",
|
||||
"skip": "Don't ask",
|
||||
"line": {
|
||||
"ask": "A card waits for your OK.",
|
||||
"skip": "It runs at once."
|
||||
},
|
||||
"warnTitle": "Read this before you choose Don't ask.",
|
||||
"warn": {
|
||||
"power-action": "A chat can then switch this machine off or reset it at once, with no card to confirm, and unsaved work on it is lost. If an administrator allowed hard power actions, those run without asking too.",
|
||||
"send-key": "A chat can then press keys such as Ctrl+Alt+Delete, Escape or Enter on this machine at once, with no card to confirm."
|
||||
},
|
||||
"textAlways": "Typed text always asks. It can enter a password or a command, so it has no Don't ask.",
|
||||
"who": "Don't ask holds for your own chats and for messaging senders you paired with the bot. Everyone else is still asked: senders only on the allowlist, senders let in because the bot accepts anyone, scheduled runs and sub-agents.",
|
||||
"needsWrite": "A connection set to Read only runs nothing, whatever you choose here.",
|
||||
"audit": "Every run without asking is written to the log with the tool, the connection and the result. The typed text and the keys are not.",
|
||||
"loading": "Reading your choice...",
|
||||
"loadFailed": "Your choice could not be read. Reload the page to try again.",
|
||||
"saveFailed": "Your choice could not be saved. The earlier choice is still in force.",
|
||||
"saved": "Saved. The next call follows it."
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,8 @@
|
||||
"agentOf": "Agente: {{name}}",
|
||||
"inactive": "Não está rodando: {{reason}}.",
|
||||
"title": "PiKVM",
|
||||
"intro": "Deixe seus chats e agentes verem um PiKVM e a máquina que ele controla. Se você permitir, eles também podem apertar os botões de energia e usar o teclado dele, e uma pessoa confirma cada uma dessas ações.",
|
||||
"limits": "Um agente que pode apertar um botão de energia ou digitar em uma máquina real pode perder trabalho ou executar comandos. Cada ação espera a sua confirmação. A digitação e as ações de energia forçadas ficam desligadas até um administrador ligá-las, abaixo. Um PiKVM não tem direitos por usuário: qualquer usuário que ele conheça pode fazer tudo, então os limites deste complemento são os únicos.",
|
||||
"intro": "Deixe seus chats e agentes verem um PiKVM e a máquina que ele controla. Se você permitir, eles também podem apertar os botões de energia e usar o teclado dele, e uma pessoa confirma essas ações, a menos que você escolha o contrário abaixo.",
|
||||
"limits": "Um agente que pode apertar um botão de energia ou digitar em uma máquina real pode perder trabalho ou executar comandos. Por padrão, cada ação espera a sua confirmação. Para as ações de energia e as teclas pressionadas você pode desligar isso abaixo, nunca para o texto digitado. A digitação e as ações de energia forçadas ficam desligadas até um administrador ligá-las, abaixo. Um PiKVM não tem direitos por usuário: qualquer usuário que ele conheça pode fazer tudo, então os limites deste complemento são os únicos.",
|
||||
"new": "Nova conexão do PiKVM",
|
||||
"edit": "Editar conexão do PiKVM",
|
||||
"user": "Nome de usuário do PiKVM",
|
||||
@@ -67,16 +67,16 @@
|
||||
},
|
||||
"writeMode": {
|
||||
"label": "O que o agente pode fazer com este dispositivo",
|
||||
"hint": "Ler o estado é sempre permitido. Cada ação é confirmada por você, seja qual for a sua escolha.",
|
||||
"hint": "Ler o estado é sempre permitido. Cada ação é confirmada por você, a menos que você escolha o contrário para uma ferramenta na seção abaixo.",
|
||||
"off": "Somente leitura",
|
||||
"propose": "Pergunta antes",
|
||||
"line": {
|
||||
"off": "Ele só pode ler.",
|
||||
"propose": "Cada ação espera o seu OK."
|
||||
"propose": "Cada ação espera o seu OK, a menos que você desligue isso."
|
||||
},
|
||||
"note": {
|
||||
"off": "O acesso de escrita está desligado: esta conexão só lê o estado.",
|
||||
"propose": "Cada ação de energia, texto e tecla pressionada espera nas suas aprovações até você confirmar. A digitação e as ações de energia forçadas também precisam do interruptor abaixo."
|
||||
"propose": "Cada ação de energia, texto e tecla pressionada espera nas suas aprovações até você confirmar, a menos que você tenha desligado isso para essa ferramenta. A digitação e as ações de energia forçadas também precisam do interruptor abaixo."
|
||||
},
|
||||
"row": {
|
||||
"off": "Somente leitura",
|
||||
@@ -90,17 +90,17 @@
|
||||
"saveFailed": "Não foi possível salvar os interruptores.",
|
||||
"intro": "Estes interruptores são guardados por dispositivo e valem para todo usuário deste servidor. Ficam desligados até um administrador alterá-los.",
|
||||
"physicalTitle": "Efeitos físicos.",
|
||||
"physical": "Uma ação de energia pode perder trabalho não salvo. A digitação e as teclas vão para o que estiver em foco na máquina: um shell, um menu da BIOS, uma tela de login. Uma pessoa confirma cada uma e vê o texto ou as teclas exatos.",
|
||||
"physical": "Uma ação de energia pode perder trabalho não salvo. A digitação e as teclas vão para o que estiver em foco na máquina: um shell, um menu da BIOS, uma tela de login. Uma pessoa confirma cada uma e vê o texto ou as teclas exatos, a menos que você tenha desligado isso para as ações de energia ou as teclas.",
|
||||
"readOnly": "Só um administrador pode alterar estes interruptores.",
|
||||
"broken": "Não foi possível ler os interruptores salvos, então tudo é tratado como desligado. Salve-os de novo para repará-los.",
|
||||
"none": "Adicione uma conexão do PiKVM para definir os interruptores dela.",
|
||||
"typing": "Permitir digitação e teclas pressionadas",
|
||||
"typingHint": "Deixa um chat pedir para digitar texto e pressionar teclas. Cada pedido ainda espera a sua confirmação.",
|
||||
"typingHint": "Deixa um chat pedir para digitar texto e pressionar teclas. O texto digitado sempre espera a sua confirmação. As teclas também, a menos que você tenha desligado isso.",
|
||||
"maxChars": "Texto mais longo",
|
||||
"maxCharsHint": "Caracteres em um texto digitado. Padrão {{default}}, no máximo 1000.",
|
||||
"maxCharsShape": "Digite um número inteiro de 1 a 1000.",
|
||||
"hard": "Permitir ações de energia forçadas",
|
||||
"hardHint": "Deixa um chat pedir um desligamento forçado, um reset de hardware ou um toque longo no botão de energia. Cada pedido ainda espera a sua confirmação.",
|
||||
"hardHint": "Deixa um chat pedir um desligamento forçado, um reset de hardware ou um toque longo no botão de energia. Cada pedido espera a sua confirmação, a menos que você tenha desligado isso para as ações de energia.",
|
||||
"save": "Salvar interruptores",
|
||||
"saved": "Salvo. A próxima chamada usa os novos interruptores.",
|
||||
"neverTitle": "Nunca disponível",
|
||||
@@ -129,6 +129,31 @@
|
||||
"counts": "{{chars}} caracteres, {{lines}} linha(s)",
|
||||
"typeEffect": "O texto é digitado no teclado da máquina, no que estiver em foco. Uma quebra de linha pressiona Enter.",
|
||||
"keys": "Teclas, em ordem",
|
||||
"keysEffect": "As teclas são pressionadas nesta ordem no teclado da máquina."
|
||||
"keysEffect": "As teclas são pressionadas nesta ordem no teclado da máquina.",
|
||||
"title": "Ask before acting",
|
||||
"intro": "By default a power action, a key press or a typed text waits for your confirmation. For the two kinds of action below you can choose not to be asked. The choice is yours alone, it applies to all your PiKVM connections, and only this page can change it.",
|
||||
"tool": {
|
||||
"power-action": "Power actions (on, off, reset)",
|
||||
"send-key": "Key presses"
|
||||
},
|
||||
"ask": "Ask each time",
|
||||
"skip": "Don't ask",
|
||||
"line": {
|
||||
"ask": "A card waits for your OK.",
|
||||
"skip": "It runs at once."
|
||||
},
|
||||
"warnTitle": "Read this before you choose Don't ask.",
|
||||
"warn": {
|
||||
"power-action": "A chat can then switch this machine off or reset it at once, with no card to confirm, and unsaved work on it is lost. If an administrator allowed hard power actions, those run without asking too.",
|
||||
"send-key": "A chat can then press keys such as Ctrl+Alt+Delete, Escape or Enter on this machine at once, with no card to confirm."
|
||||
},
|
||||
"textAlways": "Typed text always asks. It can enter a password or a command, so it has no Don't ask.",
|
||||
"who": "Don't ask holds for your own chats and for messaging senders you paired with the bot. Everyone else is still asked: senders only on the allowlist, senders let in because the bot accepts anyone, scheduled runs and sub-agents.",
|
||||
"needsWrite": "A connection set to Read only runs nothing, whatever you choose here.",
|
||||
"audit": "Every run without asking is written to the log with the tool, the connection and the result. The typed text and the keys are not.",
|
||||
"loading": "Reading your choice...",
|
||||
"loadFailed": "Your choice could not be read. Reload the page to try again.",
|
||||
"saveFailed": "Your choice could not be saved. The earlier choice is still in force.",
|
||||
"saved": "Saved. The next call follows it."
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,8 @@
|
||||
"agentOf": "智能体:{{name}}",
|
||||
"inactive": "未运行:{{reason}}。",
|
||||
"title": "PiKVM",
|
||||
"intro": "让你的聊天和智能体检查 PiKVM 以及它所控制的机器。如果你允许,它们还可以按下机器的电源按钮并使用它的键盘,而且每一项这样的操作都需要有人确认。",
|
||||
"limits": "能按电源按钮或在真实机器上输入的智能体,可能造成工作丢失或执行命令。每项操作都会等待你的确认。输入和强制电源操作默认关闭,需由管理员在下方开启。PiKVM 没有用户权限:它认识的任何用户都可以做所有事,因此此附加组件的限制是唯一的限制。",
|
||||
"intro": "让你的聊天和智能体检查 PiKVM 以及它所控制的机器。如果你允许,它们还可以按下机器的电源按钮并使用它的键盘,这些操作需要有人确认,除非你在下方另行设置。",
|
||||
"limits": "能按电源按钮或在真实机器上输入的智能体,可能造成工作丢失或执行命令。默认情况下,每项操作都会等待你的确认。电源操作和按键可以在下方关闭确认,输入的文本则始终不能。输入和强制电源操作默认关闭,需由管理员在下方开启。PiKVM 没有用户权限:它认识的任何用户都可以做所有事,因此此附加组件的限制是唯一的限制。",
|
||||
"new": "新建 PiKVM 连接",
|
||||
"edit": "编辑 PiKVM 连接",
|
||||
"user": "PiKVM 用户名",
|
||||
@@ -67,16 +67,16 @@
|
||||
},
|
||||
"writeMode": {
|
||||
"label": "智能体可以对这台设备做什么",
|
||||
"hint": "读取状态始终允许。无论你如何选择,每项操作都由你确认。",
|
||||
"hint": "读取状态始终允许。每项操作都由你确认,除非你在下方为某个工具另行设置。",
|
||||
"off": "只读",
|
||||
"propose": "先问你",
|
||||
"line": {
|
||||
"off": "它只能读取。",
|
||||
"propose": "每项操作都等待你的确认。"
|
||||
"propose": "每项操作都等待你的确认,除非你为它关闭了确认。"
|
||||
},
|
||||
"note": {
|
||||
"off": "写入权限已关闭:此连接只读取状态。",
|
||||
"propose": "每次电源操作、文本输入和按键都在你的审批中等待,直到你确认。输入和强制电源操作还需要下方的开关。"
|
||||
"propose": "每次电源操作、文本输入和按键都在你的审批中等待,直到你确认,除非你为该工具关闭了确认。输入和强制电源操作还需要下方的开关。"
|
||||
},
|
||||
"row": {
|
||||
"off": "只读",
|
||||
@@ -90,17 +90,17 @@
|
||||
"saveFailed": "无法保存开关。",
|
||||
"intro": "这些开关按设备保存,并适用于此服务器的所有用户。在管理员更改之前,它们都是关闭的。",
|
||||
"physicalTitle": "物理影响。",
|
||||
"physical": "电源操作可能丢失未保存的工作。输入和按键会发送到机器上获得焦点的任何位置:shell、BIOS 菜单、登录界面。每一项都由人确认,并能看到确切的文本或按键。",
|
||||
"physical": "电源操作可能丢失未保存的工作。输入和按键会发送到机器上获得焦点的任何位置:shell、BIOS 菜单、登录界面。每一项都由人确认,并能看到确切的文本或按键,除非你为电源操作或按键关闭了确认。",
|
||||
"readOnly": "只有管理员可以更改这些开关。",
|
||||
"broken": "无法读取已保存的开关,因此一律视为关闭。请重新保存以修复。",
|
||||
"none": "添加 PiKVM 连接后才能设置它的开关。",
|
||||
"typing": "允许输入和按键",
|
||||
"typingHint": "允许聊天请求输入文本和按键。每项请求仍需等待你的确认。",
|
||||
"typingHint": "允许聊天请求输入文本和按键。输入的文本始终等待你的确认。按键也是,除非你关闭了确认。",
|
||||
"maxChars": "文本最大长度",
|
||||
"maxCharsHint": "单次输入文本的字符数。默认 {{default}},最多 1000。",
|
||||
"maxCharsShape": "请输入 1 到 1000 的整数。",
|
||||
"hard": "允许强制电源操作",
|
||||
"hardHint": "允许聊天请求强制断电、硬件重置或长按电源按钮。每项请求仍需等待你的确认。",
|
||||
"hardHint": "允许聊天请求强制断电、硬件重置或长按电源按钮。每项请求都等待你的确认,除非你为电源操作关闭了确认。",
|
||||
"save": "保存开关",
|
||||
"saved": "已保存。下一次调用将使用新的开关。",
|
||||
"neverTitle": "永不可用",
|
||||
@@ -129,6 +129,31 @@
|
||||
"counts": "{{chars}} 个字符,{{lines}} 行",
|
||||
"typeEffect": "文本会在机器的键盘上输入,进入任何获得焦点的位置。换行会按下 Enter。",
|
||||
"keys": "按键,按顺序",
|
||||
"keysEffect": "这些键会按此顺序在机器的键盘上按下。"
|
||||
"keysEffect": "这些键会按此顺序在机器的键盘上按下。",
|
||||
"title": "Ask before acting",
|
||||
"intro": "By default a power action, a key press or a typed text waits for your confirmation. For the two kinds of action below you can choose not to be asked. The choice is yours alone, it applies to all your PiKVM connections, and only this page can change it.",
|
||||
"tool": {
|
||||
"power-action": "Power actions (on, off, reset)",
|
||||
"send-key": "Key presses"
|
||||
},
|
||||
"ask": "Ask each time",
|
||||
"skip": "Don't ask",
|
||||
"line": {
|
||||
"ask": "A card waits for your OK.",
|
||||
"skip": "It runs at once."
|
||||
},
|
||||
"warnTitle": "Read this before you choose Don't ask.",
|
||||
"warn": {
|
||||
"power-action": "A chat can then switch this machine off or reset it at once, with no card to confirm, and unsaved work on it is lost. If an administrator allowed hard power actions, those run without asking too.",
|
||||
"send-key": "A chat can then press keys such as Ctrl+Alt+Delete, Escape or Enter on this machine at once, with no card to confirm."
|
||||
},
|
||||
"textAlways": "Typed text always asks. It can enter a password or a command, so it has no Don't ask.",
|
||||
"who": "Don't ask holds for your own chats and for messaging senders you paired with the bot. Everyone else is still asked: senders only on the allowlist, senders let in because the bot accepts anyone, scheduled runs and sub-agents.",
|
||||
"needsWrite": "A connection set to Read only runs nothing, whatever you choose here.",
|
||||
"audit": "Every run without asking is written to the log with the tool, the connection and the result. The typed text and the keys are not.",
|
||||
"loading": "Reading your choice...",
|
||||
"loadFailed": "Your choice could not be read. Reload the page to try again.",
|
||||
"saveFailed": "Your choice could not be saved. The earlier choice is still in force.",
|
||||
"saved": "Saved. The next call follows it."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { ASK, SKIP, TOOLS, declared, keyOf, valuesOf, approvalRevision, approvalsChanged, onApprovalsChanged } from '../src/approval.js'
|
||||
import { approvalState, runningUnasked } from '../../../internal/connector-ui/src/approvals.js'
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
const manifest = JSON.parse(readFileSync(new URL('../../manifest.json', import.meta.url), 'utf8'))
|
||||
|
||||
test('pikvm approval: only power actions and key presses can stop asking, typed text never', () => {
|
||||
assert.deepEqual(TOOLS, ['power-action', 'send-key'])
|
||||
assert.equal(TOOLS.includes('type-text'), false)
|
||||
assert.equal(keyOf('power-action'), 'approval-power-action')
|
||||
})
|
||||
|
||||
test('pikvm approval: the page names the settings the manifest declares, and only for the tools it marks', () => {
|
||||
const caps = manifest.capabilities
|
||||
const overridable = caps.toolDefinitions.filter((d) => d.approvalOverridable).map((d) => d.name)
|
||||
assert.deepEqual(overridable, TOOLS)
|
||||
for (const d of caps.toolDefinitions) assert.equal(d.approvalOverridable === true, TOOLS.includes(d.name), d.name)
|
||||
for (const tool of TOOLS) {
|
||||
const s = caps.userSettings.find((x) => x.key === keyOf(tool))
|
||||
assert.deepEqual(s.values, [ASK, SKIP], tool)
|
||||
assert.equal(s.default, ASK, tool)
|
||||
}
|
||||
assert.equal(caps.userSettings.some((x) => x.key === keyOf('type-text')), false)
|
||||
})
|
||||
|
||||
test('pikvm approval: an answer the host does not report or does not know reads as ask', () => {
|
||||
assert.deepEqual(valuesOf(undefined), { 'power-action': ASK, 'send-key': ASK })
|
||||
assert.deepEqual(valuesOf({ settings: [{ key: 'approval-power-action', value: 'skip' }, { key: 'approval-send-key', value: 'always' }] }), { 'power-action': SKIP, 'send-key': ASK })
|
||||
assert.deepEqual(valuesOf({ settings: [{ key: 'approval-type-text', value: 'skip' }] }), { 'power-action': ASK, 'send-key': ASK })
|
||||
})
|
||||
|
||||
test('pikvm approval: a package that declares no such setting shows nothing', () => {
|
||||
assert.equal(declared({ settings: [{ key: 'other', value: 'x' }] }), false)
|
||||
assert.equal(declared({ settings: [{ key: 'approval-send-key', value: 'ask' }] }), true)
|
||||
assert.equal(declared(null), false)
|
||||
})
|
||||
|
||||
test('pikvm approval: a save tells the rows of the connections to read the view again', () => {
|
||||
let calls = 0
|
||||
const off = onApprovalsChanged(() => { calls += 1 })
|
||||
const before = approvalRevision()
|
||||
approvalsChanged()
|
||||
assert.equal(approvalRevision(), before + 1)
|
||||
assert.equal(calls, 1)
|
||||
off()
|
||||
approvalsChanged()
|
||||
assert.equal(calls, 1)
|
||||
})
|
||||
|
||||
test('approvals view: the owner view says asks, runs or idle, from what the host reported', () => {
|
||||
assert.equal(approvalState({ tool: 'send-key', skip: false }), 'asks')
|
||||
assert.equal(approvalState(undefined), 'asks')
|
||||
assert.equal(approvalState({ tool: 'send-key', skip: true, runs_without_asking: ['owner', 'paired'] }), 'runs')
|
||||
assert.equal(approvalState({ tool: 'send-key', skip: true }), 'idle')
|
||||
assert.equal(approvalState({ tool: 'send-key', skip: true, runs_without_asking: [] }), 'idle')
|
||||
})
|
||||
|
||||
test('approvals view: only a paired sender skips the card; a listed sender, anyone and a role are still asked', () => {
|
||||
const a = { tool: 'power-action', skip: true, runs_without_asking: ['owner', 'paired'] }
|
||||
assert.equal(approvalState(a, 'paired'), 'runs')
|
||||
for (const sender of ['listed', 'anyone', 'role:member', 'channel_member']) assert.equal(approvalState(a, sender), 'other', sender)
|
||||
assert.equal(approvalState({ tool: 'power-action', skip: false }, 'paired'), 'asks')
|
||||
})
|
||||
|
||||
test('approvals view: the list of tools that run without asking keeps the host order', () => {
|
||||
const list = [
|
||||
{ tool: 'power-action', skip: true, runs_without_asking: ['owner', 'paired'] },
|
||||
{ tool: 'send-key', skip: false },
|
||||
]
|
||||
assert.deepEqual(runningUnasked(list), ['power-action'])
|
||||
assert.deepEqual(runningUnasked(null), [])
|
||||
})
|
||||
@@ -418,7 +418,7 @@ var _ = Describe("apps routes outcomes and documentation", Label("apps"), func()
|
||||
Expect(json.Unmarshal(b, &m)).To(Succeed())
|
||||
return m
|
||||
}
|
||||
full := apps.App{ID: "a", Name: "a", Credentialed: true, ContextFiles: []contract.ContextFile{{Scope: "user", Path: "Memory/index.md"}}, UserSettings: []contract.UserSetting{{Key: "save-mode", Values: []string{"ask", "never"}, Default: "ask", Role: "memory.save-mode"}}, Network: &apps.NetworkView{Kind: contract.NetworkKindEgressCheck, Title: "N", Presets: []contract.NetworkPreset{{ID: "off", Title: "Closed"}}}, Title: "t", Titles: map[string]string{"en": "t"}, Description: "d", Descriptions: map[string]string{"en": "d"}, Language: "it", LocalizedTitle: "t", LocalizedDescription: "d", Icon: "i", Version: "1", InstalledAt: "2026-10-30T09:15:00Z", Generation: 1, Enabled: true, State: "running", Update: "update-pending", Operation: &apps.Operation{Kind: "upgrade", Phase: "downloading", Since: "2026-11-04T17:18:00Z"}, MainPage: "/m", HasUI: true, LaunchMode: "always", Tools: []string{"x"}, ToolCount: 1, Shadows: []string{"bash"}, ToolDefinitions: []contract.ToolDefinition{{Name: "x", Description: "d", Kind: "read", InputSchema: json.RawMessage(`{"type":"object"}`), UIOnly: true, Approval: contract.ApprovalAlways, ApprovalOverridable: true, Shadows: "bash", Execution: &contract.ExecutionDescriptor{Class: "commands", Title: "T", Note: "n", Skills: &contract.ExecutionSkills{ProvidesRuntimeFor: []string{"documents"}}}}}, Skills: []contract.SkillDefinition{{Name: "xlsx", Shadows: "xlsx", Slot: &contract.SlotDefinition{Kind: contract.SlotKindDocuments}}}, SkillShadows: []string{"xlsx"}, UI: &apps.UIView{Entry: "/e", CSS: []string{"/c"}, SDK: contract.VersionRange{Min: 1, Max: 1}, Actions: []apps.ChatActionView{{ID: "a", Label: "A", Labels: map[string]string{"en": "A"}, Icon: "star", PackagedIcon: true, Placement: "composer", Tool: "t", Component: "C"}}, Slots: []apps.SlotView{{ID: "s", Slot: "files.preview", Component: "C", Extensions: []string{"docx"}}}, Renderers: []contract.Renderer{{Kind: "approval-card", Tool: "t", Component: "C"}}, Pages: []string{"p"}, Navigation: []string{"p"}, HomeCommands: []apps.HomeCommandView{{ID: "h", Page: "p", Label: "H", Icon: "star"}}, ModelActions: []contract.ModelAction{{Capability: "FLAG_IMAGE", Page: "p"}}, ChatActions: []string{"a"}, Controls: []string{"network-policy"}}, Admin: &apps.AdminView{CanStart: true, CanStop: true, ManagementURL: "/x", HostOperations: &hostops.RequestSummary{Required: []string{"r"}}}}
|
||||
full := apps.App{ID: "a", Name: "a", Credentialed: true, ContextFiles: []contract.ContextFile{{Scope: "user", Path: "Memory/index.md"}}, UserSettings: []contract.UserSetting{{Key: "save-mode", Values: []string{"ask", "never"}, Default: "ask", Role: "memory.save-mode"}}, Network: &apps.NetworkView{Kind: contract.NetworkKindEgressCheck, Title: "N", Presets: []contract.NetworkPreset{{ID: "off", Title: "Closed"}}}, Title: "t", Titles: map[string]string{"en": "t"}, Description: "d", Descriptions: map[string]string{"en": "d"}, Language: "it", LocalizedTitle: "t", LocalizedDescription: "d", Icon: "i", Version: "1", InstalledAt: "2026-10-30T09:15:00Z", Generation: 1, Enabled: true, State: "running", Update: "update-pending", Operation: &apps.Operation{Kind: "upgrade", Phase: "downloading", Since: "2026-11-04T17:18:00Z"}, MainPage: "/m", HasUI: true, LaunchMode: "always", Tools: []string{"x"}, ToolCount: 1, Shadows: []string{"bash"}, ToolDefinitions: []contract.ToolDefinition{{Name: "x", Description: "d", Kind: "read", InputSchema: json.RawMessage(`{"type":"object"}`), UIOnly: true, Approval: contract.ApprovalAlways, ApprovalOverridable: true, Enumerates: true, Shadows: "bash", Execution: &contract.ExecutionDescriptor{Class: "commands", Title: "T", Note: "n", Skills: &contract.ExecutionSkills{ProvidesRuntimeFor: []string{"documents"}}}}}, Skills: []contract.SkillDefinition{{Name: "xlsx", Shadows: "xlsx", Slot: &contract.SlotDefinition{Kind: contract.SlotKindDocuments}}}, SkillShadows: []string{"xlsx"}, UI: &apps.UIView{Entry: "/e", CSS: []string{"/c"}, SDK: contract.VersionRange{Min: 1, Max: 1}, Actions: []apps.ChatActionView{{ID: "a", Label: "A", Labels: map[string]string{"en": "A"}, Icon: "star", PackagedIcon: true, Placement: "composer", Tool: "t", Component: "C"}}, Slots: []apps.SlotView{{ID: "s", Slot: "files.preview", Component: "C", Extensions: []string{"docx"}}}, Renderers: []contract.Renderer{{Kind: "approval-card", Tool: "t", Component: "C"}}, Pages: []string{"p"}, Navigation: []string{"p"}, HomeCommands: []apps.HomeCommandView{{ID: "h", Page: "p", Label: "H", Icon: "star"}}, ModelActions: []contract.ModelAction{{Capability: "FLAG_IMAGE", Page: "p"}}, ChatActions: []string{"a"}, Controls: []string{"network-policy"}}, Admin: &apps.AdminView{CanStart: true, CanStop: true, ManagementURL: "/x", HostOperations: &hostops.RequestSummary{Required: []string{"r"}}}}
|
||||
for name, v := range map[string]any{"apps.App": full, "apps.AdminView": *full.Admin, "apps.UIView": *full.UI, "apps.ChatActionView": full.UI.Actions[0], "apps.SlotView": full.UI.Slots[0], "contract.Renderer": full.UI.Renderers[0], "contract.VersionRange": full.UI.SDK, "contract.ToolDefinition": full.ToolDefinitions[0], "contract.ExecutionDescriptor": *full.ToolDefinitions[0].Execution, "contract.ExecutionSkills": *full.ToolDefinitions[0].Execution.Skills, "contract.SkillDefinition": full.Skills[0], "apps.List": apps.List{Apps: []apps.App{full}}, "apps.Operation": *full.Operation} {
|
||||
def, ok := doc.Definitions[name]
|
||||
Expect(ok).To(BeTrue(), name)
|
||||
|
||||
@@ -647,4 +647,44 @@ test.describe('Connector permissions', () => {
|
||||
await page.getByRole('button', { name: 'Edit' }).click()
|
||||
await expect(page.getByTestId('connector-permissions')).toHaveCount(0)
|
||||
})
|
||||
test('a tool the owner relaxed is listed as running without asking, for the owner and paired senders only', async ({ page }) => {
|
||||
const approvals = [
|
||||
{ tool: 'power-action', skip: true, runs_without_asking: ['owner', 'paired'] },
|
||||
{ tool: 'send-key', skip: false },
|
||||
]
|
||||
const a = api(page, [SET], {
|
||||
access: { ...ACCESS, approvals },
|
||||
preview: (body) => ({ ...previewFor(body, ROLES_KIND.roles), approvals }),
|
||||
})
|
||||
await a.install()
|
||||
await page.goto(PAGE)
|
||||
await page.getByRole('button', { name: 'Edit' }).click()
|
||||
// The saved view is the owner's own: the relaxed tool runs, the other asks.
|
||||
const saved = page.getByTestId('connector-access-approvals')
|
||||
await expect(saved.getByTestId('connector-approval-power-action')).toHaveAttribute('data-state', 'runs')
|
||||
await expect(saved.getByTestId('connector-approval-power-action')).toContainText('Runs without asking')
|
||||
await expect(saved.getByTestId('connector-approval-send-key')).toHaveAttribute('data-state', 'asks')
|
||||
// The preview follows the kind of sender chosen: only a paired one skips the card.
|
||||
const can = page.getByTestId('connector-can')
|
||||
const listed = can.getByTestId('connector-can-approvals')
|
||||
await expect(listed.getByTestId('connector-approval-power-action')).toHaveAttribute('data-state', 'other')
|
||||
await expect(listed.getByTestId('connector-approval-power-action')).toContainText('Still asks this sender')
|
||||
await can.getByTestId('connector-can-sender').selectOption('paired')
|
||||
await expect(can.getByTestId('connector-can-approvals').getByTestId('connector-approval-power-action')).toHaveAttribute('data-state', 'runs')
|
||||
await can.getByTestId('connector-can-sender').selectOption('channel_member')
|
||||
await expect(can.getByTestId('connector-can-approvals').getByTestId('connector-approval-power-action')).toHaveAttribute('data-state', 'other')
|
||||
await shot(page, 'perm-6-approvals')
|
||||
})
|
||||
|
||||
test('a run without asking appears in the latest decisions in plain words, with no typed text', async ({ page }) => {
|
||||
const recent = [{ at: '2026-11-03T09:30:00Z', senders: ['U001'], capability: 'tools.write', allowed: true, via: 'no-confirmation', reason: 'power-action: applied' }]
|
||||
const a = api(page, [SET], { access: { ...ACCESS, recent } })
|
||||
await a.install()
|
||||
await page.goto(PAGE)
|
||||
await page.getByRole('button', { name: 'Edit' }).click()
|
||||
const audit = page.getByTestId('connector-audit')
|
||||
await expect(audit).toContainText('ran without asking')
|
||||
await expect(audit).toContainText('power-action: applied')
|
||||
await expect(audit).not.toContainText('via no-confirmation')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,176 @@
|
||||
import { test, expect } from './coverage-fixtures.js'
|
||||
import { mockChat } from './chat-fixtures.js'
|
||||
import { installStudioAddons } from './studio-addon.js'
|
||||
import { connectorApi } from './connector-fixtures.js'
|
||||
|
||||
// The page of the PiKVM add-on (the real built bundle, inside the add-on
|
||||
// shell): the choice of what is still asked before a power action or a key
|
||||
// press, and the connection row that says which tools run without asking. The
|
||||
// core API and the add-on's tools are stubbed; the choice is a user setting of
|
||||
// the signed-in person, so the stub keeps it the way the host does.
|
||||
|
||||
const PAGE = '/app/addons/pikvm/apps'
|
||||
const SHOTS = process.env.UI_SHOTS_DIR
|
||||
const BINDING = { id: 'b1', name: 'PiKVM: pikvm.local', kind: 'pikvm', agent_id: '', credentials_only: true, enabled: false, accept_any: false, allow: [], resources: ['https://pikvm.local'], write_mode: 'propose', has_token: true }
|
||||
const KIND = { capabilities: [], kindExtra: { title: 'PiKVM', files: undefined, agent_optional: true } }
|
||||
const api = (page, initial = []) => connectorApi(page, initial, 'pikvm', [], undefined, KIND)
|
||||
|
||||
const DECLARED = ['power-action', 'send-key'].map(tool => ({ key: `approval-${tool}`, values: ['ask', 'skip'], default: 'ask' }))
|
||||
|
||||
async function shot(page, name) {
|
||||
if (SHOTS) await page.screenshot({ path: `${SHOTS}/${name}.png`, fullPage: true })
|
||||
}
|
||||
|
||||
// The user settings of the add-on and the access view that follows them. The
|
||||
// host reports a tool as running without asking only while the write mode of
|
||||
// the connection is not off.
|
||||
async function host(page, { declared = DECLARED, values = {}, failSave = false, writeMode = 'propose' } = {}) {
|
||||
const state = { values: { ...values }, puts: [], accessReads: 0 }
|
||||
const view = () => declared.map(d => ({ ...d, value: state.values[d.key] || d.default }))
|
||||
await page.route('**/api/addons/pikvm/user-settings', route => {
|
||||
if (route.request().method() === 'PUT') {
|
||||
const body = route.request().postDataJSON()
|
||||
state.puts.push(body)
|
||||
if (failSave) return route.fulfill({ status: 503, json: { error: { message: 'Settings unavailable' } } })
|
||||
state.values[body.key] = body.value
|
||||
}
|
||||
return route.fulfill({ json: { settings: view() } })
|
||||
})
|
||||
await page.route(/\/api\/chats\/connectors\/[^/?]+\/access(\?.*)?$/, route => {
|
||||
state.accessReads += 1
|
||||
const approvals = ['power-action', 'send-key'].map(tool => {
|
||||
const skip = state.values[`approval-${tool}`] === 'skip'
|
||||
return { tool, skip, ...(skip && writeMode !== 'off' ? { runs_without_asking: ['owner', 'paired'] } : {}) }
|
||||
})
|
||||
return route.fulfill({ json: { binding: 'b1', subjects: [], recent: [], approvals } })
|
||||
})
|
||||
return state
|
||||
}
|
||||
|
||||
// Gets and sets of the add-on's settings (switches by device) go through the gateway.
|
||||
const tools = ({ admin = true } = {}) => ({
|
||||
dispatch: (body) => {
|
||||
const a = body.arguments || {}
|
||||
const ok = (data) => ({ status: 200, contentType: 'application/json', body: JSON.stringify({ payload: { data } }) })
|
||||
if (a.tool === 'get-settings') return ok({ devices: [], defaults: { allowTyping: false, allowHardPower: false, maxTypeChars: 200 }, admin, broken: false, keys: ['a', 'enter'] })
|
||||
return null
|
||||
},
|
||||
})
|
||||
|
||||
test.describe('PiKVM add-on approvals', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await mockChat(page)
|
||||
})
|
||||
const install = (page) => installStudioAddons(page, { ids: ['pikvm'], dispatch: tools().dispatch })
|
||||
|
||||
test('every action asks by default, and typed text has no choice at all', async ({ page }) => {
|
||||
await install(page)
|
||||
await api(page, [BINDING]).install()
|
||||
await host(page)
|
||||
await page.goto(PAGE)
|
||||
const section = page.getByTestId('pikvm-approval')
|
||||
await expect(section).toBeVisible()
|
||||
for (const tool of ['power-action', 'send-key']) {
|
||||
const group = page.getByTestId(`pikvm-approval-${tool}`)
|
||||
await expect(group.getByRole('radio', { name: /Ask each time/ })).toBeChecked()
|
||||
await expect(group.getByRole('radio', { name: /Don't ask/ })).not.toBeChecked()
|
||||
}
|
||||
await expect(section.getByRole('radiogroup')).toHaveCount(2)
|
||||
await expect(page.getByTestId('pikvm-approval-text')).toContainText('Typed text always asks')
|
||||
await expect(page.getByTestId('pikvm-approval-text')).toContainText('no Don\'t ask')
|
||||
await expect(page.getByTestId('pikvm-approval-who')).toContainText('Everyone else is still asked')
|
||||
await expect(page.getByTestId('pikvm-approval-audit')).toContainText('written to the log')
|
||||
})
|
||||
|
||||
test('the power action warning is on the page before the choice is made, in plain words', async ({ page }) => {
|
||||
await install(page)
|
||||
await api(page, [BINDING]).install()
|
||||
await host(page)
|
||||
await page.goto(PAGE)
|
||||
const warn = page.getByTestId('pikvm-approval-warn-power-action')
|
||||
await expect(warn).toBeVisible()
|
||||
await expect(warn).toContainText('switch this machine off or reset it at once')
|
||||
await expect(warn).toContainText('unsaved work on it is lost')
|
||||
await expect(warn).toContainText('hard power actions')
|
||||
await expect(page.getByTestId('pikvm-approval-warn-send-key')).toContainText('no card to confirm')
|
||||
})
|
||||
|
||||
test('choosing Don\'t ask saves that one tool as the person\'s own setting and the row says it runs without asking', async ({ page }) => {
|
||||
await install(page)
|
||||
await api(page, [BINDING]).install()
|
||||
const h = await host(page)
|
||||
await page.goto(PAGE)
|
||||
const row = page.getByTestId('connector-row')
|
||||
await expect(row.getByTestId('connector-approval-power-action')).toHaveAttribute('data-state', 'asks')
|
||||
await shot(page, 'pikvm-1-default')
|
||||
await page.getByTestId('pikvm-approval-power-action').getByRole('radio', { name: /Don't ask/ }).click()
|
||||
await expect(page.getByTestId('pikvm-approval-status')).toHaveText('Saved. The next call follows it.')
|
||||
expect(h.puts).toEqual([{ key: 'approval-power-action', value: 'skip' }])
|
||||
await expect(page.getByTestId('pikvm-approval-power-action').getByRole('radio', { name: /Don't ask/ })).toBeChecked()
|
||||
await expect(page.getByTestId('pikvm-approval-send-key').getByRole('radio', { name: /Ask each time/ })).toBeChecked()
|
||||
// The effective view of the connection follows without a reload.
|
||||
await expect(row.getByTestId('connector-approval-power-action')).toHaveAttribute('data-state', 'runs')
|
||||
await expect(row.getByTestId('connector-approval-power-action')).toContainText('For the owner\'s own chats and for senders paired with the bot')
|
||||
await expect(row.getByTestId('connector-approval-send-key')).toHaveAttribute('data-state', 'asks')
|
||||
await shot(page, 'pikvm-2-power-skip')
|
||||
// Back to asking.
|
||||
await page.getByTestId('pikvm-approval-power-action').getByRole('radio', { name: /Ask each time/ }).click()
|
||||
await expect(row.getByTestId('connector-approval-power-action')).toHaveAttribute('data-state', 'asks')
|
||||
expect(h.puts.at(-1)).toEqual({ key: 'approval-power-action', value: 'ask' })
|
||||
})
|
||||
|
||||
test('a saved choice is read back when the page opens', async ({ page }) => {
|
||||
await install(page)
|
||||
await api(page, [BINDING]).install()
|
||||
await host(page, { values: { 'approval-send-key': 'skip' } })
|
||||
await page.goto(PAGE)
|
||||
await expect(page.getByTestId('pikvm-approval-send-key').getByRole('radio', { name: /Don't ask/ })).toBeChecked()
|
||||
await expect(page.getByTestId('pikvm-approval-power-action').getByRole('radio', { name: /Ask each time/ })).toBeChecked()
|
||||
await expect(page.getByTestId('connector-row').getByTestId('connector-approval-send-key')).toHaveAttribute('data-state', 'runs')
|
||||
})
|
||||
|
||||
test('a connection that is read only says that the choice does nothing', async ({ page }) => {
|
||||
await install(page)
|
||||
await api(page, [{ ...BINDING, write_mode: 'off' }]).install()
|
||||
await host(page, { values: { 'approval-power-action': 'skip' }, writeMode: 'off' })
|
||||
await page.goto(PAGE)
|
||||
const row = page.getByTestId('connector-row').getByTestId('connector-approval-power-action')
|
||||
await expect(row).toHaveAttribute('data-state', 'idle')
|
||||
await expect(row).toContainText('Read only')
|
||||
})
|
||||
|
||||
test('a save that fails keeps the earlier choice on screen and says so', async ({ page }) => {
|
||||
await install(page)
|
||||
await api(page, [BINDING]).install()
|
||||
const h = await host(page, { failSave: true })
|
||||
await page.goto(PAGE)
|
||||
await page.getByTestId('pikvm-approval-power-action').getByRole('radio', { name: /Don't ask/ }).click()
|
||||
await expect(page.getByTestId('pikvm-approval-failed')).toContainText('The earlier choice is still in force')
|
||||
expect(h.puts).toHaveLength(1)
|
||||
await expect(page.getByTestId('pikvm-approval-power-action').getByRole('radio', { name: /Ask each time/ })).toBeChecked()
|
||||
await expect(page.getByTestId('connector-row').getByTestId('connector-approval-power-action')).toHaveAttribute('data-state', 'asks')
|
||||
})
|
||||
|
||||
test('a package that declares no approval setting shows nothing about it', async ({ page }) => {
|
||||
await install(page)
|
||||
await api(page, [BINDING]).install()
|
||||
await host(page, { declared: [] })
|
||||
await page.goto(PAGE)
|
||||
await expect(page.getByTestId('pikvm-settings')).toBeVisible()
|
||||
await expect(page.getByTestId('pikvm-approval')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('the page follows the interface language: Italian words, no raw keys', async ({ page }) => {
|
||||
await page.addInitScript(() => localStorage.setItem('localai-language', 'it'))
|
||||
await install(page)
|
||||
await api(page, [BINDING]).install()
|
||||
await host(page)
|
||||
await page.goto(PAGE)
|
||||
const section = page.getByTestId('pikvm-approval')
|
||||
await expect(section.getByRole('heading', { name: 'Chiedere prima di agire' })).toBeVisible()
|
||||
await expect(page.getByTestId('pikvm-approval-power-action').getByRole('radio', { name: /Non chiedere/ })).toBeVisible()
|
||||
await expect(page.getByTestId('pikvm-approval-warn-power-action')).toContainText('spegnere questa macchina')
|
||||
await expect(section).not.toContainText('approval.')
|
||||
await shot(page, 'pikvm-3-italian')
|
||||
})
|
||||
})
|
||||
+2
-2
@@ -26,7 +26,7 @@ export async function buildAddonBundles(ids) {
|
||||
// The add-ons the specs serve from their build output: Studio, Talk and Voice
|
||||
// through installStudioAddons, Face, Fine-tuning and Memory through their own
|
||||
// routes.
|
||||
export async function buildStudioAddons(ids = ['studio', 'talk', 'voice', 'face', 'fine-tuning', 'telegram', 'slack', 'github', 'discord', 'home-assistant', 'jellyfin', 'memory']) {
|
||||
export async function buildStudioAddons(ids = ['studio', 'talk', 'voice', 'face', 'fine-tuning', 'telegram', 'slack', 'github', 'discord', 'home-assistant', 'jellyfin', 'memory', 'pikvm']) {
|
||||
await buildAddonBundles(ids)
|
||||
process.env[BUILT] = '1'
|
||||
}
|
||||
@@ -62,7 +62,7 @@ export function packagedApp(id, over = {}) {
|
||||
export async function installStudioAddons(page, { ids = ['studio', 'talk'], ...options } = {}) {
|
||||
const dirs = await Promise.all(ids.map(build))
|
||||
await stubAddonRoutes(page)
|
||||
await page.route(/\/api\/addons\/(studio|talk|voice|telegram|slack|github|discord|home-assistant|jellyfin)\/assets\//, route => {
|
||||
await page.route(/\/api\/addons\/(studio|talk|voice|telegram|slack|github|discord|home-assistant|jellyfin|pikvm)\/assets\//, route => {
|
||||
const parts = new URL(route.request().url()).pathname.split('/')
|
||||
const id = parts[3]
|
||||
const file = parts.slice(6).join('/')
|
||||
|
||||
@@ -5,7 +5,7 @@ weight = 24
|
||||
url = "/features/pikvm/"
|
||||
+++
|
||||
|
||||
The PiKVM add-on lets your [chats](/features/chats/) and [saved agents](/features/chats-agents/) look at a [PiKVM](https://pikvm.org) (or another device that runs the kvmd API) and the machine it controls: is it on, what is the device, is the keyboard connected. If you allow it, they can also press the power buttons of that machine and type on its keyboard. A **person confirms every one of those actions**, and typing and forced power actions are off until an administrator switches them on.
|
||||
The PiKVM add-on lets your [chats](/features/chats/) and [saved agents](/features/chats-agents/) look at a [PiKVM](https://pikvm.org) (or another device that runs the kvmd API) and the machine it controls: is it on, what is the device, is the keyboard connected. If you allow it, they can also press the power buttons of that machine and type on its keyboard. A **person confirms those actions** unless you choose otherwise for power actions and key presses (see [Not asking for some actions](#not-asking-for-some-actions)), and typing and forced power actions are off until an administrator switches them on.
|
||||
|
||||
A chat can answer questions such as:
|
||||
|
||||
@@ -58,9 +58,9 @@ The add-on then accepts that exact certificate for that exact address, and nothi
|
||||
| `get-info` | read | Model, board, platform, kvmd and streamer versions, kernel and CPU temperature. No serial number, host name or network details. |
|
||||
| `get-power-state` | read | Whether ATX control is enabled and busy, the power LED (on or off) and whether the disk LED shows activity. |
|
||||
| `get-keyboard-state` | read | Whether the keyboard of the device is connected to the target, and the Caps, Num and Scroll Lock LEDs. |
|
||||
| `power-action` | write, always confirmed | Press a power button: `on`, `off` or `reset`. The forced actions `off_hard`, `reset_hard` and `long_press` need a switch. |
|
||||
| `power-action` | write, confirmed (you can choose Don't ask) | Press a power button: `on`, `off` or `reset`. The forced actions `off_hard`, `reset_hard` and `long_press` need a switch. |
|
||||
| `type-text` | write, always confirmed, needs the typing switch | Type text on the target. |
|
||||
| `send-key` | write, always confirmed, needs the typing switch | Press keys from a short allowlist. |
|
||||
| `send-key` | write, confirmed (you can choose Don't ask), needs the typing switch | Press keys from a short allowlist. |
|
||||
|
||||
Two more tools, `get-settings` and `set-settings`, belong to the add-on page. A model never sees them.
|
||||
|
||||
@@ -68,7 +68,18 @@ Everything the device returns is marked as untrusted data, and the add-on's skil
|
||||
|
||||
## Changes and confirmation
|
||||
|
||||
All three write tools declare `approval: always` in the manifest. A person confirms **every call**, whatever the write mode of the connection is: the mode **Read only** refuses them all, and **Asks you first** stores each as a proposal. The write tools never act without a confirmation card. A proposal expires after 10 minutes (power) or 5 minutes (typing and keys).
|
||||
All three write tools declare `approval: always` in the manifest. By default a person confirms **every call**, whatever the write mode of the connection is: the mode **Read only** refuses them all, and **Asks you first** stores each as a proposal that waits for a confirmation card. `type-text` always asks. For `power-action` and `send-key` you can change that, as described next. A proposal expires after 10 minutes (power) or 5 minutes (typing and keys).
|
||||
|
||||
### Not asking for some actions
|
||||
|
||||
On the add-on page, the section **Ask before acting** has one choice for **Power actions** and one for **Key presses**: **Ask each time** (the default) or **Don't ask**. There is no choice for typed text, because it can enter a password or a command.
|
||||
|
||||
- The choice is yours alone. It is saved from your own browser session, it applies to all your PiKVM connections, and no chat, agent or model can change it.
|
||||
- With **Don't ask**, a chat can switch the machine off or reset it at once, with no card, and unsaved work on it is lost. If an administrator allowed the hard power actions, those run without asking too. Read the warning on the page before you choose it.
|
||||
- It holds only for your own chats and for messaging senders you paired with the bot. Senders that are only on the allowlist, senders let in because the bot accepts anyone, roles, scheduled runs and sub-agents are still asked.
|
||||
- A connection set to **Read only** runs nothing, whatever you choose.
|
||||
- Every run without asking is written to the server log and to the latest decisions of the connection, with the tool, the connection and the result. The typed text and the keys are not written.
|
||||
- The row of each connection lists which tools run without asking now.
|
||||
|
||||
The add-on notes an operation before it sends it and never sends one twice. The PiKVM API has no idempotency key. If the connection breaks after the request left, the result is "unknown", and you decide what to do. Read the power state to see what happened.
|
||||
|
||||
|
||||
Reference in new issue
Block a user