Add CodeMirror-based script editor, verified against real hardware
New Script section: loads the device's current Lua script (GET /api/script), edits it in a real code editor (CodeMirror 6 with Lua syntax highlighting via @codemirror/legacy-modes -- no dedicated @codemirror/lang-lua package exists, verified against the npm registry), and saves it back (POST /api/script) as raw text, not JSON, matching the device's endpoint. Not Blockly yet -- that's its own, much larger task; this is the raw-text step toward it. Verified end-to-end against a real ESP32-C6: load, edit, save hot-reloads on-device within one tick, and the change survives a power cycle. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
158
package-lock.json
generated
158
package-lock.json
generated
@@ -7,11 +7,106 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "kvida-sdk",
|
"name": "kvida-sdk",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/legacy-modes": "^6.0.0",
|
||||||
|
"codemirror": "^6.0.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.6.0",
|
"typescript": "^5.6.0",
|
||||||
"vite": "^6.0.0"
|
"vite": "^6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@codemirror/autocomplete": {
|
||||||
|
"version": "6.20.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz",
|
||||||
|
"integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.17.0",
|
||||||
|
"@lezer/common": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/commands": {
|
||||||
|
"version": "6.10.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.4.tgz",
|
||||||
|
"integrity": "sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.7.0",
|
||||||
|
"@codemirror/view": "^6.27.0",
|
||||||
|
"@lezer/common": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/language": {
|
||||||
|
"version": "6.12.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz",
|
||||||
|
"integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.23.0",
|
||||||
|
"@lezer/common": "^1.5.0",
|
||||||
|
"@lezer/highlight": "^1.0.0",
|
||||||
|
"@lezer/lr": "^1.0.0",
|
||||||
|
"style-mod": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/legacy-modes": {
|
||||||
|
"version": "6.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.3.tgz",
|
||||||
|
"integrity": "sha512-xCsmIzH78MyWkib9jlPaaun57XNkfbMIhagfaZVd0iLTqlpw3jXaIcbZm72MTmmn64eTZpBVNjbyYh+QXnxRsg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "^6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/lint": {
|
||||||
|
"version": "6.9.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz",
|
||||||
|
"integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.42.0",
|
||||||
|
"crelt": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/search": {
|
||||||
|
"version": "6.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.1.tgz",
|
||||||
|
"integrity": "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.37.0",
|
||||||
|
"crelt": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/state": {
|
||||||
|
"version": "6.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.1.tgz",
|
||||||
|
"integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@marijn/find-cluster-break": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/view": {
|
||||||
|
"version": "6.43.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.6.tgz",
|
||||||
|
"integrity": "sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.7.0",
|
||||||
|
"crelt": "^1.0.6",
|
||||||
|
"style-mod": "^4.1.0",
|
||||||
|
"w3c-keyname": "^2.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||||
@@ -454,6 +549,36 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@lezer/common": {
|
||||||
|
"version": "1.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz",
|
||||||
|
"integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/highlight": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/lr": {
|
||||||
|
"version": "1.4.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz",
|
||||||
|
"integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@marijn/find-cluster-break": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.62.2",
|
"version": "4.62.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
|
||||||
@@ -811,6 +936,27 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/codemirror": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
|
"@codemirror/commands": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/lint": "^6.0.0",
|
||||||
|
"@codemirror/search": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/crelt": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||||
@@ -1009,6 +1155,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/style-mod": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.17",
|
"version": "0.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||||
@@ -1114,6 +1266,12 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/w3c-keyname": {
|
||||||
|
"version": "2.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||||
|
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||||
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,11 @@
|
|||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/legacy-modes": "^6.0.0",
|
||||||
|
"codemirror": "^6.0.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.6.0",
|
"typescript": "^5.6.0",
|
||||||
"vite": "^6.0.0"
|
"vite": "^6.0.0"
|
||||||
|
|||||||
@@ -55,3 +55,26 @@ export async function saveMqttConfig(address: string, config: SaveMqttConfig): P
|
|||||||
throw new Error(data.error ?? `POST /api/mqtt failed: ${res.status}`);
|
throw new Error(data.error ?? `POST /api/mqtt failed: ${res.status}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unlike the rest of this API, /api/script is raw Lua text in both
|
||||||
|
// directions, not JSON -- see kvida-os's transport README for why
|
||||||
|
// (scripts contain quotes/backslashes/newlines the device's hand-rolled,
|
||||||
|
// non-escaping JSON parser can't round-trip safely).
|
||||||
|
export async function getScript(address: string): Promise<string> {
|
||||||
|
const res = await fetch(`${baseUrl(address)}/api/script`);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`GET /api/script failed: ${res.status}`);
|
||||||
|
}
|
||||||
|
return res.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveScript(address: string, script: string): Promise<void> {
|
||||||
|
const res = await fetch(`${baseUrl(address)}/api/script`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'text/plain' },
|
||||||
|
body: script,
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`POST /api/script failed: ${res.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
28
src/editor.ts
Normal file
28
src/editor.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// Thin wrapper around CodeMirror 6 for editing a device's Lua script.
|
||||||
|
// There's no dedicated @codemirror/lang-lua package (verified against
|
||||||
|
// the npm registry and @codemirror/legacy-modes' source) -- Lua
|
||||||
|
// highlighting comes from the legacy StreamLanguage-based mode instead.
|
||||||
|
|
||||||
|
import { EditorView, basicSetup } from 'codemirror';
|
||||||
|
import { StreamLanguage } from '@codemirror/language';
|
||||||
|
import { lua } from '@codemirror/legacy-modes/mode/lua';
|
||||||
|
|
||||||
|
const luaLanguage = StreamLanguage.define(lua);
|
||||||
|
|
||||||
|
export function createScriptEditor(parent: HTMLElement, initialDoc: string): EditorView {
|
||||||
|
return new EditorView({
|
||||||
|
parent,
|
||||||
|
doc: initialDoc,
|
||||||
|
extensions: [basicSetup, luaLanguage],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEditorText(view: EditorView): string {
|
||||||
|
return view.state.doc.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setEditorText(view: EditorView, text: string): void {
|
||||||
|
view.dispatch({
|
||||||
|
changes: { from: 0, to: view.state.doc.length, insert: text },
|
||||||
|
});
|
||||||
|
}
|
||||||
54
src/main.ts
54
src/main.ts
@@ -1,4 +1,6 @@
|
|||||||
import { getMqttConfig, getStatus, saveMqttConfig } from './device';
|
import { getMqttConfig, getScript, getStatus, saveMqttConfig, saveScript } from './device';
|
||||||
|
import { createScriptEditor, getEditorText, setEditorText } from './editor';
|
||||||
|
import type { EditorView } from 'codemirror';
|
||||||
import './style.css';
|
import './style.css';
|
||||||
|
|
||||||
const STORAGE_KEY = 'kvida-sdk:device-address';
|
const STORAGE_KEY = 'kvida-sdk:device-address';
|
||||||
@@ -30,6 +32,14 @@ app.innerHTML = `
|
|||||||
<button type="submit">Save</button>
|
<button type="submit">Save</button>
|
||||||
<p id="mqtt-status"></p>
|
<p id="mqtt-status"></p>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<section id="script-section" hidden>
|
||||||
|
<h2>Script</h2>
|
||||||
|
<div id="script-editor"></div>
|
||||||
|
<button id="script-load">Reload from device</button>
|
||||||
|
<button id="script-save">Save (hot-reloads on device)</button>
|
||||||
|
<p id="script-status"></p>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -41,9 +51,23 @@ const hostInput = document.querySelector<HTMLInputElement>('#host')!;
|
|||||||
const portInput = document.querySelector<HTMLInputElement>('#port')!;
|
const portInput = document.querySelector<HTMLInputElement>('#port')!;
|
||||||
const usernameInput = document.querySelector<HTMLInputElement>('#username')!;
|
const usernameInput = document.querySelector<HTMLInputElement>('#username')!;
|
||||||
const passwordInput = document.querySelector<HTMLInputElement>('#password')!;
|
const passwordInput = document.querySelector<HTMLInputElement>('#password')!;
|
||||||
|
const scriptSection = document.querySelector<HTMLElement>('#script-section')!;
|
||||||
|
const scriptEditorContainer = document.querySelector<HTMLDivElement>('#script-editor')!;
|
||||||
|
const scriptStatus = document.querySelector<HTMLParagraphElement>('#script-status')!;
|
||||||
|
|
||||||
addressInput.value = localStorage.getItem(STORAGE_KEY) ?? '';
|
addressInput.value = localStorage.getItem(STORAGE_KEY) ?? '';
|
||||||
|
|
||||||
|
let scriptEditor: EditorView | null = null;
|
||||||
|
|
||||||
|
async function loadScriptIntoEditor(address: string) {
|
||||||
|
const script = await getScript(address);
|
||||||
|
if (!scriptEditor) {
|
||||||
|
scriptEditor = createScriptEditor(scriptEditorContainer, script);
|
||||||
|
} else {
|
||||||
|
setEditorText(scriptEditor, script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function connect() {
|
async function connect() {
|
||||||
const address = addressInput.value.trim();
|
const address = addressInput.value.trim();
|
||||||
if (!address) {
|
if (!address) {
|
||||||
@@ -53,6 +77,7 @@ async function connect() {
|
|||||||
|
|
||||||
statusLine.textContent = 'Connecting...';
|
statusLine.textContent = 'Connecting...';
|
||||||
mqttForm.hidden = true;
|
mqttForm.hidden = true;
|
||||||
|
scriptSection.hidden = true;
|
||||||
try {
|
try {
|
||||||
const status = await getStatus(address);
|
const status = await getStatus(address);
|
||||||
statusLine.textContent =
|
statusLine.textContent =
|
||||||
@@ -65,6 +90,10 @@ async function connect() {
|
|||||||
usernameInput.value = mqtt.username;
|
usernameInput.value = mqtt.username;
|
||||||
passwordInput.value = '';
|
passwordInput.value = '';
|
||||||
mqttForm.hidden = false;
|
mqttForm.hidden = false;
|
||||||
|
|
||||||
|
scriptStatus.textContent = '';
|
||||||
|
await loadScriptIntoEditor(address);
|
||||||
|
scriptSection.hidden = false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
statusLine.textContent = `Failed to reach device: ${err}`;
|
statusLine.textContent = `Failed to reach device: ${err}`;
|
||||||
}
|
}
|
||||||
@@ -88,6 +117,29 @@ mqttForm.addEventListener('submit', async (e) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.querySelector<HTMLButtonElement>('#script-load')!.addEventListener('click', async () => {
|
||||||
|
scriptStatus.textContent = 'Loading...';
|
||||||
|
try {
|
||||||
|
await loadScriptIntoEditor(addressInput.value.trim());
|
||||||
|
scriptStatus.textContent = 'Loaded from device.';
|
||||||
|
} catch (err) {
|
||||||
|
scriptStatus.textContent = `Failed to load: ${err}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector<HTMLButtonElement>('#script-save')!.addEventListener('click', async () => {
|
||||||
|
if (!scriptEditor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scriptStatus.textContent = 'Saving...';
|
||||||
|
try {
|
||||||
|
await saveScript(addressInput.value.trim(), getEditorText(scriptEditor));
|
||||||
|
scriptStatus.textContent = 'Saved. Device hot-reloaded the script.';
|
||||||
|
} catch (err) {
|
||||||
|
scriptStatus.textContent = `Failed to save: ${err}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (addressInput.value) {
|
if (addressInput.value) {
|
||||||
connect();
|
connect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,28 @@
|
|||||||
body {
|
body {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
max-width: 360px;
|
max-width: 720px;
|
||||||
margin: 2em auto;
|
margin: 2em auto;
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#mqtt-form,
|
||||||
|
#status-line {
|
||||||
|
max-width: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#script-editor {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#script-editor .cm-editor {
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#script-editor .cm-scroller {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
|
|||||||
Reference in New Issue
Block a user