Compare commits

...

6 Commits

Author SHA1 Message Date
Gunnar Skjold
4d128700c1 Added UI readme 2026-03-05 16:40:05 +01:00
Gunnar Skjold
6743750d8f Made proxy target configurable 2026-03-05 16:39:20 +01:00
Gunnar Skjold
640e957065 Optimizing footprint 2026-03-05 16:34:10 +01:00
Gunnar Skjold
d4f11c0412 Updated node version in workflows 2026-03-05 16:22:40 +01:00
Gunnar Skjold
01acc6d6e8 Consolidated new routes and old components 2026-03-05 16:22:19 +01:00
Gunnar Skjold
e89bb53941 Initial changes to migrate to Svelte 5 2026-03-05 15:51:06 +01:00
29 changed files with 1858 additions and 1719 deletions

View File

@@ -51,7 +51,7 @@ jobs:
- name: Set up node - name: Set up node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '19.x' node-version: '22.x'
- name: Build with node - name: Build with node
run: | run: |
cd lib/SvelteUi/app cd lib/SvelteUi/app

View File

@@ -73,7 +73,7 @@ jobs:
- name: Set up node - name: Set up node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '19.x' node-version: '22.x'
- name: Build with node - name: Build with node
run: | run: |
cd lib/SvelteUi/app cd lib/SvelteUi/app

View File

@@ -0,0 +1,59 @@
# SvelteUi App
Web interface for AMS Reader firmware built with Svelte 5 and Vite 6.
## Development Setup
### Prerequisites
- Node.js 20.x or 22.x LTS (required for Vite 6)
- npm
### Local Development Configuration
To develop against your AMS reader device, you need to configure the proxy target:
1. Copy the example config file:
```bash
cp vite.config.local.example.js vite.config.local.js
```
2. Edit `vite.config.local.js` and update the IP address to match your device:
```javascript
export default {
proxyTarget: "http://192.168.1.100" // Your device's IP
}
```
3. The `vite.config.local.js` file is gitignored, so your personal settings won't be committed.
### Running Development Server
```bash
npm install
npm run dev
```
The dev server will proxy API requests to your configured device IP.
### Building for Production
```bash
npm run build
```
The build output will be in the `dist/` directory.
## Project Structure
- `src/` - Application source code
- `routes/` - Page components using svelte-spa-router
- `lib/` - Shared components and utilities
- `public/` - Static assets (favicon, etc.)
- `dist/` - Build output (not committed to git)
## Key Technologies
- **Svelte 5.17.0** - UI framework
- **Vite 6.0.7** - Build tool
- **svelte-spa-router 4.0.1** - Hash-based routing
- **Tailwind CSS** - Styling

File diff suppressed because one or more lines are too long

View File

@@ -8,10 +8,9 @@
<link rel="mask-icon" href="/favicon.svg" color="#000000"> <link rel="mask-icon" href="/favicon.svg" color="#000000">
<title>AMS reader</title> <title>AMS reader</title>
<script type="module" crossorigin src="/index.js"></script> <script type="module" crossorigin src="/index.js"></script>
<link rel="stylesheet" href="/index.css"> <link rel="stylesheet" crossorigin href="/index.css">
</head> </head>
<body class="bg-gray-100 dark:bg-gray-900"> <body class="bg-gray-100 dark:bg-gray-900">
<div id="app"></div> <div id="app"></div>
</body> </body>
</html> </html>

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -9,28 +9,22 @@
"build": "vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview"
}, },
"overrides": {
"svelte-navigator": {
"svelte": ">=4.x"
}
},
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^2.1.0", "@sveltejs/vite-plugin-svelte": "^5.0.2",
"@tailwindcss/forms": "^0.5.3", "@tailwindcss/forms": "^0.5.3",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"http-proxy-middleware": "^2.0.9", "http-proxy-middleware": "^2.0.9",
"postcss": "^8.4.31", "postcss": "^8.4.31",
"postcss-load-config": "^4.0.1", "postcss-load-config": "^4.0.1",
"svelte": "^4.2.19", "svelte": "^5.17.0",
"svelte-navigator": "^3.2.2", "svelte-spa-router": "^4.0.1",
"svelte-preprocess": "^5.0.3", "svelte-preprocess": "^6.0.3",
"svelte-qrcode": "^1.0.0", "svelte-qrcode": "^1.0.0",
"tailwindcss": "^3.3.1", "tailwindcss": "^3.3.1",
"vite": "^4.5.14" "vite": "^6.0.7"
}, },
"dependencies": { "dependencies": {
"cssnano": "^5.1.15", "cssnano": "^5.1.15",
"esbuild": ">=0.25.0", "esbuild": ">=0.25.0"
"ipaddr.js": "^2.3.0"
} }
} }

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
<title>Amsleser</title>
<g transform="translate(-29.5,-83)">
<circle r="4.8016944" cy="123.56455" cx="55.064552"
style="fill:none;stroke:#045c7c;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path d="m 41.298717,103.9049 a 24,24 0 0 1 27.531669,0"
style="fill:none;stroke:#045c7c;stroke-width:3.3;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path d="m 35.562952,95.713384 a 34,34 0 0 1 39.003199,-2e-6"
style="fill:none;stroke:#045c7c;stroke-width:3.3;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path d="m 47.034482,112.09642 a 14,14 0 0 1 16.06014,0"
style="fill:none;stroke:#045c7c;stroke-width:3.3;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle r="3" cy="105.99158" cx="38.181862"
style="fill:none;stroke:#045c7c;stroke-width:2.4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle r="3" cy="97.959579" cx="77.491386"
style="fill:none;stroke:#045c7c;stroke-width:2.4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,49 +1,27 @@
<script> <script>
import { Router, Route, navigate } from "svelte-navigator"; import Router from "svelte-spa-router";
import { getTariff, tariffStore, sysinfoStore, dataStore, importPricesStore, exportPricesStore, dayPlotStore, monthPlotStore, temperaturesStore, getSysinfo } from './lib/DataStores.js'; import { push } from "svelte-spa-router";
import { getTariff, sysinfoStore, dataStore, getSysinfo } from './lib/DataStores.js';
import { translationsStore, getTranslations } from "./lib/TranslationService.js"; import { translationsStore, getTranslations } from "./lib/TranslationService.js";
import Favicon from './assets/favicon.svg'; // Need this for the build
import Header from './lib/Header.svelte'; import Header from './lib/Header.svelte';
import Dashboard from './lib/Dashboard.svelte'; import DashboardRoute from './routes/DashboardRoute.svelte';
import ConfigurationPanel from './lib/ConfigurationPanel.svelte'; import ConfigurationRoute from './routes/ConfigurationRoute.svelte';
import StatusPage from './lib/StatusPage.svelte'; import StatusRoute from './routes/StatusRoute.svelte';
import VendorPanel from './lib/VendorPanel.svelte'; import PriceConfigRoute from './routes/PriceConfigRoute.svelte';
import SetupPanel from './lib/SetupPanel.svelte'; import MqttCaRoute from './routes/MqttCaRoute.svelte';
import MqttCertRoute from './routes/MqttCertRoute.svelte';
import MqttKeyRoute from './routes/MqttKeyRoute.svelte';
import ConsentRoute from './routes/ConsentRoute.svelte';
import SetupRoute from './routes/SetupRoute.svelte';
import VendorRoute from './routes/VendorRoute.svelte';
import EditDayRoute from './routes/EditDayRoute.svelte';
import EditMonthRoute from './routes/EditMonthRoute.svelte';
import Mask from './lib/Mask.svelte'; import Mask from './lib/Mask.svelte';
import FileUploadComponent from "./lib/FileUploadComponent.svelte";
import ConsentComponent from "./lib/ConsentComponent.svelte";
import PriceConfig from "./lib/PriceConfig.svelte";
import DataEdit from "./lib/DataEdit.svelte";
import { updateRealtime } from "./lib/RealtimeStore.js"; import { updateRealtime } from "./lib/RealtimeStore.js";
let basepath = document.getElementsByTagName('base')[0].getAttribute("href"); let basepath = document.getElementsByTagName('base')[0].getAttribute("href");
if(!basepath) basepath = "/"; if(!basepath) basepath = "/";
let importPrices;
importPricesStore.subscribe(update => {
importPrices = update;
});
let exportPrices;
exportPricesStore.subscribe(update => {
exportPrices = update;
});
let dayPlot;
dayPlotStore.subscribe(update => {
dayPlot = update;
});
let monthPlot;
monthPlotStore.subscribe(update => {
monthPlot = update;
});
let temperatures;
temperaturesStore.subscribe(update => {
temperatures = update;
});
let translations = {}; let translations = {};
translationsStore.subscribe(update => { translationsStore.subscribe(update => {
translations = update; translations = update;
@@ -56,11 +34,11 @@
sysinfoStore.subscribe(update => { sysinfoStore.subscribe(update => {
sysinfo = update; sysinfo = update;
if(sysinfo.vndcfg === false) { if(sysinfo.vndcfg === false) {
navigate(basepath + "vendor"); push("/vendor");
} else if(sysinfo.usrcfg === false) { } else if(sysinfo.usrcfg === false) {
navigate(basepath + "setup"); push("/setup");
} else if(sysinfo.fwconsent === 0) { } else if(sysinfo.fwconsent === 0) {
navigate(basepath + "consent"); push("/consent");
} }
if(sysinfo.ui.k === 1) { if(sysinfo.ui.k === 1) {
@@ -94,53 +72,26 @@
updateRealtime(update); updateRealtime(update);
}); });
let tariffData = {};
tariffStore.subscribe(update => {
tariffData = update;
});
getTariff(); getTariff();
</script> </script>
<div class="container mx-auto m-3"> <div class="container mx-auto m-3">
<Router basepath={basepath}> <Header data={data} basepath={basepath}/>
<Header data={data} basepath={basepath}/>
<Route path="/"> <Router routes={{
<Dashboard data={data} sysinfo={sysinfo} importPrices={importPrices} exportPrices={exportPrices} dayPlot={dayPlot} monthPlot={monthPlot} temperatures={temperatures} translations={translations} tariffData={tariffData}/> '/': DashboardRoute,
</Route> '/configuration': ConfigurationRoute,
<Route path="/configuration"> '/priceconfig': PriceConfigRoute,
<ConfigurationPanel sysinfo={sysinfo} basepath={basepath} data={data}/> '/status': StatusRoute,
</Route> '/mqtt-ca': MqttCaRoute,
<Route path="/priceconfig"> '/mqtt-cert': MqttCertRoute,
<PriceConfig basepath={basepath}/> '/mqtt-key': MqttKeyRoute,
</Route> '/consent': ConsentRoute,
<Route path="/status"> '/setup': SetupRoute,
<StatusPage sysinfo={sysinfo} data={data}/> '/vendor': VendorRoute,
</Route> '/edit-day': EditDayRoute,
<Route path="/mqtt-ca"> '/edit-month': EditMonthRoute,
<FileUploadComponent title="CA" action="/mqtt-ca"/> }} />
</Route>
<Route path="/mqtt-cert">
<FileUploadComponent title="certificate" action="/mqtt-cert"/>
</Route>
<Route path="/mqtt-key">
<FileUploadComponent title="private key" action="/mqtt-key"/>
</Route>
<Route path="/consent">
<ConsentComponent sysinfo={sysinfo} basepath={basepath}/>
</Route>
<Route path="/setup">
<SetupPanel sysinfo={sysinfo}/>
</Route>
<Route path="/vendor">
<VendorPanel sysinfo={sysinfo} basepath={basepath}/>
</Route>
<Route path="/edit-day">
<DataEdit prefix="UTC Hour" data={dayPlot} url="/dayplot" basepath={basepath}/>
</Route>
<Route path="/edit-month">
<DataEdit prefix="Day" data={monthPlot} url="/monthplot" basepath={basepath}/>
</Route>
</Router>
{#if sysinfo.booting} {#if sysinfo.booting}
{#if sysinfo.trying} {#if sysinfo.trying}

View File

@@ -1,5 +1,4 @@
<script> <script>
import { Link } from "svelte-navigator";
import { tooltip } from './tooltip'; import { tooltip } from './tooltip';
export let config; export let config;
@@ -47,7 +46,7 @@
{#if config.link} {#if config.link}
<div class="text-xs text-right"> <div class="text-xs text-right">
{#if config.link.route} {#if config.link.route}
<Link to={config.link.url}>{config.link.text}</Link> <a href={"#" + config.link.url}>{config.link.text}</a>
{:else} {:else}
<a href={config.link.url} target={config.link.target}>{config.link.text}</a> <a href={config.link.url} target={config.link.target}>{config.link.text}</a>
{/if} {/if}

View File

@@ -1,6 +1,6 @@
<script> <script>
import { translationsStore } from './TranslationService'; import { translationsStore } from './TranslationService';
import { navigate } from 'svelte-navigator'; import { push } from 'svelte-spa-router';
import Mask from './Mask.svelte' import Mask from './Mask.svelte'
export let prefix; export let prefix;
@@ -59,7 +59,7 @@
let res = (await response.json()) let res = (await response.json())
saving = false; saving = false;
navigate(basepath); push(basepath);
} }
</script> </script>
<form on:submit|preventDefault={handleSubmit} autocomplete="off"> <form on:submit|preventDefault={handleSubmit} autocomplete="off">
@@ -70,7 +70,7 @@
{#each importElements as el} {#each importElements as el}
<label class="flex w-60 m-1"> <label class="flex w-60 m-1">
<span class="in-pre">{el.name}</span> <span class="in-pre">{el.name}</span>
<input name="{el.key}" bind:value={data[el.key]} type="number" step="0.01" class="in-txt w-full text-right"/> <input name="{el.key}" bind:value={data[el.key]} type="number" step="0.001" class="in-txt w-full text-right"/>
<span class="in-post">kWh</span> <span class="in-post">kWh</span>
</label> </label>
{/each} {/each}
@@ -82,7 +82,7 @@
{#each exportElements as el} {#each exportElements as el}
<label class="flex w-60 m-1"> <label class="flex w-60 m-1">
<span class="in-pre">{el.name}</span> <span class="in-pre">{el.name}</span>
<input name="{el.key}" bind:value={data[el.key]} type="number" step="0.01" class="in-txt w-full text-right"/> <input name="{el.key}" bind:value={data[el.key]} type="number" step="0.001" class="in-txt w-full text-right"/>
<span class="in-post">kWh</span> <span class="in-post">kWh</span>
</label> </label>
{/each} {/each}

View File

@@ -1,5 +1,4 @@
<script> <script>
import { Link } from "svelte-navigator";
import { sysinfoStore } from './DataStores.js'; import { sysinfoStore } from './DataStores.js';
import { upgrade, upgradeWarningText } from './UpgradeHelper'; import { upgrade, upgradeWarningText } from './UpgradeHelper';
import { boardtype, isBusPowered, wiki, bcol } from './Helpers.js'; import { boardtype, isBusPowered, wiki, bcol } from './Helpers.js';
@@ -46,7 +45,7 @@
<nav class="hdr"> <nav class="hdr">
<div class="flex flex-wrap space-x-4 text-sm text-gray-300"> <div class="flex flex-wrap space-x-4 text-sm text-gray-300">
<div class="flex text-lg text-gray-100 p-2"> <div class="flex text-lg text-gray-100 p-2">
<Link to="/">AMS reader <span>{sysinfo.version}</span></Link> <a href={basepath}>AMS reader <span>{sysinfo.version}</span></a>
</div> </div>
<div class="flex-none my-auto p-2 flex space-x-4"> <div class="flex-none my-auto p-2 flex space-x-4">
<div class="flex-none my-auto"><Uptime epoch={data.u}/></div> <div class="flex-none my-auto"><Uptime epoch={data.u}/></div>
@@ -79,10 +78,10 @@
</div> </div>
{#if sysinfo.vndcfg && sysinfo.usrcfg} {#if sysinfo.vndcfg && sysinfo.usrcfg}
<div class="flex-none px-1 mt-1" title={translations.header?.config ?? ""}> <div class="flex-none px-1 mt-1" title={translations.header?.config ?? ""}>
<Link to="/configuration"><GearIcon/></Link> <a href="#/configuration"><GearIcon/></a>
</div> </div>
<div class="flex-none px-1 mt-1" title={translations.header?.status ?? ""}> <div class="flex-none px-1 mt-1" title={translations.header?.status ?? ""}>
<Link to="/status"><InfoIcon/></Link> <a href="#/status"><InfoIcon/></a>
</div> </div>
{/if} {/if}
<div class="flex-none px-1 mt-1" title={translations.header?.doc ?? ""}> <div class="flex-none px-1 mt-1" title={translations.header?.doc ?? ""}>

View File

@@ -41,22 +41,22 @@
for(i = 0; i < tariffData.p.length; i++) { for(i = 0; i < tariffData.p.length; i++) {
let peak = tariffData.p[i]; let peak = tariffData.p[i];
let title = ""; let peakTitle = "";
let daylabel = "-"; let daylabel = "-";
if(peak.d > 0) { if(peak.d > 0) {
daylabel = zeropad(peak.d) + "."; daylabel = zeropad(peak.d) + ".";
title = zeropad(peak.d) + "." + (translations.months ? translations.months?.[new Date().getMonth()] : zeropad(new Date().getMonth()+1)); peakTitle = zeropad(peak.d) + "." + (translations.months ? translations.months?.[new Date().getMonth()] : zeropad(new Date().getMonth()+1));
if(tariffData.p.length < 4) { if(tariffData.p.length < 4) {
daylabel = title; daylabel = peakTitle;
} }
} }
if(!isNaN(peak.h)) if(!isNaN(peak.h))
title = title + ' ' + zeropad(peak.h) + ':00'; peakTitle = peakTitle + ' ' + zeropad(peak.h) + ':00';
title = title + ': ' + peak.v.toFixed(2) + ' kWh'; peakTitle = peakTitle + ': ' + peak.v.toFixed(2) + ' kWh';
points.push({ points.push({
label: peak.v.toFixed(2), label: peak.v.toFixed(2),
value: peak.v, value: peak.v,
title: title, title: peakTitle,
color: dark ? '#5c2da5' : '#7c3aed' color: dark ? '#5c2da5' : '#7c3aed'
}); });
xTicks.push({ xTicks.push({

View File

@@ -1,7 +1,8 @@
import "./app.postcss"; import "./app.postcss";
import { mount } from "svelte";
import App from "./App.svelte"; import App from "./App.svelte";
const app = new App({ const app = mount(App, {
target: document.getElementById("app"), target: document.getElementById("app"),
}); });

View File

@@ -1,21 +1,24 @@
<script> <script>
import { getConfiguration, configurationStore } from './ConfigurationStore' import { getConfiguration, configurationStore } from '../lib/ConfigurationStore'
import { sysinfoStore, networksStore } from './DataStores.js'; import { sysinfoStore, networksStore, dataStore } from '../lib/DataStores.js';
import fetchWithTimeout from './fetchWithTimeout'; import fetchWithTimeout from '../lib/fetchWithTimeout';
import { translationsStore } from './TranslationService'; import { translationsStore } from '../lib/TranslationService';
import { wiki, ipPattern, asciiPattern, asciiPatternExt, charAndNumPattern, hexPattern, numPattern, isBusPowered } from './Helpers.js'; import { wiki, ipPattern, asciiPattern, asciiPatternExt, charAndNumPattern, hexPattern, numPattern, isBusPowered } from '../lib/Helpers.js';
import UartSelectOptions from './UartSelectOptions.svelte'; import UartSelectOptions from '../lib/UartSelectOptions.svelte';
import Mask from './Mask.svelte' import Mask from '../lib/Mask.svelte'
import Badge from './Badge.svelte'; import Badge from '../lib/Badge.svelte';
import CountrySelectOptions from './CountrySelectOptions.svelte'; import CountrySelectOptions from '../lib/CountrySelectOptions.svelte';
import { Link, navigate } from 'svelte-navigator'; import { push } from 'svelte-spa-router';
import SubnetOptions from './SubnetOptions.svelte'; import SubnetOptions from '../lib/SubnetOptions.svelte';
import QrCode from 'svelte-qrcode'; import QrCode from 'svelte-qrcode';
export let basepath = "/"; let basepath = "/";
export let sysinfo = {}; let sysinfo = {};
export let data; let data;
sysinfoStore.subscribe(v => sysinfo = v);
dataStore.subscribe(v => data = v);
let translations = {}; let translations = {};
translationsStore.subscribe(update => { translationsStore.subscribe(update => {
translations = update; translations = update;
@@ -150,7 +153,7 @@
}); });
saving = false; saving = false;
navigate(basepath); push(basepath);
} }
async function reboot() { async function reboot() {
@@ -336,7 +339,7 @@
</div> </div>
</div> </div>
<div class="my-1"> <div class="my-1">
<Link to="/priceconfig" class="text-blue-600 hover:text-blue-800">{translations.conf?.price?.conf ?? "Configure"}</Link> <a href="#/priceconfig" class="text-blue-600 hover:text-blue-800">{translations.conf?.price?.conf ?? "Configure"}</a>
</div> </div>
<div class="my-1"> <div class="my-1">
<label><input type="checkbox" name="pe" value="true" bind:checked={configuration.p.e} class="rounded mb-1"/> {translations.conf?.price?.enabled ?? "Enabled"}</label> <label><input type="checkbox" name="pe" value="true" bind:checked={configuration.p.e} class="rounded mb-1"/> {translations.conf?.price?.enabled ?? "Enabled"}</label>
@@ -603,28 +606,28 @@
<div class="my-1 flex"> <div class="my-1 flex">
<span class="flex pr-2"> <span class="flex pr-2">
{#if configuration.q.s.c} {#if configuration.q.s.c}
<span class="bd-on"><Link to="/mqtt-ca">{translations.conf?.mqtt?.ca_ok ?? "CA OK"}</Link></span> <span class="bd-on"><a href="#/mqtt-ca">{translations.conf?.mqtt?.ca_ok ?? "CA OK"}</a></span>
<span class="bd-off" on:click={askDeleteCa} on:keypress={askDeleteCa}>&#128465;</span> <span class="bd-off" on:click={askDeleteCa} on:keypress={askDeleteCa}>&#128465;</span>
{:else} {:else}
<Link to="/mqtt-ca"><Badge color="blue" text={translations.conf?.mqtt?.btn_ca_upload ?? "Upload CA"} title={translations.conf?.mqtt?.title_ca ?? ""}/></Link> <a href="#/mqtt-ca"><Badge color="blue" text={translations.conf?.mqtt?.btn_ca_upload ?? "Upload CA"} title={translations.conf?.mqtt?.title_ca ?? ""}/></a>
{/if} {/if}
</span> </span>
<span class="flex pr-2"> <span class="flex pr-2">
{#if configuration.q.s.r} {#if configuration.q.s.r}
<span class="bd-on"><Link to="/mqtt-cert">{translations.conf?.mqtt?.crt_ok ?? "Cert OK"}</Link></span> <span class="bd-on"><a href="#/mqtt-cert">{translations.conf?.mqtt?.crt_ok ?? "Cert OK"}</a></span>
<span class="bd-off" on:click={askDeleteCert} on:keypress={askDeleteCert}>&#128465;</span> <span class="bd-off" on:click={askDeleteCert} on:keypress={askDeleteCert}>&#128465;</span>
{:else} {:else}
<Link to="/mqtt-cert"><Badge color="blue" text={translations.conf?.mqtt?.btn_crt_upload ?? "Upload cert"} title={translations.conf?.mqtt?.title_crt ?? ""}/></Link> <a href="#/mqtt-cert"><Badge color="blue" text={translations.conf?.mqtt?.btn_crt_upload ?? "Upload cert"} title={translations.conf?.mqtt?.title_crt ?? ""}/></a>
{/if} {/if}
</span> </span>
<span class="flex pr-2"> <span class="flex pr-2">
{#if configuration.q.s.k} {#if configuration.q.s.k}
<span class="bd-on"><Link to="/mqtt-key">{translations.conf?.mqtt?.key_ok ?? "Key OK"}</Link></span> <span class="bd-on"><a href="#/mqtt-key">{translations.conf?.mqtt?.key_ok ?? "Key OK"}</a></span>
<span class="bd-off" on:click={askDeleteKey} on:keypress={askDeleteKey}>&#128465;</span> <span class="bd-off" on:click={askDeleteKey} on:keypress={askDeleteKey}>&#128465;</span>
{:else} {:else}
<Link to="/mqtt-key"><Badge color="blue" text={translations.conf?.mqtt?.btn_key_upload ?? "Upload key"} title={translations.conf?.mqtt?.title_key ?? ""}/></Link> <a href="#/mqtt-key"><Badge color="blue" text={translations.conf?.mqtt?.btn_key_upload ?? "Upload key"} title={translations.conf?.mqtt?.title_key ?? ""}/></a>
{/if} {/if}
</span> </span>
</div> </div>

View File

@@ -1,18 +1,19 @@
<script> <script>
import { sysinfoStore } from './DataStores.js'; import { sysinfoStore } from '../lib/DataStores.js';
import { translationsStore } from './TranslationService.js'; import { translationsStore } from '../lib/TranslationService.js';
import Mask from './Mask.svelte' import Mask from '../lib/Mask.svelte'
import { navigate } from 'svelte-navigator'; import { push } from 'svelte-spa-router';
import { wiki } from './Helpers';
export let basepath = "/"; let basepath = "/";
export let sysinfo = {}; let sysinfo = {};
let translations = {}; let translations = {};
translationsStore.subscribe(update => { translationsStore.subscribe(update => {
translations = update; translations = update;
}); });
sysinfoStore.subscribe(v => sysinfo = v);
let loadingOrSaving = false; let loadingOrSaving = false;
async function handleSubmit(e) { async function handleSubmit(e) {
@@ -36,7 +37,7 @@
s.booting = res.reboot; s.booting = res.reboot;
return s; return s;
}); });
navigate(basepath); push(basepath);
} }
</script> </script>

View File

@@ -1,26 +1,38 @@
<script> <script>
import { ampcol, exportcol, metertype, uiVisibility, formatUnit, fmtnum, formatCurrency } from './Helpers.js'; import { ampcol, exportcol, metertype, uiVisibility, formatUnit, fmtnum, formatCurrency } from '../lib/Helpers.js';
import PowerGauge from './PowerGauge.svelte'; import PowerGauge from '../lib/PowerGauge.svelte';
import VoltPlot from './VoltPlot.svelte'; import VoltPlot from '../lib/VoltPlot.svelte';
import ReactiveData from './ReactiveData.svelte'; import ReactiveData from '../lib/ReactiveData.svelte';
import AccountingData from './AccountingData.svelte'; import AccountingData from '../lib/AccountingData.svelte';
import PricePlot from './PricePlot.svelte'; import PricePlot from '../lib/PricePlot.svelte';
import DayPlot from './DayPlot.svelte'; import DayPlot from '../lib/DayPlot.svelte';
import MonthPlot from './MonthPlot.svelte'; import MonthPlot from '../lib/MonthPlot.svelte';
import TemperaturePlot from './TemperaturePlot.svelte'; import TemperaturePlot from '../lib/TemperaturePlot.svelte';
import TariffPeakChart from './TariffPeakChart.svelte'; import TariffPeakChart from '../lib/TariffPeakChart.svelte';
import RealtimePlot from './RealtimePlot.svelte'; import RealtimePlot from '../lib/RealtimePlot.svelte';
import PerPhasePlot from './PerPhasePlot.svelte'; import PerPhasePlot from '../lib/PerPhasePlot.svelte';
import { dataStore, sysinfoStore, importPricesStore, exportPricesStore, dayPlotStore, monthPlotStore, temperaturesStore, tariffStore } from '../lib/DataStores.js';
import { translationsStore } from '../lib/TranslationService.js';
export let data = {} let data = {}
export let sysinfo = {} let sysinfo = {}
export let importPrices = {} let importPrices = {}
export let exportPrices = {} let exportPrices = {}
export let dayPlot = {} let dayPlot = {}
export let monthPlot = {} let monthPlot = {}
export let temperatures = {}; let temperatures = {};
export let translations = {}; let translations = {};
export let tariffData = {}; let tariffData = {};
dataStore.subscribe(v => data = v);
sysinfoStore.subscribe(v => sysinfo = v);
importPricesStore.subscribe(v => importPrices = v);
exportPricesStore.subscribe(v => exportPrices = v);
dayPlotStore.subscribe(v => dayPlot = v);
monthPlotStore.subscribe(v => monthPlot = v);
temperaturesStore.subscribe(v => temperatures = v);
translationsStore.subscribe(v => translations = v);
tariffStore.subscribe(v => tariffData = v);
let it,et,threePhase, l1e, l2e, l3e; let it,et,threePhase, l1e, l2e, l3e;
$: { $: {

View File

@@ -0,0 +1,11 @@
<script>
import DataEdit from '../lib/DataEdit.svelte';
import { dayPlotStore } from '../lib/DataStores.js';
let basepath = "/";
let dayPlot;
dayPlotStore.subscribe(v => dayPlot = v);
</script>
<DataEdit prefix="UTC Hour" data={dayPlot} url="/dayplot" {basepath} />

View File

@@ -0,0 +1,11 @@
<script>
import DataEdit from '../lib/DataEdit.svelte';
import { monthPlotStore } from '../lib/DataStores.js';
let basepath = "/";
let monthPlot;
monthPlotStore.subscribe(v => monthPlot = v);
</script>
<DataEdit prefix="Day" data={monthPlot} url="/monthplot" {basepath} />

View File

@@ -1,9 +1,6 @@
<script> <script>
import Mask from "./Mask.svelte"; import Mask from "../lib/Mask.svelte";
import { translationsStore } from "./TranslationService"; import { translationsStore } from "../lib/TranslationService";
export let action;
export let title;
let translations = {}; let translations = {};
translationsStore.subscribe(update => { translationsStore.subscribe(update => {
@@ -15,12 +12,12 @@
<div class="grid xl:grid-cols-4 lg:grid-cols-2 md:grid-cols-2"> <div class="grid xl:grid-cols-4 lg:grid-cols-2 md:grid-cols-2">
<div class="cnt"> <div class="cnt">
<strong>{translations.upload?.title ?? "Upload"} {title}</strong> <strong>{translations.upload?.title ?? "Upload"} CA</strong>
<p class="mb-4">{translations.upload?.desc ?? ""}</p> <p class="mb-4">{translations.upload?.desc ?? ""}</p>
<form action="{action}" enctype="multipart/form-data" method="post" on:submit={() => uploading=true} autocomplete="off"> <form action="/mqtt-ca" enctype="multipart/form-data" method="post" on:submit={() => uploading=true} autocomplete="off">
<input name="file" type="file"> <input name="file" type="file">
<div class="w-full text-right mt-4"> <div class="w-full text-right mt-4">
<button type="submit" class="btn-pri"><p class="mb-4">{translations.btn?.upload ?? "Upload"}</button> <button type="submit" class="btn-pri">{translations.btn?.upload ?? "Upload"}</button>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -0,0 +1,25 @@
<script>
import Mask from "../lib/Mask.svelte";
import { translationsStore } from "../lib/TranslationService";
let translations = {};
translationsStore.subscribe(update => {
translations = update;
});
let uploading = false;
</script>
<div class="grid xl:grid-cols-4 lg:grid-cols-2 md:grid-cols-2">
<div class="cnt">
<strong>{translations.upload?.title ?? "Upload"} certificate</strong>
<p class="mb-4">{translations.upload?.desc ?? ""}</p>
<form action="/mqtt-cert" enctype="multipart/form-data" method="post" on:submit={() => uploading=true} autocomplete="off">
<input name="file" type="file">
<div class="w-full text-right mt-4">
<button type="submit" class="btn-pri">{translations.btn?.upload ?? "Upload"}</button>
</div>
</form>
</div>
</div>
<Mask active={uploading} message={translations.upload?.mask ?? "Uploading"}/>

View File

@@ -0,0 +1,25 @@
<script>
import Mask from "../lib/Mask.svelte";
import { translationsStore } from "../lib/TranslationService";
let translations = {};
translationsStore.subscribe(update => {
translations = update;
});
let uploading = false;
</script>
<div class="grid xl:grid-cols-4 lg:grid-cols-2 md:grid-cols-2">
<div class="cnt">
<strong>{translations.upload?.title ?? "Upload"} private key</strong>
<p class="mb-4">{translations.upload?.desc ?? ""}</p>
<form action="/mqtt-key" enctype="multipart/form-data" method="post" on:submit={() => uploading=true} autocomplete="off">
<input name="file" type="file">
<div class="w-full text-right mt-4">
<button type="submit" class="btn-pri">{translations.btn?.upload ?? "Upload"}</button>
</div>
</form>
</div>
</div>
<Mask active={uploading} message={translations.upload?.mask ?? "Uploading"}/>

View File

@@ -1,11 +1,9 @@
<script> <script>
import { priceConfigStore, getPriceConfig } from './ConfigurationStore' import { priceConfigStore, getPriceConfig } from '../lib/ConfigurationStore'
import { translationsStore } from './TranslationService'; import { translationsStore } from '../lib/TranslationService';
import { wiki, zeropad } from './Helpers.js'; import { wiki, zeropad } from '../lib/Helpers.js';
import Mask from './Mask.svelte' import Mask from '../lib/Mask.svelte'
import { navigate } from 'svelte-navigator'; import { push } from 'svelte-spa-router';
export let basepath = "/";
let translations = {}; let translations = {};
translationsStore.subscribe(update => { translationsStore.subscribe(update => {
@@ -53,7 +51,7 @@
let res = (await response.json()) let res = (await response.json())
saving = false; saving = false;
navigate(basepath + "configuration"); push("/configuration");
} }
let toggleDay = function(arr, day) { let toggleDay = function(arr, day) {

View File

@@ -1,9 +1,9 @@
<script> <script>
import { sysinfoStore, networksStore } from './DataStores.js'; import { sysinfoStore, networksStore } from '../lib/DataStores.js';
import { translationsStore } from './TranslationService.js'; import { translationsStore } from '../lib/TranslationService.js';
import Mask from './Mask.svelte' import Mask from '../lib/Mask.svelte'
import SubnetOptions from './SubnetOptions.svelte'; import SubnetOptions from '../lib/SubnetOptions.svelte';
import { scanForDevice, charAndNumPattern, asciiPatternExt, ipPattern } from './Helpers.js'; import { scanForDevice, charAndNumPattern, asciiPatternExt, ipPattern } from '../lib/Helpers.js';
let translations = {}; let translations = {};
translationsStore.subscribe(update => { translationsStore.subscribe(update => {
@@ -16,7 +16,8 @@
networks = update; networks = update;
}); });
export let sysinfo = {} let sysinfo = {}
sysinfoStore.subscribe(v => sysinfo = v);
let staticIp = false; let staticIp = false;
let connectionMode = 1; let connectionMode = 1;

View File

@@ -1,16 +1,70 @@
<script> <script>
import { metertype, boardtype, isBusPowered, getBaseChip, wiki } from './Helpers.js'; import { metertype, boardtype, isBusPowered, getBaseChip, wiki } from '../lib/Helpers.js';
import { getSysinfo, sysinfoStore } from './DataStores.js'; import { getSysinfo, sysinfoStore, dataStore } from '../lib/DataStores.js';
import { upgrade, upgradeWarningText } from './UpgradeHelper'; import { upgrade, upgradeWarningText } from '../lib/UpgradeHelper';
import { translationsStore } from './TranslationService.js'; import { translationsStore } from '../lib/TranslationService.js';
import { Link } from 'svelte-navigator'; import Clock from '../lib/Clock.svelte';
import Clock from './Clock.svelte'; import Mask from '../lib/Mask.svelte';
import Mask from './Mask.svelte'; import { scanForDevice } from '../lib/Helpers.js';
import { scanForDevice } from './Helpers.js';
import ipaddr from 'ipaddr.js';
export let data; let data;
export let sysinfo; let sysinfo;
dataStore.subscribe(v => data = v);
sysinfoStore.subscribe(v => sysinfo = v);
// Format IPv6 address to compact form (RFC 5952)
const formatIPv6 = (addr) => {
if (!addr) return addr;
// Split into groups
const groups = addr.toLowerCase().split(':');
// Remove leading zeros from each group
const normalized = groups.map(g => g.replace(/^0+/, '') || '0');
// Find longest sequence of consecutive zeros
let maxStart = -1, maxLen = 0;
let currStart = -1, currLen = 0;
for (let i = 0; i < normalized.length; i++) {
if (normalized[i] === '0') {
if (currStart === -1) currStart = i;
currLen++;
} else {
if (currLen > maxLen) {
maxStart = currStart;
maxLen = currLen;
}
currStart = -1;
currLen = 0;
}
}
// Check final sequence
if (currLen > maxLen) {
maxStart = currStart;
maxLen = currLen;
}
// Only compress if we have 2 or more consecutive zeros
if (maxLen > 1) {
const before = normalized.slice(0, maxStart);
const after = normalized.slice(maxStart + maxLen);
if (before.length === 0 && after.length === 0) {
return '::';
} else if (before.length === 0) {
return '::' + after.join(':');
} else if (after.length === 0) {
return before.join(':') + '::';
} else {
return before.join(':') + '::' + after.join(':');
}
}
return normalized.join(':');
};
let cfgItems = [{ let cfgItems = [{
name: 'WiFi', name: 'WiFi',
@@ -73,11 +127,11 @@
} }
let firmwareFileInput; let firmwareFileInput;
let firmwareFiles = []; let firmwareFiles = null;
let firmwareUploading = false; let firmwareUploading = false;
let configFileInput; let configFileInput;
let configFiles = []; let configFiles = null;
let configUploading = false; let configUploading = false;
getSysinfo(); getSysinfo();
@@ -119,7 +173,7 @@
}; };
$: { $: {
if(configFiles.length == 1) { if(configFiles && configFiles.length == 1) {
let file = configFiles[0]; let file = configFiles[0];
let reader = new FileReader(); let reader = new FileReader();
let parseConfigFile = ( e ) => { let parseConfigFile = ( e ) => {
@@ -146,7 +200,7 @@
{translations.status?.device?.chip ?? "Chip"}: {sysinfo.chip} {#if sysinfo.cpu}({sysinfo.cpu}MHz){/if} {translations.status?.device?.chip ?? "Chip"}: {sysinfo.chip} {#if sysinfo.cpu}({sysinfo.cpu}MHz){/if}
</div> </div>
<div class="my-2"> <div class="my-2">
{translations.status?.device?.device ?? "Device"}: <Link to="/vendor">{boardtype(sysinfo.chip, sysinfo.board)}</Link> {translations.status?.device?.device ?? "Device"}: <a href="#/vendor">{boardtype(sysinfo.chip, sysinfo.board)}</a>
</div> </div>
<div class="my-2"> <div class="my-2">
{translations.status?.device?.mac ?? "MAC"}: {sysinfo.mac} {translations.status?.device?.mac ?? "MAC"}: {sysinfo.mac}
@@ -169,9 +223,9 @@
{/if} {/if}
{#if data?.a} {#if data?.a}
<div class="my-2"> <div class="my-2">
<Link to="/consent"> <a href="#/consent">
<span class="btn-pri-sm">{translations.status?.device?.btn_consents ?? "Consents"}</span> <span class="btn-pri-sm">{translations.status?.device?.btn_consents ?? "Consents"}</span>
</Link> </a>
<button on:click={askReboot} class="btn-yellow-sm float-right">{translations.btn?.reboot ?? "Reboot"}</button> <button on:click={askReboot} class="btn-yellow-sm float-right">{translations.btn?.reboot ?? "Reboot"}</button>
</div> </div>
{/if} {/if}
@@ -208,11 +262,11 @@
</div> </div>
{#if sysinfo.net.ipv6} {#if sysinfo.net.ipv6}
<div class="my-2"> <div class="my-2">
IPv6: <span style="font-size: 14px;">{ipaddr.parse(sysinfo.net.ipv6)}</span> IPv6: <span style="font-size: 14px;">{formatIPv6(sysinfo.net.ipv6)}</span>
</div> </div>
<div class="my-2"> <div class="my-2">
{#if sysinfo.net.dns1v6}DNSv6: <span style="font-size: 14px;">{ipaddr.parse(sysinfo.net.dns1v6)}</span>{/if} {#if sysinfo.net.dns1v6}DNSv6: <span style="font-size: 14px;">{formatIPv6(sysinfo.net.dns1v6)}</span>{/if}
{#if sysinfo.net.dns2v6}DNSv6: <span style="font-size: 14px;">{ipaddr.parse(sysinfo.net.dns2v6)}</span>{/if} {#if sysinfo.net.dns2v6}DNSv6: <span style="font-size: 14px;">{formatIPv6(sysinfo.net.dns2v6)}</span>{/if}
</div> </div>
{/if} {/if}
</div> </div>
@@ -267,7 +321,7 @@
<div class="my-2 flex"> <div class="my-2 flex">
<form action="firmware" enctype="multipart/form-data" method="post" on:submit={() => firmwareUploading=true} autocomplete="off"> <form action="firmware" enctype="multipart/form-data" method="post" on:submit={() => firmwareUploading=true} autocomplete="off">
<input style="display:none" name="file" type="file" accept=".bin" bind:this={firmwareFileInput} bind:files={firmwareFiles}> <input style="display:none" name="file" type="file" accept=".bin" bind:this={firmwareFileInput} bind:files={firmwareFiles}>
{#if firmwareFiles.length == 0} {#if !firmwareFiles || firmwareFiles.length == 0}
<button type="button" on:click={()=>{firmwareFileInput.click();}} class="btn-pri-sm float-right">{translations.status?.firmware?.btn_select_file ?? "Select file"}</button> <button type="button" on:click={()=>{firmwareFileInput.click();}} class="btn-pri-sm float-right">{translations.status?.firmware?.btn_select_file ?? "Select file"}</button>
{:else} {:else}
{firmwareFiles[0].name} {firmwareFiles[0].name}
@@ -287,13 +341,13 @@
{/each} {/each}
<label class="my-1 mx-3 col-span-2"><input type="checkbox" class="rounded" name="ic" value="true"/> {translations.status?.backup?.secrets ?? "Include secrets"}<br/><small>{translations.status?.backup?.secrets_desc ?? ""}</small></label> <label class="my-1 mx-3 col-span-2"><input type="checkbox" class="rounded" name="ic" value="true"/> {translations.status?.backup?.secrets ?? "Include secrets"}<br/><small>{translations.status?.backup?.secrets_desc ?? ""}</small></label>
</div> </div>
{#if configFiles.length == 0} {#if !configFiles || configFiles.length == 0}
<button type="submit" class="btn-pri-sm float-right">{translations.status?.backup?.btn_download ?? "Download"}</button> <button type="submit" class="btn-pri-sm float-right">{translations.status?.backup?.btn_download ?? "Download"}</button>
{/if} {/if}
</form> </form>
<form on:submit|preventDefault={uploadConfigFile} autocomplete="off"> <form on:submit|preventDefault={uploadConfigFile} autocomplete="off">
<input style="display:none" name="file" type="file" accept=".cfg" bind:this={configFileInput} bind:files={configFiles}> <input style="display:none" name="file" type="file" accept=".cfg" bind:this={configFileInput} bind:files={configFiles}>
{#if configFiles.length == 0} {#if !configFiles || configFiles.length == 0}
<button type="button" on:click={()=>{configFileInput.click();}} class="btn-pri-sm">{translations.status?.backup?.btn_select_file ?? "Select file"}</button> <button type="button" on:click={()=>{configFileInput.click();}} class="btn-pri-sm">{translations.status?.backup?.btn_select_file ?? "Select file"}</button>
{:else} {:else}
{configFiles[0].name} {configFiles[0].name}

View File

@@ -1,12 +1,11 @@
<script> <script>
import { sysinfoStore } from './DataStores.js'; import { sysinfoStore } from '../lib/DataStores.js';
import BoardTypeSelectOptions from './BoardTypeSelectOptions.svelte'; import BoardTypeSelectOptions from '../lib/BoardTypeSelectOptions.svelte';
import UartSelectOptions from './UartSelectOptions.svelte'; import UartSelectOptions from '../lib/UartSelectOptions.svelte';
import Mask from './Mask.svelte' import Mask from '../lib/Mask.svelte'
import { navigate } from 'svelte-navigator'; import { push } from 'svelte-spa-router';
export let basepath = "/"; let sysinfo = {};
export let sysinfo = {};
let loadingOrSaving = false; let loadingOrSaving = false;
async function handleSubmit(e) { async function handleSubmit(e) {
@@ -32,7 +31,7 @@
return s; return s;
}); });
navigate(basepath + (sysinfo.usrcfg ? "" : "setup")); push(sysinfo.usrcfg ? "/" : "/setup");
} }
let cc = true; let cc = true;

View File

@@ -1,45 +1,62 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte' import { svelte } from '@sveltejs/vite-plugin-svelte'
// Try to import local config, fall back to default if not found
let localConfig = { proxyTarget: "http://192.168.4.1" };
try {
const imported = await import('./vite.config.local.js');
localConfig = imported.default;
} catch (e) {
console.log('No vite.config.local.js found, using default proxy target:', localConfig.proxyTarget);
console.log('Copy vite.config.local.example.js to vite.config.local.js to customize');
}
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
build: { build: {
outDir: 'dist', outDir: 'dist',
assetsDir: '.', assetsDir: '.',
minify: 'esbuild',
target: 'es2020',
rollupOptions: { rollupOptions: {
output: { output: {
assetFileNames: '[name][extname]', assetFileNames: '[name][extname]',
chunkFileNames: '[name].js', chunkFileNames: '[name].js',
entryFileNames: '[name].js' entryFileNames: '[name].js',
manualChunks: undefined
} }
} }
}, },
plugins: [svelte()], plugins: [svelte({
compilerOptions: {
dev: false
}
})],
server: { server: {
proxy: { proxy: {
"/data.json": "http://192.168.21.122", "/data.json": localConfig.proxyTarget,
"/energyprice.json": "http://192.168.21.122", "/energyprice.json": localConfig.proxyTarget,
"/importprice.json": "http://192.168.21.122", "/importprice.json": localConfig.proxyTarget,
"/exportprice.json": "http://192.168.21.122", "/exportprice.json": localConfig.proxyTarget,
"/dayplot.json": "http://192.168.21.122", "/dayplot.json": localConfig.proxyTarget,
"/monthplot.json": "http://192.168.21.122", "/monthplot.json": localConfig.proxyTarget,
"/temperature.json": "http://192.168.21.122", "/temperature.json": localConfig.proxyTarget,
"/sysinfo.json": "http://192.168.21.122", "/sysinfo.json": localConfig.proxyTarget,
"/configuration.json": "http://192.168.21.122", "/configuration.json": localConfig.proxyTarget,
"/tariff.json": "http://192.168.21.122", "/tariff.json": localConfig.proxyTarget,
"/realtime.json": "http://192.168.21.122", "/realtime.json": localConfig.proxyTarget,
"/priceconfig.json": "http://192.168.21.122", "/priceconfig.json": localConfig.proxyTarget,
"/translations.json": "http://192.168.21.122", "/translations.json": localConfig.proxyTarget,
"/cloudkey.json": "http://192.168.21.122", "/cloudkey.json": localConfig.proxyTarget,
"/wifiscan.json": "http://192.168.21.122", "/wifiscan.json": localConfig.proxyTarget,
"/save": "http://192.168.21.122", "/save": localConfig.proxyTarget,
"/reboot": "http://192.168.21.122", "/reboot": localConfig.proxyTarget,
"/configfile": "http://192.168.21.122", "/configfile": localConfig.proxyTarget,
"/upgrade": "http://192.168.21.122", "/upgrade": localConfig.proxyTarget,
"/mqtt-ca": "http://192.168.21.122", "/mqtt-ca": localConfig.proxyTarget,
"/mqtt-cert": "http://192.168.21.122", "/mqtt-cert": localConfig.proxyTarget,
"/mqtt-key": "http://192.168.21.122", "/mqtt-key": localConfig.proxyTarget,
"/logo.svg": "http://192.168.21.122", "/logo.svg": localConfig.proxyTarget,
} }
} }
}) })

View File

@@ -0,0 +1,7 @@
// Copy this file to vite.config.local.js and update with your device's IP address
// vite.config.local.js is ignored by git so your settings won't be committed
export default {
// The IP address of your AMS reader device for local development
proxyTarget: "http://192.168.4.1"
}