Initial commit
This commit is contained in:
commit
b7a3b14e06
41 changed files with 27493 additions and 0 deletions
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
# jetbrains setting folder
|
||||
.idea/
|
||||
|
||||
.vscode/
|
||||
|
||||
|
||||
# Tauri build output
|
||||
src-tauri/target/
|
||||
47
README.md
Normal file
47
README.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Astro Starter Kit: Minimal
|
||||
|
||||
```sh
|
||||
pnpm create astro@latest -- --template minimal
|
||||
```
|
||||
|
||||
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal)
|
||||
[](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal)
|
||||
[](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/minimal/devcontainer.json)
|
||||
|
||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||
|
||||
## 🚀 Project Structure
|
||||
|
||||
Inside of your Astro project, you'll see the following folders and files:
|
||||
|
||||
```text
|
||||
/
|
||||
├── public/
|
||||
├── src/
|
||||
│ └── pages/
|
||||
│ └── index.astro
|
||||
└── package.json
|
||||
```
|
||||
|
||||
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
||||
|
||||
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
||||
|
||||
Any static assets, like images, can be placed in the `public/` directory.
|
||||
|
||||
## 🧞 Commands
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
| Command | Action |
|
||||
| :------------------------ | :----------------------------------------------- |
|
||||
| `pnpm install` | Installs dependencies |
|
||||
| `pnpm dev` | Starts local dev server at `localhost:4321` |
|
||||
| `pnpm build` | Build your production site to `./dist/` |
|
||||
| `pnpm preview` | Preview your build locally, before deploying |
|
||||
| `pnpm astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||
| `pnpm astro -- --help` | Get help using the Astro CLI |
|
||||
|
||||
## 👀 Want to learn more?
|
||||
|
||||
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
||||
18
astro.config.mjs
Normal file
18
astro.config.mjs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import tailwind from '@astrojs/tailwind';
|
||||
import playformCompress from '@playform/compress';
|
||||
|
||||
import compressor from 'astro-compressor';
|
||||
import Sonda from 'sonda/astro';
|
||||
|
||||
import preact from '@astrojs/preact';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
build: {
|
||||
sourcemap: true
|
||||
}
|
||||
},
|
||||
integrations: [tailwind(), playformCompress(), compressor(), preact()],//Sonda()
|
||||
});
|
||||
10270
package-lock.json
generated
Normal file
10270
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
36
package.json
Normal file
36
package.json
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"name": "laser-driver-dashboard",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/charts": "^2.6.1",
|
||||
"@astrojs/preact": "^4.1.1",
|
||||
"@astrojs/ts-plugin": "^1.10.5",
|
||||
"@nanostores/preact": "^1.0.0",
|
||||
"@playform/compress": "^0.1.9",
|
||||
"@tabler/icons-preact": "^3.34.1",
|
||||
"@types/websocket": "^1.0.10",
|
||||
"astro": "^5.14.7",
|
||||
"astro-compressor": "^1.0.0",
|
||||
"astro-min": "^1.3.1",
|
||||
"chart.js": "^4.5.0",
|
||||
"dompurify": "^3.2.6",
|
||||
"marked": "^16.1.1",
|
||||
"nanostores": "^1.0.1",
|
||||
"preact": "^10.26.9",
|
||||
"websocket": "^1.0.35"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/tailwind": "^6.0.2",
|
||||
"daisyui": "^4.12.24",
|
||||
"sonda": "^0.7.1",
|
||||
"tailwindcss": "^3.4.17"
|
||||
}
|
||||
}
|
||||
7115
pnpm-lock.yaml
generated
Normal file
7115
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
9
public/favicon.svg
Normal file
9
public/favicon.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||
<style>
|
||||
path { fill: #000; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path { fill: #FFF; }
|
||||
}
|
||||
</style>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 749 B |
9
sonda-report-client.html
Normal file
9
sonda-report-client.html
Normal file
File diff suppressed because one or more lines are too long
BIN
src/assets/Audiowide-Regular.woff2
Normal file
BIN
src/assets/Audiowide-Regular.woff2
Normal file
Binary file not shown.
BIN
src/assets/VarelaRound-Regular.woff2
Normal file
BIN
src/assets/VarelaRound-Regular.woff2
Normal file
Binary file not shown.
1
src/assets/astro.svg
Normal file
1
src/assets/astro.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48"><path fill="#17191E" d="M7.78 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="url(#a)" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="#17191E" d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z"/><defs><linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
1
src/assets/background.svg
Normal file
1
src/assets/background.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1024" fill="none"><path fill="url(#a)" fill-rule="evenodd" d="M-217.58 475.75c91.82-72.02 225.52-29.38 341.2-44.74C240 415.56 372.33 315.14 466.77 384.9c102.9 76.02 44.74 246.76 90.31 366.31 29.83 78.24 90.48 136.14 129.48 210.23 57.92 109.99 169.67 208.23 155.9 331.77-13.52 121.26-103.42 264.33-224.23 281.37-141.96 20.03-232.72-220.96-374.06-196.99-151.7 25.73-172.68 330.24-325.85 315.72-128.6-12.2-110.9-230.73-128.15-358.76-12.16-90.14 65.87-176.25 44.1-264.57-26.42-107.2-167.12-163.46-176.72-273.45-10.15-116.29 33.01-248.75 124.87-320.79Z" clip-rule="evenodd" style="opacity:.154"/><path fill="url(#b)" fill-rule="evenodd" d="M1103.43 115.43c146.42-19.45 275.33-155.84 413.5-103.59 188.09 71.13 409 212.64 407.06 413.88-1.94 201.25-259.28 278.6-414.96 405.96-130 106.35-240.24 294.39-405.6 265.3-163.7-28.8-161.93-274.12-284.34-386.66-134.95-124.06-436-101.46-445.82-284.6-9.68-180.38 247.41-246.3 413.54-316.9 101.01-42.93 207.83 21.06 316.62 6.61Z" clip-rule="evenodd" style="opacity:.154"/><defs><linearGradient id="b" x1="373" x2="1995.44" y1="1100" y2="118.03" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient><linearGradient id="a" x1="107.37" x2="1130.66" y1="1993.35" y2="1026.31" gradientUnits="userSpaceOnUse"><stop stop-color="#3245FF"/><stop offset="1" stop-color="#BC52EE"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
689
src/components/AppProviderStore.tsx
Normal file
689
src/components/AppProviderStore.tsx
Normal file
|
|
@ -0,0 +1,689 @@
|
|||
import { atom } from 'nanostores';
|
||||
import pkg from "websocket";
|
||||
const { w3cwebsocket } = pkg;
|
||||
|
||||
// Update Status Enum - matches backend enum in update_notifier.h
|
||||
export enum UpdateStatus {
|
||||
ERROR = -1,
|
||||
STARTED = 0,
|
||||
IN_PROGRESS = 1,
|
||||
SUCCESS = 2,
|
||||
NO_UPDATE = 3
|
||||
}
|
||||
|
||||
export const $websocket = atom<WebSocket | null>(null);
|
||||
export const $activePage = atom<string>('dashboard');
|
||||
export const $freeInternalHeap = atom<number>(375 * 1024);
|
||||
export const $totalInternalHeap = atom<number>(375 * 1024); // ESP32-P4 has 375KB internal heap (heap_caps_get_info mit MALLOC_CAP_INTERNAL)
|
||||
export const $modulationInput = atom<number>(0);
|
||||
export const $inputVoltage = atom<number>(0);
|
||||
export const $driverTemp = atom<number>(0);
|
||||
export const $counter = atom<number>(0);
|
||||
export const $ref = atom<number>(0);
|
||||
export const $dcdcState = atom<number>(0);
|
||||
export const $dcdc_errorStatus = atom<string>('--');
|
||||
export const $idCountDashboard = atom<number>(2);
|
||||
export const $wsConnectionStatus = atom<string>('Disconnected');
|
||||
export const $modulationInputMethod = atom<string>('--');
|
||||
export const $gpioState = atom<{ [key: number]: boolean }>({
|
||||
4: false,
|
||||
5: false,
|
||||
6: false,
|
||||
7: false,
|
||||
8: false,
|
||||
9: false,
|
||||
});
|
||||
export const $validGpioKeys = atom<number[]>([4, 5, 6, 7, 8, 9]);
|
||||
/* DCDC */
|
||||
export const $dcdc_state = atom<number>(0);
|
||||
export const $dcdc_state_string = atom<string>('--');
|
||||
export const $dcdc_state_desc_string = atom<string>('--');
|
||||
export const $dcdc_error_flag = atom<number>(0);
|
||||
export const $POWERGOOD = atom<number>(0);
|
||||
export const $POWERGOOD_string = atom<string>('--');
|
||||
export const $ref_value = atom<number>(0);
|
||||
/* LASER */
|
||||
export const $laser_state = atom<number>(2); // LASER_STOP
|
||||
export const $laser_state_string = atom<string>('--');
|
||||
export const $laser_state_desc_string = atom<string>('--');
|
||||
export const $is_REF_calibrated = atom<number>(0);
|
||||
export const $has_duty_lookup_table = atom<number>(0);
|
||||
export const $laser_error_flag = atom<number>(0);
|
||||
export const $laser_error_flag_string = atom<string>('--');
|
||||
export const $LASERGOOD = atom<number>(0);
|
||||
export const $LASERGOOD_string = atom<string>('--');
|
||||
export const $laser_set_temp = atom<number>(0);
|
||||
export const $laser_max_temp = atom<number>(0);
|
||||
export const $interlock = atom<number>(0);
|
||||
export const $set_current = atom<number>(0);
|
||||
export const $max_current = atom<number>(0);
|
||||
export const $threshold_current = atom<number>(0);
|
||||
export const $current_reading = atom<number>(0);
|
||||
export const $current_reading_string = atom<string>('--');
|
||||
export const $laser_temp = atom<number>(0);
|
||||
export const $laser_temp_string = atom<string>('--');
|
||||
|
||||
/* TEMPERATURE CHART DATA */
|
||||
export const $temperature_chart_data = atom<{
|
||||
timestamps: string[];
|
||||
driverTemps: number[];
|
||||
laserTemps: number[];
|
||||
}>({
|
||||
timestamps: [],
|
||||
driverTemps: [],
|
||||
laserTemps: []
|
||||
});
|
||||
|
||||
/* DRIVER */
|
||||
export const $driver_temp = atom<number>(0);
|
||||
export const $driver_temp_string = atom<string>('--');
|
||||
export const $driver_max_temp = atom<number>(0);
|
||||
export const $driver_max_temp_string = atom<string>('--');
|
||||
export const $vcc_voltage = atom<number>(0);
|
||||
export const $vcc_voltage_string = atom<string>('--');
|
||||
export const $pd_voltage = atom<number>(0);
|
||||
export const $pd_voltage_string = atom<string>('--');
|
||||
/* CALIBRATION TABLE */
|
||||
export const $calibration_table_data = atom<{
|
||||
values: number[];
|
||||
min_duty: number;
|
||||
max_duty: number;
|
||||
points_count: number;
|
||||
}>
|
||||
({
|
||||
values: [],
|
||||
min_duty: 0,
|
||||
max_duty: 0,
|
||||
points_count: 0,
|
||||
});
|
||||
|
||||
/* UPDATE SYSTEM */
|
||||
export interface UpdateInfo {
|
||||
core: string;
|
||||
currentVersion: string;
|
||||
newVersion: string;
|
||||
icon: string;
|
||||
hasProgressBar: boolean;
|
||||
updateAvailable: boolean;
|
||||
}
|
||||
|
||||
// Neue Status-Definitionen
|
||||
export enum TargetUpdateStatus {
|
||||
WAITING = 'waiting',
|
||||
IN_PROGRESS = 'in_progress',
|
||||
COMPLETED = 'completed',
|
||||
ERROR = 'error',
|
||||
NO_UPDATE = 'no_update'
|
||||
}
|
||||
|
||||
export enum TotalUpdateStatus {
|
||||
INITIAL = 'initial',
|
||||
STARTED = 'started',
|
||||
UPDATING = 'updating',
|
||||
SUCCESS = 'success',
|
||||
ERROR = 'error',
|
||||
RESTARTING = 'restarting'
|
||||
}
|
||||
|
||||
export const $availableUpdates = atom<UpdateInfo[]>([]);
|
||||
export const $updateChangelog = atom<string>('');
|
||||
export const $updateInProgress = atom<boolean>(false);
|
||||
|
||||
// Separate progress stores for each update type
|
||||
// [progress, status] where status uses UpdateStatus enum
|
||||
export const $p4FirmwareProgress = atom<[number, TargetUpdateStatus]>([0, TargetUpdateStatus.NO_UPDATE]); // [progress, TargetUpdateStatus]
|
||||
export const $p4WebinterfaceProgress = atom<[number, TargetUpdateStatus]>([0, TargetUpdateStatus.NO_UPDATE]); // [progress, TargetUpdateStatus]
|
||||
export const $stm32FirmwareProgress = atom<[number, TargetUpdateStatus]>([0, TargetUpdateStatus.NO_UPDATE]); // [progress, TargetUpdateStatus]
|
||||
export const $c6FirmwareProgress = atom<[number, TargetUpdateStatus]>([0, TargetUpdateStatus.NO_UPDATE]); // [progress, TargetUpdateStatus]
|
||||
|
||||
// Neuer Store für den Gesamt-Update-Status
|
||||
export const $totalUpdateStatus = atom<TotalUpdateStatus>(TotalUpdateStatus.INITIAL);
|
||||
|
||||
export const $currentUpdateTargetIndex = atom<number>(0);
|
||||
export const $updateErrorMessages = atom<{ [key: number]: string }>({});
|
||||
export const $updatedTargets = atom<boolean[]>([false, false, false, false]);
|
||||
|
||||
// Add update state to global store
|
||||
export const $updateState = atom<'initial' | 'updating' | 'success' | 'error' | 'reloading' | 'restarting'>('initial');
|
||||
// Global modal state
|
||||
export const $isUpdaterModalOpen = atom<boolean>(false);
|
||||
export const $isSuccessPopupOpen = atom<boolean>(false);
|
||||
export const $isErrorPopupOpen = atom<boolean>(false);
|
||||
|
||||
export const getDcdcStateString = (state: number): string => {
|
||||
switch (state) {
|
||||
case 0: return "IDLE";
|
||||
case 1: return "INIT";
|
||||
case 2: return "START";
|
||||
case 3: return "RUN";
|
||||
case 4: return "STOP";
|
||||
case 5: return "ERROR";
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
};
|
||||
|
||||
export const getDcdcStateDescString = (state: number): string => {
|
||||
|
||||
switch (state) {
|
||||
case 0: return "DCDC is idle";
|
||||
case 1: return "Initializing Voltage ramping";
|
||||
case 2: return "Ramping to target voltage";
|
||||
case 3: return "DCDC is running";
|
||||
case 4: return "DCDC is stopped";
|
||||
case 5: return "DCDC is in error state"; // TODO: add error description
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
};
|
||||
|
||||
export const getLaserStateString = (state: number): string => {
|
||||
switch (state) {
|
||||
case 0: return "INTERNAL MODULATION";
|
||||
case 1: return "EXTERNAL MODULATION";
|
||||
case 2: return "STOP";
|
||||
case 3: return "ERROR";
|
||||
case 4: return "REF CALIBRATION";
|
||||
case 5: return "LASER CALIBRATION";
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
};
|
||||
|
||||
export const getLaserStateDescString = (state: number): string => {
|
||||
switch (state) {
|
||||
case 0: return "Internal laser modulation running";
|
||||
case 1: return "External laser modulation running";
|
||||
case 2: return "Laser stopped";
|
||||
case 3: return "Laser in error state"; // TODO: add error description
|
||||
case 4: return "Calibrating REF valie";
|
||||
case 5: return "Calibrating laser";
|
||||
default: return "Unknown laser state";
|
||||
}
|
||||
};
|
||||
|
||||
const setDCDCState = (state: number) => {
|
||||
$dcdc_state.set(state);
|
||||
$dcdc_state_string.set(getDcdcStateString(state));
|
||||
$dcdc_state_desc_string.set(getDcdcStateDescString(state));
|
||||
}
|
||||
|
||||
const setLaserState = (state: number) => {
|
||||
$laser_state.set(state);
|
||||
$laser_state_string.set(getLaserStateString(state));
|
||||
$laser_state_desc_string.set(getLaserStateDescString(state));
|
||||
}
|
||||
|
||||
const setPOWERGOOD = (state: number) => {
|
||||
$POWERGOOD.set(state);
|
||||
$POWERGOOD_string.set(state ? "Yes" : "No");
|
||||
}
|
||||
|
||||
const setLASERGOOD = (state: number) => {
|
||||
$LASERGOOD.set(state);
|
||||
$LASERGOOD_string.set(state ? "Yes" : "No");
|
||||
}
|
||||
|
||||
|
||||
const setDcdcErrorStatus = (state: number) => {
|
||||
$dcdc_error_flag.set(state);
|
||||
$dcdc_errorStatus.set(state ? "Error" : "No Errors");
|
||||
}
|
||||
const setLaserErrorStatus = (state: number) => {
|
||||
$laser_error_flag.set(state);
|
||||
$laser_error_flag_string.set(state ? "Error" : "No Errors");
|
||||
}
|
||||
|
||||
const setREF_value = (state: number) => {
|
||||
$ref_value.set(state);
|
||||
}
|
||||
|
||||
const setModulationInputMethod = (state: number) => {
|
||||
$modulationInputMethod.set(state === 0 ? "INTERNAL MODULATION" : state === 1 ? "EXTERNAL MODULATION" : "---");
|
||||
}
|
||||
|
||||
const setREF_calibrated = (state: number) => {
|
||||
$is_REF_calibrated.set(state);
|
||||
}
|
||||
const setHAS_DUTY_LOOKUP_TABLE = (state: number) => {
|
||||
$has_duty_lookup_table.set(state);
|
||||
}
|
||||
|
||||
const setLaserSetTemp = (state: number) => {
|
||||
$laser_set_temp.set(state);
|
||||
}
|
||||
const setLaserMaxTemp = (state: number) => {
|
||||
$laser_max_temp.set(state);
|
||||
}
|
||||
const setInterlock = (state: number) => {
|
||||
$interlock.set(state);
|
||||
}
|
||||
|
||||
const setModulationInput = (state: number) => {
|
||||
$modulationInput.set(state);
|
||||
}
|
||||
const setSetCurrent = (state: number) => {
|
||||
$set_current.set(state);
|
||||
}
|
||||
const setMaxCurrent = (state: number) => {
|
||||
$max_current.set(state);
|
||||
}
|
||||
|
||||
const setThresholdCurrent = (state: number) => {
|
||||
$threshold_current.set(state);
|
||||
}
|
||||
const setCurrentReading = (state: number) => {
|
||||
$current_reading.set(state);
|
||||
$current_reading_string.set(state + " mA");
|
||||
}
|
||||
const setLaserTemp = (state: number) => {
|
||||
$laser_temp.set(state);
|
||||
$laser_temp_string.set(state + " °C");
|
||||
}
|
||||
|
||||
const setDriverTemp = (state: number) => {
|
||||
$driver_temp.set(state);
|
||||
$driver_temp_string.set(state + " °C");
|
||||
}
|
||||
const setDriverMaxTemp = (state: number) => {
|
||||
$driver_max_temp.set(state);
|
||||
$driver_max_temp_string.set(state + " °C");
|
||||
}
|
||||
|
||||
// Temperature chart data management
|
||||
const maxTemperatureDataPoints = 300; // 5 minutes worth of data at 1 second intervals
|
||||
|
||||
export const addTemperatureDataPoint = (driverTemp: number, laserTemp: number) => {
|
||||
const currentData = $temperature_chart_data.get();
|
||||
const now = new Date();
|
||||
const timeString = now.toLocaleTimeString();
|
||||
|
||||
const newData = {
|
||||
timestamps: [...currentData.timestamps, timeString],
|
||||
driverTemps: [...currentData.driverTemps, driverTemp],
|
||||
laserTemps: [...currentData.laserTemps, laserTemp]
|
||||
};
|
||||
|
||||
// Remove old data points to maintain 5-minute window
|
||||
if (newData.timestamps.length > maxTemperatureDataPoints) {
|
||||
newData.timestamps = newData.timestamps.slice(-maxTemperatureDataPoints);
|
||||
newData.driverTemps = newData.driverTemps.slice(-maxTemperatureDataPoints);
|
||||
newData.laserTemps = newData.laserTemps.slice(-maxTemperatureDataPoints);
|
||||
}
|
||||
|
||||
$temperature_chart_data.set(newData);
|
||||
}
|
||||
|
||||
export const clearTemperatureData = () => {
|
||||
$temperature_chart_data.set({
|
||||
timestamps: [],
|
||||
driverTemps: [],
|
||||
laserTemps: []
|
||||
});
|
||||
}
|
||||
const setVccVoltage = (state: number) => {
|
||||
$vcc_voltage.set(state);
|
||||
$vcc_voltage_string.set(state + " V");
|
||||
}
|
||||
const setPdVoltage = (state: number) => {
|
||||
$pd_voltage.set(state);
|
||||
$pd_voltage_string.set(state + " V");
|
||||
}
|
||||
|
||||
const setCalibrationTableData = (values: number[], min_duty: number, max_duty: number, points_count: number) => {
|
||||
$calibration_table_data.set({
|
||||
values: values,
|
||||
min_duty: min_duty,
|
||||
max_duty: max_duty,
|
||||
points_count: points_count
|
||||
});
|
||||
}
|
||||
|
||||
const setAvailableUpdates = (updates: UpdateInfo[]) => {
|
||||
$availableUpdates.set(updates);
|
||||
}
|
||||
const setUpdateChangelog = (changelog: string) => {
|
||||
$updateChangelog.set(changelog);
|
||||
}
|
||||
const setUpdateInProgress = (inProgress: boolean) => {
|
||||
$updateInProgress.set(inProgress);
|
||||
}
|
||||
|
||||
const setP4FirmwareProgress = (progress: number, status: TargetUpdateStatus = TargetUpdateStatus.NO_UPDATE) => {
|
||||
$p4FirmwareProgress.set([progress, status]);
|
||||
}
|
||||
|
||||
const setP4WebinterfaceProgress = (progress: number, status: TargetUpdateStatus = TargetUpdateStatus.NO_UPDATE) => {
|
||||
$p4WebinterfaceProgress.set([progress, status]);
|
||||
}
|
||||
|
||||
const setStm32FirmwareProgress = (progress: number, status: TargetUpdateStatus = TargetUpdateStatus.NO_UPDATE) => {
|
||||
$stm32FirmwareProgress.set([progress, status]);
|
||||
}
|
||||
|
||||
const setC6FirmwareProgress = (progress: number, status: TargetUpdateStatus = TargetUpdateStatus.NO_UPDATE) => {
|
||||
$c6FirmwareProgress.set([progress, status]);
|
||||
}
|
||||
|
||||
const setTotalUpdateStatus = (status: TotalUpdateStatus) => {
|
||||
console.log(`🎯 Total update status change: ${$totalUpdateStatus.get()} → ${status}`);
|
||||
$totalUpdateStatus.set(status);
|
||||
}
|
||||
|
||||
const startUpdate = () => {
|
||||
console.log(`🚀 Starting update process`);
|
||||
setTotalUpdateStatus(TotalUpdateStatus.STARTED);
|
||||
}
|
||||
|
||||
const setCurrentUpdateTargetIndex = (index: number) => {
|
||||
$currentUpdateTargetIndex.set(index);
|
||||
}
|
||||
|
||||
const setUpdateErrorMessages = (errorMessages: { [key: number]: string }) => {
|
||||
$updateErrorMessages.set(errorMessages);
|
||||
}
|
||||
|
||||
const setUpdateState = (state: 'initial' | 'updating' | 'success' | 'error' | 'restarting') => {
|
||||
console.log(`🎯 State change: ${$updateState.get()} → ${state}`);
|
||||
$updateState.set(state);
|
||||
|
||||
// Synchronize with total update status
|
||||
switch (state) {
|
||||
case 'initial':
|
||||
setTotalUpdateStatus(TotalUpdateStatus.INITIAL);
|
||||
break;
|
||||
case 'updating':
|
||||
setTotalUpdateStatus(TotalUpdateStatus.UPDATING);
|
||||
break;
|
||||
case 'success':
|
||||
setTotalUpdateStatus(TotalUpdateStatus.SUCCESS);
|
||||
break;
|
||||
case 'error':
|
||||
setTotalUpdateStatus(TotalUpdateStatus.ERROR);
|
||||
break;
|
||||
|
||||
case 'restarting':
|
||||
setTotalUpdateStatus(TotalUpdateStatus.UPDATING);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const setUpdaterModalOpen = (open: boolean) => {
|
||||
console.log(`🚀 Setting updater modal: ${open ? 'OPEN' : 'CLOSED'}`);
|
||||
$isUpdaterModalOpen.set(open);
|
||||
}
|
||||
// reset all states to offiline states, when websocket is disconnected
|
||||
const setWebSocketConnectionStatus = (status: string) => {
|
||||
$wsConnectionStatus.set(status);
|
||||
// set states to offline states! But do not reset calibration table/update states etc.
|
||||
if (status === "Disconnected") {
|
||||
$modulationInput.set(0);
|
||||
$inputVoltage.set(0);
|
||||
$driverTemp.set(0);
|
||||
$counter.set(0);
|
||||
$ref.set(0);
|
||||
$dcdcState.set(0);
|
||||
$dcdc_errorStatus.set('--');
|
||||
$modulationInputMethod.set('--');
|
||||
$gpioState.set({
|
||||
4: false,
|
||||
5: false,
|
||||
6: false,
|
||||
7: false,
|
||||
8: false,
|
||||
9: false,
|
||||
});
|
||||
/* DCDC */
|
||||
$dcdc_state.set(0);
|
||||
$dcdc_state_string.set('--');
|
||||
$dcdc_state_desc_string.set('--');
|
||||
$dcdc_error_flag.set(0);
|
||||
$POWERGOOD.set(0);
|
||||
$POWERGOOD_string.set('--');
|
||||
$ref_value.set(0);
|
||||
/* LASER */
|
||||
$laser_state.set(2); // LASER_STOP
|
||||
$laser_state_string.set('--');
|
||||
$laser_state_desc_string.set('--');
|
||||
$is_REF_calibrated.set(0);
|
||||
$has_duty_lookup_table.set(0);
|
||||
$laser_error_flag.set(0);
|
||||
$laser_error_flag_string.set('--');
|
||||
$LASERGOOD.set(0);
|
||||
$LASERGOOD_string.set('--');
|
||||
$laser_set_temp.set(0);
|
||||
$laser_max_temp.set(0);
|
||||
$interlock.set(0);
|
||||
$set_current.set(0);
|
||||
$max_current.set(0);
|
||||
$threshold_current.set(0);
|
||||
$current_reading.set(0);
|
||||
$current_reading_string.set('--');
|
||||
$laser_temp.set(0);
|
||||
$laser_temp_string.set('--');
|
||||
/* TEMPERATURE CHART DATA */
|
||||
clearTemperatureData();
|
||||
/* DRIVER */
|
||||
$driver_temp.set(0);
|
||||
$driver_temp_string.set('--');
|
||||
$driver_max_temp.set(0);
|
||||
$driver_max_temp_string.set('--');
|
||||
$vcc_voltage.set(0);
|
||||
$vcc_voltage_string.set('--');
|
||||
$pd_voltage.set(0);
|
||||
$pd_voltage_string.set('--');
|
||||
}
|
||||
}
|
||||
|
||||
const resetUpdateStatus = () => {
|
||||
console.log("Resetting update status");
|
||||
$updateState.set('initial');
|
||||
$updateInProgress.set(false);
|
||||
$updateErrorMessages.set({});
|
||||
$currentUpdateTargetIndex.set(0);
|
||||
$p4FirmwareProgress.set([0, TargetUpdateStatus.NO_UPDATE]);
|
||||
$p4WebinterfaceProgress.set([0, TargetUpdateStatus.NO_UPDATE]);
|
||||
$stm32FirmwareProgress.set([0, TargetUpdateStatus.NO_UPDATE]);
|
||||
$c6FirmwareProgress.set([0, TargetUpdateStatus.NO_UPDATE]);
|
||||
$availableUpdates.set([]);
|
||||
$updateChangelog.set('');
|
||||
$updatedTargets.set([false, false, false, false]);
|
||||
}
|
||||
|
||||
const setErrorPopupOpen = (isOpen: boolean) => {
|
||||
console.log(`❌ Setting error popup: ${isOpen ? 'OPEN' : 'CLOSED'}`);
|
||||
$isErrorPopupOpen.set(isOpen);
|
||||
}
|
||||
|
||||
const setSuccessPopupOpen = (isOpen: boolean) => {
|
||||
console.log(`🎉 Setting success popup: ${isOpen ? 'OPEN' : 'CLOSED'}`);
|
||||
$isSuccessPopupOpen.set(isOpen);
|
||||
}
|
||||
|
||||
const setFreeInternalHeap = (heap: number) => {
|
||||
$freeInternalHeap.set(heap);
|
||||
}
|
||||
|
||||
const setUpdatedTargets = (targets: boolean[]) => {
|
||||
console.log("🎯 Setting updated targets:", targets);
|
||||
$updatedTargets.set(targets);
|
||||
}
|
||||
|
||||
export const stateSetters = {
|
||||
setWebSocketConnectionStatus,
|
||||
setDCDCState,
|
||||
setLaserState,
|
||||
setPOWERGOOD,
|
||||
setLASERGOOD,
|
||||
setDcdcErrorStatus,
|
||||
setLaserErrorStatus,
|
||||
setREF_value,
|
||||
setModulationInputMethod,
|
||||
setREF_calibrated,
|
||||
setHAS_DUTY_LOOKUP_TABLE,
|
||||
setLaserSetTemp,
|
||||
setLaserMaxTemp,
|
||||
setInterlock,
|
||||
setModulationInput,
|
||||
setSetCurrent,
|
||||
setMaxCurrent,
|
||||
setThresholdCurrent,
|
||||
setCurrentReading,
|
||||
setLaserTemp,
|
||||
setDriverTemp,
|
||||
setDriverMaxTemp,
|
||||
setVccVoltage,
|
||||
setPdVoltage,
|
||||
setCalibrationTableData,
|
||||
setAvailableUpdates,
|
||||
setUpdateChangelog,
|
||||
setUpdateInProgress,
|
||||
setP4FirmwareProgress,
|
||||
setP4WebinterfaceProgress,
|
||||
setStm32FirmwareProgress,
|
||||
setC6FirmwareProgress,
|
||||
setTotalUpdateStatus,
|
||||
startUpdate,
|
||||
setCurrentUpdateTargetIndex,
|
||||
setUpdateErrorMessages,
|
||||
setUpdateState,
|
||||
setUpdaterModalOpen,
|
||||
setSuccessPopupOpen,
|
||||
setErrorPopupOpen,
|
||||
resetUpdateStatus,
|
||||
setFreeInternalHeap,
|
||||
setUpdatedTargets
|
||||
};
|
||||
|
||||
const send_uart_cmd_without_data = (command: number) => {
|
||||
if ($websocket.get() !== null && $websocket.get()?.readyState === $websocket.get()?.OPEN) {
|
||||
const buffer = new Uint8Array([0xAB, command]);
|
||||
$websocket.get()?.send(buffer);
|
||||
} else {
|
||||
console.error("WebSocket ist nicht verbunden");
|
||||
}
|
||||
};
|
||||
|
||||
const send_uart_cmd_with_data = (command: number, data: number) => {
|
||||
if ($websocket.get() !== null && $websocket.get()?.readyState === $websocket.get()?.OPEN) {
|
||||
const buffer = new Uint8Array(6);
|
||||
buffer[0] = 0xAA;
|
||||
buffer[1] = command;
|
||||
buffer[2] = (data >> 24) & 0xFF;
|
||||
buffer[3] = (data >> 16) & 0xFF;
|
||||
buffer[4] = (data >> 8) & 0xFF;
|
||||
buffer[5] = data & 0xFF;
|
||||
$websocket.get()?.send(buffer);
|
||||
}
|
||||
};
|
||||
|
||||
const send_network_cmd = (command: number) => {
|
||||
if ($websocket.get() !== null && $websocket.get()?.readyState === $websocket.get()?.OPEN) {
|
||||
const buffer = new Uint8Array([0xBA, command]);
|
||||
$websocket.get()?.send(buffer);
|
||||
} else {
|
||||
console.error("WebSocket ist nicht verbunden");
|
||||
}
|
||||
};
|
||||
|
||||
const send_firmware_update_cmd = (command: number) => {
|
||||
if ($websocket.get() !== null && $websocket.get()?.readyState === $websocket.get()?.OPEN) {
|
||||
const buffer = new Uint8Array([0xBB, command]);
|
||||
$websocket.get()?.send(buffer);
|
||||
} else {
|
||||
console.error("WebSocket ist nicht verbunden");
|
||||
}
|
||||
}
|
||||
|
||||
export const start_DCDC = () => {
|
||||
send_uart_cmd_without_data(0x00);
|
||||
}
|
||||
|
||||
export const stop_DCDC = () => {
|
||||
send_uart_cmd_without_data(0x01);
|
||||
}
|
||||
|
||||
export const reset_DCDC = () => {
|
||||
send_uart_cmd_without_data(0x02);
|
||||
}
|
||||
|
||||
export const run_Laser_External = () => {
|
||||
send_uart_cmd_without_data(0x03);
|
||||
}
|
||||
|
||||
export const run_Laser_Internal = () => {
|
||||
send_uart_cmd_without_data(0x04);
|
||||
}
|
||||
|
||||
export const stop_Laser = () => {
|
||||
send_uart_cmd_without_data(0x05);
|
||||
}
|
||||
|
||||
export const reset_Laser = () => {
|
||||
send_uart_cmd_without_data(0x06);
|
||||
}
|
||||
|
||||
export const start_REF_Calibration = () => {
|
||||
send_uart_cmd_without_data(0x07);
|
||||
}
|
||||
|
||||
export const start_Laser_Calibration = () => {
|
||||
send_uart_cmd_without_data(0x08);
|
||||
}
|
||||
|
||||
export const set_Laser_Threshold = (threshold: number) => {
|
||||
send_uart_cmd_with_data(0x09, threshold);
|
||||
}
|
||||
|
||||
export const set_Laser_MAX_Current = (current: number) => {
|
||||
send_uart_cmd_with_data(0x0A, current);
|
||||
}
|
||||
|
||||
export const set_Laser_Set_Current = (current: number) => {
|
||||
send_uart_cmd_with_data(0x0B, current);
|
||||
}
|
||||
|
||||
export const set_Laser_Set_Temp = (temp: number) => {
|
||||
send_uart_cmd_with_data(0x0C, temp);
|
||||
}
|
||||
export const set_Laser_Max_Temp = (temp: number) => {
|
||||
send_uart_cmd_with_data(0x0D, temp);
|
||||
}
|
||||
export const set_Driver_Max_Temp = (temp: number) => {
|
||||
send_uart_cmd_with_data(0x0E, temp);
|
||||
}
|
||||
export const get_calibration_table = () => {
|
||||
console.log("Getting calibration table");
|
||||
send_uart_cmd_without_data(0x0F);
|
||||
}
|
||||
export const set_REF_value = (value: number) => {
|
||||
send_uart_cmd_with_data(0x10, value);
|
||||
}
|
||||
export const set_Overcurrent_Protection_Value = (value: number) => {
|
||||
send_uart_cmd_with_data(0x11, value);
|
||||
}
|
||||
|
||||
// UPDATE SYSTEM COMMANDS
|
||||
export const request_available_updates = () => {
|
||||
console.log("Requesting available updates");
|
||||
send_firmware_update_cmd(0x00);
|
||||
}
|
||||
|
||||
|
||||
export const start_firmware_update_p4_fw = () => {
|
||||
console.log("Starting P4 Firmware update");
|
||||
send_firmware_update_cmd(0x02);
|
||||
}
|
||||
export const start_firmware_update_p4_web = () => {
|
||||
console.log("Starting P4 Webinterface update");
|
||||
send_firmware_update_cmd(0x03);
|
||||
}
|
||||
export const start_firmware_update_stm32_fw = () => {
|
||||
console.log("Starting STM32 Firmware update");
|
||||
send_firmware_update_cmd(0x04);
|
||||
}
|
||||
export const start_firmware_update_c6_fw = () => {
|
||||
console.log("Starting C6 Firmware update");
|
||||
send_firmware_update_cmd(0x05);
|
||||
}
|
||||
export const request_update_progress = () => {
|
||||
console.log("Requesting update progress");
|
||||
|
||||
}
|
||||
45
src/components/ConnectionStatusBadge.tsx
Normal file
45
src/components/ConnectionStatusBadge.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { useStore } from '@nanostores/preact';
|
||||
import { $wsConnectionStatus } from './AppProviderStore';
|
||||
|
||||
export default function ConnectionStatusBadge() {
|
||||
const status = useStore($wsConnectionStatus);
|
||||
|
||||
let colorClass = 'bg-gray-400 text-white';
|
||||
if (status.startsWith('Connected')) colorClass = 'bg-green-500 text-white';
|
||||
else if (status.startsWith('Connecting')) colorClass = 'bg-yellow-400 text-black';
|
||||
else if (
|
||||
status.startsWith('Error') ||
|
||||
status.startsWith('Failed') ||
|
||||
status.startsWith('Disconnected')
|
||||
)
|
||||
colorClass = 'bg-red-500 text-white';
|
||||
|
||||
// Passe die Breite ggf. an den längsten Status an!
|
||||
const maxText = 'Connecting ...';
|
||||
|
||||
return (
|
||||
<span
|
||||
class={`badge ${colorClass} font-mono relative inline-block transition-all duration-200`}
|
||||
style={{ width: '120px', minWidth: '120px', maxWidth: '120px', padding: 0 }}
|
||||
>
|
||||
{/* Unsichtbarer Platzhalter für feste Breite und Position */}
|
||||
<span
|
||||
class="opacity-0 select-none pointer-events-none"
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
{maxText}
|
||||
</span>
|
||||
{/* Sichtbarer Status absolut darüber */}
|
||||
<span
|
||||
class="absolute left-0 top-0 w-full h-full flex items-center"
|
||||
style={{ justifyContent: 'left', paddingLeft: '0.75rem' /* ggf. anpassen */ }}
|
||||
>
|
||||
{status}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
234
src/components/ControlPage.astro
Normal file
234
src/components/ControlPage.astro
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
---
|
||||
const pageTitle = "Laser Control";
|
||||
---
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<h1 class="text-2xl font-bold">{pageTitle}</h1>
|
||||
|
||||
<!-- Control Panel -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<!-- DCDC Controls -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">DCDC Control</h2>
|
||||
|
||||
<!-- Mode Selection -->
|
||||
<div class="join w-full mb-4">
|
||||
<button class="btn join-item flex-1" id="dcdc-start">Start</button>
|
||||
<button class="btn join-item flex-1 btn-success" id="dcdc-run">Run</button>
|
||||
<button class="btn join-item flex-1 btn-error" id="dcdc-stop">Stop</button>
|
||||
<button class="btn join-item flex-1 btn-error error" id="dcdc-error">Error</button>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Target Voltage (V)</span>
|
||||
<span class="label-text-alt" id="voltage-display">25V</span>
|
||||
</label>
|
||||
<input type="range" min="10" max="50" value="25" class="range range-primary" step="1" id="voltage-slider" />
|
||||
<div class="w-full flex justify-between text-xs px-2 mt-1">
|
||||
<span>10V</span>
|
||||
<span>20V</span>
|
||||
<span>30V</span>
|
||||
<span>40V</span>
|
||||
<span>50V</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 mt-6">
|
||||
<div class="badge badge-success gap-2 p-4">
|
||||
<span class="font-semibold">Input Voltage:</span> <span id="input-voltage">24.5V</span>
|
||||
</div>
|
||||
<div class="badge badge-error gap-2 p-4">
|
||||
<span class="font-semibold">PD:</span> <span id="pd-voltage">3.2V</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex gap-2">
|
||||
<button class="btn btn-primary" id="apply-dcdc">Apply Settings</button>
|
||||
<button class="btn btn-outline" id="reset-dcdc">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Laser Control -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Laser Control</h2>
|
||||
|
||||
<div class="join w-full mb-4">
|
||||
<button class="btn join-item flex-1" id="laser-start">Start</button>
|
||||
<button class="btn join-item flex-1 btn-success" id="laser-run">Run</button>
|
||||
<button class="btn join-item flex-1 btn-error" id="laser-stop">Stop</button>
|
||||
<button class="btn join-item flex-1 btn-error error" id="laser-error">Error</button>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<span class="text-lg">Interlock Status:</span>
|
||||
<div class="badge badge-success gap-2 p-4" id="interlock-status">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 10.5V6.75a4.5 4.5 0 1 1 9 0v3.75M3.75 21.75h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H3.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" />
|
||||
</svg>
|
||||
<span>Unlocked</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control mt-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Response Time</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" id="response-time">
|
||||
<option>Fast (High Bandwidth)</option>
|
||||
<option selected>Normal</option>
|
||||
<option>Slow (Low Noise)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control mt-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Stability Mode</span>
|
||||
</label>
|
||||
<div class="join w-full">
|
||||
<button class="btn join-item flex-1 btn-active" id="stability-normal">Normal</button>
|
||||
<button class="btn join-item flex-1" id="stability-high">High Stability</button>
|
||||
<button class="btn join-item flex-1" id="stability-low">Low Noise</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>The laser mode is dependent on the DCDC status and safety settings.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Safety Settings -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Safety Settings</h2>
|
||||
|
||||
<!-- Horizontal row of toggles -->
|
||||
<div class="flex flex-wrap gap-4 mt-4">
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Interlock Check</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked id="toggle-interlock" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Temperature Protection</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked id="toggle-temp" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">OverCurrent Protection</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked id="toggle-overcurrent" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Input Voltage Protection</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked id="toggle-voltage" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Autostart DCDC</span>
|
||||
<input type="checkbox" class="toggle toggle-error" id="toggle-autostart-dcdc" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Autostart Laser</span>
|
||||
<input type="checkbox" class="toggle toggle-error" id="toggle-autostart-laser" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="alert alert-warning">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<span>Disabling safety features may damage your hardware or create hazardous conditions.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Voltage slider interaction
|
||||
const voltageSlider = document.getElementById('voltage-slider') as HTMLInputElement;
|
||||
const voltageDisplay = document.getElementById('voltage-display');
|
||||
|
||||
if (voltageSlider && voltageDisplay) {
|
||||
voltageSlider.addEventListener('input', function() {
|
||||
voltageDisplay.textContent = this.value + 'V';
|
||||
});
|
||||
}
|
||||
|
||||
// DCDC button interactions
|
||||
const dcdcButtons = {
|
||||
start: document.getElementById('dcdc-start'),
|
||||
run: document.getElementById('dcdc-run'),
|
||||
stop: document.getElementById('dcdc-stop'),
|
||||
error: document.getElementById('dcdc-error')
|
||||
};
|
||||
|
||||
Object.entries(dcdcButtons).forEach(([action, btn]) => {
|
||||
if (btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
console.log('DCDC:', action);
|
||||
// Send WebSocket command
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Laser button interactions
|
||||
const laserButtons = {
|
||||
start: document.getElementById('laser-start'),
|
||||
run: document.getElementById('laser-run'),
|
||||
stop: document.getElementById('laser-stop'),
|
||||
error: document.getElementById('laser-error')
|
||||
};
|
||||
|
||||
Object.entries(laserButtons).forEach(([action, btn]) => {
|
||||
if (btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
console.log('Laser:', action);
|
||||
// Send WebSocket command
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Safety toggle interactions
|
||||
const toggles = document.querySelectorAll('.toggle');
|
||||
toggles.forEach(toggle => {
|
||||
toggle.addEventListener('change', () => {
|
||||
updateToggleClass(toggle as HTMLInputElement);
|
||||
});
|
||||
updateToggleClass(toggle as HTMLInputElement);
|
||||
});
|
||||
|
||||
function updateToggleClass(toggle: HTMLInputElement) {
|
||||
if (toggle.checked) {
|
||||
toggle.classList.remove('toggle-error');
|
||||
toggle.classList.add('toggle-success');
|
||||
} else {
|
||||
toggle.classList.remove('toggle-success');
|
||||
toggle.classList.add('toggle-error');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
549
src/components/DashboardPage.astro
Normal file
549
src/components/DashboardPage.astro
Normal file
|
|
@ -0,0 +1,549 @@
|
|||
---
|
||||
import {
|
||||
$wsConnectionStatus,
|
||||
$dcdc_state_string,
|
||||
$dcdc_state_desc_string,
|
||||
$laser_state_string,
|
||||
$laser_state_desc_string,
|
||||
$laser_temp,
|
||||
$laser_temp_string,
|
||||
$driver_temp,
|
||||
$driver_temp_string,
|
||||
$driver_max_temp,
|
||||
$driver_max_temp_string,
|
||||
$temperature_chart_data,
|
||||
addTemperatureDataPoint,
|
||||
} from "./AppProviderStore";
|
||||
const pageTitle = "Dashboard";
|
||||
---
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<h1 class="text-2xl font-bold">{pageTitle}</h1>
|
||||
|
||||
<!-- Status Cards - Static HTML that gets updated via WebSocket -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 bg-base-100">
|
||||
<div class="stats shadow bg-base-200 rounded-lg border-0">
|
||||
<div class="stat bg-base-200">
|
||||
<div class="stat-title">DCDC State</div>
|
||||
<div class="stat-value" id="dcdc-state-value">
|
||||
{$dcdc_state_string.get()}
|
||||
</div>
|
||||
<div class="stat-desc flex items-center">
|
||||
<span class="text-info mr-1" id="dcdc-state-icon">● </span>
|
||||
<span id="dcdc-state-text">{$dcdc_state_desc_string.get()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats shadow bg-base-200 rounded-lg border-0">
|
||||
<div class="stat bg-base-200">
|
||||
<div class="stat-title">Laser State</div>
|
||||
<div class="stat-value" id="laser-state-value">
|
||||
{$laser_state_string.get()}
|
||||
</div>
|
||||
<div class="stat-desc flex items-center">
|
||||
<span class="text-error mr-1" id="laser-state-icon">●</span>
|
||||
<span id="laser-state-text">{$laser_state_desc_string.get()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats shadow bg-base-200 rounded-lg border-0">
|
||||
<div class="stat bg-base-200">
|
||||
<div class="stat-title">Driver Temperature</div>
|
||||
<div class="stat-value" id="driver-temp-value1">{$driver_temp_string.get()}</div>
|
||||
<div class="stat-desc flex items-center">
|
||||
<span class="text-info mr-1" id="driver-max-temp-icon">●</span>
|
||||
<span id="driver-max-temp-text">Maximal allowed: {$driver_max_temp_string.get()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats shadow bg-base-200 rounded-lg border-0">
|
||||
<div class="stat bg-base-200">
|
||||
<div class="stat-title">WebSocket</div>
|
||||
<div class="stat-value" id="ws-status-value">
|
||||
{$wsConnectionStatus.get()}
|
||||
</div>
|
||||
<div class="stat-desc flex items-center" id="ws-status-desc">
|
||||
<span class="text-error mr-1" id="ws-status-icon">●</span>
|
||||
Connection status
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions & Controls - Interactive Parts -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<!-- DCDC Controls -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">DCDC Control</h2>
|
||||
<div class="flex flex-col gap-2">
|
||||
<button class="btn btn-primary" id="dcdc-start-btn">Start DCDC</button
|
||||
>
|
||||
<button class="btn btn-warning" id="dcdc-stop-btn">Stop DCDC</button>
|
||||
<button class="btn btn-outline" id="dcdc-reset-btn">Reset DCDC</button
|
||||
>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<span class="text-sm"
|
||||
>Power Good: <span id="power-good-status" class="font-bold">--</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Laser Controls -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Laser Control</h2>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-row gap-3">
|
||||
<button class="btn btn-primary flex-1 rounded-lg shadow-md transition hover:scale-105" id="ref-calibrate-btn">Calibrate REF</button>
|
||||
<button class="btn btn-primary flex-1 rounded-lg shadow-md transition hover:scale-105" id="laser-calibrate-btn">Calibrate Laser</button>
|
||||
</div>
|
||||
<div class="flex flex-row gap-3">
|
||||
<button
|
||||
class="btn btn-success flex-1 rounded-lg shadow-md transition hover:scale-105"
|
||||
id="laser-start-external-btn">Start Laser External</button
|
||||
>
|
||||
<button
|
||||
class="btn btn-success flex-1 rounded-lg shadow-md transition hover:scale-105"
|
||||
id="laser-start-internal-btn">Start Laser Internal</button
|
||||
>
|
||||
</div>
|
||||
<button class="btn btn-error" id="laser-stop-btn">Stop Laser</button>
|
||||
<button class="btn btn-outline" id="laser-reset-btn"
|
||||
>Reset Laser</button
|
||||
>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<span class="text-sm"
|
||||
>Laser Good: <span id="laser-good-status" class="font-bold">--</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Status -->
|
||||
<div class="card bg-base-200 shadow-xl">
|
||||
<div class="card-body bg-base-100 rounded-lg">
|
||||
<h2 class="card-title">System Status</h2>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<span>Driver Temp:</span>
|
||||
<span id="driver-temp-value2" class="font-mono">--°C</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>VCC Voltage:</span>
|
||||
<span id="vcc-voltage-value" class="font-mono">--V</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>PD Voltage:</span>
|
||||
<span id="pd-voltage-value" class="font-mono">--V</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>Laser Current:</span>
|
||||
<span id="laser-current-value" class="font-mono">--mA</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body bg-base-100 rounded-lg relative">
|
||||
<h2 class="card-title">Calibration Table</h2>
|
||||
|
||||
<!-- Chart container with placeholder -->
|
||||
<div class="flex-1 min-h-[300px] max-h-[500px] relative">
|
||||
<!-- Placeholder when no data -->
|
||||
<div id="chart-placeholder" class="flex items-center justify-center h-full bg-base-200 rounded-lg">
|
||||
<div class="text-center">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chart-line mx-auto"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 19l16 0" /><path d="M4 15l4 -6l4 2l4 -5l4 4" /></svg>
|
||||
|
||||
<div class="text-lg font-semibold">No Calibration Data</div>
|
||||
<div class="text-sm text-base-content/70">Click the button below to request data from device</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart canvas (hidden initially) -->
|
||||
<canvas id="calibration-table-canvas" class="hidden w-full h-full"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- Small button positioned bottom right -->
|
||||
<button class="btn btn-primary btn-xs absolute bottom-2 right-2" id="calibration-table-btn">
|
||||
Request Calibration Table
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Temperature Chart -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body bg-base-100 rounded-lg relative">
|
||||
<h2 class="card-title">Temperature Monitor</h2>
|
||||
|
||||
<!-- Chart container -->
|
||||
<div class="flex-1 min-h-[300px] max-h-[500px] relative">
|
||||
<!-- Chart canvas -->
|
||||
<canvas id="temperature-chart-canvas" class="w-full h-full"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- Status indicators -->
|
||||
<div class="flex justify-between items-center mt-2 text-sm">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="w-3 h-3 bg-blue-500 rounded"></div>
|
||||
<span>Driver Temp: <span id="driver-temp-current" class="font-mono">--°C</span></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="w-3 h-3 bg-red-500 rounded"></div>
|
||||
<span>Laser Temp: <span id="laser-temp-current" class="font-mono">--°C</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-base-content/70">
|
||||
Last 5 minutes
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hydrate only the interactive parts -->
|
||||
<script>
|
||||
import {
|
||||
$wsConnectionStatus,
|
||||
$dcdc_state_string,
|
||||
$dcdc_state_desc_string,
|
||||
$laser_state_string,
|
||||
$laser_state_desc_string,
|
||||
start_DCDC,
|
||||
$laser_temp,
|
||||
$laser_temp_string,
|
||||
$driver_temp,
|
||||
$driver_temp_string,
|
||||
$driver_max_temp,
|
||||
$driver_max_temp_string,
|
||||
$temperature_chart_data,
|
||||
addTemperatureDataPoint,
|
||||
$vcc_voltage,
|
||||
$vcc_voltage_string,
|
||||
$pd_voltage,
|
||||
$pd_voltage_string,
|
||||
$current_reading,
|
||||
$current_reading_string,
|
||||
$POWERGOOD_string,
|
||||
$LASERGOOD_string,
|
||||
stop_DCDC,
|
||||
reset_DCDC,
|
||||
run_Laser_External,
|
||||
run_Laser_Internal,
|
||||
stop_Laser,
|
||||
reset_Laser,
|
||||
get_calibration_table,
|
||||
$calibration_table_data,
|
||||
start_REF_Calibration,
|
||||
start_Laser_Calibration,
|
||||
} from "./AppProviderStore";
|
||||
import { Chart, type ChartItem } from "chart.js/auto";
|
||||
// subscribe to the wsConnectionStatus store and update the value of the ws-status-value element
|
||||
$wsConnectionStatus.subscribe((value) => {
|
||||
const el = document.getElementById("ws-status-value");
|
||||
const elIcon = document.getElementById("ws-status-icon");
|
||||
// overwrite the value of the ws-status-value element
|
||||
if (el) el.textContent = value;
|
||||
// overwrite the icon color of the ws-status-icon element
|
||||
if (elIcon && value === "Connected") {
|
||||
elIcon.classList.remove("text-error");
|
||||
elIcon.classList.add("text-success");
|
||||
} else if (elIcon && value === "Disconnected") {
|
||||
elIcon.classList.remove("text-success");
|
||||
elIcon.classList.add("text-error");
|
||||
} else if (elIcon && value === "Error") {
|
||||
elIcon.classList.remove("text-success");
|
||||
elIcon.classList.add("text-error");
|
||||
} else if (elIcon && value === "Failed") {
|
||||
elIcon.classList.remove("text-success");
|
||||
elIcon.classList.add("text-error");
|
||||
} else if (elIcon && value === "Connecting") {
|
||||
elIcon.classList.remove("text-error");
|
||||
elIcon.classList.add("text-warning");
|
||||
}
|
||||
});
|
||||
// subscribe to the dcdcStateString store and update the value of the dcdc-state-value element
|
||||
$dcdc_state_string.subscribe((value) => {
|
||||
const el = document.getElementById("dcdc-state-value");
|
||||
console.log("dcdcStateString", value);
|
||||
if (el) el.textContent = value;
|
||||
});
|
||||
// subscribe to the laserStateString store and update the value of the laser-state-value element
|
||||
$laser_state_string.subscribe((value) => {
|
||||
const el = document.getElementById("laser-state-value");
|
||||
if (el) el.textContent = value;
|
||||
});
|
||||
$driver_temp.subscribe((value) => {
|
||||
const el1 = document.getElementById("driver-temp-value1");
|
||||
const el2 = document.getElementById("driver-temp-value2");
|
||||
const elMax = document.getElementById("driver-max-temp-text");
|
||||
if (el1) el1.textContent = $driver_temp_string.get();
|
||||
if (el2) el2.textContent = $driver_temp_string.get();
|
||||
});
|
||||
$driver_max_temp_string.subscribe((value) => {
|
||||
const elMax = document.getElementById("driver-max-temp-text");
|
||||
if (elMax) elMax.textContent = "Maximal allowed: " + $driver_max_temp_string.get();
|
||||
});
|
||||
|
||||
// Temperature chart subscriptions
|
||||
$driver_temp.subscribe((value) => {
|
||||
const el = document.getElementById("driver-temp-current");
|
||||
if (el) el.textContent = $driver_temp_string.get();
|
||||
|
||||
// Add data point to temperature chart using store function
|
||||
addTemperatureDataPoint($driver_temp.get(), $laser_temp.get());
|
||||
});
|
||||
|
||||
$laser_temp.subscribe((value) => {
|
||||
const el = document.getElementById("laser-temp-current");
|
||||
if (el) el.textContent = $laser_temp_string.get();
|
||||
|
||||
// Temperature data will be added when driver temp updates to avoid duplicate entries
|
||||
});
|
||||
$vcc_voltage.subscribe((value) => {
|
||||
const el = document.getElementById("vcc-voltage-value");
|
||||
if (el) el.textContent = $vcc_voltage_string.get();
|
||||
});
|
||||
$pd_voltage_string.subscribe((value) => {
|
||||
const el = document.getElementById("pd-voltage-value");
|
||||
if (el) el.textContent = $pd_voltage_string.get();
|
||||
});
|
||||
$current_reading_string.subscribe((value) => {
|
||||
const el = document.getElementById("laser-current-value");
|
||||
if (el) el.textContent = $current_reading_string.get();
|
||||
});
|
||||
|
||||
$POWERGOOD_string.subscribe((value) => {
|
||||
const el = document.getElementById("power-good-status");
|
||||
if (el) el.textContent = $POWERGOOD_string.get();
|
||||
});
|
||||
$LASERGOOD_string.subscribe((value) => {
|
||||
const el = document.getElementById("laser-good-status");
|
||||
if (el) el.textContent = $LASERGOOD_string.get();
|
||||
});
|
||||
$dcdc_state_desc_string.subscribe((value) => {
|
||||
const el = document.getElementById("dcdc-state-text");
|
||||
if (el) el.textContent = $dcdc_state_desc_string.get();
|
||||
});
|
||||
$laser_state_desc_string.subscribe((value) => {
|
||||
const el = document.getElementById("laser-state-text");
|
||||
if (el) el.textContent = $laser_state_desc_string.get();
|
||||
});
|
||||
// Button click handlers
|
||||
document
|
||||
.getElementById("dcdc-start-btn")
|
||||
?.addEventListener("click", start_DCDC);
|
||||
document
|
||||
.getElementById("dcdc-stop-btn")
|
||||
?.addEventListener("click", stop_DCDC);
|
||||
document
|
||||
.getElementById("dcdc-reset-btn")
|
||||
?.addEventListener("click", reset_DCDC);
|
||||
document
|
||||
.getElementById("laser-start-external-btn")
|
||||
?.addEventListener("click", run_Laser_External);
|
||||
document
|
||||
.getElementById("laser-start-internal-btn")
|
||||
?.addEventListener("click", run_Laser_Internal);
|
||||
document
|
||||
.getElementById("laser-stop-btn")
|
||||
?.addEventListener("click", stop_Laser);
|
||||
document
|
||||
.getElementById("laser-reset-btn")
|
||||
?.addEventListener("click", reset_Laser);
|
||||
|
||||
document.getElementById("ref-calibrate-btn")?.addEventListener("click", start_REF_Calibration);
|
||||
document.getElementById("laser-calibrate-btn")?.addEventListener("click", start_Laser_Calibration);
|
||||
|
||||
// Chart management
|
||||
let chart: Chart | null = null;
|
||||
const canvas = document.getElementById("calibration-table-canvas");
|
||||
const placeholder = document.getElementById("chart-placeholder");
|
||||
|
||||
function showPlaceholder() {
|
||||
if (placeholder) placeholder.classList.remove("hidden");
|
||||
if (canvas) canvas.classList.add("hidden");
|
||||
if (chart) {
|
||||
chart.destroy();
|
||||
chart = null;
|
||||
}
|
||||
}
|
||||
|
||||
function showChart() {
|
||||
if (placeholder) placeholder.classList.add("hidden");
|
||||
if (canvas) canvas.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function createChart(data: any) {
|
||||
if (chart) {
|
||||
chart.destroy();
|
||||
}
|
||||
|
||||
chart = new Chart(canvas as ChartItem, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: [500,1000,1500,2000,2500,3000,3500],
|
||||
datasets: [
|
||||
{
|
||||
label: "Calibration Curve",
|
||||
data: data,
|
||||
fill: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: {
|
||||
duration: 0 // Disable animations to prevent text stretching during resize
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Duty Cycle'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Current'
|
||||
},
|
||||
min:0,
|
||||
max:6000,
|
||||
ticks: {
|
||||
stepSize: 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
// create chart when calibration table data is available
|
||||
$calibration_table_data.subscribe((value) => {
|
||||
console.log("Calibration table data updated");
|
||||
createChart(value.values);
|
||||
showChart();
|
||||
});
|
||||
|
||||
// Request calibration data button
|
||||
document.getElementById("calibration-table-btn")?.addEventListener("click", () => {
|
||||
// reuest table
|
||||
get_calibration_table();
|
||||
});
|
||||
|
||||
// Temperature Chart Management
|
||||
let temperatureChart: Chart | null = null;
|
||||
const temperatureCanvas = document.getElementById("temperature-chart-canvas");
|
||||
|
||||
function createTemperatureChart() {
|
||||
if (temperatureChart) {
|
||||
temperatureChart.destroy();
|
||||
}
|
||||
|
||||
const chartData = $temperature_chart_data.get();
|
||||
|
||||
temperatureChart = new Chart(temperatureCanvas as ChartItem, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: chartData.timestamps,
|
||||
datasets: [
|
||||
{
|
||||
label: "Driver Temperature",
|
||||
data: chartData.driverTemps,
|
||||
borderColor: "#3B82F6", // blue-500
|
||||
backgroundColor: "#3B82F680",
|
||||
fill: false,
|
||||
tension: 0.1,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 5,
|
||||
},
|
||||
{
|
||||
label: "Laser Temperature",
|
||||
data: chartData.laserTemps,
|
||||
borderColor: "#EF4444", // red-500
|
||||
backgroundColor: "#EF444480",
|
||||
fill: false,
|
||||
tension: 0.1,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: {
|
||||
duration: 0 // Disable animations for real-time performance
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Time'
|
||||
},
|
||||
ticks: {
|
||||
maxTicksLimit: 10 // Show only every nth label to avoid crowding
|
||||
}
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Temperature (°C)'
|
||||
},
|
||||
min: 0,
|
||||
max: 100,
|
||||
ticks: {
|
||||
stepSize: 10
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top' as const
|
||||
}
|
||||
},
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index' as const
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize temperature chart
|
||||
createTemperatureChart();
|
||||
|
||||
// Subscribe to temperature chart data updates
|
||||
$temperature_chart_data.subscribe((chartData) => {
|
||||
if (temperatureChart) {
|
||||
temperatureChart.data.labels = chartData.timestamps;
|
||||
temperatureChart.data.datasets[0].data = chartData.driverTemps;
|
||||
temperatureChart.data.datasets[1].data = chartData.laserTemps;
|
||||
temperatureChart.update('none'); // No animation for real-time
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize with placeholder
|
||||
showPlaceholder();
|
||||
</script>
|
||||
283
src/components/DcdcPage.astro
Normal file
283
src/components/DcdcPage.astro
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
---
|
||||
const pageTitle = "DCDC Control";
|
||||
---
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<h1 class="text-2xl font-bold">{pageTitle}</h1>
|
||||
|
||||
<!-- Mode Control -->
|
||||
<div class="card bg-base-200 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Mode Control</h2>
|
||||
<div class="join w-full">
|
||||
<button class="btn join-item flex-1 btn-primary" id="dcdc-start">Start</button>
|
||||
<button class="btn join-item flex-1 btn-success" id="dcdc-run">Run</button>
|
||||
<button class="btn join-item flex-1 btn-error" id="dcdc-stop">Stop</button>
|
||||
<button class="btn join-item flex-1 btn-error error-striped" id="dcdc-error">Error</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Voltage Control & Monitoring -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Voltage Control</h2>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Target Output Voltage</span>
|
||||
<span class="label-text-alt" id="voltage-display">25V</span>
|
||||
</label>
|
||||
<input type="range" min="10" max="50" value="25" class="range range-primary" step="0.1" id="voltage-slider" />
|
||||
<div class="w-full flex justify-between text-xs px-2 mt-1">
|
||||
<span>10V</span>
|
||||
<span>20V</span>
|
||||
<span>30V</span>
|
||||
<span>40V</span>
|
||||
<span>50V</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mt-6">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Input Voltage</div>
|
||||
<div class="stat-value text-sm" id="input-voltage">24.5V</div>
|
||||
<div class="stat-desc">Range: 18-28V</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Output Voltage</div>
|
||||
<div class="stat-value text-sm" id="output-voltage">25.0V</div>
|
||||
<div class="stat-desc" id="voltage-accuracy">±0.1V</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button class="btn btn-primary mr-2" id="apply-voltage">Apply</button>
|
||||
<button class="btn btn-outline" id="reset-voltage">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Current & Power</h2>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Current</div>
|
||||
<div class="stat-value text-sm" id="output-current">2.1A</div>
|
||||
<div class="stat-desc">Max: 5.0A</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Power</div>
|
||||
<div class="stat-value text-sm" id="output-power">52.5W</div>
|
||||
<div class="stat-desc">Max: 150W</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="flex justify-between text-sm mb-1">
|
||||
<span>Efficiency</span>
|
||||
<span id="efficiency">92.5%</span>
|
||||
</div>
|
||||
<progress class="progress progress-success w-full" value="92.5" max="100" id="efficiency-progress"></progress>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="flex justify-between text-sm mb-1">
|
||||
<span>Temperature</span>
|
||||
<span id="dcdc-temp">45.2°C</span>
|
||||
</div>
|
||||
<progress class="progress progress-warning w-full" value="45.2" max="85" id="temp-progress"></progress>
|
||||
<div class="text-xs text-gray-500 mt-1">Warning at 85°C</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Protection Settings -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Protection Settings</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mt-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Over-Current Limit (A)</span>
|
||||
</label>
|
||||
<input type="range" min="1" max="6" value="5" step="0.1" class="range range-error" id="overcurrent-limit" />
|
||||
<div class="w-full flex justify-between text-xs px-2">
|
||||
<span>1A</span>
|
||||
<span>3A</span>
|
||||
<span>6A</span>
|
||||
</div>
|
||||
<div class="label">
|
||||
<span class="label-text-alt" id="overcurrent-display">5.0A</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Over-Temperature (°C)</span>
|
||||
</label>
|
||||
<input type="range" min="60" max="100" value="85" step="1" class="range range-error" id="overtemp-limit" />
|
||||
<div class="w-full flex justify-between text-xs px-2">
|
||||
<span>60°C</span>
|
||||
<span>80°C</span>
|
||||
<span>100°C</span>
|
||||
</div>
|
||||
<div class="label">
|
||||
<span class="label-text-alt" id="overtemp-display">85°C</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Under-Voltage (V)</span>
|
||||
</label>
|
||||
<input type="range" min="15" max="22" value="18" step="0.1" class="range range-warning" id="undervoltage-limit" />
|
||||
<div class="w-full flex justify-between text-xs px-2">
|
||||
<span>15V</span>
|
||||
<span>18V</span>
|
||||
<span>22V</span>
|
||||
</div>
|
||||
<div class="label">
|
||||
<span class="label-text-alt" id="undervoltage-display">18.0V</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-4 mt-6">
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Auto-Start on Power</span>
|
||||
<input type="checkbox" class="toggle toggle-primary" id="toggle-autostart" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Soft-Start Enable</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked id="toggle-softstart" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status & Diagnostics -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Status & Diagnostics</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
||||
<div>
|
||||
<h3 class="font-semibold mb-2">Operating Status</h3>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<span>PWM Frequency:</span>
|
||||
<span id="pwm-freq">100kHz</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>Duty Cycle:</span>
|
||||
<span id="duty-cycle">65.2%</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>Switching Losses:</span>
|
||||
<span id="switching-losses">2.1W</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>Uptime:</span>
|
||||
<span id="dcdc-uptime">2h 45m</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-semibold mb-2">Error Log</h3>
|
||||
<div class="bg-base-200 p-3 rounded-lg h-32 overflow-y-auto" id="error-log">
|
||||
<div class="text-sm space-y-1">
|
||||
<div class="text-success">14:32 - System started</div>
|
||||
<div class="text-info">14:33 - Voltage ramped to 25V</div>
|
||||
<div class="text-warning">14:35 - Temp spike to 60°C</div>
|
||||
<div class="text-success">14:36 - Normal operation</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.error-striped {
|
||||
background-image: repeating-linear-gradient(45deg, hsl(var(--er)) 0, hsl(var(--er)) 10px, hsl(var(--er) / 0.8) 10px, hsl(var(--er) / 0.8) 20px);
|
||||
animation: error-blink 0.25s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes error-blink {
|
||||
0% { opacity: 0.7; }
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0.7; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Voltage slider
|
||||
const voltageSlider = document.getElementById('voltage-slider') as HTMLInputElement;
|
||||
const voltageDisplay = document.getElementById('voltage-display');
|
||||
|
||||
if (voltageSlider && voltageDisplay) {
|
||||
voltageSlider.addEventListener('input', function() {
|
||||
voltageDisplay.textContent = parseFloat(this.value).toFixed(1) + 'V';
|
||||
});
|
||||
}
|
||||
|
||||
// Protection limit sliders
|
||||
const sliders = [
|
||||
{ id: 'overcurrent-limit', display: 'overcurrent-display', unit: 'A', decimals: 1 },
|
||||
{ id: 'overtemp-limit', display: 'overtemp-display', unit: '°C', decimals: 0 },
|
||||
{ id: 'undervoltage-limit', display: 'undervoltage-display', unit: 'V', decimals: 1 }
|
||||
];
|
||||
|
||||
sliders.forEach(slider => {
|
||||
const element = document.getElementById(slider.id) as HTMLInputElement;
|
||||
const display = document.getElementById(slider.display);
|
||||
|
||||
if (element && display) {
|
||||
element.addEventListener('input', function() {
|
||||
const value = parseFloat(this.value).toFixed(slider.decimals);
|
||||
display.textContent = value + slider.unit;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// DCDC mode buttons
|
||||
const modeButtons = ['dcdc-start', 'dcdc-run', 'dcdc-stop', 'dcdc-error'];
|
||||
modeButtons.forEach(buttonId => {
|
||||
const button = document.getElementById(buttonId);
|
||||
if (button) {
|
||||
button.addEventListener('click', function() {
|
||||
console.log('DCDC mode:', buttonId.replace('dcdc-', ''));
|
||||
// Send WebSocket command
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Function to update DCDC values (called from WebSocket)
|
||||
(window as any).updateDcdcValues = function(data: any) {
|
||||
const fields = [
|
||||
'input-voltage', 'output-voltage', 'output-current', 'output-power',
|
||||
'efficiency', 'dcdc-temp', 'pwm-freq', 'duty-cycle', 'switching-losses', 'dcdc-uptime'
|
||||
];
|
||||
|
||||
fields.forEach(field => {
|
||||
const element = document.getElementById(field);
|
||||
if (element && data[field.replace('-', '_')]) {
|
||||
element.textContent = data[field.replace('-', '_')];
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
</script>
|
||||
165
src/components/GlobalModals.tsx
Normal file
165
src/components/GlobalModals.tsx
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
import { useState, useEffect } from 'preact/hooks';
|
||||
import { useStore } from '@nanostores/preact';
|
||||
import { $wsConnectionStatus, $updateState } from './AppProviderStore';
|
||||
import UpdaterModal from './UpdaterModal';
|
||||
import UpdateSuccessPopup from './UpdateSuccessPopup';
|
||||
import UpdateErrorPopup from './UpdateErrorPopup';
|
||||
import { $isUpdaterModalOpen, $isSuccessPopupOpen, $isErrorPopupOpen, stateSetters } from './AppProviderStore';
|
||||
|
||||
// Extend window interface
|
||||
declare global {
|
||||
interface Window {
|
||||
openStatusModal?: () => void;
|
||||
openFirmwareModal?: () => void;
|
||||
openUpdaterModal?: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
export default function GlobalModals() {
|
||||
const [isStatusModalOpen, setIsStatusModalOpen] = useState(false);
|
||||
const [isFirmwareModalOpen, setIsFirmwareModalOpen] = useState(false);
|
||||
|
||||
const updateState = useStore($updateState);
|
||||
const isUpdaterModalOpen = useStore($isUpdaterModalOpen);
|
||||
const isSuccessPopupOpen = useStore($isSuccessPopupOpen);
|
||||
const isErrorPopupOpen = useStore($isErrorPopupOpen);
|
||||
|
||||
// Auto-open success/error popups when update state changes
|
||||
useEffect(() => {
|
||||
console.log("🎭 GlobalModals useEffect triggered:", {
|
||||
updateState,
|
||||
isSuccessPopupOpen,
|
||||
isErrorPopupOpen,
|
||||
isUpdaterModalOpen
|
||||
});
|
||||
|
||||
if (updateState === 'success' && !isSuccessPopupOpen) {
|
||||
console.log("🟢 Opening SUCCESS popup - update completed");
|
||||
stateSetters.setSuccessPopupOpen(true);
|
||||
} else if (updateState === 'restarting') {
|
||||
console.log("🔄 RESTARTING state - ensuring SUCCESS popup stays open for countdown");
|
||||
// Ensure success popup is open for restarting state (it will show restart UI)
|
||||
if (!isSuccessPopupOpen) {
|
||||
console.log("🔄 Opening SUCCESS popup for RESTARTING state - will show countdown");
|
||||
stateSetters.setSuccessPopupOpen(true);
|
||||
} else {
|
||||
console.log("🔄 SUCCESS popup already open - countdown should be visible");
|
||||
}
|
||||
} else if (updateState === 'error' && !isErrorPopupOpen) {
|
||||
console.log("❌ Opening ERROR popup - update failed");
|
||||
stateSetters.setErrorPopupOpen(true);
|
||||
} else {
|
||||
console.log("🎭 No modal action needed - conditions not met");
|
||||
}
|
||||
}, [updateState, isSuccessPopupOpen, isErrorPopupOpen]);
|
||||
|
||||
// Listen for global modal events
|
||||
if (typeof window !== 'undefined') {
|
||||
window.openStatusModal = () => setIsStatusModalOpen(true);
|
||||
window.openFirmwareModal = () => setIsFirmwareModalOpen(true);
|
||||
window.openUpdaterModal = () => {
|
||||
if (!$isUpdaterModalOpen.get()) {
|
||||
stateSetters.setUpdaterModalOpen(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Status Modal */}
|
||||
{isStatusModalOpen && (
|
||||
<dialog class="modal modal-open">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mb-4">System Status</h3>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="font-bold">WebSocket Status</td>
|
||||
<td>{useStore($wsConnectionStatus)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-bold">P4 Firmware Version</td>
|
||||
<td>1.2.5</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-bold">C6 Firmware Version</td>
|
||||
<td>0.9.3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-bold">G4 Firmware Version</td>
|
||||
<td>2.1.0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-bold">Activated Plugins</td>
|
||||
<td>- none -</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-bold">Serial Number</td>
|
||||
<td>HydroBuddy-00001</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-bold">Operating Hours</td>
|
||||
<td>127.5 h</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-between">
|
||||
<button class="btn btn-primary">Customer Service</button>
|
||||
<button class="btn" onClick={() => setIsStatusModalOpen(false)}>Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop" onClick={() => setIsStatusModalOpen(false)}></div>
|
||||
</dialog>
|
||||
)}
|
||||
|
||||
{/* Firmware Update Modal */}
|
||||
{isFirmwareModalOpen && (
|
||||
<dialog class="modal modal-open">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mb-4">Firmware Update</h3>
|
||||
|
||||
<div class="flex flex-col items-center justify-center py-6">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
<p class="mt-4 text-center">Checking for firmware updates...</p>
|
||||
</div>
|
||||
|
||||
<div class="modal-action">
|
||||
<button class="btn" onClick={() => setIsFirmwareModalOpen(false)}>Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop" onClick={() => setIsFirmwareModalOpen(false)}></div>
|
||||
</dialog>
|
||||
)}
|
||||
|
||||
{/* Updater Modal */}
|
||||
<UpdaterModal
|
||||
isOpen={isUpdaterModalOpen}
|
||||
onClose={() => { stateSetters.setUpdaterModalOpen(false); }}
|
||||
/>
|
||||
|
||||
{/* Update Success Popup */}
|
||||
<UpdateSuccessPopup
|
||||
isOpen={isSuccessPopupOpen}
|
||||
onClose={() => {
|
||||
stateSetters.setSuccessPopupOpen(false);
|
||||
// Reset update state after popup is closed
|
||||
stateSetters.setUpdateState('initial');
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Update Error Popup */}
|
||||
<UpdateErrorPopup
|
||||
isOpen={isErrorPopupOpen}
|
||||
onClose={() => {
|
||||
stateSetters.setErrorPopupOpen(false);
|
||||
// Reset update state after popup is closed
|
||||
stateSetters.setUpdateState('initial');
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
144
src/components/LogsPage.astro
Normal file
144
src/components/LogsPage.astro
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
---
|
||||
const pageTitle = "Logs";
|
||||
---
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold">System Logs</h1>
|
||||
<div class="join">
|
||||
<button class="btn join-item">Export</button>
|
||||
<button class="btn join-item btn-primary">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card bg-base-200 shadow-lg mb-4">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title text-lg">Filters</h2>
|
||||
|
||||
<form class="filter grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Log Level</span>
|
||||
</label>
|
||||
<div class="join w-full">
|
||||
<input class="btn join-item flex-1" type="radio" name="log-level" aria-label="All" checked />
|
||||
<input class="btn join-item flex-1" type="radio" name="log-level" aria-label="Error" />
|
||||
<input class="btn join-item flex-1" type="radio" name="log-level" aria-label="Warning" />
|
||||
<input class="btn join-item flex-1" type="radio" name="log-level" aria-label="Info" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Date Range</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full">
|
||||
<option selected>Today</option>
|
||||
<option>Last 3 Days</option>
|
||||
<option>Last Week</option>
|
||||
<option>Last Month</option>
|
||||
<option>Custom Range</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Search</span>
|
||||
</label>
|
||||
<div class="join w-full">
|
||||
<input type="text" placeholder="Search logs..." class="input input-bordered join-item w-full" />
|
||||
<button class="btn join-item">Search</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-1 md:col-span-3">
|
||||
<button type="reset" class="btn btn-sm btn-outline">Reset Filters</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card bg-base-200 shadow-lg">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Level</th>
|
||||
<th>Component</th>
|
||||
<th>Message</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>2025-03-15 14:32:45</td>
|
||||
<td><div class="badge badge-error">ERROR</div></td>
|
||||
<td>Driver</td>
|
||||
<td>Failed to initialize temperature sensor</td>
|
||||
<td><button class="btn btn-xs btn-ghost">Details</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2025-03-15 14:30:12</td>
|
||||
<td><div class="badge badge-warning">WARN</div></td>
|
||||
<td>Modulator</td>
|
||||
<td>PWM frequency outside optimal range</td>
|
||||
<td><button class="btn btn-xs btn-ghost">Details</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2025-03-15 14:28:05</td>
|
||||
<td><div class="badge badge-info">INFO</div></td>
|
||||
<td>System</td>
|
||||
<td>User authentication successful</td>
|
||||
<td><button class="btn btn-xs btn-ghost">Details</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2025-03-15 14:25:59</td>
|
||||
<td><div class="badge">DEBUG</div></td>
|
||||
<td>Control</td>
|
||||
<td>PID controller parameters updated</td>
|
||||
<td><button class="btn btn-xs btn-ghost">Details</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2025-03-15 14:20:33</td>
|
||||
<td><div class="badge badge-info">INFO</div></td>
|
||||
<td>System</td>
|
||||
<td>Laser driver initialization complete</td>
|
||||
<td><button class="btn btn-xs btn-ghost">Details</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2025-03-15 14:18:22</td>
|
||||
<td><div class="badge badge-success">SUCCESS</div></td>
|
||||
<td>Hardware</td>
|
||||
<td>All hardware components detected</td>
|
||||
<td><button class="btn btn-xs btn-ghost">Details</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2025-03-15 14:15:07</td>
|
||||
<td><div class="badge badge-info">INFO</div></td>
|
||||
<td>System</td>
|
||||
<td>System boot initiated</td>
|
||||
<td><button class="btn btn-xs btn-ghost">Details</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
<div>
|
||||
<span class="text-sm">Showing 1-7 of 42 entries</span>
|
||||
</div>
|
||||
<div class="join">
|
||||
<button class="join-item btn btn-sm">«</button>
|
||||
<button class="join-item btn btn-sm btn-active">1</button>
|
||||
<button class="join-item btn btn-sm">2</button>
|
||||
<button class="join-item btn btn-sm">3</button>
|
||||
<button class="join-item btn btn-sm">»</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
103
src/components/MemoryProgressBar.tsx
Normal file
103
src/components/MemoryProgressBar.tsx
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import { useStore } from '@nanostores/preact';
|
||||
import { $freeInternalHeap, $totalInternalHeap } from './AppProviderStore';
|
||||
|
||||
interface MemoryProgressBarProps {
|
||||
showDetails?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function MemoryProgressBar({ showDetails = true, className = "" }: MemoryProgressBarProps) {
|
||||
const freeInternalHeap = useStore($freeInternalHeap);
|
||||
const totalInternalHeap = useStore($totalInternalHeap);
|
||||
|
||||
// Calculate used and percentage
|
||||
const usedInternalHeap = totalInternalHeap - freeInternalHeap;
|
||||
const usagePercentage = totalInternalHeap > 0 ? (usedInternalHeap / totalInternalHeap) * 100 : 0;
|
||||
const freePercentage = 100 - usagePercentage;
|
||||
|
||||
// Determine color based on usage
|
||||
const getProgressColor = (percentage: number) => {
|
||||
if (percentage < 50) return 'bg-green-500';
|
||||
if (percentage < 75) return 'bg-yellow-500';
|
||||
if (percentage < 90) return 'bg-orange-500';
|
||||
return 'bg-red-500';
|
||||
};
|
||||
|
||||
const getTextColor = (percentage: number) => {
|
||||
if (percentage < 50) return 'text-green-600';
|
||||
if (percentage < 75) return 'text-yellow-600';
|
||||
if (percentage < 90) return 'text-orange-600';
|
||||
return 'text-red-600';
|
||||
};
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
// Create segments for the memory bar (like Linux htop)
|
||||
const totalSegments = 20;
|
||||
const usedSegments = Math.round((usagePercentage / 100) * totalSegments);
|
||||
|
||||
const getSegmentColor = (segmentIndex: number) => {
|
||||
if (segmentIndex >= usedSegments) return 'bg-gray-300'; // Empty
|
||||
|
||||
const percentageAtSegment = (segmentIndex / totalSegments) * 100;
|
||||
if (percentageAtSegment < 50) return 'bg-green-500';
|
||||
if (percentageAtSegment < 75) return 'bg-yellow-500';
|
||||
if (percentageAtSegment < 90) return 'bg-orange-500';
|
||||
return 'bg-red-500';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`space-y-2 ${className}`}>
|
||||
{/* Linux-style Memory Bar with segments */}
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm font-medium text-gray-700">Internal Heap</span>
|
||||
<div className="text-right">
|
||||
<span className={`text-sm font-medium ${getTextColor(usagePercentage)}`}>
|
||||
{usagePercentage.toFixed(1)}%
|
||||
</span>
|
||||
<div className="text-xs text-gray-600">
|
||||
{formatBytes(usedInternalHeap)}/{formatBytes(totalInternalHeap)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Memory segments */}
|
||||
<div className="flex space-x-0.5">
|
||||
{Array.from({ length: totalSegments }, (_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`h-3 flex-1 transition-colors duration-300 ${getSegmentColor(i)}`}
|
||||
style={{ minWidth: '2px' }}
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Memory Stats */}
|
||||
{showDetails && (
|
||||
<div className="grid grid-cols-3 gap-3 text-xs text-gray-600">
|
||||
<div className="flex justify-between">
|
||||
<span>Used:</span>
|
||||
<span className="font-medium">{formatBytes(usedInternalHeap)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Free:</span>
|
||||
<span className="font-medium text-green-600">{formatBytes(freeInternalHeap)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Total:</span>
|
||||
<span className="font-medium">{formatBytes(totalInternalHeap)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
505
src/components/ModulationPage.astro
Normal file
505
src/components/ModulationPage.astro
Normal file
|
|
@ -0,0 +1,505 @@
|
|||
---
|
||||
// This is server-side - runs at build time
|
||||
const pageTitle = "Modulation";
|
||||
---
|
||||
<div class="flex flex-col gap-4">
|
||||
<h1 class="text-2xl font-bold">Laser Modulation</h1>
|
||||
|
||||
<!-- Modulation Mode Selection -->
|
||||
<div class="card bg-base-200 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Modulation Mode</h2>
|
||||
|
||||
<!-- Main Mode Tabs - Keep horizontal even on mobile -->
|
||||
<div class="tabs tabs-boxed mb-4">
|
||||
<button class="tab tab-active flex-1" id="internal-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
|
||||
<svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M9 12l2 2 4-4"/>
|
||||
<path d="M21 12c-1 0-3-1-3-3s2-3 3-3 3 1 3 3-2 3-3 3"/>
|
||||
<path d="M3 12c1 0 3-1 3-3s-2-3-3-3-3 1-3 3 2 3 3 3"/>
|
||||
<path d="M3 12h6m6 0h6"/>
|
||||
</svg>
|
||||
Internal
|
||||
</button>
|
||||
<button class="tab flex-1" id="external-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
|
||||
<svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M6 3v12"/>
|
||||
<path d="M18 3v12"/>
|
||||
<path d="M6 21l12-9-12-3z"/>
|
||||
</svg>
|
||||
External
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Internal Modes - Stack on mobile -->
|
||||
<div id="internal-modes" class="mb-4">
|
||||
<h3 class="font-medium mb-3">Internal Modulation Modes</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-2">
|
||||
<button class="btn btn-active" id="cc-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
|
||||
</svg>
|
||||
Constant Current
|
||||
</button>
|
||||
<button class="btn" id="cp-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="4"/>
|
||||
<path d="M12 2v2"/>
|
||||
<path d="M12 20v2"/>
|
||||
<path d="m4.93 4.93 1.41 1.41"/>
|
||||
<path d="m17.66 17.66 1.41 1.41"/>
|
||||
<path d="M2 12h2"/>
|
||||
<path d="M20 12h2"/>
|
||||
<path d="m6.34 17.66-1.41 1.41"/>
|
||||
<path d="m19.07 4.93-1.41 1.41"/>
|
||||
</svg>
|
||||
Constant Power
|
||||
</button>
|
||||
<button class="btn" id="pattern-continuous-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 12h6l4 8 4-16 4 8h6"/>
|
||||
</svg>
|
||||
Pattern Continuous
|
||||
</button>
|
||||
<button class="btn" id="pattern-triggered-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M8 6l4-4 4 4"/>
|
||||
<path d="M12 3v9"/>
|
||||
<path d="M3 12h6l4 8 4-8h6"/>
|
||||
</svg>
|
||||
Pattern Triggered
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- External Modes - Stack on mobile -->
|
||||
<div id="external-modes" class="mb-4 hidden">
|
||||
<h3 class="font-medium mb-3">External Modulation Modes</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||
<button class="btn btn-active" id="ttl-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M5 12h14"/>
|
||||
<path d="M5 12v7"/>
|
||||
<path d="M19 12v7"/>
|
||||
<path d="M5 5v7"/>
|
||||
<path d="M19 5v7"/>
|
||||
</svg>
|
||||
TTL
|
||||
</button>
|
||||
<button class="btn" id="mod2current-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 3v18h18"/>
|
||||
<path d="M7 12l4-4 4 4 4-4"/>
|
||||
</svg>
|
||||
Mod2Current
|
||||
</button>
|
||||
<button class="btn" id="schmitt-trigger-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 12h6l2-8 2 16 2-8h6"/>
|
||||
</svg>
|
||||
Schmitt Trigger
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mode Configuration Panels -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
|
||||
<!-- Constant Current Panel -->
|
||||
<div id="cc-panel" class="card bg-base-200 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
|
||||
</svg>
|
||||
Constant Current Mode
|
||||
</h2>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Current (A)</span>
|
||||
<span class="label-text-alt">5.0A</span>
|
||||
</label>
|
||||
<input type="range" min="0" max="10" value="5" step="0.1" class="range range-primary" />
|
||||
<div class="w-full flex justify-between text-xs px-2 mt-1">
|
||||
<span>0A</span>
|
||||
<span>2.5A</span>
|
||||
<span>5.0A</span>
|
||||
<span>7.5A</span>
|
||||
<span>10.0A</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control mt-4">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Enable Soft Start</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>Constant Current mode provides stable current output regardless of load changes.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Constant Power Panel -->
|
||||
<div id="cp-panel" class="card bg-base-200 shadow hidden">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="4"/>
|
||||
<path d="M12 2v2"/>
|
||||
<path d="M12 20v2"/>
|
||||
<path d="m4.93 4.93 1.41 1.41"/>
|
||||
<path d="m17.66 17.66 1.41 1.41"/>
|
||||
<path d="M2 12h2"/>
|
||||
<path d="M20 12h2"/>
|
||||
<path d="m6.34 17.66-1.41 1.41"/>
|
||||
<path d="m19.07 4.93-1.41 1.41"/>
|
||||
</svg>
|
||||
Constant Power Mode
|
||||
</h2>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Power (W)</span>
|
||||
<span class="label-text-alt">5.0W</span>
|
||||
</label>
|
||||
<input type="range" min="0" max="10" value="5" step="0.1" class="range range-secondary" />
|
||||
<div class="w-full flex justify-between text-xs px-2 mt-1">
|
||||
<span>0W</span>
|
||||
<span>2.5W</span>
|
||||
<span>5.0W</span>
|
||||
<span>7.5W</span>
|
||||
<span>10.0W</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control mt-4">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Enable Feedback Control</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>Constant Power mode uses feedback from the photodiode to maintain stable output power.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pattern Continuous Panel -->
|
||||
<div id="pattern-continuous-panel" class="card bg-base-200 shadow hidden">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 12h6l4 8 4-16 4 8h6"/>
|
||||
</svg>
|
||||
Pattern Continuous Mode
|
||||
</h2>
|
||||
|
||||
<div class="bg-base-200 rounded-lg p-6">
|
||||
<div class="flex items-center justify-center h-40">
|
||||
<div class="text-center">
|
||||
<svg class="w-16 h-16 mx-auto mb-4 opacity-50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 2v20"/>
|
||||
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
|
||||
</svg>
|
||||
<p class="text-lg font-bold">Coming Soon</p>
|
||||
<p class="text-sm mt-2">Pattern configuration interface under development</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pattern Triggered Panel -->
|
||||
<div id="pattern-triggered-panel" class="card bg-base-200 shadow-xl border-l-4 border-l-warning hidden">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title text-warning">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M8 6l4-4 4 4"/>
|
||||
<path d="M12 3v9"/>
|
||||
<path d="M3 12h6l4 8 4-8h6"/>
|
||||
</svg>
|
||||
Pattern Triggered Mode
|
||||
</h2>
|
||||
|
||||
<div class="bg-warning/5 border border-warning/20 rounded-lg p-6">
|
||||
<div class="flex items-center justify-center h-40">
|
||||
<div class="text-center">
|
||||
<svg class="w-16 h-16 mx-auto mb-4 text-warning opacity-50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M8 6l4-4 4 4"/>
|
||||
<path d="M12 3v9"/>
|
||||
<path d="M3 12h6l4 8 4-8h6"/>
|
||||
</svg>
|
||||
<p class="text-lg font-bold">Coming Soon</p>
|
||||
<p class="text-sm mt-2">Triggered pattern configuration interface under development</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TTL Panel -->
|
||||
<div id="ttl-panel" class="card bg-base-200 shadow-xl border-l-4 border-l-info hidden">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title text-info">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M5 12h14"/>
|
||||
<path d="M5 12v7"/>
|
||||
<path d="M19 12v7"/>
|
||||
<path d="M5 5v7"/>
|
||||
<path d="M19 5v7"/>
|
||||
</svg>
|
||||
TTL Mode
|
||||
</h2>
|
||||
|
||||
<div class="bg-info/5 border border-info/20 rounded-lg p-6">
|
||||
<div class="flex items-center justify-center h-40">
|
||||
<div class="text-center">
|
||||
<svg class="w-16 h-16 mx-auto mb-4 text-info opacity-50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M5 12h14"/>
|
||||
<path d="M5 12v7"/>
|
||||
<path d="M19 12v7"/>
|
||||
<path d="M5 5v7"/>
|
||||
<path d="M19 5v7"/>
|
||||
</svg>
|
||||
<p class="text-lg font-bold">Coming Soon</p>
|
||||
<p class="text-sm mt-2">TTL interface configuration under development</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mod2Current Panel -->
|
||||
<div id="mod2current-panel" class="card bg-base-200 shadow-xl border-l-4 border-l-success hidden">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title text-success">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 3v18h18"/>
|
||||
<path d="M7 12l4-4 4 4 4-4"/>
|
||||
</svg>
|
||||
Mod2Current Mode
|
||||
</h2>
|
||||
|
||||
<div class="bg-success/5 border border-success/20 rounded-lg p-4">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2 mb-4">
|
||||
<button class="btn btn-active" onclick="if('vibrate' in navigator) navigator.vibrate(15)">0-5V</button>
|
||||
<button class="btn" onclick="if('vibrate' in navigator) navigator.vibrate(15)">0-10V</button>
|
||||
<button class="btn" onclick="if('vibrate' in navigator) navigator.vibrate(15)">Custom</button>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Current Limit (A)</span>
|
||||
<span class="label-text-alt">7.5A</span>
|
||||
</label>
|
||||
<input type="range" min="0" max="10" value="7.5" step="0.1" class="range range-success" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>Mod2Current converts an analog input voltage to a proportional current output.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schmitt Trigger Panel -->
|
||||
<div id="schmitt-trigger-panel" class="card bg-base-200 shadow-xl border-l-4 border-l-error hidden">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title text-error">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 12h6l2-8 2 16 2-8h6"/>
|
||||
</svg>
|
||||
Schmitt Trigger Mode
|
||||
</h2>
|
||||
|
||||
<div class="bg-error/5 border border-error/20 rounded-lg p-6">
|
||||
<div class="flex items-center justify-center h-40">
|
||||
<div class="text-center">
|
||||
<svg class="w-16 h-16 mx-auto mb-4 text-error opacity-50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 12h6l2-8 2 16 2-8h6"/>
|
||||
</svg>
|
||||
<p class="text-lg font-bold">Coming Soon</p>
|
||||
<p class="text-sm mt-2">Schmitt Trigger configuration interface under development</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Advanced Parameters -->
|
||||
<div class="card bg-base-200 shadow-xl border-l-4 border-l-primary">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z"/>
|
||||
<path d="M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"/>
|
||||
<path d="M12 2v2"/>
|
||||
<path d="M12 22v-2"/>
|
||||
<path d="m17 20.66-1-1.73"/>
|
||||
<path d="M11 10.27 7 3.34"/>
|
||||
<path d="m20.66 17-1.73-1"/>
|
||||
<path d="m3.34 7 1.73 1"/>
|
||||
<path d="M14 12h8"/>
|
||||
<path d="M2 12h2"/>
|
||||
<path d="m20.66 7-1.73 1"/>
|
||||
<path d="m3.34 17 1.73-1"/>
|
||||
<path d="m17 3.34-1 1.73"/>
|
||||
<path d="m11 13.73-4 6.93"/>
|
||||
</svg>
|
||||
Advanced Parameters
|
||||
</h2>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Response Time</span>
|
||||
</label>
|
||||
<label class="select select-bordered w-full flex items-center">
|
||||
<svg class="w-4 h-4 opacity-70 absolute left-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="12,6 12,12 16,14"/>
|
||||
</svg>
|
||||
<select class="pl-10">
|
||||
<option>Fast (High Bandwidth)</option>
|
||||
<option selected>Normal</option>
|
||||
<option>Slow (Low Noise)</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-control mt-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Stability Mode</span>
|
||||
</label>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||
<button class="btn btn-active" onclick="if('vibrate' in navigator) navigator.vibrate(15)">Normal</button>
|
||||
<button class="btn" onclick="if('vibrate' in navigator) navigator.vibrate(15)">High Stability</button>
|
||||
<button class="btn" onclick="if('vibrate' in navigator) navigator.vibrate(15)">Low Noise</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row justify-end mt-4 gap-2">
|
||||
<button class="btn btn-outline" onclick="if('vibrate' in navigator) navigator.vibrate(15)">Save Preset</button>
|
||||
<button class="btn btn-primary" onclick="if('vibrate' in navigator) navigator.vibrate([100, 200, 100])">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Toggle color management function
|
||||
function updateToggleColor(toggle: HTMLInputElement) {
|
||||
if (toggle.checked) {
|
||||
toggle.classList.remove('toggle-error');
|
||||
toggle.classList.add('toggle-success');
|
||||
} else {
|
||||
toggle.classList.remove('toggle-success');
|
||||
toggle.classList.add('toggle-error');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize toggle colors
|
||||
const toggles = document.querySelectorAll('.toggle');
|
||||
toggles.forEach(toggle => {
|
||||
updateToggleColor(toggle as HTMLInputElement);
|
||||
});
|
||||
|
||||
// Tab switching logic
|
||||
const internalTab = document.getElementById('internal-tab');
|
||||
const externalTab = document.getElementById('external-tab');
|
||||
const internalModes = document.getElementById('internal-modes');
|
||||
const externalModes = document.getElementById('external-modes');
|
||||
|
||||
// Mode panels
|
||||
const panels = {
|
||||
'cc': document.getElementById('cc-panel'),
|
||||
'cp': document.getElementById('cp-panel'),
|
||||
'pattern-continuous': document.getElementById('pattern-continuous-panel'),
|
||||
'pattern-triggered': document.getElementById('pattern-triggered-panel'),
|
||||
'ttl': document.getElementById('ttl-panel'),
|
||||
'mod2current': document.getElementById('mod2current-panel'),
|
||||
'schmitt-trigger': document.getElementById('schmitt-trigger-panel')
|
||||
};
|
||||
|
||||
// Mode buttons
|
||||
const modeButtons = {
|
||||
'cc': document.getElementById('cc-tab'),
|
||||
'cp': document.getElementById('cp-tab'),
|
||||
'pattern-continuous': document.getElementById('pattern-continuous-tab'),
|
||||
'pattern-triggered': document.getElementById('pattern-triggered-tab'),
|
||||
'ttl': document.getElementById('ttl-tab'),
|
||||
'mod2current': document.getElementById('mod2current-tab'),
|
||||
'schmitt-trigger': document.getElementById('schmitt-trigger-tab')
|
||||
};
|
||||
|
||||
// Show/hide panels
|
||||
function showPanel(panelId: string) {
|
||||
Object.values(panels).forEach(panel => panel?.classList.add('hidden'));
|
||||
panels[panelId as keyof typeof panels]?.classList.remove('hidden');
|
||||
|
||||
// Update button states
|
||||
Object.values(modeButtons).forEach(btn => btn?.classList.remove('btn-active'));
|
||||
modeButtons[panelId as keyof typeof modeButtons]?.classList.add('btn-active');
|
||||
}
|
||||
|
||||
// Tab switching
|
||||
if (internalTab && externalTab && internalModes && externalModes) {
|
||||
internalTab.addEventListener('click', function() {
|
||||
internalTab.classList.add('tab-active');
|
||||
externalTab.classList.remove('tab-active');
|
||||
internalModes.classList.remove('hidden');
|
||||
externalModes.classList.add('hidden');
|
||||
showPanel('cc'); // Show default internal mode
|
||||
});
|
||||
|
||||
externalTab.addEventListener('click', function() {
|
||||
externalTab.classList.add('tab-active');
|
||||
internalTab.classList.remove('tab-active');
|
||||
externalModes.classList.remove('hidden');
|
||||
internalModes.classList.add('hidden');
|
||||
showPanel('ttl'); // Show default external mode
|
||||
});
|
||||
}
|
||||
|
||||
// Mode button event listeners
|
||||
Object.entries(modeButtons).forEach(([mode, button]) => {
|
||||
if (button) {
|
||||
button.addEventListener('click', () => showPanel(mode));
|
||||
}
|
||||
});
|
||||
|
||||
// Stability mode buttons
|
||||
const stabilityButtons = document.querySelectorAll('.grid .btn');
|
||||
stabilityButtons.forEach(btn => {
|
||||
btn.addEventListener('click', function(this: HTMLElement) {
|
||||
// Only update buttons in the same grid
|
||||
const grid = this.closest('.grid');
|
||||
if (grid) {
|
||||
grid.querySelectorAll('.btn').forEach(b => b.classList.remove('btn-active'));
|
||||
this.classList.add('btn-active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize with Constant Current mode
|
||||
showPanel('cc');
|
||||
});
|
||||
</script>
|
||||
193
src/components/MonitoringPage.astro
Normal file
193
src/components/MonitoringPage.astro
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
---
|
||||
// This is server-side - runs at build time
|
||||
const pageTitle = "Monitoring";
|
||||
---
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<h1 class="text-2xl font-bold">{pageTitle}</h1>
|
||||
|
||||
<!-- Real-time Metrics -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div class="card bg-base-200 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Temperature</h2>
|
||||
<div class="flex items-center justify-center h-40">
|
||||
<div class="text-center">
|
||||
<div class="radial-progress text-primary" style="--value:65; --size:8rem;" id="temp-progress">65%</div>
|
||||
<div class="mt-2" id="temp-reading">25.6°C</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm mt-2">
|
||||
<span>Min: <span id="temp-min">22.1°C</span></span>
|
||||
<span>Max: <span id="temp-max">28.3°C</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-200 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Current</h2>
|
||||
<div class="flex items-center justify-center h-40">
|
||||
<div class="text-center">
|
||||
<div class="radial-progress text-secondary" style="--value:42; --size:8rem;" id="current-progress">42%</div>
|
||||
<div class="mt-2" id="current-reading">125mA</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm mt-2">
|
||||
<span>Min: <span id="current-min">0mA</span></span>
|
||||
<span>Max: <span id="current-max">300mA</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-200 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Output Power</h2>
|
||||
<div class="flex items-center justify-center h-40">
|
||||
<div class="text-center">
|
||||
<div class="radial-progress text-accent" style="--value:50; --size:8rem;" id="power-progress">50%</div>
|
||||
<div class="mt-2" id="power-reading">500mW</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm mt-2">
|
||||
<span>Min: <span id="power-min">0mW</span></span>
|
||||
<span>Max: <span id="power-max">1000mW</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Health -->
|
||||
<div class="card bg-base-200 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">System Health</h2>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>CPU Temperature</td>
|
||||
<td class="w-full">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-full h-4 bg-base-300 rounded-lg relative">
|
||||
<div class="absolute inset-0 flex justify-between px-1 text-[10px] text-base-content z-10">
|
||||
<span>0°C</span>
|
||||
<span>30°C</span>
|
||||
<span>60°C</span>
|
||||
<span>90°C</span>
|
||||
<span>120°C</span>
|
||||
</div>
|
||||
<div class="absolute top-0 bottom-0 left-0 rounded-lg"
|
||||
style="width: 35.25%; background: linear-gradient(90deg, #3b82f6 0%, #22c55e 37.5%, #f59e0b 75%, #ef4444 100%);" id="cpu-temp-bar">
|
||||
</div>
|
||||
</div>
|
||||
<span id="cpu-temp">42.3°C</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>GaN Temperature</td>
|
||||
<td class="w-full">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-full h-4 bg-base-300 rounded-lg relative">
|
||||
<div class="absolute inset-0 flex justify-between px-1 text-[10px] text-base-content z-10">
|
||||
<span>0°C</span>
|
||||
<span>30°C</span>
|
||||
<span>60°C</span>
|
||||
<span>90°C</span>
|
||||
<span>120°C</span>
|
||||
</div>
|
||||
<div class="absolute top-0 bottom-0 left-0 rounded-lg"
|
||||
style="width: 54.8%; background: linear-gradient(90deg, #3b82f6 0%, #22c55e 37.5%, #f59e0b 75%, #ef4444 100%);" id="gan-temp-bar">
|
||||
</div>
|
||||
</div>
|
||||
<span id="gan-temp">65.8°C</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WiFi Signal Strength</td>
|
||||
<td class="w-full">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-full h-4 bg-base-300 rounded-lg relative">
|
||||
<div class="absolute inset-0 flex justify-between px-1 text-[10px] text-base-content z-10">
|
||||
<span>-120dBm</span>
|
||||
<span>-90dBm</span>
|
||||
<span>-60dBm</span>
|
||||
<span>-30dBm</span>
|
||||
<span>0dBm</span>
|
||||
</div>
|
||||
<div class="absolute top-0 bottom-0 left-0 rounded-lg"
|
||||
style="width: 65%; background: linear-gradient(90deg, #ef4444 0%, #f59e0b 40%, #22c55e 80%, #22c55e 100%);" id="wifi-signal-bar">
|
||||
</div>
|
||||
</div>
|
||||
<span id="wifi-signal">-42dBm</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Historical Data -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div class="card bg-base-200 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Temperature History</h2>
|
||||
<div class="flex items-center justify-center h-64 bg-base-200 rounded-box">
|
||||
<p class="text-base-content opacity-50">Temperature Chart Placeholder</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-200 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Power Output History</h2>
|
||||
<div class="flex items-center justify-center h-64 bg-base-200 rounded-box">
|
||||
<p class="text-base-content opacity-50">Power Chart Placeholder</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Update radial progress values
|
||||
function updateRadialProgress(elementId: string, value: number) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
element.style.setProperty('--value', value.toString());
|
||||
element.textContent = value + '%';
|
||||
}
|
||||
}
|
||||
|
||||
// Function to update monitoring values (called from WebSocket)
|
||||
(window as any).updateMonitoringValues = function(data: any) {
|
||||
if (data.temperature) {
|
||||
const tempProgress = Math.round((data.temperature / 100) * 100);
|
||||
updateRadialProgress('temp-progress', tempProgress);
|
||||
|
||||
const tempReading = document.getElementById('temp-reading');
|
||||
if (tempReading) tempReading.textContent = data.temperature + '°C';
|
||||
}
|
||||
|
||||
if (data.current) {
|
||||
const currentProgress = Math.round((data.current / 300) * 100);
|
||||
updateRadialProgress('current-progress', currentProgress);
|
||||
|
||||
const currentReading = document.getElementById('current-reading');
|
||||
if (currentReading) currentReading.textContent = data.current + 'mA';
|
||||
}
|
||||
|
||||
if (data.power) {
|
||||
const powerProgress = Math.round((data.power / 1000) * 100);
|
||||
updateRadialProgress('power-progress', powerProgress);
|
||||
|
||||
const powerReading = document.getElementById('power-reading');
|
||||
if (powerReading) powerReading.textContent = data.power + 'mW';
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
723
src/components/NetworkPage.astro
Normal file
723
src/components/NetworkPage.astro
Normal file
|
|
@ -0,0 +1,723 @@
|
|||
---
|
||||
const pageTitle = "Network Configuration";
|
||||
---
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<h1 class="text-2xl font-bold">{pageTitle}</h1>
|
||||
|
||||
<!-- Connection Status -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<h2 class="text-xl font-bold">Connection Status</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
||||
<div class="stats shadow bg-base-200 rounded-lg border-0">
|
||||
<div class="stat bg-base-200 p-3">
|
||||
<div class="stat-title text-xs flex items-center gap-1">
|
||||
Network Status
|
||||
</div>
|
||||
<div class="stat-value text-sm" id="wifi-status">Connected over WiFi</div>
|
||||
<div class="stat-desc text-xs">Station Mode</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats shadow bg-base-200 rounded-lg border-0">
|
||||
<div class="stat bg-base-200 p-3">
|
||||
<div class="stat-title text-xs flex items-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-70">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
||||
</svg>
|
||||
IP Address
|
||||
</div>
|
||||
<div class="stat-value text-sm" id="current-ip">192.168.1.158</div>
|
||||
<div class="stat-desc text-xs">DHCP Assigned</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats shadow bg-base-200 rounded-lg border-0">
|
||||
<div class="stat bg-base-200 p-3">
|
||||
<div class="stat-title text-xs flex items-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-radar opacity-70">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 10a2 2 0 0 1 1.678 .911l.053 .089h7.269l.117 .007a1 1 0 0 1 .883 .993c0 5.523 -4.477 10 -10 10a1 1 0 0 1 -1 -1v-7.269l-.089 -.053a2 2 0 0 1 -.906 -1.529l-.005 -.149a2 2 0 0 1 2 -2m9.428 -1.334a1 1 0 0 1 -1.884 .668a8 8 0 1 0 -10.207 10.218a1 1 0 0 1 -.666 1.886a10 10 0 1 1 12.757 -12.772m-4.628 -.266a1 1 0 0 1 -1.6 1.2a4 4 0 1 0 -5.6 5.6a1 1 0 0 1 -1.2 1.6a6 6 0 1 1 8.4 -8.4" />
|
||||
</svg>
|
||||
Hostname
|
||||
</div>
|
||||
<div class="stat-value text-sm" id="current-hostname">hydrobuddy.local</div>
|
||||
<div class="stat-desc text-xs">mDNS Active</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats shadow bg-base-200 rounded-lg border-0">
|
||||
<div class="stat bg-base-200 p-3">
|
||||
<div class="stat-title text-xs flex items-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-cell-signal-5 opacity-70">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M20 20h-15.269a.731 .731 0 0 1 -.517 -1.249l14.537 -14.537a.731 .731 0 0 1 1.249 .517v15.269z" />
|
||||
<path d="M16 7v13" />
|
||||
<path d="M12 20v-9" />
|
||||
<path d="M8 20v-5" />
|
||||
</svg>
|
||||
Signal Strength
|
||||
</div>
|
||||
<div class="stat-value text-sm" id="signal-strength">-42dBm</div>
|
||||
<div class="stat-desc text-xs">
|
||||
<progress class="progress progress-success w-full h-2 mt-1" value="64" max="100" id="signal-progress"></progress>
|
||||
<div class="flex justify-between text-[10px] mt-1 opacity-70">
|
||||
<span>-100dBm</span>
|
||||
<span>-10dBm</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats shadow bg-base-200 rounded-lg border-0">
|
||||
<div class="stat bg-base-200 p-3">
|
||||
<div class="stat-title text-xs flex items-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-devices opacity-70">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M13 9a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v10a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1v-10z" />
|
||||
<path d="M18 8v-3a1 1 0 0 0 -1 -1h-13a1 1 0 0 0 -1 1v12a1 1 0 0 0 1 1h9" />
|
||||
<path d="M16 9h2" />
|
||||
</svg>
|
||||
Client Count
|
||||
</div>
|
||||
<div class="stat-value text-sm" id="client-count">2</div>
|
||||
<div class="stat-desc text-xs">Connected Devices</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Network Info and WiFi Settings -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<!-- Network Info -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Network Info</h2>
|
||||
|
||||
<div class="space-y-3 mt-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm font-medium">Hostname</span>
|
||||
<span class="text-sm" id="info-hostname">hydrobuddy.local</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm font-medium">IP Address</span>
|
||||
<span class="text-sm" id="info-ip">192.168.1.158</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm font-medium">Subnet</span>
|
||||
<span class="text-sm" id="info-subnet">255.255.255.0</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm font-medium">Gateway</span>
|
||||
<span class="text-sm" id="info-gateway">192.168.1.1</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm font-medium">MAC Address</span>
|
||||
<span class="text-sm" id="info-mac">AA:BB:CC:DD:EE:FF</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm font-medium">Connection Method</span>
|
||||
<span class="text-sm badge badge-info" id="info-method">WiFi</span>
|
||||
</div>
|
||||
|
||||
<div class="divider my-2"></div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-3">Connected Devices</h3>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center p-2 bg-base-300 rounded-lg">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm">💻</span>
|
||||
<div>
|
||||
<div class="text-sm font-medium">laptop-office</div>
|
||||
<div class="text-xs opacity-70">192.168.1.102</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-xs btn-error" onclick="kickDevice('laptop-office', '192.168.1.102')" title="Disconnect device">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 6 6 18M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex justify-between items-center p-2 bg-base-300 rounded-lg">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm">📱</span>
|
||||
<div>
|
||||
<div class="text-sm font-medium">smartphone-dev</div>
|
||||
<div class="text-xs opacity-70">192.168.1.103</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-xs btn-error" onclick="kickDevice('smartphone-dev', '192.168.1.103')" title="Disconnect device">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 6 6 18M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex justify-between items-center p-2 bg-base-300 rounded-lg">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm">💻</span>
|
||||
<div>
|
||||
<div class="text-sm font-medium">tablet-main</div>
|
||||
<div class="text-xs opacity-70">192.168.1.104</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-xs btn-error" onclick="kickDevice('tablet-main', '192.168.1.104')" title="Disconnect device">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 6 6 18M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WiFi Settings -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">WiFi Settings</h2>
|
||||
|
||||
<!-- Antenna Selection -->
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-switch">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M15 4l4 0l0 4" />
|
||||
<path d="M14.75 9.25l4.25 -5.25" />
|
||||
<path d="M5 19l4 -4" />
|
||||
<path d="M15 19l4 0l0 -4" />
|
||||
<path d="M5 5l14 14" />
|
||||
</svg>
|
||||
Antenna Selection
|
||||
</span>
|
||||
</label>
|
||||
<div class="join">
|
||||
<input class="join-item btn btn-sm" type="radio" name="antenna" aria-label="📶 Internal" checked id="antenna-internal" onchange="if('vibrate' in navigator) navigator.vibrate(15)" />
|
||||
<input class="join-item btn btn-sm" type="radio" name="antenna" aria-label="🔌 External" id="antenna-external" onchange="if('vibrate' in navigator) navigator.vibrate(15)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transmission Power -->
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-waves-electricity">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M3 12c.576 -.643 1.512 -1.017 2.5 -1c.988 -.017 1.924 .357 2.5 1c.576 .643 1.512 1.017 2.5 1c.988 .017 1.924 -.357 2.5 -1" />
|
||||
<path d="M3 16c.576 -.643 1.512 -1.017 2.5 -1c.988 -.017 1.924 .357 2.5 1c.576 .643 1.512 1.017 2.5 1c.988 .017 1.924 -.357 2.5 -1" />
|
||||
<path d="M3 8c.576 -.643 1.512 -1.017 2.5 -1c.988 -.017 1.924 .357 2.5 1c.576 .643 1.512 1.017 2.5 1c.988 .017 1.924 -.357 2.5 -1" />
|
||||
<path d="M20 7l-3 5h4l-3 5" />
|
||||
</svg>
|
||||
Transmission Power
|
||||
</span>
|
||||
</label>
|
||||
<div class="join w-full">
|
||||
<input class="join-item btn btn-sm flex-1" type="radio" name="tx-power" aria-label="1mW" value="1" id="tx-power-1" onchange="if('vibrate' in navigator) navigator.vibrate(15)" />
|
||||
<input class="join-item btn btn-sm flex-1" type="radio" name="tx-power" aria-label="10mW" value="10" id="tx-power-10" onchange="if('vibrate' in navigator) navigator.vibrate(15)" />
|
||||
<input class="join-item btn btn-sm flex-1" type="radio" name="tx-power" aria-label="50mW" value="50" checked id="tx-power-50" onchange="if('vibrate' in navigator) navigator.vibrate(15)" />
|
||||
<input class="join-item btn btn-sm flex-1" type="radio" name="tx-power" aria-label="100mW" value="100" id="tx-power-100" onchange="if('vibrate' in navigator) navigator.vibrate(15)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mode Selection -->
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-home-signal">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M15 22v-2" />
|
||||
<path d="M18 22v-4" />
|
||||
<path d="M21 22v-6" />
|
||||
<path d="M19 12.494v-.494h2l-9 -9l-9 9h2v7a2 2 0 0 0 2 2h4" />
|
||||
<path d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v.5" />
|
||||
</svg>
|
||||
WiFi Mode
|
||||
</span>
|
||||
</label>
|
||||
<div class="join w-full">
|
||||
<button class="btn join-item flex-1" id="station-mode-btn" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-router">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M3 13m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v4a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" />
|
||||
<path d="M17 17l0 .01" />
|
||||
<path d="M13 17l0 .01" />
|
||||
<path d="M15 13l0 -2" />
|
||||
<path d="M11.75 8.75a4 4 0 0 1 6.5 0" />
|
||||
<path d="M8.5 6.5a8 8 0 0 1 13 0" />
|
||||
</svg>
|
||||
Station
|
||||
</button>
|
||||
<button class="btn join-item flex-1 btn-active" id="ap-mode-btn" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-wifi">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 18l.01 0" />
|
||||
<path d="M9.172 15.172a4 4 0 0 1 5.656 0" />
|
||||
<path d="M6.343 12.343a8 8 0 0 1 11.314 0" />
|
||||
<path d="M3.515 9.515c4.686 -4.687 12.284 -4.687 17 0" />
|
||||
</svg>
|
||||
Access Point
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Station Mode Settings -->
|
||||
<div id="station-settings" class="space-y-4 hidden">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-label">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M16.52 7h-10.52a2 2 0 0 0 -2 2v6a2 2 0 0 0 2 2h10.52a1 1 0 0 0 .78 -.375l3.7 -4.625l-3.7 -4.625a1 1 0 0 0 -.78 -.375" />
|
||||
</svg>
|
||||
Network SSID
|
||||
</span>
|
||||
</label>
|
||||
<div class="join w-full">
|
||||
<input type="text" class="input input-bordered join-item flex-1" placeholder="Enter SSID" id="station-ssid" />
|
||||
<button class="btn btn-outline join-item" id="scan-networks-btn" onclick="if('vibrate' in navigator) navigator.vibrate(15); openNetworkScanModal()">Scan</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-key">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M16.555 3.843l3.602 3.602a2.877 2.877 0 0 1 0 4.069l-2.643 2.643a2.877 2.877 0 0 1 -4.069 0l-.301 -.301l-6.558 6.558a2 2 0 0 1 -1.239 .578l-.175 .008h-1.172a1 1 0 0 1 -.993 -.883l-.007 -.117v-1.172a2 2 0 0 1 .467 -1.284l.119 -.13l.414 -.414h2v-2h2v-2l2.144 -2.144l-.301 -.301a2.877 2.877 0 0 1 0 -4.069l2.643 -2.643a2.877 2.877 0 0 1 4.069 0z" />
|
||||
<path d="M15 9h.01" />
|
||||
</svg>
|
||||
Password
|
||||
</span>
|
||||
</label>
|
||||
<input type="password" class="input input-bordered w-full" placeholder="WiFi Password" id="station-password" minlength="8" />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-shield-lock">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3" />
|
||||
<path d="M12 11m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M12 12l0 2.5" />
|
||||
</svg>
|
||||
Security
|
||||
</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" id="station-security">
|
||||
<option value="wpa2" selected>WPA2</option>
|
||||
<option value="wpa3">WPA3</option>
|
||||
<option value="open">Open</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Access Point Mode Settings -->
|
||||
<div id="ap-settings">
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-label">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M16.52 7h-10.52a2 2 0 0 0 -2 2v6a2 2 0 0 0 2 2h10.52a1 1 0 0 0 .78 -.375l3.7 -4.625l-3.7 -4.625a1 1 0 0 0 -.78 -.375" />
|
||||
</svg>
|
||||
Access Point SSID
|
||||
</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" value="Alpha-Driver-AP" id="ap-ssid" />
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-key">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M16.555 3.843l3.602 3.602a2.877 2.877 0 0 1 0 4.069l-2.643 2.643a2.877 2.877 0 0 1 -4.069 0l-.301 -.301l-6.558 6.558a2 2 0 0 1 -1.239 .578l-.175 .008h-1.172a1 1 0 0 1 -.993 -.883l-.007 -.117v-1.172a2 2 0 0 1 .467 -1.284l.119 -.13l.414 -.414h2v-2h2v-2l2.144 -2.144l-.301 -.301a2.877 2.877 0 0 1 0 -4.069l2.643 -2.643a2.877 2.877 0 0 1 4.069 0z" />
|
||||
<path d="M15 9h.01" />
|
||||
</svg>
|
||||
Password
|
||||
</span>
|
||||
</label>
|
||||
<input type="password" class="input input-bordered w-full" placeholder="Min. 8 characters" id="ap-password" minlength="8"
|
||||
pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}"
|
||||
title="Must be more than 8 characters, including number, lowercase letter, uppercase letter" />
|
||||
<p class="text-xs text-base-content/70 mt-1 hidden validator-hint">
|
||||
Must be more than 8 characters, including<br/>
|
||||
At least one number<br/>
|
||||
At least one lowercase letter<br/>
|
||||
At least one uppercase letter
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">WiFi Channel</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" id="ap-channel">
|
||||
<option value="auto" selected>Auto</option>
|
||||
<option value="1">Channel 1 (2.412 GHz)</option>
|
||||
<option value="2">Channel 2 (2.417 GHz)</option>
|
||||
<option value="3">Channel 3 (2.422 GHz)</option>
|
||||
<option value="4">Channel 4 (2.427 GHz)</option>
|
||||
<option value="5">Channel 5 (2.432 GHz)</option>
|
||||
<option value="6">Channel 6 (2.437 GHz)</option>
|
||||
<option value="7">Channel 7 (2.442 GHz)</option>
|
||||
<option value="8">Channel 8 (2.447 GHz)</option>
|
||||
<option value="9">Channel 9 (2.452 GHz)</option>
|
||||
<option value="10">Channel 10 (2.457 GHz)</option>
|
||||
<option value="11">Channel 11 (2.462 GHz)</option>
|
||||
<option value="12">Channel 12 (2.467 GHz)</option>
|
||||
<option value="13">Channel 13 (2.472 GHz)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-shield-lock">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3" />
|
||||
<path d="M12 11m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M12 12l0 2.5" />
|
||||
</svg>
|
||||
Security
|
||||
</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" id="ap-security">
|
||||
<option value="wpa2" selected>WPA2</option>
|
||||
<option value="wpa3">WPA3</option>
|
||||
<option value="open">Open (Not Recommended)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- General Network Settings -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">General Network Settings</h2>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-4">
|
||||
<div>
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">IP Configuration</span>
|
||||
</label>
|
||||
<div class="join w-full">
|
||||
<button class="btn join-item flex-1 btn-active" id="dhcp-btn" onclick="if('vibrate' in navigator) navigator.vibrate(15)">DHCP</button>
|
||||
<button class="btn join-item flex-1" id="static-btn" onclick="if('vibrate' in navigator) navigator.vibrate(15)">Static IP</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Static IP Settings -->
|
||||
<div id="static-ip-settings" class="space-y-2 hidden bg-base-300 rounded-lg p-3 mb-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">IP Address</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" placeholder="192.168.1.100" id="static-ip-addr" />
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Subnet Mask</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" value="255.255.255.0" id="subnet-mask" />
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Gateway</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" placeholder="192.168.1.1" id="gateway" />
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">DNS Server</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" placeholder="8.8.8.8" id="dns-server" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Hostname (mDNS)</span>
|
||||
<span class="label-text-alt">Format: hydrobuddy.local</span>
|
||||
</label>
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-radar opacity-70">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 10a2 2 0 0 1 1.678 .911l.053 .089h7.269l.117 .007a1 1 0 0 1 .883 .993c0 5.523 -4.477 10 -10 10a1 1 0 0 1 -1 -1v-7.269l-.089 -.053a2 2 0 0 1 -.906 -1.529l-.005 -.149a2 2 0 0 1 2 -2m9.428 -1.334a1 1 0 0 1 -1.884 .668a8 8 0 1 0 -10.207 10.218a1 1 0 0 1 -.666 1.886a10 10 0 1 1 12.757 -12.772m-4.628 -.266a1 1 0 0 1 -1.6 1.2a4 4 0 1 0 -5.6 5.6a1 1 0 0 1 -1.2 1.6a6 6 0 1 1 8.4 -8.4" />
|
||||
</svg>
|
||||
<input type="text" class="grow" value="alpha" id="hostname" pattern="[a-zA-Z0-9-]+" title="Only letters, numbers, and hyphens allowed" />
|
||||
<span class="opacity-70">.local</span>
|
||||
</label>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Only letters, numbers, and hyphens allowed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="form-control mb-4">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Enable mDNS Discovery</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked id="toggle-mdns" onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-refresh-dot">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
|
||||
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
|
||||
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
</svg>
|
||||
Auto-reconnect WiFi
|
||||
</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked id="toggle-auto-reconnect" onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Network Scan Modal -->
|
||||
<dialog id="network-scan-modal" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg">Available Networks</h3>
|
||||
<div class="py-4">
|
||||
<div class="space-y-2" id="scanned-networks">
|
||||
<div class="flex justify-between items-center p-3 hover:bg-base-200 rounded cursor-pointer" onclick="selectNetwork('HomeNetwork_5G', 'WPA2')">
|
||||
<div>
|
||||
<div class="font-semibold">HomeNetwork_5G</div>
|
||||
<div class="text-xs opacity-70">WPA2 • -35dBm</div>
|
||||
</div>
|
||||
<div class="text-xs">🔒</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-center p-3 hover:bg-base-200 rounded cursor-pointer" onclick="selectNetwork('OfficeWiFi', 'WPA3')">
|
||||
<div>
|
||||
<div class="font-semibold">OfficeWiFi</div>
|
||||
<div class="text-xs opacity-70">WPA3 • -52dBm</div>
|
||||
</div>
|
||||
<div class="text-xs">🔒</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-center p-3 hover:bg-base-200 rounded cursor-pointer" onclick="selectNetwork('GuestNetwork', 'Open')">
|
||||
<div>
|
||||
<div class="font-semibold">GuestNetwork</div>
|
||||
<div class="text-xs opacity-70">Open • -68dBm</div>
|
||||
</div>
|
||||
<div class="text-xs">📶</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-center p-3 hover:bg-base-200 rounded cursor-pointer" onclick="selectNetwork('CafeWiFi', 'WPA2')">
|
||||
<div>
|
||||
<div class="font-semibold">CafeWiFi</div>
|
||||
<div class="text-xs opacity-70">WPA2 • -71dBm</div>
|
||||
</div>
|
||||
<div class="text-xs">🔒</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<button class="btn btn-outline" onclick="refreshNetworkScan()">Refresh</button>
|
||||
<button class="btn" onclick="closeNetworkScanModal()">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
// Toggle color management function
|
||||
function updateToggleColor(toggle: HTMLInputElement) {
|
||||
if (toggle.checked) {
|
||||
toggle.classList.remove('toggle-error');
|
||||
toggle.classList.add('toggle-success');
|
||||
} else {
|
||||
toggle.classList.remove('toggle-success');
|
||||
toggle.classList.add('toggle-error');
|
||||
}
|
||||
}
|
||||
|
||||
// Device kick function
|
||||
function kickDevice(deviceName: string, deviceIP: string) {
|
||||
if ('vibrate' in navigator) navigator.vibrate([100, 50, 100]);
|
||||
|
||||
// Show confirmation
|
||||
if (confirm(`Disconnect ${deviceName} (${deviceIP})?`)) {
|
||||
console.log(`Kicking device: ${deviceName} at ${deviceIP}`);
|
||||
|
||||
// In a real implementation, you would make an API call here
|
||||
// For now, we'll just show a toast or update the UI
|
||||
const button = event?.target as HTMLElement;
|
||||
const deviceRow = button?.closest('.flex');
|
||||
|
||||
if (deviceRow && deviceRow instanceof HTMLElement) {
|
||||
deviceRow.style.opacity = '0.5';
|
||||
setTimeout(() => {
|
||||
deviceRow.remove();
|
||||
// Update client count
|
||||
const clientCount = document.getElementById('client-count');
|
||||
if (clientCount) {
|
||||
const current = parseInt(clientCount.textContent || '0');
|
||||
clientCount.textContent = (current - 1).toString();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Network scan modal functions
|
||||
function openNetworkScanModal() {
|
||||
const modal = document.getElementById('network-scan-modal') as HTMLDialogElement;
|
||||
modal?.showModal();
|
||||
}
|
||||
|
||||
function closeNetworkScanModal() {
|
||||
const modal = document.getElementById('network-scan-modal') as HTMLDialogElement;
|
||||
modal?.close();
|
||||
}
|
||||
|
||||
function selectNetwork(ssid: string, security: string) {
|
||||
const ssidInput = document.getElementById('station-ssid') as HTMLInputElement;
|
||||
const securitySelect = document.getElementById('station-security') as HTMLSelectElement;
|
||||
|
||||
if (ssidInput) ssidInput.value = ssid;
|
||||
if (securitySelect) {
|
||||
securitySelect.value = security.toLowerCase();
|
||||
}
|
||||
|
||||
closeNetworkScanModal();
|
||||
if ('vibrate' in navigator) navigator.vibrate(15);
|
||||
}
|
||||
|
||||
function refreshNetworkScan() {
|
||||
// Simulate refreshing the network list
|
||||
if ('vibrate' in navigator) navigator.vibrate(15);
|
||||
console.log('Refreshing network scan...');
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize toggle colors
|
||||
const toggles = document.querySelectorAll('.toggle');
|
||||
toggles.forEach(toggle => {
|
||||
updateToggleColor(toggle as HTMLInputElement);
|
||||
});
|
||||
|
||||
// Password validation
|
||||
const passwordInputs = document.querySelectorAll('input[type="password"][pattern]');
|
||||
passwordInputs.forEach(input => {
|
||||
const hint = (input as HTMLInputElement).parentElement?.nextElementSibling;
|
||||
|
||||
input.addEventListener('focus', function() {
|
||||
if (hint && hint.classList.contains('validator-hint')) {
|
||||
hint.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener('blur', function() {
|
||||
if (hint && hint.classList.contains('validator-hint') && (input as HTMLInputElement).validity.valid) {
|
||||
hint.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener('input', function() {
|
||||
const isValid = (input as HTMLInputElement).validity.valid;
|
||||
const parentElement = (input as HTMLInputElement).parentElement;
|
||||
|
||||
if (isValid) {
|
||||
parentElement?.classList.remove('input-error');
|
||||
parentElement?.classList.add('input-success');
|
||||
if (hint) hint.classList.add('hidden');
|
||||
} else {
|
||||
parentElement?.classList.remove('input-success');
|
||||
parentElement?.classList.add('input-error');
|
||||
if (hint) hint.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Update signal strength progress bar
|
||||
function updateSignalStrength(dbm: number) {
|
||||
const signalProgress = document.getElementById('signal-progress');
|
||||
const signalStrength = document.getElementById('signal-strength');
|
||||
|
||||
if (signalProgress && signalStrength) {
|
||||
// Convert dBm to percentage (-100dBm = 0%, -10dBm = 100%)
|
||||
const percentage = Math.max(0, Math.min(100, ((dbm + 100) / 90) * 100));
|
||||
(signalProgress as HTMLProgressElement).value = percentage;
|
||||
signalStrength.textContent = dbm + 'dBm';
|
||||
|
||||
// Update progress bar color based on signal strength
|
||||
signalProgress.className = 'progress w-full h-2 mt-1 ' +
|
||||
(percentage >= 70 ? 'progress-success' :
|
||||
percentage >= 40 ? 'progress-warning' : 'progress-error');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize with current signal strength
|
||||
updateSignalStrength(-42);
|
||||
|
||||
// Mode switching
|
||||
const stationBtn = document.getElementById('station-mode-btn');
|
||||
const apBtn = document.getElementById('ap-mode-btn');
|
||||
const stationSettings = document.getElementById('station-settings');
|
||||
const apSettings = document.getElementById('ap-settings');
|
||||
|
||||
function switchToStation() {
|
||||
stationBtn?.classList.add('btn-active');
|
||||
apBtn?.classList.remove('btn-active');
|
||||
stationSettings?.classList.remove('hidden');
|
||||
apSettings?.classList.add('hidden');
|
||||
}
|
||||
|
||||
function switchToAP() {
|
||||
apBtn?.classList.add('btn-active');
|
||||
stationBtn?.classList.remove('btn-active');
|
||||
apSettings?.classList.remove('hidden');
|
||||
stationSettings?.classList.add('hidden');
|
||||
}
|
||||
|
||||
stationBtn?.addEventListener('click', switchToStation);
|
||||
apBtn?.addEventListener('click', switchToAP);
|
||||
|
||||
// IP Configuration switching
|
||||
const dhcpBtn = document.getElementById('dhcp-btn');
|
||||
const staticBtn = document.getElementById('static-btn');
|
||||
const staticSettings = document.getElementById('static-ip-settings');
|
||||
|
||||
dhcpBtn?.addEventListener('click', function() {
|
||||
dhcpBtn.classList.add('btn-active');
|
||||
staticBtn?.classList.remove('btn-active');
|
||||
staticSettings?.classList.add('hidden');
|
||||
});
|
||||
|
||||
staticBtn?.addEventListener('click', function() {
|
||||
staticBtn.classList.add('btn-active');
|
||||
dhcpBtn?.classList.remove('btn-active');
|
||||
staticSettings?.classList.remove('hidden');
|
||||
});
|
||||
|
||||
// Hostname validation
|
||||
const hostnameInput = document.getElementById('hostname');
|
||||
hostnameInput?.addEventListener('input', function() {
|
||||
const value = (this as HTMLInputElement).value;
|
||||
const valid = /^[a-zA-Z0-9-]+$/.test(value) && value.length > 0;
|
||||
|
||||
const parentLabel = this.closest('label');
|
||||
if (!valid && value !== '') {
|
||||
parentLabel?.classList.add('input-error');
|
||||
} else {
|
||||
parentLabel?.classList.remove('input-error');
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize with Access Point mode
|
||||
switchToAP();
|
||||
});
|
||||
</script>
|
||||
78
src/components/Router.astro
Normal file
78
src/components/Router.astro
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
---
|
||||
// Import all page components
|
||||
import DashboardPage from "./DashboardPage.astro";
|
||||
import TestbenchPage from "./TestbenchPage.astro";
|
||||
import MonitoringPage from "./MonitoringPage.astro";
|
||||
import ControlPage from "./ControlPage.astro";
|
||||
import DcdcPage from "./DcdcPage.astro";
|
||||
import ModulationPage from "./ModulationPage.astro";
|
||||
import LogsPage from "./LogsPage.astro";
|
||||
import NetworkPage from "./NetworkPage.astro";
|
||||
|
||||
import SettingsPage from "./SettingsPage.astro";
|
||||
import SecurityPage from "./SecurityPage.astro";
|
||||
---
|
||||
|
||||
<div id="spa-content bg-base-100">
|
||||
<!-- All pages pre-rendered, dashboard visible by default -->
|
||||
<div id="page-dashboard" class="page-content">
|
||||
<DashboardPage />
|
||||
</div>
|
||||
<div id="page-testbench" class="page-content" style="display: none;">
|
||||
<TestbenchPage />
|
||||
</div>
|
||||
<div id="page-monitoring" class="page-content" style="display: none;">
|
||||
<MonitoringPage />
|
||||
</div>
|
||||
<div id="page-control" class="page-content" style="display: none;">
|
||||
<ControlPage />
|
||||
</div>
|
||||
<div id="page-dcdc" class="page-content" style="display: none;">
|
||||
<DcdcPage />
|
||||
</div>
|
||||
<div id="page-modulation" class="page-content" style="display: none;">
|
||||
<ModulationPage />
|
||||
</div>
|
||||
<div id="page-logs" class="page-content" style="display: none;">
|
||||
<LogsPage />
|
||||
</div>
|
||||
<div id="page-network" class="page-content" style="display: none;">
|
||||
<NetworkPage />
|
||||
</div>
|
||||
<div id="page-settings" class="page-content" style="display: none;">
|
||||
<SettingsPage />
|
||||
</div>
|
||||
<div id="page-security" class="page-content" style="display: none;">
|
||||
<SecurityPage />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Navigation handling
|
||||
window.navigateTo = function(path) {
|
||||
// Hide all pages first
|
||||
document.querySelectorAll('.page-content').forEach(page => {
|
||||
(page as HTMLElement).style.display = 'none';
|
||||
});
|
||||
|
||||
// Show the selected page
|
||||
const pageId = `page-${path.slice(1)}`; // Remove leading slash and add 'page-' prefix
|
||||
const selectedPage = document.getElementById(pageId) as HTMLElement;
|
||||
if (selectedPage) {
|
||||
// show navigated page
|
||||
selectedPage.style.display = 'block';
|
||||
} else {
|
||||
// If no matching page found, show dashboard
|
||||
const dashboard = document.getElementById('page-dashboard') as HTMLElement;
|
||||
if (dashboard) {
|
||||
dashboard.style.display = 'block';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Show dashboard by default
|
||||
const dashboard = document.getElementById('page-dashboard') as HTMLElement;
|
||||
if (dashboard) {
|
||||
dashboard.style.display = 'block';
|
||||
}
|
||||
</script>
|
||||
864
src/components/SecurityPage.astro
Normal file
864
src/components/SecurityPage.astro
Normal file
|
|
@ -0,0 +1,864 @@
|
|||
---
|
||||
const pageTitle = "Security";
|
||||
---
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Web Access Security</h2>
|
||||
<div class="w-full max-w-md mx-auto">
|
||||
<input id="security-slider" type="range" min="0" max="3" value="0" class="range range-error" step="1" />
|
||||
<div class="flex justify-between px-2.5 mt-2 text-xs">
|
||||
<span class="cursor-pointer hover:opacity-70 transition-opacity" data-slider-value="0">
|
||||
<!-- None/0: Alert triangle icon -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-alert-triangle"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 1.67c.955 0 1.845 .467 2.39 1.247l.105 .16l8.114 13.548a2.914 2.914 0 0 1 -2.307 4.363l-.195 .008h-16.225a2.914 2.914 0 0 1 -2.582 -4.2l.099 -.185l8.11 -13.538a2.914 2.914 0 0 1 2.491 -1.403zm.01 13.33l-.127 .007a1 1 0 0 0 0 1.986l.117 .007l.127 -.007a1 1 0 0 0 0 -1.986l-.117 -.007zm-.01 -7a1 1 0 0 0 -.993 .883l-.007 .117v4l.007 .117a1 1 0 0 0 1.986 0l.007 -.117v-4l-.007 -.117a1 1 0 0 0 -.993 -.883z" /></svg>
|
||||
</span>
|
||||
<span class="cursor-pointer hover:opacity-70 transition-opacity" data-slider-value="1">
|
||||
<!-- Button icon -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-transition-bottom"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M21 17a1 1 0 0 1 1 1a4 4 0 0 1 -4 4h-12a4 4 0 0 1 -4 -4a1 1 0 0 1 2 0a2 2 0 0 0 2 2h12a2 2 0 0 0 1.995 -1.85l.005 -.15a1 1 0 0 1 1 -1m-9 1l-.082 -.004l-.119 -.016l-.111 -.03l-.111 -.044l-.098 -.052l-.096 -.067l-.09 -.08l-3 -3a1 1 0 0 1 1.414 -1.414l1.293 1.293v-4.586h-5a4 4 0 1 1 0 -8h12a4 4 0 1 1 0 8h-5v4.583l1.293 -1.29a1 1 0 0 1 1.32 -.083l.094 .083a1 1 0 0 1 0 1.414l-3 3l-.112 .097l-.11 .071l-.062 .031l-.081 .034l-.076 .024l-.149 .03z" /></svg>
|
||||
</span>
|
||||
<span class="cursor-pointer hover:opacity-70 transition-opacity" data-slider-value="2">
|
||||
<!-- Pin icon -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-dialpad"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 3h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z" /><path d="M18 3h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z" /><path d="M11 3h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z" /><path d="M4 10h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z" /><path d="M18 10h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z" /><path d="M11 10h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z" /><path d="M11 17h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z" /></svg>
|
||||
</span>
|
||||
<span class="cursor-pointer hover:opacity-70 transition-opacity" data-slider-value="3">
|
||||
<!-- Password icon -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-keyboard"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M2 6m0 2a2 2 0 0 1 2 -2h16a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-16a2 2 0 0 1 -2 -2z" /><path d="M6 10l0 .01" /><path d="M10 10l0 .01" /><path d="M14 10l0 .01" /><path d="M18 10l0 .01" /><path d="M6 14l0 .01" /><path d="M18 14l0 .01" /><path d="M10 14l4 .01" /></svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-center mt-4">
|
||||
<span id="security-mode-badge" class="badge text-xl px-6 py-3 font-bold">None</span>
|
||||
</div>
|
||||
<div class="flex justify-center mt-2">
|
||||
<span id="security-mode-explanation" class="text-center text-sm">No security, open access.</span>
|
||||
</div>
|
||||
<!-- Fixed height container for mode-specific content to ensure consistent Save button position -->
|
||||
<div class="w-full flex flex-col items-center" style="min-height: 200px;">
|
||||
<div id="none-warning" class="flex flex-col items-center mt-4" style="display:none;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-cloud-lock-open text-error"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M19 18a3.5 3.5 0 0 0 0 -7h-1c.397 -1.768 -.285 -3.593 -1.788 -4.787c-1.503 -1.193 -3.6 -1.575 -5.5 -1s-3.315 2.019 -3.712 3.787c-2.199 -.088 -4.155 1.326 -4.666 3.373c-.512 2.047 .564 4.154 2.566 5.027" /><path d="M8 15m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z" /><path d="M10 15v-2a2 2 0 0 1 3.736 -1" /></svg>
|
||||
<span class="text-error font-bold text-base mt-2">NOT RECOMMENDED</span>
|
||||
</div>
|
||||
<div id="button-toggle-row" class="flex flex-col items-center mt-16" style="display:none;">
|
||||
<label class="flex items-center gap-3">
|
||||
<span>Allow permanent access to devices</span>
|
||||
<input id="button-permanent-toggle" type="checkbox" class="toggle toggle-error" />
|
||||
</label>
|
||||
</div>
|
||||
<div id="security-input-fields" class="flex flex-col items-center mt-4 w-full" style="display:none;">
|
||||
<input id="security-input" type="password" maxlength="8" placeholder="Enter PIN/Password" class="input input-bordered mb-2 w-full" />
|
||||
<input id="security-confirm" type="password" maxlength="8" placeholder="Confirm PIN/Password" class="input input-bordered mb-2 w-full" />
|
||||
</div>
|
||||
<!-- Spacer to push Save button to bottom of container -->
|
||||
<div class="flex-1"></div>
|
||||
<div class="flex justify-center mt-4">
|
||||
<button id="security-save" class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="security-modals-buttons" class="flex flex-col gap-2 items-start mt-4">
|
||||
<button class="btn btn-accent" id="open-test-modal">Test Modal</button>
|
||||
<button class="btn btn-primary" id="open-pin-modal">PIN Login</button>
|
||||
<button class="btn btn-secondary" id="open-password-modal">Password Login</button>
|
||||
<button class="btn btn-info" id="open-updater-modal">Software Update</button>
|
||||
</div>
|
||||
|
||||
<!-- Development Controls for Updates -->
|
||||
<div class="card bg-base-100 shadow mt-4">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Development Controls</h2>
|
||||
<p class="text-sm text-base-content/70 mb-4">Toggle individual firmware update availability for testing purposes.</p>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<label class="cursor-pointer label justify-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary toggle-sm"
|
||||
checked
|
||||
id="toggle-alpha-core"
|
||||
onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)"
|
||||
/>
|
||||
<span class="label-text">Alpha-Core Update</span>
|
||||
</label>
|
||||
|
||||
<label class="cursor-pointer label justify-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary toggle-sm"
|
||||
checked
|
||||
id="toggle-filesystem"
|
||||
onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)"
|
||||
/>
|
||||
<span class="label-text">Filesystem (Web Interface)</span>
|
||||
</label>
|
||||
|
||||
<label class="cursor-pointer label justify-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary toggle-sm"
|
||||
checked
|
||||
id="toggle-wifi"
|
||||
onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)"
|
||||
/>
|
||||
<span class="label-text">WiFi Companion</span>
|
||||
</label>
|
||||
|
||||
<label class="cursor-pointer label justify-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary toggle-sm"
|
||||
checked
|
||||
id="toggle-laser-cpu"
|
||||
onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)"
|
||||
/>
|
||||
<span class="label-text">Laser Core CPU</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>These controls simulate firmware update availability. In production, this information comes from the device via WebSocket.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DaisyUI Modal for Test (Hold Button) -->
|
||||
<dialog id="test-modal" class="modal modal-bottom sm:modal-middle">
|
||||
<div class="modal-box">
|
||||
<form id="test-modal-form" class="flex flex-col gap-4" method="dialog" onsubmit="return false;">
|
||||
<h3 class="font-bold text-lg text-center">Device Registration</h3>
|
||||
<label class="floating-label">
|
||||
<div class="input input-bordered flex items-center gap-2">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-edit opacity-70">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1"/>
|
||||
<path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z"/>
|
||||
<path d="M16 5l3 3"/>
|
||||
</svg>
|
||||
<input type="text" id="device-name" class="grow bg-transparent border-none outline-none" placeholder=" " required />
|
||||
<span>Device Name</span>
|
||||
</div>
|
||||
</label>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-4 items-center">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-devices">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M13 9a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v10a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1v-10z"/>
|
||||
<path d="M18 8v-3a1 1 0 0 0 -1 -1h-13a1 1 0 0 0 -1 1v12a1 1 0 0 0 1 1h9"/>
|
||||
<path d="M16 9h2"/>
|
||||
</svg>
|
||||
<span id="device-type" class="text-sm text-gray-500">Unknown</span>
|
||||
</div>
|
||||
<div class="flex gap-4 items-center">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-password-fingerprint">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M17 8c.788 1 1 2 1 3v1"/>
|
||||
<path d="M9 11c0 -1.578 1.343 -3 3 -3s3 1.422 3 3v2"/>
|
||||
<path d="M12 11v2"/>
|
||||
<path d="M6 12v-1.397c-.006 -1.999 1.136 -3.849 2.993 -4.85a6.385 6.385 0 0 1 6.007 -.005"/>
|
||||
<path d="M12 17v4"/>
|
||||
<path d="M10 20l4 -2"/>
|
||||
<path d="M10 18l4 2"/>
|
||||
<path d="M5 17v4"/>
|
||||
<path d="M3 20l4 -2"/>
|
||||
<path d="M3 18l4 2"/>
|
||||
<path d="M19 17v4"/>
|
||||
<path d="M17 20l4 -2"/>
|
||||
<path d="M17 18l4 2"/>
|
||||
</svg>
|
||||
<span class="text-sm text-gray-500">01234</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="font-semibold mb-1 flex items-center gap-2">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-hourglass-empty">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 20v-2a6 6 0 1 1 12 0v2a1 1 0 0 1 -1 1h-10a1 1 0 0 1 -1 -1z"/>
|
||||
<path d="M6 4v2a6 6 0 1 0 12 0v-2a1 1 0 0 0 -1 -1h-10a1 1 0 0 0 -1 1z"/>
|
||||
</svg>
|
||||
Access Expiration
|
||||
</span>
|
||||
<div class="w-full flex flex-col sm:flex-row gap-2">
|
||||
<div class="join join-vertical lg:join-horizontal w-full">
|
||||
<button type="button" id="exp-session" class="btn join-item btn-outline btn-active flex-1">Session</button>
|
||||
<button type="button" id="exp-lifetime" class="btn join-item btn-outline flex-1">Lifetime</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2 mt-2">
|
||||
<span class="text-sm text-gray-500">Hold the Unlock <b>Button on the Alpha Laserdriver</b> for 3 seconds to authenticate.</span>
|
||||
<button type="button" id="hold-confirm" class="btn btn-error w-full transition-colors duration-300 text-lg" style="background-color: #dc2626;">
|
||||
<span id="hold-text" class="text-lg">Hold to Authenticate</span>
|
||||
</button>
|
||||
<div id="success-message" class="hidden flex flex-col items-center text-success mt-2">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span class="success-detail">Device added for this session.</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<!-- PIN Modal -->
|
||||
<dialog id="pin-modal" class="modal modal-bottom sm:modal-middle">
|
||||
<div class="modal-box">
|
||||
<form id="pin-modal-form" class="flex flex-col gap-6" method="dialog" onsubmit="return false;">
|
||||
<h3 class="font-bold text-lg text-center">PIN Login</h3>
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
<span class="flex items-center">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-key"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M14.52 2c1.029 0 2.015 .409 2.742 1.136l3.602 3.602a3.877 3.877 0 0 1 0 5.483l-2.643 2.643a3.88 3.88 0 0 1 -4.941 .452l-.105 -.078l-5.882 5.883a3 3 0 0 1 -1.68 .843l-.22 .027l-.221 .009h-1.172c-1.014 0 -1.867 -.759 -1.991 -1.823l-.009 -.177v-1.172c0 -.704 .248 -1.386 .73 -1.96l.149 -.161l.414 -.414a1 1 0 0 1 .707 -.293h1v-1a1 1 0 0 1 .883 -.993l.117 -.007h1v-1a1 1 0 0 1 .206 -.608l.087 -.1l1.468 -1.469l-.076 -.103a3.9 3.9 0 0 1 -.678 -1.963l-.007 -.236c0 -1.029 .409 -2.015 1.136 -2.742l2.643 -2.643a3.88 3.88 0 0 1 2.741 -1.136m.495 5h-.02a2 2 0 1 0 0 4h.02a2 2 0 1 0 0 -4"/></svg>
|
||||
</span>
|
||||
<input type="password" id="pin-input" class="grow" placeholder="Enter PIN" required maxlength="8" inputmode="numeric" pattern="[0-9]*" />
|
||||
<button type="button" id="toggle-keypad" tabindex="-1" class="ml-2">
|
||||
<svg id="keypad-open-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-dialpad"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 3h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z"/><path d="M18 3h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z"/><path d="M11 3h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z"/><path d="M4 10h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z"/><path d="M18 10h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z"/><path d="M11 10h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z"/><path d="M11 17h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z"/></svg>
|
||||
<svg id="keypad-close-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-dialpad-off" style="display:none"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 7h-4v-4"/><path d="M17 3h4v4h-4z"/><path d="M10 6v-3h4v4h-3"/><path d="M3 10h4v4h-3"/><path d="M17 13v-3h4v4h-3"/><path d="M14 14h-4v-4"/><path d="M10 17h4v4h-4z"/><path d="M3 3l18 18"/></svg>
|
||||
</button>
|
||||
</label>
|
||||
<div id="pin-keypad-container" class="w-full flex justify-center items-center" style="display:none;min-height:0;">
|
||||
<div class="pin-keypad grid place-items-center w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md">
|
||||
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">1</button>
|
||||
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">2</button>
|
||||
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">3</button>
|
||||
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">4</button>
|
||||
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">5</button>
|
||||
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">6</button>
|
||||
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">7</button>
|
||||
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">8</button>
|
||||
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">9</button>
|
||||
<div></div>
|
||||
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">0</button>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" id="pin-submit" class="btn btn-success w-full text-lg flex items-center justify-center gap-2">Login
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-login-2"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 8v-2a2 2 0 0 1 2 -2h7a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-7a2 2 0 0 1 -2 -2v-2"/><path d="M3 12h13l-3 -3"/><path d="M13 15l3 -3"/></svg>
|
||||
</button>
|
||||
<div id="pin-success-message" class="hidden flex flex-col items-center text-success mt-2">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
||||
<span>Access granted!</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<!-- Password Modal -->
|
||||
<dialog id="password-modal" class="modal modal-bottom sm:modal-middle">
|
||||
<div class="modal-box">
|
||||
<form id="password-modal-form" class="flex flex-col gap-6" method="dialog" onsubmit="return false;">
|
||||
<h3 class="font-bold text-lg text-center">Password Login</h3>
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
<span class="flex items-center">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-key"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M14.52 2c1.029 0 2.015 .409 2.742 1.136l3.602 3.602a3.877 3.877 0 0 1 0 5.483l-2.643 2.643a3.88 3.88 0 0 1 -4.941 .452l-.105 -.078l-5.882 5.883a3 3 0 0 1 -1.68 .843l-.22 .027l-.221 .009h-1.172c-1.014 0 -1.867 -.759 -1.991 -1.823l-.009 -.177v-1.172c0 -.704 .248 -1.386 .73 -1.96l.149 -.161l.414 -.414a1 1 0 0 1 .707 -.293h1v-1a1 1 0 0 1 .883 -.993l.117 -.007h1v-1a1 1 0 0 1 .206 -.608l.087 -.1l1.468 -1.469l-.076 -.103a3.9 3.9 0 0 1 -.678 -1.963l-.007 -.236c0 -1.029 .409 -2.015 1.136 -2.742l2.643 -2.643a3.88 3.88 0 0 1 2.741 -1.136m.495 5h-.02a2 2 0 1 0 0 4h.02a2 2 0 1 0 0 -4"/></svg>
|
||||
</span>
|
||||
<input type="password" id="password-input" class="grow" placeholder="Enter Password" required maxlength="64" />
|
||||
</label>
|
||||
<button type="submit" id="password-submit" class="btn btn-success w-full text-lg flex items-center justify-center gap-2">Login
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-login-2"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 8v-2a2 2 0 0 1 2 -2h7a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-7a2 2 0 0 1 -2 -2v-2"/><path d="M3 12h13l-3 -3"/><path d="M13 15l3 -3"/></svg>
|
||||
</button>
|
||||
<div id="password-success-message" class="hidden flex flex-col items-center text-success mt-2">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
||||
<span>Access granted!</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
// Toggle color management function
|
||||
function updateToggleColor(toggle: HTMLInputElement) {
|
||||
if (toggle.checked) {
|
||||
toggle.classList.remove('toggle-error');
|
||||
toggle.classList.add('toggle-success');
|
||||
} else {
|
||||
toggle.classList.remove('toggle-success');
|
||||
toggle.classList.add('toggle-error');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize toggle colors for all toggles
|
||||
const allToggles = document.querySelectorAll('.toggle');
|
||||
allToggles.forEach(toggle => {
|
||||
updateToggleColor(toggle as HTMLInputElement);
|
||||
});
|
||||
|
||||
/*
|
||||
// Function to update mock update availability based on toggles
|
||||
window.updateAvailability = function() {
|
||||
// Get toggle states
|
||||
const alphaCoreToggle = document.getElementById('toggle-alpha-core') as HTMLInputElement;
|
||||
const filesystemToggle = document.getElementById('toggle-filesystem') as HTMLInputElement;
|
||||
const wifiToggle = document.getElementById('toggle-wifi') as HTMLInputElement;
|
||||
const laserCpuToggle = document.getElementById('toggle-laser-cpu') as HTMLInputElement;
|
||||
|
||||
// Update global mock data (this would be replaced by WebSocket in production)
|
||||
if (typeof window.mockUpdates !== 'undefined') {
|
||||
window.mockUpdates[0].updateAvailable = alphaCoreToggle?.checked || false;
|
||||
window.mockUpdates[1].updateAvailable = filesystemToggle?.checked || false;
|
||||
window.mockUpdates[2].updateAvailable = laserCpuToggle?.checked || false;
|
||||
window.mockUpdates[3].updateAvailable = wifiToggle?.checked || false;
|
||||
}
|
||||
|
||||
// Check if any updates are available
|
||||
const hasUpdates = (alphaCoreToggle?.checked || false) ||
|
||||
(filesystemToggle?.checked || false) ||
|
||||
(wifiToggle?.checked || false) ||
|
||||
(laserCpuToggle?.checked || false);
|
||||
|
||||
// Show/hide notification
|
||||
const notification = document.getElementById('update-notification');
|
||||
if (notification) {
|
||||
if (hasUpdates) {
|
||||
notification.classList.remove('hidden');
|
||||
} else {
|
||||
notification.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Show/hide sidebar update button
|
||||
const updateLink = document.getElementById('update-link');
|
||||
if (updateLink) {
|
||||
if (hasUpdates) {
|
||||
updateLink.style.display = '';
|
||||
} else {
|
||||
updateLink.style.display = 'none';
|
||||
}
|
||||
}
|
||||
};*/
|
||||
/*
|
||||
// Add event listeners to all toggles
|
||||
const toggles = ['toggle-alpha-core', 'toggle-filesystem', 'toggle-wifi', 'toggle-laser-cpu'];
|
||||
toggles.forEach(toggleId => {
|
||||
const toggle = document.getElementById(toggleId);
|
||||
if (toggle) {
|
||||
toggle.addEventListener('change', updateAvailability);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize update availability on page load
|
||||
setTimeout(() => {
|
||||
if (typeof window.updateAvailability === 'function') {
|
||||
window.updateAvailability();
|
||||
}
|
||||
}, 100);*/
|
||||
|
||||
// Test Modal logic
|
||||
const openTestModalBtn = document.getElementById('open-test-modal');
|
||||
const testModal = document.getElementById('test-modal');
|
||||
const holdBtn = document.getElementById('hold-confirm') as HTMLButtonElement | null;
|
||||
const holdText = document.getElementById('hold-text');
|
||||
const successMsg = document.getElementById('success-message');
|
||||
let holdTimeout: number | undefined, holdStart: number, holdInterval: number | undefined;
|
||||
const holdDuration = 3000; // 3 seconds
|
||||
|
||||
if (openTestModalBtn && testModal) {
|
||||
openTestModalBtn.addEventListener('click', () => {
|
||||
try {
|
||||
if ((testModal as HTMLDialogElement).open) {
|
||||
(testModal as HTMLDialogElement).close();
|
||||
}
|
||||
(testModal as HTMLDialogElement).showModal();
|
||||
|
||||
// Reset device name input without focusing
|
||||
const deviceNameInput = document.getElementById('device-name') as HTMLInputElement | null;
|
||||
if (deviceNameInput) {
|
||||
deviceNameInput.value = '';
|
||||
deviceNameInput.blur(); // Ensure no focus
|
||||
}
|
||||
|
||||
if (holdBtn) {
|
||||
holdBtn.disabled = false;
|
||||
holdBtn.classList.remove('btn-success');
|
||||
holdBtn.classList.add('btn-error');
|
||||
holdBtn.style.backgroundColor = '#dc2626';
|
||||
holdBtn.classList.remove('animate-stripes');
|
||||
holdBtn.classList.add('text-black');
|
||||
holdBtn.classList.remove('text-white');
|
||||
holdBtn.classList.remove('breathing-orange');
|
||||
}
|
||||
if (holdText) {
|
||||
holdText.textContent = 'Hold to Authenticate';
|
||||
holdText.classList.add('text-black');
|
||||
holdText.classList.remove('text-white');
|
||||
}
|
||||
if (successMsg) successMsg.classList.add('hidden');
|
||||
} catch (error) {
|
||||
console.error('Error opening test modal:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (holdBtn) {
|
||||
function getHoldColor(progress: number): { color: string, textBlack: boolean } {
|
||||
let hue: number, sat: number, light: number;
|
||||
if (progress < 0.85) {
|
||||
hue = 20 + (25 * (progress / 0.85));
|
||||
sat = 100;
|
||||
light = 45 + (10 * (progress / 0.85));
|
||||
} else {
|
||||
hue = 45 + (75 * ((progress - 0.85) / 0.15));
|
||||
sat = 100;
|
||||
light = 55;
|
||||
}
|
||||
const textBlack = hue >= 40;
|
||||
return { color: `hsl(${hue}, ${sat}%, ${light}%)`, textBlack };
|
||||
}
|
||||
|
||||
const setBtnColor = (color: string, breathing = false, textBlack = false) => {
|
||||
holdBtn.style.backgroundColor = color;
|
||||
if (breathing) {
|
||||
holdBtn.classList.add('breathing-orange');
|
||||
} else {
|
||||
holdBtn.classList.remove('breathing-orange');
|
||||
}
|
||||
holdBtn.classList.add('text-black');
|
||||
holdBtn.classList.remove('text-white');
|
||||
if (holdText) {
|
||||
holdText.classList.add('text-black');
|
||||
holdText.classList.remove('text-white');
|
||||
}
|
||||
};
|
||||
|
||||
const updateProgress = () => {
|
||||
try {
|
||||
const elapsed = Date.now() - holdStart;
|
||||
const progress = Math.min(elapsed / holdDuration, 1);
|
||||
const { color, textBlack } = getHoldColor(progress);
|
||||
if (progress < 1) {
|
||||
setBtnColor(color, true, textBlack);
|
||||
} else {
|
||||
setBtnColor(getHoldColor(1).color, false, true);
|
||||
holdBtn.disabled = true;
|
||||
holdBtn.classList.remove('btn-error');
|
||||
holdBtn.classList.add('btn-success');
|
||||
if (holdText) holdText.textContent = 'Confirmed!';
|
||||
if (successMsg) {
|
||||
const detail = successMsg.querySelector('.success-detail') as HTMLElement | null;
|
||||
if (detail) {
|
||||
if (typeof expirationValue !== 'undefined' && expirationValue === 'lifetime') {
|
||||
detail.textContent = 'Device added permanently.';
|
||||
} else {
|
||||
detail.textContent = 'Device added for this session.';
|
||||
}
|
||||
}
|
||||
successMsg.classList.remove('hidden');
|
||||
}
|
||||
setTimeout(() => {
|
||||
(testModal as HTMLDialogElement).close();
|
||||
}, 3000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating progress:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Use both mouse and touch events for better mobile support
|
||||
const startHold = () => {
|
||||
try {
|
||||
if (holdBtn.disabled) return;
|
||||
holdStart = Date.now();
|
||||
setBtnColor('hsl(20, 100%, 45%)', true, false);
|
||||
holdInterval = window.setInterval(updateProgress, 50);
|
||||
holdTimeout = window.setTimeout(() => {
|
||||
updateProgress();
|
||||
}, holdDuration);
|
||||
if (holdText) holdText.textContent = 'Keep Holding...';
|
||||
} catch (error) {
|
||||
console.error('Error starting hold:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const endHold = () => {
|
||||
try {
|
||||
if (holdBtn.disabled) return;
|
||||
clearInterval(holdInterval);
|
||||
clearTimeout(holdTimeout);
|
||||
setBtnColor('#dc2626', false, false);
|
||||
if (holdText) holdText.textContent = 'Hold to Authenticate';
|
||||
} catch (error) {
|
||||
console.error('Error ending hold:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Add both mouse and touch event listeners
|
||||
holdBtn.addEventListener('mousedown', startHold);
|
||||
holdBtn.addEventListener('touchstart', startHold, { passive: true });
|
||||
holdBtn.addEventListener('mouseup', endHold);
|
||||
holdBtn.addEventListener('touchend', endHold, { passive: true });
|
||||
holdBtn.addEventListener('mouseleave', endHold);
|
||||
holdBtn.addEventListener('touchcancel', endHold, { passive: true });
|
||||
}
|
||||
|
||||
// Expiration button logic
|
||||
const expSessionBtn = document.getElementById('exp-session');
|
||||
const expLifetimeBtn = document.getElementById('exp-lifetime');
|
||||
let expirationValue = 'session';
|
||||
if (expSessionBtn && expLifetimeBtn) {
|
||||
expSessionBtn.addEventListener('click', function() {
|
||||
try {
|
||||
expSessionBtn.classList.add('btn-active');
|
||||
expLifetimeBtn.classList.remove('btn-active');
|
||||
expirationValue = 'session';
|
||||
} catch (error) {
|
||||
console.error('Error setting session expiration:', error);
|
||||
}
|
||||
});
|
||||
expLifetimeBtn.addEventListener('click', function() {
|
||||
try {
|
||||
expLifetimeBtn.classList.add('btn-active');
|
||||
expSessionBtn.classList.remove('btn-active');
|
||||
expirationValue = 'lifetime';
|
||||
} catch (error) {
|
||||
console.error('Error setting lifetime expiration:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// PIN Modal logic
|
||||
const openPinModalBtn = document.getElementById('open-pin-modal');
|
||||
const pinModal = document.getElementById('pin-modal');
|
||||
const pinForm = document.getElementById('pin-modal-form') as HTMLFormElement | null;
|
||||
const pinInput = document.getElementById('pin-input') as HTMLInputElement | null;
|
||||
const pinSuccessMsg = document.getElementById('pin-success-message');
|
||||
const isMobile = /android|iphone|ipad|ipod|opera mini|iemobile|mobile/i.test(navigator.userAgent);
|
||||
|
||||
if (openPinModalBtn && pinModal) {
|
||||
openPinModalBtn.addEventListener('click', () => {
|
||||
try {
|
||||
if ((pinModal as HTMLDialogElement).open) {
|
||||
(pinModal as HTMLDialogElement).close();
|
||||
}
|
||||
(pinModal as HTMLDialogElement).showModal();
|
||||
if (pinInput) pinInput.value = '';
|
||||
if (pinSuccessMsg) pinSuccessMsg.classList.add('hidden');
|
||||
const pinKeypadContainer = document.getElementById('pin-keypad-container');
|
||||
const toggleKeypadBtn = document.getElementById('toggle-keypad');
|
||||
if (isMobile) {
|
||||
if (pinKeypadContainer) pinKeypadContainer.style.display = 'none';
|
||||
if (toggleKeypadBtn) toggleKeypadBtn.style.display = 'none';
|
||||
} else {
|
||||
if (toggleKeypadBtn) toggleKeypadBtn.style.display = '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error opening PIN modal:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Keypad logic
|
||||
const pinKeypadContainer = document.getElementById('pin-keypad-container');
|
||||
const toggleKeypadBtn = document.getElementById('toggle-keypad');
|
||||
const keypadOpenIcon = document.getElementById('keypad-open-icon');
|
||||
const keypadCloseIcon = document.getElementById('keypad-close-icon');
|
||||
let keypadOpen = false;
|
||||
if (toggleKeypadBtn && pinKeypadContainer && keypadOpenIcon && keypadCloseIcon) {
|
||||
toggleKeypadBtn.addEventListener('click', () => {
|
||||
try {
|
||||
keypadOpen = !keypadOpen;
|
||||
pinKeypadContainer.style.display = keypadOpen ? '' : 'none';
|
||||
keypadOpenIcon.style.display = keypadOpen ? 'none' : '';
|
||||
keypadCloseIcon.style.display = keypadOpen ? '' : 'none';
|
||||
} catch (error) {
|
||||
console.error('Error toggling keypad:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (pinKeypadContainer && pinInput) {
|
||||
pinKeypadContainer.addEventListener('click', (e) => {
|
||||
try {
|
||||
const t = e.target as HTMLButtonElement | null;
|
||||
if (t && t.classList.contains('pin-key')) {
|
||||
if (pinInput.value.length < 8) pinInput.value += t.textContent ?? '';
|
||||
t.classList.add('btn-active', 'pulse-key');
|
||||
setTimeout(() => {
|
||||
t.classList.remove('btn-active', 'pulse-key');
|
||||
}, 120);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error handling keypad click:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (pinForm && pinModal && pinSuccessMsg) {
|
||||
pinForm.addEventListener('submit', (e) => {
|
||||
try {
|
||||
e.preventDefault();
|
||||
pinSuccessMsg.classList.remove('hidden');
|
||||
setTimeout(() => {
|
||||
(pinModal as HTMLDialogElement).close();
|
||||
}, 1200);
|
||||
} catch (error) {
|
||||
console.error('Error submitting PIN form:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Password Modal logic
|
||||
const openPasswordModalBtn = document.getElementById('open-password-modal');
|
||||
const passwordModal = document.getElementById('password-modal');
|
||||
const passwordForm = document.getElementById('password-modal-form') as HTMLFormElement | null;
|
||||
const passwordInput = document.getElementById('password-input') as HTMLInputElement | null;
|
||||
const passwordSuccessMsg = document.getElementById('password-success-message');
|
||||
if (openPasswordModalBtn && passwordModal) {
|
||||
openPasswordModalBtn.addEventListener('click', () => {
|
||||
try {
|
||||
if ((passwordModal as HTMLDialogElement).open) {
|
||||
(passwordModal as HTMLDialogElement).close();
|
||||
}
|
||||
(passwordModal as HTMLDialogElement).showModal();
|
||||
if (passwordInput) passwordInput.value = '';
|
||||
if (passwordSuccessMsg) passwordSuccessMsg.classList.add('hidden');
|
||||
} catch (error) {
|
||||
console.error('Error opening password modal:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (passwordForm && passwordModal && passwordSuccessMsg) {
|
||||
passwordForm.addEventListener('submit', (e) => {
|
||||
try {
|
||||
e.preventDefault();
|
||||
passwordSuccessMsg.classList.remove('hidden');
|
||||
setTimeout(() => {
|
||||
(passwordModal as HTMLDialogElement).close();
|
||||
}, 1200);
|
||||
} catch (error) {
|
||||
console.error('Error submitting password form:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Updater Modal logic
|
||||
const openUpdaterModalBtn = document.getElementById('open-updater-modal');
|
||||
if (openUpdaterModalBtn) {
|
||||
openUpdaterModalBtn.addEventListener('click', () => {
|
||||
try {
|
||||
if (typeof window.openUpdaterModal === 'function') {
|
||||
window.openUpdaterModal();
|
||||
} else {
|
||||
console.error('openUpdaterModal function not available');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error opening updater modal:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Web Access Security slider logic
|
||||
const slider = document.getElementById('security-slider');
|
||||
const badge = document.getElementById('security-mode-badge');
|
||||
const explanation = document.getElementById('security-mode-explanation');
|
||||
const inputFields = document.getElementById('security-input-fields');
|
||||
const input = document.getElementById('security-input');
|
||||
const confirm = document.getElementById('security-confirm');
|
||||
const saveBtn = document.getElementById('security-save');
|
||||
const MODES = [
|
||||
{ label: 'None', color: 'badge-error', slider: 'range-error', explain: 'No security, open access.', showInput: false },
|
||||
{ label: 'Button', color: 'badge-info', slider: 'range-info', explain: 'Physical button press required to authenticate.', showInput: false },
|
||||
{ label: 'PIN', color: 'badge-warning', slider: 'range-warning', explain: '4-6 digit numeric code required for access.', showInput: true, inputType: 'pin' },
|
||||
{ label: 'Password', color: 'badge-success', slider: 'range-success', explain: 'Alphanumeric password required for access.', showInput: true, inputType: 'password' },
|
||||
];
|
||||
let lastMode = 0;
|
||||
|
||||
function updateSecurityUI(val: number) {
|
||||
try {
|
||||
const mode = MODES[val];
|
||||
if (badge) {
|
||||
badge.textContent = mode.label;
|
||||
badge.className = `badge text-xl px-6 py-3 font-bold ${mode.color}`;
|
||||
}
|
||||
if (explanation) {
|
||||
if (val === 1) {
|
||||
explanation.textContent = 'Physical button press required to authenticate.';
|
||||
} else {
|
||||
explanation.textContent = mode.explain;
|
||||
}
|
||||
}
|
||||
if (slider) slider.className = `range ${mode.slider}`;
|
||||
if (inputFields && input && confirm) {
|
||||
const inputEl = input as HTMLInputElement;
|
||||
const confirmEl = confirm as HTMLInputElement;
|
||||
if (mode.showInput) {
|
||||
inputFields.style.display = '';
|
||||
inputEl.type = mode.inputType === 'pin' ? 'password' : 'password';
|
||||
inputEl.maxLength = mode.inputType === 'pin' ? 8 : 64;
|
||||
inputEl.placeholder = mode.inputType === 'pin' ? 'Enter PIN' : 'Enter Password';
|
||||
confirmEl.type = mode.inputType === 'pin' ? 'password' : 'password';
|
||||
confirmEl.maxLength = mode.inputType === 'pin' ? 8 : 64;
|
||||
confirmEl.placeholder = mode.inputType === 'pin' ? 'Confirm PIN' : 'Confirm Password';
|
||||
} else {
|
||||
inputFields.style.display = 'none';
|
||||
inputEl.value = '';
|
||||
confirmEl.value = '';
|
||||
}
|
||||
}
|
||||
// Show/hide the none-warning icon and text
|
||||
const noneWarning = document.getElementById('none-warning');
|
||||
if (noneWarning) {
|
||||
noneWarning.style.display = (val === 0) ? '' : 'none';
|
||||
}
|
||||
// Show/hide the button-toggle-row for BUTTON mode
|
||||
const buttonToggleRow = document.getElementById('button-toggle-row');
|
||||
if (buttonToggleRow) {
|
||||
buttonToggleRow.style.display = (val === 1) ? '' : 'none';
|
||||
}
|
||||
// Remove Save button margin manipulation - no longer needed
|
||||
lastMode = val;
|
||||
} catch (error) {
|
||||
console.error('Error updating security UI:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (slider) {
|
||||
updateSecurityUI(Number((slider as HTMLInputElement).value));
|
||||
slider.addEventListener('input', function() {
|
||||
try {
|
||||
updateSecurityUI(Number((slider as HTMLInputElement).value));
|
||||
} catch (error) {
|
||||
console.error('Error handling slider input:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (saveBtn) {
|
||||
saveBtn.addEventListener('click', function(e) {
|
||||
try {
|
||||
e.preventDefault();
|
||||
// TODO: Save logic
|
||||
} catch (error) {
|
||||
console.error('Error saving settings:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add click listeners to icons to control slider
|
||||
const iconContainers = document.querySelectorAll('[data-slider-value]');
|
||||
iconContainers.forEach(icon => {
|
||||
icon.addEventListener('click', function(this: HTMLElement) {
|
||||
try {
|
||||
const value = this.getAttribute('data-slider-value');
|
||||
if (slider && value !== null) {
|
||||
(slider as HTMLInputElement).value = value;
|
||||
updateSecurityUI(Number(value));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error handling icon click:', error);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@keyframes breathing {
|
||||
0% { filter: brightness(1) drop-shadow(0 0 0.5rem #f59e42); }
|
||||
50% { filter: brightness(1.35) drop-shadow(0 0 1.5rem #fbbf24); }
|
||||
100% { filter: brightness(1) drop-shadow(0 0 0.5rem #f59e42); }
|
||||
}
|
||||
.breathing-orange {
|
||||
animation: breathing 1.2s infinite;
|
||||
}
|
||||
.pin-keypad {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
width: 100%;
|
||||
justify-items: center;
|
||||
}
|
||||
.pin-keypad .pin-key {
|
||||
min-width: 3rem;
|
||||
min-height: 3rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
.pin-keypad { max-width: 100%; }
|
||||
.pin-keypad .pin-key { font-size: 1rem; min-width: 2.25rem; min-height: 2.25rem; }
|
||||
}
|
||||
#pin-keypad-container {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
#pin-modal-form {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.pulse-key {
|
||||
animation: pulse-btn 0.18s cubic-bezier(0.4,0,0.2,1);
|
||||
}
|
||||
@keyframes pulse-btn {
|
||||
0% { filter: brightness(1.1); }
|
||||
50% { filter: brightness(1.5); }
|
||||
100% { filter: brightness(1.1); }
|
||||
}
|
||||
@keyframes security-fade {
|
||||
0% { opacity: 0; transform: translateY(10px); }
|
||||
100% { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.animate-security-fade {
|
||||
animation: security-fade 0.3s;
|
||||
}
|
||||
/* Fix hold button border to match background color */
|
||||
#hold-confirm {
|
||||
border: 2px solid transparent !important;
|
||||
}
|
||||
|
||||
/* Floating label styles */
|
||||
.floating-label {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.floating-label > div {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.floating-label span {
|
||||
position: absolute;
|
||||
left: 3rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
pointer-events: none;
|
||||
padding: 0 0.375rem;
|
||||
z-index: 1;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
/* Use DaisyUI's standard label-text color class behavior */
|
||||
.floating-label span {
|
||||
@apply text-base-content/60 bg-base-100;
|
||||
}
|
||||
|
||||
.floating-label input:focus ~ span,
|
||||
.floating-label input:not(:placeholder-shown) ~ span {
|
||||
top: 0;
|
||||
transform: translateY(-50%);
|
||||
font-size: 0.75rem;
|
||||
left: 2.75rem;
|
||||
@apply text-primary font-medium;
|
||||
}
|
||||
|
||||
/* Ensure the input doesn't have autofocus */
|
||||
.floating-label input {
|
||||
transition: border-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.floating-label input:focus {
|
||||
border-color: hsl(var(--p));
|
||||
}
|
||||
</style>
|
||||
462
src/components/SettingsPage.astro
Normal file
462
src/components/SettingsPage.astro
Normal file
|
|
@ -0,0 +1,462 @@
|
|||
---
|
||||
const pageTitle = "System Settings";
|
||||
---
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold">{pageTitle}</h1>
|
||||
</div>
|
||||
|
||||
<!-- About this Device -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<h2 class="text-xl font-bold">About this Device</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4">
|
||||
<div class="stats shadow bg-base-200 rounded-lg border-0">
|
||||
<div class="stat bg-base-200 p-3">
|
||||
<div class="stat-title text-xs">Device Model</div>
|
||||
<div class="stat-value text-sm">Alpha GaN Driver</div>
|
||||
<div class="stat-desc text-xs">50V 25A REV 2.3</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats shadow bg-base-200 rounded-lg border-0">
|
||||
<div class="stat bg-base-200 p-3">
|
||||
<div class="stat-title text-xs">Serial Number</div>
|
||||
<div class="stat-value text-sm" id="serial-number">Alpha-0001</div>
|
||||
<div class="stat-desc text-xs">Lasertack GmbH</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats shadow bg-base-200 rounded-lg border-0">
|
||||
<div class="stat bg-base-200 p-3">
|
||||
<div class="stat-title text-xs">Operating Hours</div>
|
||||
<div class="stat-value text-sm" id="operating-hours">1,247</div>
|
||||
<div class="stat-desc text-xs">Total Runtime</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats shadow bg-base-200 rounded-lg border-0">
|
||||
<div class="stat bg-base-200 p-3">
|
||||
<div class="stat-title text-xs">Driver Core MCU</div>
|
||||
<div class="stat-value text-sm" id="driver-firmware-version">FW1.00</div>
|
||||
<div class="stat-desc text-xs">Build 05.07.2025</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats shadow bg-base-200 rounded-lg border-0">
|
||||
<div class="stat bg-base-200 p-3">
|
||||
<div class="stat-title text-xs">Host MCU</div>
|
||||
<div class="stat-value text-sm" id="companion-firmware-version">FW1.00</div>
|
||||
<div class="stat-desc text-xs">Build 07.07.2025</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats shadow bg-base-200 rounded-lg border-0">
|
||||
<div class="stat bg-base-200 p-3">
|
||||
<div class="stat-title text-xs">Wireless MCU</div>
|
||||
<div class="stat-value text-sm" id="wireless-firmware-version">FW1.00</div>
|
||||
<div class="stat-desc text-xs">Build 01.06.2025</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Preferences -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">System Preferences</h2>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-4">
|
||||
<div>
|
||||
<h3 class="font-semibold mb-4">Time & Locale</h3>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Timezone</span>
|
||||
</label>
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-70">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M20.884 10.554a9 9 0 1 0 -10.337 10.328" />
|
||||
<path d="M3.6 9h16.8" />
|
||||
<path d="M3.6 15h6.9" />
|
||||
<path d="M11.5 3a17 17 0 0 0 -1.502 14.954" />
|
||||
<path d="M12.5 3a17 17 0 0 1 2.52 7.603" />
|
||||
<path d="M18 18m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" />
|
||||
<path d="M18 16.5v1.5l.5 .5" />
|
||||
</svg>
|
||||
<select class="grow bg-transparent border-none outline-none" id="timezone">
|
||||
<option value="UTC">UTC</option>
|
||||
<option value="Europe/Berlin" selected>Europe/Berlin</option>
|
||||
<option value="America/New_York">America/New_York</option>
|
||||
<option value="Asia/Tokyo">Asia/Tokyo</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Time Format</span>
|
||||
</label>
|
||||
<div class="join w-full">
|
||||
<button class="btn btn-outline join-item flex-1" id="time-12h" onclick="if('vibrate' in navigator) navigator.vibrate(15)">12 Hour</button>
|
||||
<button class="btn btn-active join-item flex-1" id="time-24h" onclick="if('vibrate' in navigator) navigator.vibrate(15)">24 Hour</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Current Time</span>
|
||||
</label>
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-70">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="12,6 12,12 16,14"/>
|
||||
</svg>
|
||||
<input type="text" class="grow" readonly id="current-time" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-semibold mb-4">Safety & Logging</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Enable System Logging</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked id="toggle-logging" onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Auto-save Settings</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked id="toggle-autosave" onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Watchdog Timer</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked id="toggle-watchdog" onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Temperature Monitoring</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked id="toggle-temp-monitoring" onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Device Configuration -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Device Configuration</h2>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-4">
|
||||
<div>
|
||||
<h3 class="font-semibold mb-4">Hardware Settings</h3>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">CPU Frequency</span>
|
||||
</label>
|
||||
<select class="select select-bordered" id="cpu-frequency">
|
||||
<option value="240">240 MHz (Default)</option>
|
||||
<option value="160">160 MHz (Power Save)</option>
|
||||
<option value="80">80 MHz (Low Power)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Flash Frequency</span>
|
||||
</label>
|
||||
<select class="select select-bordered" id="flash-frequency">
|
||||
<option value="80" selected>80 MHz</option>
|
||||
<option value="40">40 MHz</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Enable Brownout Detector</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked id="toggle-brownout" onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-semibold mb-4">Performance Settings</h3>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Task Watchdog Timeout</span>
|
||||
</label>
|
||||
<select class="select select-bordered" id="task-watchdog">
|
||||
<option value="5" selected>5 seconds</option>
|
||||
<option value="10">10 seconds</option>
|
||||
<option value="30">30 seconds</option>
|
||||
<option value="0">Disabled</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Stack Size (KB)</span>
|
||||
</label>
|
||||
<input type="range" min="4" max="16" value="8" step="1" class="range range-primary" id="stack-size" />
|
||||
<div class="w-full flex justify-between text-xs px-2 mt-1">
|
||||
<span>4KB</span>
|
||||
<span>8KB</span>
|
||||
<span>12KB</span>
|
||||
<span>16KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Enable Debug Output</span>
|
||||
<input type="checkbox" class="toggle toggle-error" id="toggle-debug" onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">System Actions</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mt-4">
|
||||
<button class="btn btn-primary" id="save-settings" onclick="if('vibrate' in navigator) navigator.vibrate([100, 200, 100])">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||
</svg>
|
||||
Save Settings
|
||||
</button>
|
||||
|
||||
<button class="btn btn-outline" id="export-config" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 3.75H6.912a2.25 2.25 0 00-2.15 1.588L2.35 13.177a2.25 2.25 0 00-.1.661V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18v-4.162c0-.224-.034-.447-.1-.661L19.24 5.338a2.25 2.25 0 00-2.15-1.588H15M2.25 13.5h3.86a2.25 2.25 0 012.012 1.244l.256.512a2.25 2.25 0 002.013 1.244h3.218a2.25 2.25 0 002.013-1.244l.256-.512a2.25 2.25 0 012.013-1.244h3.859m-19.5.338V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18v-4.162c0-.224-.034-.447-.1-.661L19.24 5.338a2.25 2.25 0 00-2.15-1.588H15" />
|
||||
</svg>
|
||||
Export Config
|
||||
</button>
|
||||
|
||||
<button class="btn btn-warning" id="restart-system" onclick="if('vibrate' in navigator) navigator.vibrate([500, 200, 500, 200, 500])">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
|
||||
</svg>
|
||||
Restart
|
||||
</button>
|
||||
|
||||
<button class="btn btn-error" id="factory-reset" onclick="if('vibrate' in navigator) navigator.vibrate([500, 200, 500, 200, 500])">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
||||
</svg>
|
||||
Factory Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Toggle color management function
|
||||
function updateToggleColor(toggle: HTMLInputElement) {
|
||||
if (toggle.checked) {
|
||||
toggle.classList.remove('toggle-error');
|
||||
toggle.classList.add('toggle-success');
|
||||
} else {
|
||||
toggle.classList.remove('toggle-success');
|
||||
toggle.classList.add('toggle-error');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize toggle colors
|
||||
const toggles = document.querySelectorAll('.toggle');
|
||||
toggles.forEach(toggle => {
|
||||
updateToggleColor(toggle as HTMLInputElement);
|
||||
});
|
||||
|
||||
// Update current time
|
||||
function updateTime() {
|
||||
const now = new Date();
|
||||
const timeElement = document.getElementById('current-time');
|
||||
if (timeElement) {
|
||||
(timeElement as HTMLInputElement).value = now.toLocaleString();
|
||||
}
|
||||
}
|
||||
|
||||
// Update time every second
|
||||
updateTime();
|
||||
setInterval(updateTime, 1000);
|
||||
|
||||
// Time format buttons
|
||||
const time12hBtn = document.getElementById('time-12h');
|
||||
const time24hBtn = document.getElementById('time-24h');
|
||||
|
||||
if (time12hBtn && time24hBtn) {
|
||||
time12hBtn.addEventListener('click', function() {
|
||||
time12hBtn.classList.add('btn-active');
|
||||
time24hBtn.classList.remove('btn-active');
|
||||
});
|
||||
|
||||
time24hBtn.addEventListener('click', function() {
|
||||
time24hBtn.classList.add('btn-active');
|
||||
time12hBtn.classList.remove('btn-active');
|
||||
});
|
||||
}
|
||||
|
||||
// Stack size slider
|
||||
const stackSizeSlider = document.getElementById('stack-size');
|
||||
if (stackSizeSlider) {
|
||||
stackSizeSlider.addEventListener('input', function() {
|
||||
console.log('Stack size:', (this as HTMLInputElement).value + 'KB');
|
||||
});
|
||||
}
|
||||
|
||||
// Save settings
|
||||
const saveSettingsBtn = document.getElementById('save-settings');
|
||||
if (saveSettingsBtn) {
|
||||
saveSettingsBtn.addEventListener('click', function() {
|
||||
console.log('Saving settings...');
|
||||
// Collect all settings and send via WebSocket
|
||||
|
||||
// Show success feedback
|
||||
const originalText = saveSettingsBtn.textContent;
|
||||
saveSettingsBtn.textContent = 'Saved!';
|
||||
saveSettingsBtn.classList.add('btn-success');
|
||||
|
||||
setTimeout(() => {
|
||||
saveSettingsBtn.textContent = originalText;
|
||||
saveSettingsBtn.classList.remove('btn-success');
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
// Export config
|
||||
const exportConfigBtn = document.getElementById('export-config');
|
||||
if (exportConfigBtn) {
|
||||
exportConfigBtn.addEventListener('click', function() {
|
||||
// Generate and download config file
|
||||
const config = {
|
||||
system: {
|
||||
timezone: (document.getElementById('timezone') as HTMLSelectElement)?.value,
|
||||
logging: (document.getElementById('toggle-logging') as HTMLInputElement)?.checked,
|
||||
cpu_frequency: (document.getElementById('cpu-frequency') as HTMLSelectElement)?.value
|
||||
}
|
||||
};
|
||||
|
||||
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `alpha-driver-config-${new Date().toISOString().slice(0, 10)}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
}
|
||||
|
||||
// Restart system
|
||||
const restartBtn = document.getElementById('restart-system');
|
||||
if (restartBtn) {
|
||||
restartBtn.addEventListener('click', function() {
|
||||
if (confirm('Are you sure you want to restart the system? This will temporarily interrupt operations.')) {
|
||||
console.log('Restarting system...');
|
||||
// Send restart command via WebSocket
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Factory reset
|
||||
const factoryResetBtn = document.getElementById('factory-reset');
|
||||
if (factoryResetBtn) {
|
||||
factoryResetBtn.addEventListener('click', function() {
|
||||
if (confirm('WARNING: This will reset all settings to factory defaults. This action cannot be undone.\n\nAre you sure you want to continue?')) {
|
||||
console.log('Performing factory reset...');
|
||||
// Send factory reset command via WebSocket
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Function to update settings from WebSocket
|
||||
(window as any).updateSystemSettings = function(data: any) {
|
||||
if (data.firmware_version) {
|
||||
const firmwareEl = document.getElementById('firmware-version');
|
||||
if (firmwareEl) firmwareEl.textContent = data.firmware_version;
|
||||
}
|
||||
|
||||
if (data.mac_address) {
|
||||
const macEl = document.getElementById('mac-address');
|
||||
if (macEl) macEl.textContent = data.mac_address;
|
||||
}
|
||||
};
|
||||
|
||||
// Device Type detection
|
||||
const deviceTypeEl = document.getElementById('device-type');
|
||||
if (deviceTypeEl) {
|
||||
let type = 'Unknown';
|
||||
const ua = navigator.userAgent;
|
||||
if (/android/i.test(ua)) type = 'Android Phone';
|
||||
else if (/iPhone|iPad|iPod/i.test(ua)) type = 'iPhone';
|
||||
else if (/Windows/i.test(ua)) type = 'Windows Computer';
|
||||
else if (/Macintosh/i.test(ua)) type = 'Mac Computer';
|
||||
else if (/Linux/i.test(ua)) type = 'Linux Computer';
|
||||
deviceTypeEl.textContent = type;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@keyframes breathing {
|
||||
0% { filter: brightness(1) drop-shadow(0 0 0.5rem #f59e42); }
|
||||
50% { filter: brightness(1.35) drop-shadow(0 0 1.5rem #fbbf24); }
|
||||
100% { filter: brightness(1) drop-shadow(0 0 0.5rem #f59e42); }
|
||||
}
|
||||
.breathing-orange {
|
||||
animation: breathing 1.2s infinite;
|
||||
}
|
||||
.pin-keypad {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
width: 100%;
|
||||
justify-items: center;
|
||||
}
|
||||
.pin-keypad .pin-key {
|
||||
min-width: 3rem;
|
||||
min-height: 3rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
.pin-keypad { max-width: 100%; }
|
||||
.pin-keypad .pin-key { font-size: 1rem; min-width: 2.25rem; min-height: 2.25rem; }
|
||||
}
|
||||
#pin-keypad-container {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
#pin-modal-form {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.pulse-key {
|
||||
animation: pulse-btn 0.18s cubic-bezier(0.4,0,0.2,1);
|
||||
}
|
||||
@keyframes pulse-btn {
|
||||
0% { filter: brightness(1.1); }
|
||||
50% { filter: brightness(1.5); }
|
||||
100% { filter: brightness(1.1); }
|
||||
}
|
||||
</style>
|
||||
246
src/components/Sidebar.astro
Normal file
246
src/components/Sidebar.astro
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
---
|
||||
import ConnectionStatusBadge from "./ConnectionStatusBadge.tsx";
|
||||
import TestbenchConnectionStatusBadge from "./TestbenchConnectionStatusBadge.tsx";
|
||||
import { $availableUpdates } from "./AppProviderStore.tsx";
|
||||
interface NavigationItem {
|
||||
path: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
viewBox?: string;
|
||||
}
|
||||
|
||||
// Laser section items
|
||||
const laserNavigationItems: NavigationItem[] = [
|
||||
{
|
||||
path: "/dashboard",
|
||||
title: "Dashboard",
|
||||
icon: '<rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/>',
|
||||
viewBox: "0 0 24 24",
|
||||
},
|
||||
{
|
||||
path: "/testbench",
|
||||
title: "Testbench",
|
||||
icon: '<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/><circle cx="6" cy="8" r="1"/><circle cx="10" cy="8" r="1"/><circle cx="14" cy="8" r="1"/><circle cx="18" cy="8" r="1"/>',
|
||||
viewBox: "0 0 24 24",
|
||||
},
|
||||
{
|
||||
path: "/control",
|
||||
title: "Control",
|
||||
icon: '<line x1="21" x2="14" y1="4" y2="4"/><line x1="10" x2="3" y1="4" y2="4"/><line x1="21" x2="12" y1="12" y2="12"/><line x1="8" x2="3" y1="12" y2="12"/><line x1="21" x2="16" y1="20" y2="20"/><line x1="12" x2="3" y1="20" y2="20"/><line x1="14" x2="14" y1="2" y2="6"/><line x1="8" x2="8" y1="10" y2="14"/><line x1="16" x2="16" y1="18" y2="22"/>',
|
||||
viewBox: "0 0 24 24",
|
||||
},
|
||||
{
|
||||
path: "/modulation",
|
||||
title: "Laser",
|
||||
icon: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>',
|
||||
viewBox: "0 0 24 24",
|
||||
},
|
||||
{
|
||||
path: "/dcdc",
|
||||
title: "Power",
|
||||
icon: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>',
|
||||
viewBox: "0 0 24 24",
|
||||
},
|
||||
|
||||
{
|
||||
path: "/logs",
|
||||
title: "Logs",
|
||||
icon: '<path d="M15 12h-5"/><path d="M15 8h-5"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"/>',
|
||||
viewBox: "0 0 24 24",
|
||||
},
|
||||
];
|
||||
|
||||
// System section items
|
||||
const systemNavigationItems: NavigationItem[] = [
|
||||
{
|
||||
path: "/network",
|
||||
title: "Network",
|
||||
icon: '<rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"/><path d="M12 12V8"/>',
|
||||
viewBox: "0 0 24 24",
|
||||
},
|
||||
{
|
||||
path: "/settings",
|
||||
title: "Settings",
|
||||
icon: '<path d="M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z"/><path d="M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"/><path d="M12 2v2"/><path d="M12 22v-2"/><path d="m17 20.66-1-1.73"/><path d="M11 10.27 7 3.34"/><path d="m20.66 17-1.73-1"/><path d="m3.34 7 1.73 1"/><path d="M14 12h8"/><path d="M2 12h2"/><path d="m20.66 7-1.73 1"/><path d="m3.34 17 1.73-1"/><path d="m17 3.34-1 1.73"/><path d="m11 13.73-4 6.93"/>',
|
||||
viewBox: "0 0 24 24",
|
||||
},
|
||||
{
|
||||
path: "/security",
|
||||
title: "Security",
|
||||
icon: '<path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M11.998 2l.118 .007l.059 .008l.061 .013l.111 .034a.993 .993 0 0 1 .217 .112l.104 .082l.255 .218a11 11 0 0 0 7.189 2.537l.342 -.01a1 1 0 0 1 1.005 .717a13 13 0 0 1 -9.208 16.25a1 1 0 0 1 -.502 0a13 13 0 0 1 -9.209 -16.25a1 1 0 0 1 1.005 -.717a11 11 0 0 0 7.531 -2.527l.263 -.225l.096 -.075a.993 .993 0 0 1 .217 -.112l.112 -.034a.97 .97 0 0 1 .119 -.021l.115 -.007zm.002 7a2 2 0 0 0 -1.995 1.85l-.005 .15l.005 .15a2 2 0 0 0 .995 1.581v1.769l.007 .117a1 1 0 0 0 1.993 -.117l.001 -1.768a2 2 0 0 0 -1.001 -3.732z" />',
|
||||
viewBox: "0 0 24 24",
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<aside class="menu bg-base-200 w-70 min-h-full p-4 text-base-content">
|
||||
<div class="flex items-center justify-center mb-6">
|
||||
<div class="text-xl font-bold">Lasertack</div>
|
||||
</div>
|
||||
|
||||
<ul class="menu menu-md text-lg">
|
||||
<li>
|
||||
<h2 class="menu-title text-base">Laser</h2>
|
||||
<ul>
|
||||
{
|
||||
laserNavigationItems.map((item) => (
|
||||
<li>
|
||||
<a href={item.path} class="cursor-pointer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox={item.viewBox}
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="w-5 h-5"
|
||||
set:html={item.icon}
|
||||
/>
|
||||
{item.title}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h2 class="menu-title text-base">System</h2>
|
||||
<ul>
|
||||
{
|
||||
systemNavigationItems.map((item) => (
|
||||
<li>
|
||||
<a href={item.path} class="cursor-pointer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox={item.viewBox}
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="w-5 h-5"
|
||||
set:html={item.icon}
|
||||
/>
|
||||
{item.title}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
<li>
|
||||
<a href="/modal" class="cursor-pointer flex items-center gap-2" id="status-link">
|
||||
<span class="text-sm">Driver:</span>
|
||||
<ConnectionStatusBadge client:load />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/modal" class="cursor-pointer flex items-center gap-2" id="testbench-status-link">
|
||||
<span class="text-sm">Testbench:</span>
|
||||
<TestbenchConnectionStatusBadge client:load />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/update" class="cursor-pointer" id="update-link">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="icon icon-tabler icons-tabler-outline icon-tabler-refresh-alert"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path>
|
||||
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path>
|
||||
<path d="M12 9l0 3"></path>
|
||||
<path d="M12 15l.01 0"></path>
|
||||
</svg>
|
||||
<span id="update-status" class="text-neutral-500"
|
||||
>No updates available</span
|
||||
>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<script>
|
||||
// Type declaration for global functions
|
||||
declare global {
|
||||
interface Window {
|
||||
handleNavigation: (path: string) => void;
|
||||
openStatusModal?: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
// Import the store
|
||||
import { $availableUpdates } from "./AppProviderStore.tsx";
|
||||
|
||||
// Function to update the UI based on available updates
|
||||
function updateUpdateStatus() {
|
||||
const updateStatus = document.getElementById("update-status");
|
||||
const updateLink = document.getElementById("update-link");
|
||||
|
||||
if (updateStatus && updateLink) {
|
||||
const updates = $availableUpdates.get();
|
||||
const hasUpdates = updates && updates.length > 0;
|
||||
|
||||
if (hasUpdates) {
|
||||
updateStatus.textContent = "Update Available";
|
||||
updateStatus.className = "text-success";
|
||||
updateLink.classList.add("btn-success", "text-success");
|
||||
} else {
|
||||
updateStatus.textContent = "No updates available";
|
||||
updateStatus.className = "text-neutral-500";
|
||||
updateLink.classList.remove("btn-success", "text-success");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Warten bis das DOM geladen ist
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// Initial update
|
||||
updateUpdateStatus();
|
||||
|
||||
// Subscribe to store changes
|
||||
$availableUpdates.subscribe(() => {
|
||||
updateUpdateStatus();
|
||||
});
|
||||
// Alle Links in der Sidebar abfangen
|
||||
document.querySelectorAll('.menu a[href^="/"]').forEach((link) => {
|
||||
link.addEventListener(
|
||||
"click",
|
||||
function (this: HTMLAnchorElement, e: Event) {
|
||||
e.preventDefault();
|
||||
const path = this.getAttribute("href");
|
||||
if (path === "/modal") {
|
||||
if (typeof window.openStatusModal === "function") {
|
||||
window.openStatusModal();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (path === "/update") {
|
||||
if (typeof window.openUpdaterModal === "function") {
|
||||
window.openUpdaterModal();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (
|
||||
window.handleNavigation &&
|
||||
typeof window.handleNavigation === "function" &&
|
||||
path
|
||||
) {
|
||||
window.handleNavigation(path);
|
||||
// Optional: URL in der Adresszeile aktualisieren
|
||||
//window.history.pushState({}, '', path);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
45
src/components/TestbenchConnectionStatusBadge.tsx
Normal file
45
src/components/TestbenchConnectionStatusBadge.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { useStore } from '@nanostores/preact';
|
||||
import { $testbenchWebsocketConnectionStatus } from './TestbenchStore';
|
||||
|
||||
export default function TestbenchConnectionStatusBadge() {
|
||||
const status = useStore($testbenchWebsocketConnectionStatus);
|
||||
|
||||
let colorClass = 'bg-gray-400 text-white';
|
||||
if (status.startsWith('Connected')) colorClass = 'bg-green-500 text-white';
|
||||
else if (status.startsWith('Connecting')) colorClass = 'bg-yellow-400 text-black';
|
||||
else if (
|
||||
status.startsWith('Error') ||
|
||||
status.startsWith('Failed') ||
|
||||
status.startsWith('Disconnected')
|
||||
)
|
||||
colorClass = 'bg-red-500 text-white';
|
||||
|
||||
// Passe die Breite ggf. an den längsten Status an!
|
||||
const maxText = 'Connecting ...';
|
||||
|
||||
return (
|
||||
<span
|
||||
class={`badge ${colorClass} font-mono relative inline-block transition-all duration-200`}
|
||||
style={{ width: '120px', minWidth: '120px', maxWidth: '120px', padding: 0 }}
|
||||
>
|
||||
{/* Unsichtbarer Platzhalter für feste Breite und Position */}
|
||||
<span
|
||||
class="opacity-0 select-none pointer-events-none"
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
{maxText}
|
||||
</span>
|
||||
{/* Sichtbarer Status absolut darüber */}
|
||||
<span
|
||||
class="absolute left-0 top-0 w-full h-full flex items-center"
|
||||
style={{ justifyContent: 'left', paddingLeft: '0.75rem' /* ggf. anpassen */ }}
|
||||
>
|
||||
{status}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
692
src/components/TestbenchPage.astro
Normal file
692
src/components/TestbenchPage.astro
Normal file
|
|
@ -0,0 +1,692 @@
|
|||
---
|
||||
import TestbenchConnectionStatusBadge from "./TestbenchConnectionStatusBadge.tsx";
|
||||
const pageTitle = "Test Bench Dashboard";
|
||||
---
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<h1 class="text-2xl font-bold">{pageTitle}</h1>
|
||||
|
||||
<!-- Board Connection Status -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Board Connection Status</h2>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="badge badge-lg" id="board-status-badge">
|
||||
<span class="mr-2">●</span>
|
||||
<span id="board-status-text">Disconnected</span>
|
||||
</div>
|
||||
<div class="badge badge-lg badge-neutral" id="vcc-display">
|
||||
VCC: 12V
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium">Testbench:</span>
|
||||
<TestbenchConnectionStatusBadge client:load />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2"></div>
|
||||
</div>
|
||||
|
||||
<!-- Control Panel -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-4">
|
||||
<!-- Board Power Relay -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg p-4">
|
||||
<h2 class="card-title text-lg mb-3">Power Control</h2>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium">Board Relay:</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle border-red-600 bg-red-500 checked:border-orange-500 checked:bg-orange-400"
|
||||
id="power-relay-toggle"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium">GND Sense:</span>
|
||||
<div class="badge badge-lg" id="gnd-sense-status">
|
||||
<span class="mr-2">●</span>
|
||||
<span id="gnd-sense-text">No Board Connected</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- UART Routing -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg p-4">
|
||||
<h2 class="card-title text-lg mb-3">UART Routing</h2>
|
||||
<div class="join w-full">
|
||||
<input
|
||||
class="join-item btn flex-1"
|
||||
type="radio"
|
||||
name="uart_routing"
|
||||
aria-label="ST"
|
||||
id="uart-st"
|
||||
checked
|
||||
/>
|
||||
<input
|
||||
class="join-item btn flex-1"
|
||||
type="radio"
|
||||
name="uart_routing"
|
||||
aria-label="P4"
|
||||
id="uart-p4"
|
||||
/>
|
||||
<input
|
||||
class="join-item btn flex-1"
|
||||
type="radio"
|
||||
name="uart_routing"
|
||||
aria-label="C6"
|
||||
id="uart-c6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootloader Control -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg p-4">
|
||||
<h2 class="card-title text-lg mb-3">Bootloader Control</h2>
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
class="btn btn-error btn-sm w-full"
|
||||
id="p4-bootloader-btn"
|
||||
>
|
||||
P4 Bootloader OFF
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-error btn-sm w-full"
|
||||
id="c6-bootloader-btn"
|
||||
>
|
||||
C6 Bootloader OFF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Enable Control -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg p-4">
|
||||
<h2 class="card-title text-lg mb-3">Enable Control</h2>
|
||||
<div class="space-y-2">
|
||||
<button class="btn btn-error btn-sm w-full" id="p4-enable-btn">
|
||||
P4 Disabled
|
||||
</button>
|
||||
<button class="btn btn-error btn-sm w-full" id="c6-enable-btn">
|
||||
C6 Disabled
|
||||
</button>
|
||||
<button class="btn btn-error btn-sm w-full" id="st-enable-btn">
|
||||
ST Disabled
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- UART Console -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title text-lg mb-3">UART Console</h2>
|
||||
|
||||
<!-- Console Output -->
|
||||
<div
|
||||
class="mockup-code w-full h-64 overflow-y-auto mb-4"
|
||||
id="uart-console"
|
||||
>
|
||||
<pre
|
||||
data-prefix=">"
|
||||
class="text-info"><code>UART Console Ready</code></pre>
|
||||
<pre
|
||||
data-prefix="RX"
|
||||
class="text-success"><code>Waiting for data...</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- Input Controls -->
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter UART message"
|
||||
class="input input-bordered flex-1"
|
||||
id="uart-input"
|
||||
/>
|
||||
<button class="btn btn-primary" id="send-uart">Send</button>
|
||||
<button class="btn btn-outline btn-sm" id="clear-console"
|
||||
>Clear</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Number Conversion Utility -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title text-lg mb-3">Number Conversion Utility</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<!-- Decimal Input -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Decimal</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="255"
|
||||
class="input input-bordered"
|
||||
id="decimal-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Hex Output -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Hexadecimal</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="0xFF"
|
||||
class="input input-bordered"
|
||||
id="hex-output"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Binary Output -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Binary</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="11111111"
|
||||
class="input input-bordered"
|
||||
id="binary-output"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mt-4">
|
||||
<button class="btn btn-outline btn-sm" id="copy-hex"
|
||||
>Copy Hex</button
|
||||
>
|
||||
<button class="btn btn-outline btn-sm" id="copy-binary"
|
||||
>Copy Binary</button
|
||||
>
|
||||
<button class="btn btn-outline btn-sm" id="clear-converter"
|
||||
>Clear</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import {
|
||||
$testbenchStates,
|
||||
sendCommandToBackend,
|
||||
TESTBENCH_COMMANDS,
|
||||
} from "./TestbenchStore.tsx";
|
||||
import { $testbenchWebsocketConnectionStatus } from "./TestbenchStore.tsx";
|
||||
|
||||
// Extend Window interface for custom properties
|
||||
declare global {
|
||||
interface Window {
|
||||
updateTestBenchStatus: (data: any) => void;
|
||||
}
|
||||
}
|
||||
|
||||
// State management
|
||||
let currentUartRouting = "st"; // 'st', 'p4', 'c6' - default to ST
|
||||
let p4BootloaderActive = false;
|
||||
let c6BootloaderActive = false;
|
||||
let p4Enabled = false;
|
||||
let c6Enabled = false;
|
||||
let stEnabled = false;
|
||||
let boardConnected = false;
|
||||
let powerRelayEnabled = false;
|
||||
let gndSense = false;
|
||||
let vccVoltage = 12; // Default VCC voltage
|
||||
|
||||
// Update board connection status
|
||||
// Global function to update WebSocket connection status (for HydroBuddy address)
|
||||
function updateWebSocketStatus(connected: boolean) {
|
||||
const badge = document.getElementById("board-status-badge");
|
||||
const text = document.getElementById("board-status-text");
|
||||
if (badge && text) {
|
||||
if (connected) {
|
||||
badge.className = "badge badge-lg badge-success";
|
||||
text.textContent = "Connected";
|
||||
} else {
|
||||
badge.className = "badge badge-lg badge-error";
|
||||
text.textContent = "Disconnected";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global function to update board connection status (for GND Sense)
|
||||
function updateBoardConnectionStatus(connected: boolean) {
|
||||
boardConnected = connected;
|
||||
const badge = document.getElementById("board-connection-badge");
|
||||
const text = document.getElementById("board-connection-text");
|
||||
if (badge && text) {
|
||||
if (connected) {
|
||||
badge.className = "badge badge-lg badge-success";
|
||||
text.textContent = "Board Connected";
|
||||
} else {
|
||||
badge.className = "badge badge-lg badge-error";
|
||||
text.textContent = "Board Disconnected";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update VCC voltage display
|
||||
function updateVccDisplay(voltage: number) {
|
||||
vccVoltage = voltage;
|
||||
const vccDisplay = document.getElementById("vcc-display");
|
||||
if (vccDisplay) {
|
||||
vccDisplay.textContent = `VCC: ${voltage}V`;
|
||||
}
|
||||
}
|
||||
|
||||
// Update GND Sense status display (based on boardConnected status)
|
||||
function updateGndSenseStatus(boardConnected: boolean) {
|
||||
gndSense = !boardConnected; // GND Sense is LOW when board is connected
|
||||
// Update both GND Sense display and Board Connection display
|
||||
const gndBadge = document.getElementById("gnd-sense-status");
|
||||
const gndText = document.getElementById("gnd-sense-text");
|
||||
if (gndBadge && gndText) {
|
||||
if (boardConnected) {
|
||||
gndBadge.className = "badge badge-lg badge-success";
|
||||
gndText.textContent = "Connected";
|
||||
} else {
|
||||
gndBadge.className = "badge badge-lg badge-error";
|
||||
gndText.textContent = "Not Connected";
|
||||
}
|
||||
}
|
||||
// Also update the board connection status
|
||||
updateBoardConnectionStatus(boardConnected);
|
||||
}
|
||||
|
||||
// Toggle power relay
|
||||
function togglePowerRelay() {
|
||||
if ($testbenchStates.get().powerRelayEnabled) {
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_POWER_RELAY_OFF);
|
||||
} else {
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_POWER_RELAY_ON);
|
||||
}
|
||||
}
|
||||
|
||||
// Update UART routing display
|
||||
function updateUartRoutingDisplay() {
|
||||
const buttons = document.querySelectorAll('input[name="uart_routing"]');
|
||||
const currentState = $testbenchStates.get();
|
||||
|
||||
// Update button selection
|
||||
buttons.forEach((button) => {
|
||||
const inputButton = button as HTMLInputElement;
|
||||
if (inputButton) {
|
||||
const label = inputButton.getAttribute("aria-label")?.toLowerCase();
|
||||
if (label) {
|
||||
inputButton.checked = label === currentState.uartRouting;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Set UART routing
|
||||
function setUartRouting(target: string) {
|
||||
if (target === "st") {
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_UART_ROUTING_ST);
|
||||
} else if (target === "c6") {
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_UART_ROUTING_C6);
|
||||
} else if (target === "p4") {
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_UART_ROUTING_P4);
|
||||
}
|
||||
console.log(`Clicked UART routing button: ${target}`);
|
||||
}
|
||||
|
||||
// Update bootloader status
|
||||
function updateBootloaderStatus(target: string, active: boolean) {
|
||||
const button = document.getElementById(`${target}-bootloader-btn`);
|
||||
if (!button) return;
|
||||
if (active) {
|
||||
button.textContent = `${target.toUpperCase()} Bootloader ON`;
|
||||
button.className = "btn btn-success btn-sm w-full";
|
||||
} else {
|
||||
button.textContent = `${target.toUpperCase()} Bootloader OFF`;
|
||||
button.className = "btn btn-error btn-sm w-full";
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle bootloader
|
||||
function toggleBootloader(target: string) {
|
||||
if (target === "p4") {
|
||||
if ($testbenchStates.get().p4BootloaderActive) {
|
||||
// Turn off P4 bootloader
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_P4_BOOTLOADER_OFF);
|
||||
console.log("Turned off P4 bootloader");
|
||||
} else {
|
||||
// Turn on P4 bootloader, turn off C6 bootloader and enable P4
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_P4_BOOTLOADER_ON);
|
||||
console.log("Turned on P4 bootloader");
|
||||
}
|
||||
} else if (target === "c6") {
|
||||
if ($testbenchStates.get().c6BootloaderActive) {
|
||||
// Turn off C6 bootloader
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_C6_BOOTLOADER_OFF);
|
||||
console.log("Turned off C6 bootloader");
|
||||
} else {
|
||||
// Turn on C6 bootloader, turn off P4 bootloader and enable C6
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_C6_BOOTLOADER_ON);
|
||||
console.log("Turned on C6 bootloader");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update enable status based on testbench states
|
||||
function updateEnableStatus(target: string, enabled: boolean) {
|
||||
const button = document.getElementById(`${target}-enable-btn`);
|
||||
if (!button) return;
|
||||
|
||||
// Get current testbench states
|
||||
const states = $testbenchStates.get();
|
||||
|
||||
// Check if target is actually enabled in the store
|
||||
const isActuallyEnabled = states[
|
||||
`${target}Enabled` as keyof typeof states
|
||||
] as boolean;
|
||||
|
||||
if (isActuallyEnabled) {
|
||||
button.textContent = `${target.toUpperCase()} Enabled`;
|
||||
button.className = "btn btn-success btn-sm w-full";
|
||||
} else {
|
||||
button.textContent = `${target.toUpperCase()} Disabled`;
|
||||
button.className = "btn btn-error btn-sm w-full";
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle enable
|
||||
function toggleEnable(target: string) {
|
||||
const currentState = $testbenchStates.get();
|
||||
switch (target) {
|
||||
case "p4":
|
||||
if (!currentState.c6BootloaderActive) {
|
||||
// Only allow if not in bootloader mode
|
||||
if (currentState.p4Enabled) {
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_P4_DISABLED);
|
||||
} else {
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_P4_ENABLED);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "c6":
|
||||
if (!currentState.p4BootloaderActive) {
|
||||
// Only allow if not in bootloader mode
|
||||
if (currentState.c6Enabled) {
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_C6_DISABLED);
|
||||
} else {
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_C6_ENABLED);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "st":
|
||||
if (currentState.stEnabled) {
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_ST_DISABLED);
|
||||
} else {
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_ST_ENABLED);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Update UI based on current state
|
||||
const state = $testbenchStates.get();
|
||||
updateEnableStatus("p4", state.p4Enabled);
|
||||
updateEnableStatus("c6", state.c6Enabled);
|
||||
updateEnableStatus("st", state.stEnabled);
|
||||
|
||||
console.log(
|
||||
`P4 Enable: ${state.p4Enabled}, C6 Enable: ${state.c6Enabled}, ST Enable: ${state.stEnabled}`
|
||||
);
|
||||
}
|
||||
|
||||
// Add message to console (global function)
|
||||
function addConsoleMessage(
|
||||
prefix: string,
|
||||
message: string,
|
||||
className = ""
|
||||
) {
|
||||
const uartConsole = document.getElementById("uart-console");
|
||||
if (uartConsole) {
|
||||
const pre = document.createElement("pre");
|
||||
pre.setAttribute("data-prefix", prefix);
|
||||
if (className) pre.className = className;
|
||||
|
||||
const code = document.createElement("code");
|
||||
code.textContent = message;
|
||||
pre.appendChild(code);
|
||||
|
||||
uartConsole.appendChild(pre);
|
||||
uartConsole.scrollTop = uartConsole.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// UART Console functionality
|
||||
const uartConsole = document.getElementById("uart-console");
|
||||
const uartInput = document.getElementById("uart-input");
|
||||
const sendButton = document.getElementById("send-uart");
|
||||
const clearButton = document.getElementById("clear-console");
|
||||
|
||||
// Number conversion functionality
|
||||
const decimalInput = document.getElementById("decimal-input");
|
||||
const hexOutput = document.getElementById("hex-output");
|
||||
const binaryOutput = document.getElementById("binary-output");
|
||||
const copyHexBtn = document.getElementById("copy-hex");
|
||||
const copyBinaryBtn = document.getElementById("copy-binary");
|
||||
const clearConverterBtn = document.getElementById("clear-converter");
|
||||
|
||||
// Send UART message
|
||||
function sendUartMessage() {
|
||||
const input = uartInput as HTMLInputElement;
|
||||
|
||||
if (!input.value.trim()) return;
|
||||
|
||||
const message = input.value.trim();
|
||||
|
||||
// Display sent message
|
||||
addConsoleMessage("TX", message, "text-warning");
|
||||
|
||||
// TODO: Send via WebSocket as text
|
||||
console.log("Sending UART text:", message);
|
||||
|
||||
input.value = "";
|
||||
}
|
||||
|
||||
// Clear console
|
||||
function clearConsole() {
|
||||
if (uartConsole) {
|
||||
uartConsole.innerHTML =
|
||||
'<pre data-prefix=">" class="text-info"><code>Console cleared</code></pre>';
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners for UART console
|
||||
if (sendButton) {
|
||||
sendButton.addEventListener("click", sendUartMessage);
|
||||
}
|
||||
|
||||
if (clearButton) {
|
||||
clearButton.addEventListener("click", clearConsole);
|
||||
}
|
||||
|
||||
if (uartInput) {
|
||||
uartInput.addEventListener("keypress", function (e) {
|
||||
if (e.key === "Enter") {
|
||||
sendUartMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Event listeners for number conversion
|
||||
if (decimalInput) {
|
||||
decimalInput.addEventListener("input", updateConversion);
|
||||
}
|
||||
|
||||
if (copyHexBtn) {
|
||||
copyHexBtn.addEventListener("click", () => {
|
||||
const hexOut = hexOutput as HTMLInputElement;
|
||||
if (hexOut.value) copyToClipboard(hexOut.value);
|
||||
});
|
||||
}
|
||||
|
||||
if (copyBinaryBtn) {
|
||||
copyBinaryBtn.addEventListener("click", () => {
|
||||
const binOut = binaryOutput as HTMLInputElement;
|
||||
if (binOut.value) copyToClipboard(binOut.value);
|
||||
});
|
||||
}
|
||||
|
||||
if (clearConverterBtn) {
|
||||
clearConverterBtn.addEventListener("click", clearConverter);
|
||||
}
|
||||
|
||||
// Number conversion functions
|
||||
function updateConversion() {
|
||||
const decInput = decimalInput as HTMLInputElement;
|
||||
const hexOut = hexOutput as HTMLInputElement;
|
||||
const binOut = binaryOutput as HTMLInputElement;
|
||||
|
||||
const decimal = parseInt(decInput.value);
|
||||
|
||||
if (isNaN(decimal) || decInput.value === "") {
|
||||
hexOut.value = "";
|
||||
binOut.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert to hex (with 0x prefix)
|
||||
hexOut.value =
|
||||
"0x" + decimal.toString(16).toUpperCase().padStart(2, "0");
|
||||
|
||||
// Convert to binary (8-bit)
|
||||
binOut.value = decimal.toString(2).padStart(8, "0");
|
||||
}
|
||||
|
||||
function copyToClipboard(text: string) {
|
||||
navigator.clipboard
|
||||
.writeText(text)
|
||||
.then(() => {
|
||||
console.log("Copied to clipboard:", text);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to copy:", err);
|
||||
});
|
||||
}
|
||||
|
||||
function clearConverter() {
|
||||
const decInput = decimalInput as HTMLInputElement;
|
||||
const hexOut = hexOutput as HTMLInputElement;
|
||||
const binOut = binaryOutput as HTMLInputElement;
|
||||
|
||||
decInput.value = "";
|
||||
hexOut.value = "";
|
||||
binOut.value = "";
|
||||
}
|
||||
|
||||
// Simulate incoming UART data (remove this when connecting to real WebSocket)
|
||||
setTimeout(() => {
|
||||
addConsoleMessage("RX", "Hello from device!", "text-success");
|
||||
}, 2000);
|
||||
|
||||
// UART routing buttons
|
||||
document
|
||||
.querySelectorAll('input[name="uart_routing"]')
|
||||
.forEach((tab) => {
|
||||
const inputTab = tab as HTMLInputElement;
|
||||
inputTab.addEventListener("change", function () {
|
||||
if (this.checked) {
|
||||
const target = this.getAttribute("aria-label")?.toLowerCase();
|
||||
if (target) {
|
||||
setUartRouting(target);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const p4BootloaderBtn = document.getElementById("p4-bootloader-btn");
|
||||
const c6BootloaderBtn = document.getElementById("c6-bootloader-btn");
|
||||
const p4EnableBtn = document.getElementById("p4-enable-btn");
|
||||
const c6EnableBtn = document.getElementById("c6-enable-btn");
|
||||
const stEnableBtn = document.getElementById("st-enable-btn");
|
||||
const powerRelayToggle = document.getElementById("power-relay-toggle");
|
||||
|
||||
// Bootloader buttons
|
||||
if (p4BootloaderBtn) {
|
||||
p4BootloaderBtn.addEventListener("click", () =>
|
||||
toggleBootloader("p4")
|
||||
);
|
||||
}
|
||||
if (c6BootloaderBtn) {
|
||||
c6BootloaderBtn.addEventListener("click", () =>
|
||||
toggleBootloader("c6")
|
||||
);
|
||||
}
|
||||
|
||||
// Enable buttons
|
||||
if (p4EnableBtn) {
|
||||
p4EnableBtn.addEventListener("click", () => toggleEnable("p4"));
|
||||
}
|
||||
if (c6EnableBtn) {
|
||||
c6EnableBtn.addEventListener("click", () => toggleEnable("c6"));
|
||||
}
|
||||
if (stEnableBtn) {
|
||||
stEnableBtn.addEventListener("click", () => toggleEnable("st"));
|
||||
}
|
||||
|
||||
// Power relay toggle
|
||||
if (powerRelayToggle) {
|
||||
powerRelayToggle.addEventListener("change", togglePowerRelay);
|
||||
}
|
||||
|
||||
// Initialize display
|
||||
updateWebSocketStatus(false);
|
||||
updateVccDisplay(12);
|
||||
updateUartRoutingDisplay();
|
||||
updateBootloaderStatus("p4", false);
|
||||
updateBootloaderStatus("c6", false);
|
||||
updateEnableStatus("p4", false);
|
||||
updateEnableStatus("c6", false);
|
||||
updateEnableStatus("st", false);
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Subscribe to testbench state changes for automatic UI updates
|
||||
$testbenchStates.subscribe((newStates) => {
|
||||
// Update enable status buttons based on store state
|
||||
updateEnableStatus("p4", newStates.p4Enabled);
|
||||
updateEnableStatus("c6", newStates.c6Enabled);
|
||||
updateEnableStatus("st", newStates.stEnabled);
|
||||
|
||||
// Update bootloader status
|
||||
updateBootloaderStatus("p4", newStates.p4BootloaderActive);
|
||||
updateBootloaderStatus("c6", newStates.c6BootloaderActive);
|
||||
|
||||
// Update UART routing
|
||||
currentUartRouting = newStates.uartRouting;
|
||||
updateUartRoutingDisplay();
|
||||
|
||||
// Update power relay
|
||||
powerRelayEnabled = newStates.powerRelayEnabled;
|
||||
const toggle = document.getElementById(
|
||||
"power-relay-toggle"
|
||||
) as HTMLInputElement;
|
||||
if (toggle) {
|
||||
toggle.checked = powerRelayEnabled;
|
||||
}
|
||||
|
||||
updateGndSenseStatus(newStates.boardConnected);
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
143
src/components/TestbenchStore.tsx
Normal file
143
src/components/TestbenchStore.tsx
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import { atom } from 'nanostores';
|
||||
import pkg from "websocket";
|
||||
const { w3cwebsocket } = pkg;
|
||||
|
||||
export const $testbenchWebsocket = atom<WebSocket | null>(null);
|
||||
export const $testbenchWebsocketConnectionStatus = atom<string>('Disconnected');
|
||||
|
||||
|
||||
export function setTestbenchWebsocket(websocket: WebSocket) {
|
||||
$testbenchWebsocket.set(websocket);
|
||||
}
|
||||
|
||||
export function setTestbenchWebsocketConnectionStatus(status: string) {
|
||||
$testbenchWebsocketConnectionStatus.set(status);
|
||||
}
|
||||
|
||||
export const TESTBENCH_COMMANDS = {
|
||||
SET_UART_ROUTING_ST: 0,
|
||||
SET_UART_ROUTING_C6: 1,
|
||||
SET_UART_ROUTING_P4: 2,
|
||||
SET_P4_BOOTLOADER_ON: 3,
|
||||
SET_P4_BOOTLOADER_OFF: 4,
|
||||
SET_C6_BOOTLOADER_ON: 5,
|
||||
SET_C6_BOOTLOADER_OFF: 6,
|
||||
SET_P4_ENABLED: 7,
|
||||
SET_P4_DISABLED: 8,
|
||||
SET_C6_ENABLED: 9,
|
||||
SET_C6_DISABLED: 10,
|
||||
SET_ST_ENABLED: 11,
|
||||
SET_ST_DISABLED: 12,
|
||||
SET_POWER_RELAY_ON: 13,
|
||||
SET_POWER_RELAY_OFF: 14,
|
||||
};
|
||||
|
||||
|
||||
// GPIO Pin Mapping Constants
|
||||
export const GPIO_PINS = {
|
||||
// Communication
|
||||
SDA: 1,
|
||||
SCL: 2,
|
||||
DTR: 3,
|
||||
RTS: 14,
|
||||
FTDI_UART_TX: 17,
|
||||
FTDI_UART_RX: 18,
|
||||
|
||||
// Switches
|
||||
SW_A: 4,
|
||||
SW_B: 5,
|
||||
SW_C: 6,
|
||||
SW_D: 7,
|
||||
// Route switches
|
||||
SW_ST_C6: 12,
|
||||
SW_C6_P4: 13,
|
||||
|
||||
// Power & Sensing
|
||||
BOARD_POWER: 15,
|
||||
GND_SENSE: 16,
|
||||
|
||||
// P4 Bootloader Pins
|
||||
P4_35: 47,
|
||||
P4_36: 21,
|
||||
P4_ENABLE: 9,
|
||||
|
||||
// STM32
|
||||
STM32_RESET: 38,
|
||||
|
||||
// C6 Control
|
||||
C6_ENABLE: 10,
|
||||
C6_BOOT: 8,
|
||||
} as const;
|
||||
|
||||
// GPIO State Store
|
||||
export const $gpioStates = atom<Record<number, boolean>>({
|
||||
[GPIO_PINS.SW_A]: false,
|
||||
[GPIO_PINS.SW_B]: false,
|
||||
[GPIO_PINS.SW_C]: false,
|
||||
[GPIO_PINS.SW_D]: false,
|
||||
[GPIO_PINS.BOARD_POWER]: false,
|
||||
[GPIO_PINS.GND_SENSE]: true,
|
||||
[GPIO_PINS.P4_35]: true,
|
||||
[GPIO_PINS.P4_36]: false,
|
||||
[GPIO_PINS.STM32_RESET]: false,
|
||||
[GPIO_PINS.C6_ENABLE]: false,
|
||||
[GPIO_PINS.C6_BOOT]: true,
|
||||
});
|
||||
|
||||
// Testbench State Type
|
||||
export type TestbenchState = {
|
||||
boardConnected: boolean;
|
||||
vccVoltage: number;
|
||||
powerRelayEnabled: boolean;
|
||||
uartRouting: 'st' | 'p4' | 'c6';
|
||||
p4BootloaderActive: boolean;
|
||||
c6BootloaderActive: boolean;
|
||||
p4Enabled: boolean;
|
||||
c6Enabled: boolean;
|
||||
stEnabled: boolean;
|
||||
gndSense: boolean;
|
||||
};
|
||||
|
||||
// Testbench Control States
|
||||
export const $testbenchStates = atom<TestbenchState>({
|
||||
// Board Connection
|
||||
boardConnected: false,
|
||||
vccVoltage: 12,
|
||||
|
||||
// Power Control
|
||||
powerRelayEnabled: false,
|
||||
|
||||
// UART Routing
|
||||
uartRouting: 'p4',
|
||||
|
||||
// Bootloader States
|
||||
p4BootloaderActive: false,
|
||||
c6BootloaderActive: false,
|
||||
|
||||
// Enable States
|
||||
p4Enabled: false,
|
||||
c6Enabled: false,
|
||||
stEnabled: false,
|
||||
gndSense: true,
|
||||
});
|
||||
|
||||
export function getGpioState(pin: number): boolean {
|
||||
return $gpioStates.get()[pin] || false;
|
||||
}
|
||||
|
||||
export function sendCommandToBackend(command: number) {
|
||||
const websocket = $testbenchWebsocket.get();
|
||||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||
const message = {
|
||||
message_type: "testbench_command",
|
||||
command: command
|
||||
};
|
||||
|
||||
try {
|
||||
websocket.send(JSON.stringify(message));
|
||||
console.log("Testbench command sent:", command);
|
||||
} catch (error) {
|
||||
console.error("Failed to send Testbench command:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
227
src/components/TestbenchWebsocketProvider.tsx
Normal file
227
src/components/TestbenchWebsocketProvider.tsx
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
import { useEffect } from 'preact/hooks';
|
||||
import { $gpioStates, $testbenchWebsocket, $testbenchWebsocketConnectionStatus, GPIO_PINS,
|
||||
setTestbenchWebsocketConnectionStatus, $testbenchStates } from './TestbenchStore';
|
||||
|
||||
// Heartbeat configuration
|
||||
const HEARTBEAT_INTERVAL = 1500; // Send ping every 1 second
|
||||
const HEARTBEAT_TIMEOUT = 700; // Wait 1 second for pong response
|
||||
const MAX_MISSED_HEARTBEATS = 5; // Close connection after 5 missed pongs
|
||||
|
||||
// Heartbeat state
|
||||
let heartbeatInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let heartbeatTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
let missedHeartbeats = 0;
|
||||
|
||||
// Reconnect state
|
||||
let isReconnecting = false;
|
||||
let reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// Restart detection state
|
||||
let disconnectDetected = false;
|
||||
|
||||
|
||||
function onMessage(message: MessageEvent) {
|
||||
//console.log("WebSocket message received:", message.data.toString());
|
||||
// Verarbeite JSON Nachrichten
|
||||
const dataFromServer = JSON.parse(message.data.toString());
|
||||
if (dataFromServer.message_type === "pong") {
|
||||
// reset missed heartbeats and clear pending timeout
|
||||
if (heartbeatTimeout) {
|
||||
clearTimeout(heartbeatTimeout);
|
||||
heartbeatTimeout = null;
|
||||
}
|
||||
missedHeartbeats = 0;
|
||||
console.log("Heartbeat pong received");
|
||||
}
|
||||
else if (dataFromServer.message_type === "initial_testbench_state_response" || dataFromServer.message_type === "testbench_state_response") {
|
||||
console.log("Testbench state response received");
|
||||
console.log("Testbench state:", dataFromServer);
|
||||
if (dataFromServer.gpio_states) {
|
||||
|
||||
$gpioStates.set({
|
||||
[GPIO_PINS.SW_A]: dataFromServer.gpio_states["4"],
|
||||
[GPIO_PINS.SW_B]: dataFromServer.gpio_states["5"],
|
||||
[GPIO_PINS.SW_C]: dataFromServer.gpio_states["6"],
|
||||
[GPIO_PINS.SW_D]: dataFromServer.gpio_states["7"],
|
||||
[GPIO_PINS.BOARD_POWER]: dataFromServer.gpio_states["15"],
|
||||
[GPIO_PINS.GND_SENSE]: dataFromServer.gpio_states["16"],
|
||||
[GPIO_PINS.P4_35]: dataFromServer.gpio_states["47"],
|
||||
[GPIO_PINS.P4_36]: dataFromServer.gpio_states["21"],
|
||||
[GPIO_PINS.STM32_RESET]: dataFromServer.gpio_states["38"],
|
||||
[GPIO_PINS.C6_ENABLE]: dataFromServer.gpio_states["10"],
|
||||
[GPIO_PINS.C6_BOOT]: dataFromServer.gpio_states["8"],
|
||||
});
|
||||
$testbenchStates.set({
|
||||
boardConnected: dataFromServer.testbenchStates.boardConnected,
|
||||
vccVoltage: dataFromServer.testbenchStates.vccVoltage,
|
||||
powerRelayEnabled: dataFromServer.testbenchStates.powerRelayEnabled,
|
||||
uartRouting: dataFromServer.testbenchStates.uartRouting,
|
||||
p4BootloaderActive: dataFromServer.testbenchStates.p4BootloaderActive,
|
||||
c6BootloaderActive: dataFromServer.testbenchStates.c6BootloaderActive,
|
||||
p4Enabled: dataFromServer.testbenchStates.p4Enabled,
|
||||
c6Enabled: dataFromServer.testbenchStates.c6Enabled,
|
||||
stEnabled: dataFromServer.testbenchStates.stEnabled,
|
||||
gndSense: dataFromServer.testbenchStates.gndSense,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const connectWebSocket = () => {
|
||||
// Clear any existing reconnect attempts
|
||||
clearReconnect();
|
||||
|
||||
const testbenchWSaddress = "ws://testbench.local/ws";
|
||||
console.log(`Verbinde mit: ${testbenchWSaddress}`);
|
||||
if ($testbenchWebsocket.get() !== null) {
|
||||
($testbenchWebsocket.get() as WebSocket).close(1000);
|
||||
$testbenchWebsocket.set(null);
|
||||
}
|
||||
$testbenchWebsocket.set(new WebSocket(testbenchWSaddress));
|
||||
if ($testbenchWebsocket.get() !== null) {
|
||||
($testbenchWebsocket.get() as WebSocket).onopen = onOpen;
|
||||
|
||||
($testbenchWebsocket.get() as WebSocket).onmessage = onMessage;
|
||||
|
||||
($testbenchWebsocket.get() as WebSocket).onerror = onError;
|
||||
|
||||
($testbenchWebsocket.get() as WebSocket).onclose = onClose;
|
||||
}
|
||||
}
|
||||
|
||||
function clearReconnect() {
|
||||
if (reconnectTimeout) {
|
||||
clearTimeout(reconnectTimeout);
|
||||
reconnectTimeout = null;
|
||||
}
|
||||
isReconnecting = false;
|
||||
}
|
||||
|
||||
function onOpen() {
|
||||
setTestbenchWebsocketConnectionStatus("Connected");
|
||||
console.log("WebSocket erfolgreich verbunden");
|
||||
missedHeartbeats = 0;
|
||||
|
||||
// Start heartbeat mechanism
|
||||
startHeartbeat();
|
||||
// Sende initiale Statusanfrage
|
||||
if ($testbenchWebsocket.get() !== null) {
|
||||
const initial_state_request = JSON.stringify({ message_type: "initial_testbench_state_request" });
|
||||
try {
|
||||
($testbenchWebsocket.get() as WebSocket).send(initial_state_request);
|
||||
} catch (error) {
|
||||
console.error("Failed to send initial testbench state request:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onClose(event: CloseEvent) {
|
||||
$testbenchWebsocketConnectionStatus.set("Disconnected");
|
||||
console.log("Testbench WebSocket closed with code:", event.code);
|
||||
console.log("Testbench WebSocket close reason:", event.reason);
|
||||
console.log("Testbench WebSocket clean close:", event.wasClean);
|
||||
clearHeartbeat();
|
||||
if (event.code === 1000) {
|
||||
console.log("Normal closure");
|
||||
}
|
||||
scheduleReconnect();
|
||||
}
|
||||
|
||||
function startHeartbeat() {
|
||||
clearHeartbeat(); // Clear any existing heartbeat
|
||||
|
||||
heartbeatInterval = setInterval(() => {
|
||||
sendHeartbeat();
|
||||
}, HEARTBEAT_INTERVAL);
|
||||
}
|
||||
|
||||
function clearHeartbeat() {
|
||||
if (heartbeatInterval) {
|
||||
clearInterval(heartbeatInterval);
|
||||
heartbeatInterval = null;
|
||||
}
|
||||
if (heartbeatTimeout) {
|
||||
clearTimeout(heartbeatTimeout);
|
||||
heartbeatTimeout = null;
|
||||
}
|
||||
missedHeartbeats = 0;
|
||||
}
|
||||
|
||||
|
||||
function sendHeartbeat() {
|
||||
if ($testbenchWebsocket.get() && ($testbenchWebsocket.get() as WebSocket).readyState === ($testbenchWebsocket.get() as WebSocket).OPEN) {
|
||||
// Send ping message
|
||||
const pingMessage = JSON.stringify({ message_type: "ping" });
|
||||
try {
|
||||
($testbenchWebsocket.get() as WebSocket).send(pingMessage);
|
||||
} catch (error) {
|
||||
console.error("Failed to send heartbeat ping:", error);
|
||||
missedHeartbeats++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Set timeout for pong response
|
||||
if (heartbeatTimeout) {
|
||||
clearTimeout(heartbeatTimeout);
|
||||
heartbeatTimeout = null;
|
||||
}
|
||||
heartbeatTimeout = setTimeout(() => {
|
||||
missedHeartbeats++;
|
||||
if (missedHeartbeats >= MAX_MISSED_HEARTBEATS) {
|
||||
console.error("Too many missed heartbeats, closing connection");
|
||||
|
||||
// Wichtig: Erst Heartbeat stoppen
|
||||
clearHeartbeat();
|
||||
|
||||
// Dann WebSocket schließen und null setzen
|
||||
|
||||
if ($testbenchWebsocket.get()) {
|
||||
($testbenchWebsocket.get() as WebSocket).close();
|
||||
setTestbenchWebsocketConnectionStatus("Disconnected");
|
||||
$testbenchWebsocket.set(null);
|
||||
}
|
||||
}
|
||||
}, HEARTBEAT_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
function onError(error: Event) {
|
||||
$testbenchWebsocketConnectionStatus.set("Disconnected");
|
||||
console.log("WebSocket error:", error);
|
||||
clearHeartbeat();
|
||||
scheduleReconnect();
|
||||
}
|
||||
|
||||
|
||||
function scheduleReconnect() {
|
||||
if (isReconnecting) {
|
||||
console.log("Reconnect already scheduled, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
isReconnecting = true;
|
||||
console.log("Scheduling reconnect in 1 second");
|
||||
reconnectTimeout = setTimeout(() => {
|
||||
connectWebSocket();
|
||||
isReconnecting = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
export const TestbenchWebSocketProvider = () => {
|
||||
|
||||
// Auto-connect WebSocket at first render
|
||||
useEffect(() => {
|
||||
console.log("Component mounted, starting WebSocket connection...");
|
||||
connectWebSocket();
|
||||
// Cleanup beim Unmounting
|
||||
return () => {
|
||||
clearHeartbeat();
|
||||
clearReconnect();
|
||||
($testbenchWebsocket.get() as WebSocket).close();
|
||||
$testbenchWebsocket.set(null);
|
||||
$testbenchWebsocketConnectionStatus.set("Disconnected");
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
186
src/components/UpdateErrorPopup.tsx
Normal file
186
src/components/UpdateErrorPopup.tsx
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import { useStore } from '@nanostores/preact';
|
||||
import { IconCpu2, IconWifi, IconCpu, IconFileTypeJs } from '@tabler/icons-preact';
|
||||
import {
|
||||
$availableUpdates,
|
||||
$updateState,
|
||||
$updateErrorMessages,
|
||||
$p4FirmwareProgress,
|
||||
$p4WebinterfaceProgress,
|
||||
$stm32FirmwareProgress,
|
||||
$c6FirmwareProgress,
|
||||
stateSetters,
|
||||
start_firmware_update_p4_fw,
|
||||
start_firmware_update_p4_web,
|
||||
start_firmware_update_stm32_fw,
|
||||
start_firmware_update_c6_fw,
|
||||
type UpdateInfo,
|
||||
request_available_updates
|
||||
} from './AppProviderStore';
|
||||
|
||||
interface UpdateErrorPopupProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// Function to get the appropriate icon for each update type
|
||||
const getUpdateIcon = (core: string) => {
|
||||
switch (core.toLowerCase()) {
|
||||
case 'stm32':
|
||||
return (
|
||||
<IconCpu2 strokeWidth={2} />
|
||||
);
|
||||
case 'p4 firmware':
|
||||
return (
|
||||
<IconCpu strokeWidth={2} />
|
||||
);
|
||||
case 'c6':
|
||||
return (
|
||||
<IconWifi strokeWidth={2} />
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<IconFileTypeJs strokeWidth={2} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get progress for a specific update type
|
||||
const getProgressForUpdate = (core: string) => {
|
||||
switch (core.toLowerCase()) {
|
||||
case 'p4 firmware': return $p4FirmwareProgress.get();
|
||||
case 'p4 webinterface': return $p4WebinterfaceProgress.get();
|
||||
case 'stm32': return $stm32FirmwareProgress.get();
|
||||
case 'c6': return $c6FirmwareProgress.get();
|
||||
default: return [0, 0];
|
||||
}
|
||||
};
|
||||
|
||||
// Map core types to their target indices
|
||||
const getTargetIndex = (core: string) => {
|
||||
switch (core.toLowerCase()) {
|
||||
case 'p4 webinterface': return 0; // P4 webinterface (first)
|
||||
case 'p4 firmware': return 1; // P4 firmware (second)
|
||||
case 'stm32': return 2; // STM32 firmware
|
||||
case 'c6': return 3; // C6 firmware (last)
|
||||
default: return -1;
|
||||
}
|
||||
};
|
||||
|
||||
export default function UpdateErrorPopup({ isOpen, onClose }: UpdateErrorPopupProps) {
|
||||
const availableUpdates = useStore($availableUpdates);
|
||||
const updateState = useStore($updateState);
|
||||
const updateErrorMessages = useStore($updateErrorMessages);
|
||||
|
||||
// Only show if update state is error
|
||||
if (!isOpen || updateState !== 'error') return null;
|
||||
|
||||
const handleClose = () => {
|
||||
// Don't reset update state here
|
||||
onClose();
|
||||
// reset update status:
|
||||
stateSetters.resetUpdateStatus();
|
||||
// query available updates again!
|
||||
request_available_updates();
|
||||
};
|
||||
|
||||
const retryUpdate = () => {
|
||||
// Reset state and start update again
|
||||
stateSetters.setUpdateState('updating');
|
||||
stateSetters.setUpdateErrorMessages({});
|
||||
stateSetters.setUpdateInProgress(false);
|
||||
stateSetters.setCurrentUpdateTargetIndex(0);
|
||||
stateSetters.setP4FirmwareProgress(0, 0);
|
||||
stateSetters.setP4WebinterfaceProgress(0, 0);
|
||||
stateSetters.setStm32FirmwareProgress(0, 0);
|
||||
stateSetters.setC6FirmwareProgress(0, 0);
|
||||
|
||||
// Start the update process again
|
||||
const updatesToProcess = availableUpdates.filter(update => update.updateAvailable);
|
||||
if (updatesToProcess.length > 0) {
|
||||
const firstUpdate = updatesToProcess[0];
|
||||
switch (firstUpdate.core.toLowerCase()) {
|
||||
case 'stm32':
|
||||
start_firmware_update_stm32_fw();
|
||||
break;
|
||||
case 'p4 webinterface':
|
||||
start_firmware_update_p4_web();
|
||||
break;
|
||||
case 'p4 firmware':
|
||||
start_firmware_update_p4_fw();
|
||||
break;
|
||||
case 'c6':
|
||||
start_firmware_update_c6_fw();
|
||||
break;
|
||||
}
|
||||
}
|
||||
// close the popup
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<dialog class="modal modal-open">
|
||||
<div class="modal-box max-w-2xl">
|
||||
<div class="flex flex-col items-center justify-center py-8">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-alert-circle text-error mb-4">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
|
||||
<path d="M12 8v4" />
|
||||
<path d="M12 16h.01" />
|
||||
</svg>
|
||||
<h3 class="font-bold text-lg text-error mb-2">Update Failed</h3>
|
||||
<p class="text-center mb-6">One or more firmware updates failed to complete successfully.</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 mb-6">
|
||||
{availableUpdates.map((update, index) => {
|
||||
const [currentProgress, status] = getProgressForUpdate(update.core);
|
||||
const updateTargetIndex = getTargetIndex(update.core);
|
||||
const errorMessage = updateErrorMessages[updateTargetIndex] || '';
|
||||
const isFailed = status === -1;
|
||||
|
||||
return (
|
||||
<div key={index} class={`flex items-center justify-between p-3 rounded-lg ${isFailed ? 'bg-error/10' : 'bg-success/10'}`}>
|
||||
<div class="flex items-center gap-3">
|
||||
{getUpdateIcon(update.core)}
|
||||
<div class="flex-1">
|
||||
<span class="font-medium">{update.core}</span>
|
||||
{isFailed && errorMessage && (
|
||||
<div class="text-sm text-error mt-1">{errorMessage}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{isFailed ? (
|
||||
<>
|
||||
<span class="text-sm text-error">Failed</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-x text-error">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M18 6l-12 12" />
|
||||
<path d="M6 6l12 12" />
|
||||
</svg>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span class="text-sm">v{update.newVersion}</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-circle-check text-success">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
|
||||
<path d="M9 12l2 2l4 -4" />
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div class="modal-action">
|
||||
<button class="btn btn-outline" onClick={handleClose}>Close</button>
|
||||
<button class="btn btn-primary" onClick={retryUpdate}>Retry Update</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop" onClick={handleClose}></div>
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
135
src/components/UpdateSuccessPopup.tsx
Normal file
135
src/components/UpdateSuccessPopup.tsx
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { useStore } from '@nanostores/preact';
|
||||
import { useEffect, useState, useRef } from 'preact/hooks';
|
||||
import { IconCpu2, IconWifi, IconCpu, IconFileTypeJs, IconRefresh } from '@tabler/icons-preact';
|
||||
import {
|
||||
$availableUpdates,
|
||||
$updateState,
|
||||
stateSetters,
|
||||
type UpdateInfo
|
||||
} from './AppProviderStore';
|
||||
|
||||
interface UpdateSuccessPopupProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// Function to get the appropriate icon for each update type
|
||||
const getUpdateIcon = (core: string) => {
|
||||
switch (core.toLowerCase()) {
|
||||
case 'stm32':
|
||||
return (
|
||||
<IconCpu2 strokeWidth={2} />
|
||||
);
|
||||
case 'p4 firmware':
|
||||
return (
|
||||
<IconCpu strokeWidth={2} />
|
||||
);
|
||||
case 'c6':
|
||||
return (
|
||||
<IconWifi strokeWidth={2} />
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<IconFileTypeJs strokeWidth={2} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default function UpdateSuccessPopup({ isOpen, onClose }: UpdateSuccessPopupProps) {
|
||||
const availableUpdates = useStore($availableUpdates);
|
||||
const updateState = useStore($updateState);
|
||||
const [showRestarting, setShowRestarting] = useState(false);
|
||||
|
||||
// Only show if explicitly opened via isOpen prop (don't auto-close based on updateState)
|
||||
if (!isOpen) return null;
|
||||
|
||||
// Handle restarting state - just show animation, no timer
|
||||
useEffect(() => {
|
||||
console.log("🎉 UpdateSuccessPopup useEffect triggered:", {
|
||||
updateState,
|
||||
isOpen,
|
||||
showRestarting
|
||||
});
|
||||
|
||||
if (updateState === 'restarting') {
|
||||
console.log("🔄 RESTARTING state detected - showing restart animation");
|
||||
setShowRestarting(true);
|
||||
} else {
|
||||
console.log("🎉 Non-restarting state - hiding restart animation");
|
||||
setShowRestarting(false);
|
||||
}
|
||||
}, [updateState]);
|
||||
|
||||
const handleClose = () => {
|
||||
// Don't reset update state here if we're restarting
|
||||
if (updateState !== 'restarting') {
|
||||
onClose();
|
||||
}
|
||||
// reset update status:
|
||||
stateSetters.resetUpdateStatus();
|
||||
};
|
||||
|
||||
return (
|
||||
<dialog class="modal modal-open">
|
||||
<div class="modal-box max-w-2xl">
|
||||
<div class="flex flex-col items-center justify-center py-8">
|
||||
{/* Always show success checkmark first */}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-circle-check text-success mb-4">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
|
||||
<path d="M9 12l2 2l4 -4" />
|
||||
</svg>
|
||||
<h3 class="font-bold text-lg text-success mb-2">Update Completed Successfully!</h3>
|
||||
<p class="text-center mb-6">All listed components have been updated successfully.</p>
|
||||
|
||||
{/* Show restarting section below if in restarting state */}
|
||||
{updateState === 'restarting' && (
|
||||
<div class="border-t border-base-300 pt-6 mt-2 w-full">
|
||||
<div class="flex flex-col items-center">
|
||||
<IconRefresh
|
||||
size={48}
|
||||
class="text-primary animate-spin mb-3"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<h4 class="font-semibold text-md text-primary mb-2">Restarting Device...</h4>
|
||||
<p class="text-center text-sm text-base-content/70">
|
||||
The device is restarting to apply the firmware update. Please wait for reconnection.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Show update list in both success and restarting state */}
|
||||
{(updateState === 'success' || updateState === 'restarting') && (
|
||||
<div class="space-y-2 mb-6">
|
||||
{availableUpdates.map((update, index) => (
|
||||
<div key={index} class="flex items-center justify-between p-3 bg-success/10 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
{getUpdateIcon(update.core)}
|
||||
<span class="font-medium">{update.core}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm">v{update.newVersion}</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-circle-check text-success">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
|
||||
<path d="M9 12l2 2l4 -4" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Button only in success state */}
|
||||
{updateState === 'success' && (
|
||||
<div class="modal-action">
|
||||
<button class="btn btn-primary" onClick={handleClose}>OK</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div class="modal-backdrop" onClick={handleClose}></div>
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
472
src/components/UpdaterModal.tsx
Normal file
472
src/components/UpdaterModal.tsx
Normal file
|
|
@ -0,0 +1,472 @@
|
|||
import { useState, useEffect } from 'preact/hooks';
|
||||
import { marked } from 'marked';
|
||||
import { useStore } from '@nanostores/preact';
|
||||
import { IconCpu2, IconWifi, IconCpu, IconFileTypeJs } from '@tabler/icons-preact';
|
||||
import {
|
||||
$availableUpdates,
|
||||
$updateChangelog,
|
||||
$updateInProgress,
|
||||
$p4FirmwareProgress,
|
||||
$p4WebinterfaceProgress,
|
||||
$stm32FirmwareProgress,
|
||||
$c6FirmwareProgress,
|
||||
$currentUpdateTargetIndex,
|
||||
$updateErrorMessages,
|
||||
$updateState,
|
||||
$updatedTargets,
|
||||
stateSetters,
|
||||
request_update_progress,
|
||||
start_firmware_update_p4_fw,
|
||||
start_firmware_update_p4_web,
|
||||
start_firmware_update_stm32_fw,
|
||||
start_firmware_update_c6_fw,
|
||||
type UpdateInfo,
|
||||
UpdateStatus,
|
||||
TargetUpdateStatus
|
||||
} from './AppProviderStore';
|
||||
|
||||
// Configure marked with basic options
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
});
|
||||
|
||||
interface UpdaterModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// Function to check if any updates are available
|
||||
const hasAvailableUpdates = (updates: UpdateInfo[]) => updates.some(update => update.updateAvailable);
|
||||
|
||||
// Function to get the appropriate icon for each update type
|
||||
const getUpdateIcon = (core: string) => {
|
||||
switch (core.toLowerCase()) {
|
||||
case 'stm32':
|
||||
return (
|
||||
<IconCpu2 strokeWidth={2} />
|
||||
);
|
||||
case 'p4 firmware':
|
||||
return (
|
||||
<IconCpu strokeWidth={2} />
|
||||
);
|
||||
case 'c6':
|
||||
return (
|
||||
<IconWifi strokeWidth={2} />
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<IconFileTypeJs strokeWidth={2} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default function UpdaterModal({ isOpen, onClose }: UpdaterModalProps) {
|
||||
const updateState = useStore($updateState);
|
||||
const updateInProgress = useStore($updateInProgress);
|
||||
const [currentUpdateIndex, setCurrentUpdateIndex] = useState(0);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
// Auto-close modal when updates are completed or failed
|
||||
useEffect(() => {
|
||||
console.log("🚀 UpdaterModal useEffect triggered:", {
|
||||
updateState,
|
||||
isOpen
|
||||
});
|
||||
|
||||
if (updateState === 'success' || updateState === 'error' || updateState === 'restarting') {
|
||||
if (isOpen) {
|
||||
console.log(`🚪 Closing updater modal due to state: '${updateState}'`);
|
||||
onClose();
|
||||
} else {
|
||||
console.log(`🚪 Updater modal already closed for state: '${updateState}'`);
|
||||
}
|
||||
} else {
|
||||
console.log("🚀 Updater modal stays open - state:", updateState);
|
||||
}
|
||||
}, [updateState, onClose]); // Removed updateInProgress to prevent loop
|
||||
|
||||
// Use useStore for reactive updates
|
||||
const availableUpdates = useStore($availableUpdates);
|
||||
const updateChangelog = useStore($updateChangelog);
|
||||
const currentUpdateTargetIndex = useStore($currentUpdateTargetIndex);
|
||||
const updateErrorMessages = useStore($updateErrorMessages);
|
||||
|
||||
// Enhanced: Use updated_targets from server response via global state
|
||||
const updatedTargetsFromServer = useStore($updatedTargets);
|
||||
|
||||
// Use reactive stores for all progress values
|
||||
const p4FirmwareProgress = useStore($p4FirmwareProgress);
|
||||
const p4WebinterfaceProgress = useStore($p4WebinterfaceProgress);
|
||||
const stm32FirmwareProgress = useStore($stm32FirmwareProgress);
|
||||
const c6FirmwareProgress = useStore($c6FirmwareProgress);
|
||||
|
||||
// Helper function to get progress for a specific update type - now using reactive stores
|
||||
const getProgressForUpdate = (core: string) => {
|
||||
switch (core.toLowerCase()) {
|
||||
case 'p4 firmware': return p4FirmwareProgress;
|
||||
case 'p4 webinterface': return p4WebinterfaceProgress;
|
||||
case 'stm32': return stm32FirmwareProgress;
|
||||
case 'c6': return c6FirmwareProgress;
|
||||
default: return [0, 0];
|
||||
}
|
||||
};
|
||||
|
||||
// Enhanced: Check if a target is completed using updated_targets array from server
|
||||
const isTargetCompletedFromServer = (core: string) => {
|
||||
const targetIndex = getTargetIndex(core);
|
||||
return targetIndex >= 0 && targetIndex < updatedTargetsFromServer.length
|
||||
? updatedTargetsFromServer[targetIndex]
|
||||
: false;
|
||||
};
|
||||
|
||||
// Map core types to their target indices
|
||||
const getTargetIndex = (core: string) => {
|
||||
const coreLower = core.toLowerCase();
|
||||
console.log(`🔍 getTargetIndex called with: "${core}" -> "${coreLower}"`);
|
||||
|
||||
switch (coreLower) {
|
||||
case 'p4 webinterface':
|
||||
console.log(`✅ P4 webinterface -> index 1`);
|
||||
return 1; // P4 webinterface (second)
|
||||
case 'p4 firmware':
|
||||
console.log(`✅ P4 firmware -> index 2`);
|
||||
return 2; // P4 firmware (third)
|
||||
case 'stm32':
|
||||
console.log(`✅ STM32 -> index 0`);
|
||||
return 0; // STM32 firmware (first)
|
||||
case 'c6':
|
||||
console.log(`✅ C6 -> index 3`);
|
||||
return 3; // C6 firmware (last)
|
||||
default:
|
||||
console.warn(`❌ Unknown core type: "${core}" -> "${coreLower}" -> index -1`);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
const startUpdate = async () => {
|
||||
stateSetters.setUpdateState('updating');
|
||||
|
||||
const updatesToProcess = availableUpdates.filter(update => update.updateAvailable);
|
||||
|
||||
for (let i = 0; i < updatesToProcess.length; i++) {
|
||||
setCurrentUpdateIndex(i);
|
||||
const currentUpdate = updatesToProcess[i];
|
||||
|
||||
const updateTargetIndex = getTargetIndex(currentUpdate.core);
|
||||
|
||||
// Start the appropriate update based on the core type
|
||||
switch (currentUpdate.core.toLowerCase()) {
|
||||
case 'stm32':
|
||||
start_firmware_update_stm32_fw();
|
||||
break;
|
||||
case 'p4 webinterface':
|
||||
start_firmware_update_p4_web();
|
||||
break;
|
||||
case 'p4 firmware':
|
||||
start_firmware_update_p4_fw();
|
||||
break;
|
||||
case 'c6':
|
||||
start_firmware_update_c6_fw();
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unknown update type: ${currentUpdate.core}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Wait for the update to complete by monitoring the progress
|
||||
if (currentUpdate.hasProgressBar) {
|
||||
// For updates with progress bar, wait for completion
|
||||
let updateCompleted = false;
|
||||
let updateFailed = false;
|
||||
|
||||
while (!updateCompleted && !updateFailed) {
|
||||
// Get progress for this specific update type - need to use .get() here for current values in async function
|
||||
const [currentProgress, status] = (() => {
|
||||
switch (currentUpdate.core.toLowerCase()) {
|
||||
case 'p4 firmware': return $p4FirmwareProgress.get();
|
||||
case 'p4 webinterface': return $p4WebinterfaceProgress.get();
|
||||
case 'stm32': return $stm32FirmwareProgress.get();
|
||||
case 'c6': return $c6FirmwareProgress.get();
|
||||
default: return [0, 0];
|
||||
}
|
||||
})();
|
||||
|
||||
setProgress(currentProgress);
|
||||
// Check error status first
|
||||
if (status === UpdateStatus.ERROR) {
|
||||
updateFailed = true;
|
||||
console.error(`Update failed for ${currentUpdate.core}`);
|
||||
stateSetters.setUpdateState('error');
|
||||
return;
|
||||
} else if (status === UpdateStatus.SUCCESS) {
|
||||
updateCompleted = true;
|
||||
break;
|
||||
}
|
||||
// Fallback checks
|
||||
if (currentProgress === 100) {
|
||||
updateCompleted = true;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
} else {
|
||||
// For updates without progress bar, show indeterminate progress for 3 seconds
|
||||
setProgress(0);
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
}
|
||||
}
|
||||
|
||||
// Only show success if we reached here (all updates completed)
|
||||
stateSetters.setUpdateState('success');
|
||||
|
||||
// Clear all error messages on successful completion
|
||||
stateSetters.setUpdateErrorMessages({});
|
||||
};
|
||||
|
||||
const resetModal = () => {
|
||||
stateSetters.setUpdateState('initial');
|
||||
setCurrentUpdateIndex(0);
|
||||
setProgress(0);
|
||||
|
||||
stateSetters.setUpdateErrorMessages({});
|
||||
stateSetters.setUpdateInProgress(false);
|
||||
stateSetters.setCurrentUpdateTargetIndex(0);
|
||||
stateSetters.setP4FirmwareProgress(0, TargetUpdateStatus.NO_UPDATE);
|
||||
stateSetters.setP4WebinterfaceProgress(0, TargetUpdateStatus.NO_UPDATE);
|
||||
stateSetters.setStm32FirmwareProgress(0, TargetUpdateStatus.NO_UPDATE);
|
||||
stateSetters.setC6FirmwareProgress(0, TargetUpdateStatus.NO_UPDATE);
|
||||
stateSetters.setUpdaterModalOpen(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<dialog class="modal modal-open">
|
||||
<div class="modal-box max-w-4xl">
|
||||
{updateState === 'initial' && (
|
||||
<>
|
||||
<h3 class="font-bold text-lg mb-4 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-mobiledata">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M16 12v-8" />
|
||||
<path d="M8 20v-8" />
|
||||
<path d="M13 7l3 -3l3 3" />
|
||||
<path d="M5 17l3 3l3 -3" />
|
||||
</svg>
|
||||
Over The Air Update Available
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Available Updates */}
|
||||
<div>
|
||||
<h4 class="font-semibold mb-3">Available Updates:</h4>
|
||||
<div class="space-y-3">
|
||||
{availableUpdates
|
||||
.filter(update => update.updateAvailable)
|
||||
.sort((a, b) => {
|
||||
const indexA = getTargetIndex(a.core);
|
||||
const indexB = getTargetIndex(b.core);
|
||||
console.log(`🔄 Sorting: "${a.core}" (${indexA}) vs "${b.core}" (${indexB}) -> ${indexA - indexB}`);
|
||||
return indexA - indexB;
|
||||
})
|
||||
.map((update, index) => (
|
||||
<div key={index} class="flex items-center justify-between p-3 bg-base-200 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
{getUpdateIcon(update.core)}
|
||||
<div>
|
||||
<div class="font-medium">{update.core}</div>
|
||||
<div class="text-sm opacity-70">
|
||||
{update.currentVersion} → {update.newVersion}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="badge badge-success">Update</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Changelog */}
|
||||
<div>
|
||||
<h4 class="font-semibold mb-3 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-exchange">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M19 3a3 3 0 0 1 1 5.83v4.17a6 6 0 0 1 -6 6h-.585l1.292 1.293a1 1 0 0 1 .083 1.32l-.083 .094a1 1 0 0 1 -1.414 0l-2.959 -2.959a1 1 0 0 1 -.238 -.32l-.038 -.091l-.028 -.094a.98 .98 0 0 1 .187 -.866l.076 -.084l3 -3a1 1 0 0 1 1.414 1.414l-1.293 1.293h.586a4 4 0 0 0 3.995 -3.8l.005 -.2v-4.171a3 3 0 0 1 -2 -2.829l.005 -.176a3 3 0 0 1 2.995 -2.824m-8.293 -.707l3 3a.98 .98 0 0 1 .263 .95l-.01 .031l-.003 .018l-.008 .018l-.007 .027l-.016 .035l-.01 .032l-.007 .01l-.005 .014a1 1 0 0 1 -.232 .316l-2.965 2.963a1 1 0 0 1 -1.32 .083l-.094 -.083a1 1 0 0 1 0 -1.414l1.291 -1.293h-.584a4 4 0 0 0 -3.995 3.8l-.005 .2v4.171a3.001 3.001 0 1 1 -4 2.829l.005 -.176a3 3 0 0 1 1.995 -2.654v-4.17a6 6 0 0 1 6 -6h.585l-1.292 -1.293a1 1 0 0 1 -.083 -1.32l.083 -.094a1 1 0 0 1 1.414 0" />
|
||||
</svg>
|
||||
Changelog
|
||||
</h4>
|
||||
<div class="bg-base-200 rounded-lg p-4 h-80 overflow-y-auto">
|
||||
{updateChangelog ? (
|
||||
<div class="prose prose-sm max-w-none [&>h2]:text-xl [&>h2]:font-semibold [&>h2]:mb-3 [&>h2]:mt-4 [&>h3]:text-lg [&>h3]:font-medium [&>h3]:mb-2 [&>h3]:mt-3 [&>ul]:list-disc [&>ul]:ml-6 [&>ul]:mb-3 [&>li]:mb-1 [&>p]:mb-3 [&>strong]:font-semibold"
|
||||
dangerouslySetInnerHTML={{ __html: marked.parse(updateChangelog) as string }} />
|
||||
) : (
|
||||
<div class="text-center text-gray-500">
|
||||
<p>No changelog available</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!hasAvailableUpdates(availableUpdates) && (
|
||||
<div class="alert alert-info mt-4">
|
||||
<span>No updates available.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="alert alert-warning mt-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<span>The device will restart automatically after the update. This process takes approximately 2-3 minutes.</span>
|
||||
</div>
|
||||
|
||||
<div class="modal-action">
|
||||
<button class="btn btn-outline" onClick={resetModal}>Cancel</button>
|
||||
<button class="btn btn-primary" onClick={startUpdate} disabled={!hasAvailableUpdates(availableUpdates)}>Start Update</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{updateState === 'updating' && (
|
||||
<>
|
||||
<h3 class="font-bold text-lg mb-4 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-mobiledata">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M16 12v-8" />
|
||||
<path d="M8 20v-8" />
|
||||
<path d="M13 7l3 -3l3 3" />
|
||||
<path d="M5 17l3 3l3 -3" />
|
||||
</svg>
|
||||
Updating Over The Air...
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
{availableUpdates
|
||||
.filter(update => update.updateAvailable)
|
||||
.sort((a, b) => {
|
||||
const indexA = getTargetIndex(a.core);
|
||||
const indexB = getTargetIndex(b.core);
|
||||
console.log(`🔄 Sorting (updating): "${a.core}" (${indexA}) vs "${b.core}" (${indexB}) -> ${indexA - indexB}`);
|
||||
return indexA - indexB;
|
||||
})
|
||||
.map((update, index) => {
|
||||
// Enhanced: Get real progress and status for each target using updated_targets
|
||||
const [currentProgress, status] = getProgressForUpdate(update.core);
|
||||
const targetIndex = getTargetIndex(update.core);
|
||||
const isCompletedFromServer = isTargetCompletedFromServer(update.core);
|
||||
const isCurrentTarget = targetIndex === currentUpdateTargetIndex;
|
||||
|
||||
// Determine the actual state of this target
|
||||
let targetState: 'completed' | 'current' | 'waiting' | 'error';
|
||||
let displayProgress = 0;
|
||||
let statusText = '';
|
||||
|
||||
if (status === UpdateStatus.ERROR) {
|
||||
targetState = 'error';
|
||||
statusText = 'Failed';
|
||||
} else if (isCompletedFromServer || status === UpdateStatus.SUCCESS || currentProgress === 100) {
|
||||
// Target is marked as completed in updated_targets array from server OR has success status
|
||||
targetState = 'completed';
|
||||
statusText = 'Complete';
|
||||
displayProgress = 100;
|
||||
} else if (status === UpdateStatus.STARTED || status === UpdateStatus.IN_PROGRESS) {
|
||||
// Show progress for any target that has started or is in progress, regardless of currentUpdateTargetIndex
|
||||
targetState = 'current';
|
||||
displayProgress = currentProgress;
|
||||
statusText = update.hasProgressBar ? `${currentProgress}%` : 'Updating...';
|
||||
} else {
|
||||
targetState = 'waiting';
|
||||
statusText = 'Waiting';
|
||||
displayProgress = 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={index} class="flex items-center gap-4 p-4 bg-base-200 rounded-lg">
|
||||
{getUpdateIcon(update.core)}
|
||||
<div class="flex-1">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="font-medium">{update.core}</span>
|
||||
<span class="text-sm">
|
||||
{targetState === 'completed' ? (
|
||||
<span class="flex items-center gap-1 text-success">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-circle-check">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
|
||||
<path d="M9 12l2 2l4 -4" />
|
||||
</svg>
|
||||
{statusText}
|
||||
</span>
|
||||
) : targetState === 'error' ? (
|
||||
<span class="flex items-center gap-1 text-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="15" y1="9" x2="9" y2="15"/>
|
||||
<line x1="9" y1="9" x2="15" y2="15"/>
|
||||
</svg>
|
||||
{statusText}
|
||||
</span>
|
||||
) : targetState === 'current' ? (
|
||||
<span class="flex items-center gap-1 text-primary">
|
||||
{statusText}
|
||||
</span>
|
||||
) : (
|
||||
<span class="flex items-center gap-1 text-base-content/50">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-clock-pause">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M20.942 13.018a9 9 0 1 0 -7.909 7.922" />
|
||||
<path d="M12 7v5l2 2" />
|
||||
<path d="M17 17v5" />
|
||||
<path d="M21 17v5" />
|
||||
</svg>
|
||||
{statusText}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{update.hasProgressBar ? (
|
||||
<progress
|
||||
class={`progress w-full ${
|
||||
targetState === 'completed' ? 'progress-success' :
|
||||
targetState === 'current' ? 'progress-primary' :
|
||||
targetState === 'error' ? 'progress-error' :
|
||||
'progress-base-300'
|
||||
}`}
|
||||
value={displayProgress}
|
||||
max="100"
|
||||
></progress>
|
||||
) : (
|
||||
<progress
|
||||
class={`progress w-full ${
|
||||
targetState === 'completed' ? 'progress-success' :
|
||||
targetState === 'current' ? 'progress-primary' :
|
||||
targetState === 'error' ? 'progress-error' :
|
||||
'progress-base-300'
|
||||
}`}
|
||||
value={targetState === 'current' ? undefined : displayProgress}
|
||||
max="100"
|
||||
></progress>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>Please do not power off the device during the update process.</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(updateState === 'initial') && (
|
||||
<div class="modal-backdrop" onClick={resetModal}></div>
|
||||
)}
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
692
src/components/WebSocketProvider.tsx
Normal file
692
src/components/WebSocketProvider.tsx
Normal file
|
|
@ -0,0 +1,692 @@
|
|||
import { useCallback, useEffect } from 'preact/hooks';
|
||||
import { $isUpdaterModalOpen, $isSuccessPopupOpen, $isErrorPopupOpen, $websocket, $wsConnectionStatus, $updateErrorMessages, $p4FirmwareProgress, $p4WebinterfaceProgress, $stm32FirmwareProgress, $c6FirmwareProgress, $updateState, $updatedTargets, $availableUpdates, type UpdateInfo, UpdateStatus, TargetUpdateStatus, TotalUpdateStatus } from './AppProviderStore';
|
||||
import { stateSetters, request_available_updates } from './AppProviderStore';
|
||||
import UpdaterModal from './UpdaterModal';
|
||||
|
||||
// Helper function to get target index for update types
|
||||
const getTargetIndex = (core: string) => {
|
||||
switch (core.toLowerCase()) {
|
||||
case 'p4 webinterface': return 1; // P4 webinterface (first)
|
||||
case 'p4 firmware': return 2; // P4 firmware (second)
|
||||
case 'stm32': return 0; // STM32 firmware
|
||||
case 'c6': return 3; // C6 firmware (last)
|
||||
default: return -1;
|
||||
}
|
||||
};
|
||||
|
||||
// Heartbeat configuration
|
||||
const HEARTBEAT_INTERVAL = 1500; // Send ping every 1 second
|
||||
const HEARTBEAT_TIMEOUT = 700; // Wait 1 second for pong response
|
||||
const MAX_MISSED_HEARTBEATS = 5; // Close connection after 5 missed pongs
|
||||
|
||||
// Heartbeat state
|
||||
let heartbeatInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let heartbeatTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
let missedHeartbeats = 0;
|
||||
|
||||
// Reconnect state
|
||||
let isReconnecting = false;
|
||||
let reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
let reload_site_after_webinterface_update = false;
|
||||
|
||||
// Restart detection state
|
||||
let disconnectDetected = false;
|
||||
let heartbeatsAfterReconnect = 0;
|
||||
const HEARTBEATS_NEEDED_AFTER_RESTART = 3;
|
||||
|
||||
function clearHeartbeat() {
|
||||
if (heartbeatInterval) {
|
||||
clearInterval(heartbeatInterval);
|
||||
heartbeatInterval = null;
|
||||
}
|
||||
if (heartbeatTimeout) {
|
||||
clearTimeout(heartbeatTimeout);
|
||||
heartbeatTimeout = null;
|
||||
}
|
||||
missedHeartbeats = 0;
|
||||
}
|
||||
|
||||
function clearReconnect() {
|
||||
if (reconnectTimeout) {
|
||||
clearTimeout(reconnectTimeout);
|
||||
reconnectTimeout = null;
|
||||
}
|
||||
isReconnecting = false;
|
||||
}
|
||||
|
||||
function scheduleReconnect() {
|
||||
if (isReconnecting) {
|
||||
////console.log("Reconnect already scheduled, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
isReconnecting = true;
|
||||
////console.log("Scheduling reconnect in 1 second");
|
||||
reconnectTimeout = setTimeout(() => {
|
||||
connectWebSocket();
|
||||
isReconnecting = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function sendHeartbeat() {
|
||||
if ($websocket.get() && ($websocket.get() as WebSocket).readyState === ($websocket.get() as WebSocket).OPEN) {
|
||||
// Send ping message
|
||||
const pingMessage = JSON.stringify({ message_type: "ping" });
|
||||
try {
|
||||
($websocket.get() as WebSocket).send(pingMessage);
|
||||
} catch (error) {
|
||||
console.error("Failed to send heartbeat ping:", error);
|
||||
missedHeartbeats++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Set timeout for pong response
|
||||
if (heartbeatTimeout) {
|
||||
clearTimeout(heartbeatTimeout);
|
||||
heartbeatTimeout = null;
|
||||
}
|
||||
heartbeatTimeout = setTimeout(() => {
|
||||
missedHeartbeats++;
|
||||
if (missedHeartbeats >= MAX_MISSED_HEARTBEATS) {
|
||||
console.error("Too many missed heartbeats, closing connection");
|
||||
|
||||
// Check if we're in restarting state - this indicates device restart
|
||||
const currentUpdateState = $updateState.get();
|
||||
if (currentUpdateState === 'restarting') {
|
||||
//console.log("🔄 Disconnect detected during restart - marking for restart detection");
|
||||
disconnectDetected = true;
|
||||
heartbeatsAfterReconnect = 0;
|
||||
}
|
||||
|
||||
// Wichtig: Erst Heartbeat stoppen
|
||||
clearHeartbeat();
|
||||
|
||||
// Dann WebSocket schließen und null setzen
|
||||
|
||||
if ($websocket.get()) {
|
||||
($websocket.get() as WebSocket).close();
|
||||
stateSetters.setWebSocketConnectionStatus("Disconnected");
|
||||
$websocket.set(null);
|
||||
}
|
||||
}
|
||||
}, HEARTBEAT_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
function startHeartbeat() {
|
||||
clearHeartbeat(); // Clear any existing heartbeat
|
||||
|
||||
heartbeatInterval = setInterval(() => {
|
||||
sendHeartbeat();
|
||||
}, HEARTBEAT_INTERVAL);
|
||||
}
|
||||
|
||||
function onOpen() {
|
||||
stateSetters.setWebSocketConnectionStatus("Connected");
|
||||
//console.log("WebSocket erfolgreich verbunden");
|
||||
missedHeartbeats = 0;
|
||||
|
||||
// If we reconnected after detecting a restart, start counting heartbeats
|
||||
if (disconnectDetected) {
|
||||
//console.log("🔄 Reconnected after restart - starting heartbeat counter");
|
||||
heartbeatsAfterReconnect = 0;
|
||||
}
|
||||
|
||||
// Start heartbeat mechanism
|
||||
startHeartbeat();
|
||||
// Sende initiale Statusanfrage
|
||||
if ($websocket.get() !== null) {
|
||||
const initial_state_request = JSON.stringify({ message_type: "initial_state_request" });
|
||||
try {
|
||||
($websocket.get() as WebSocket).send(initial_state_request);
|
||||
} catch (error) {
|
||||
console.error("Failed to send initial state request:", error);
|
||||
}
|
||||
|
||||
// Request update status immediately to catch any missed completion messages
|
||||
setTimeout(() => {
|
||||
if ($websocket.get() && ($websocket.get() as WebSocket).readyState === ($websocket.get() as WebSocket).OPEN) {
|
||||
const update_status_request = JSON.stringify({ message_type: "request_update_status" });
|
||||
try {
|
||||
($websocket.get() as WebSocket).send(update_status_request);
|
||||
//console.log("Requested update status on reconnection");
|
||||
} catch (error) {
|
||||
console.error("Failed to send update status request:", error);
|
||||
}
|
||||
}
|
||||
}, 500); // Reduced from 1000ms to 500ms for faster response
|
||||
|
||||
// send request for available updates
|
||||
setTimeout(() => {
|
||||
if ($websocket.get() && ($websocket.get() as WebSocket).readyState === ($websocket.get() as WebSocket).OPEN) {
|
||||
request_available_updates();
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
function onMessage(message: MessageEvent) {
|
||||
////console.log("WebSocket message received:", message.data.toString());
|
||||
// Verarbeite JSON Nachrichten
|
||||
const dataFromServer = JSON.parse(message.data.toString());
|
||||
if (dataFromServer.message_type === "pong") {
|
||||
// reset missed heartbeats and clear pending timeout
|
||||
if (heartbeatTimeout) {
|
||||
clearTimeout(heartbeatTimeout);
|
||||
heartbeatTimeout = null;
|
||||
}
|
||||
missedHeartbeats = 0;
|
||||
//console.log("Heartbeat pong received");
|
||||
|
||||
// Count heartbeats after reconnect for restart detection
|
||||
if (disconnectDetected) {
|
||||
heartbeatsAfterReconnect++;
|
||||
//console.log(`🔄 Heartbeat ${heartbeatsAfterReconnect}/${HEARTBEATS_NEEDED_AFTER_RESTART} after restart`);
|
||||
|
||||
if (heartbeatsAfterReconnect >= HEARTBEATS_NEEDED_AFTER_RESTART) {
|
||||
//console.log("🎉 Device restart completed - closing success popup and resetting state");
|
||||
disconnectDetected = false;
|
||||
disconnectDetected = false;
|
||||
heartbeatsAfterReconnect = 0;
|
||||
|
||||
// Close success popup and reset state
|
||||
stateSetters.setSuccessPopupOpen(false);
|
||||
stateSetters.setUpdateState('initial');
|
||||
if (reload_site_after_webinterface_update) {
|
||||
// Hard reload ohne Cache
|
||||
//window.location.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (dataFromServer.message_type === "state_update" || dataFromServer.message_type === "initial_state_response") {
|
||||
|
||||
// If this is the initial state response, also request update status
|
||||
if (dataFromServer.message_type === "initial_state_response") {
|
||||
//console.log("📊 Initial state received, requesting update status...");
|
||||
setTimeout(() => {
|
||||
if ($websocket.get() && ($websocket.get() as WebSocket).readyState === ($websocket.get() as WebSocket).OPEN) {
|
||||
const update_status_request = JSON.stringify({ message_type: "request_update_status" });
|
||||
try {
|
||||
($websocket.get() as WebSocket).send(update_status_request);
|
||||
//console.log("Requested update status after initial state");
|
||||
} catch (error) {
|
||||
console.error("Failed to send update status request:", error);
|
||||
}
|
||||
}
|
||||
}, 200); // Short delay to ensure connection is stable
|
||||
}
|
||||
|
||||
if (dataFromServer.free_internal_heap) {
|
||||
stateSetters.setFreeInternalHeap(dataFromServer.free_internal_heap);
|
||||
}
|
||||
// DCDC State aktualisieren
|
||||
if (dataFromServer.dcdcStateMachine) {
|
||||
stateSetters.setDCDCState(dataFromServer.dcdcStateMachine.dcdc_state);
|
||||
stateSetters.setPOWERGOOD(dataFromServer.dcdcStateMachine.POWERGOOD);
|
||||
stateSetters.setDcdcErrorStatus(dataFromServer.dcdcStateMachine.error_flag);
|
||||
stateSetters.setREF_value(dataFromServer.dcdcStateMachine.ref_value);
|
||||
}
|
||||
|
||||
// Laser State aktualisieren
|
||||
if (dataFromServer.laserStateMachine) {
|
||||
// Laser state update
|
||||
stateSetters.setLaserState(dataFromServer.laserStateMachine.laser_state);
|
||||
stateSetters.setModulationInputMethod(dataFromServer.laserStateMachine.laser_state);
|
||||
stateSetters.setREF_calibrated(dataFromServer.laserStateMachine.is_REF_calibrated);
|
||||
stateSetters.setHAS_DUTY_LOOKUP_TABLE(dataFromServer.laserStateMachine.has_duty_lookup_table);
|
||||
stateSetters.setLaserErrorStatus(dataFromServer.laserStateMachine.error_flag);
|
||||
stateSetters.setLASERGOOD(dataFromServer.laserStateMachine.LASERGOOD);
|
||||
stateSetters.setLaserSetTemp(dataFromServer.laserStateMachine.set_temp);
|
||||
stateSetters.setLaserMaxTemp(dataFromServer.laserStateMachine.max_temp);
|
||||
stateSetters.setInterlock(dataFromServer.laserStateMachine.interlock);
|
||||
stateSetters.setModulationInput(dataFromServer.laserStateMachine.modulationInput);
|
||||
stateSetters.setSetCurrent(dataFromServer.laserStateMachine.set_current);
|
||||
stateSetters.setMaxCurrent(dataFromServer.laserStateMachine.max_current);
|
||||
stateSetters.setThresholdCurrent(dataFromServer.laserStateMachine.threshold_current);
|
||||
stateSetters.setCurrentReading(dataFromServer.laserStateMachine.current_reading);
|
||||
stateSetters.setLaserTemp(dataFromServer.laserStateMachine.laser_temp);
|
||||
}
|
||||
|
||||
// Driver State aktualisieren
|
||||
if (dataFromServer.driverStateMachine) {
|
||||
stateSetters.setDriverTemp(dataFromServer.driverStateMachine.driver_temp);
|
||||
stateSetters.setDriverMaxTemp(dataFromServer.driverStateMachine.max_temp);
|
||||
stateSetters.setVccVoltage(dataFromServer.driverStateMachine.vcc_voltage);
|
||||
stateSetters.setPdVoltage(dataFromServer.driverStateMachine.pd_voltage);
|
||||
}
|
||||
} else if (dataFromServer.message_type === "calibration_table") {
|
||||
// Log raw data to see the actual format of values
|
||||
//console.log("Raw calibration message:", message.data.toString().substring(0, 500) + "...");
|
||||
// Parse values and format to always show with decimal places
|
||||
const parsedValues = dataFromServer.values ?
|
||||
dataFromServer.values.map((val: any) => {
|
||||
// Convert to number and keep as float
|
||||
const numVal = Number(val);
|
||||
|
||||
return numVal;
|
||||
}) : [];
|
||||
|
||||
// Erstelle einen neuen Array mit korrekten Indizes
|
||||
// Fülle Indizes 0-499 mit Nullen und füge dann die tatsächlichen Werte ein
|
||||
const minDuty = dataFromServer.min_duty || 500;
|
||||
const completedValues = [];
|
||||
|
||||
// Fülle Nullen für Indizes von 0 bis (minDuty-1)
|
||||
for (let i = 0; i < minDuty; i++) {
|
||||
completedValues.push(0);
|
||||
}
|
||||
for (let i = 0; i < parsedValues.length; i++) {
|
||||
completedValues.push(parsedValues[i]);
|
||||
}
|
||||
|
||||
// Store values in state - use the completed values with correct indices
|
||||
stateSetters.setCalibrationTableData(
|
||||
completedValues,
|
||||
minDuty,
|
||||
dataFromServer.max_duty || 3500, (dataFromServer.points_count || parsedValues.length) + minDuty
|
||||
);
|
||||
} else if (dataFromServer.message_type === "update_information") {
|
||||
//console.log("Update information received");
|
||||
//console.log("Raw update_information data:", dataFromServer);
|
||||
// Update Information
|
||||
if (dataFromServer.update_available) {
|
||||
const infos: UpdateInfo[] = [];
|
||||
if (dataFromServer.update_available.p4_firmware) {
|
||||
infos.push({
|
||||
core: 'p4 firmware',
|
||||
currentVersion: dataFromServer.update_available.p4_firmware.current_version,
|
||||
newVersion: dataFromServer.update_available.p4_firmware.new_version,
|
||||
icon: 'p4 firmware',
|
||||
hasProgressBar: true,
|
||||
updateAvailable: true,
|
||||
});
|
||||
//console.log("P4 Firmware update available");
|
||||
}
|
||||
if (dataFromServer.update_available.p4_webinterface) {
|
||||
infos.push({
|
||||
core: "p4 webinterface",
|
||||
currentVersion: dataFromServer.update_available.p4_webinterface.current_version,
|
||||
newVersion: dataFromServer.update_available.p4_webinterface.new_version,
|
||||
icon: "p4 webinterface",
|
||||
hasProgressBar: true,
|
||||
updateAvailable: true,
|
||||
});
|
||||
//console.log("P4 Webinterface update available");
|
||||
}
|
||||
if (dataFromServer.update_available.stm32_firmware) {
|
||||
infos.push({
|
||||
core: "STM32",
|
||||
currentVersion: dataFromServer.update_available.stm32_firmware.current_version,
|
||||
newVersion: dataFromServer.update_available.stm32_firmware.new_version,
|
||||
icon: "stm32",
|
||||
hasProgressBar: true,
|
||||
updateAvailable: true,
|
||||
});
|
||||
//console.log("STM32 Firmware update available");
|
||||
}
|
||||
if (dataFromServer.update_available.c6_firmware) {
|
||||
infos.push({
|
||||
core: "C6",
|
||||
currentVersion: dataFromServer.update_available.c6_firmware.current_version,
|
||||
newVersion: dataFromServer.update_available.c6_firmware.new_version,
|
||||
icon: "c6",
|
||||
hasProgressBar: true,
|
||||
updateAvailable: true,
|
||||
});
|
||||
//console.log("C6 Firmware update available");
|
||||
}
|
||||
stateSetters.setAvailableUpdates(infos);
|
||||
stateSetters.setUpdateChangelog(dataFromServer.changelog);
|
||||
}
|
||||
// Alte einzelne Progress-Message-Handler wurden durch complete_update_status ersetzt
|
||||
|
||||
} else if (dataFromServer.message_type === "complete_update_status") {
|
||||
//console.log("📊 Complete update status received:", dataFromServer);
|
||||
|
||||
// Check if we're currently in restarting state - if so, ignore status updates
|
||||
const currentUpdateState = $updateState.get() as any;
|
||||
if (currentUpdateState === 'restarting') {
|
||||
//console.log("🔄 Ignoring complete update status during RESTARTING state - countdown in progress");
|
||||
return;
|
||||
}
|
||||
|
||||
// Process the complete status information for all targets
|
||||
if (dataFromServer.targets) {
|
||||
const targets = dataFromServer.targets;
|
||||
|
||||
// STM32 Status
|
||||
if (targets.stm32) {
|
||||
const status = targets.stm32.status;
|
||||
const progress = targets.stm32.progress || 0;
|
||||
const updateAvailable = targets.stm32.update_available || false;
|
||||
|
||||
// Convert string status to TargetUpdateStatus enum
|
||||
let statusEnum = TargetUpdateStatus.NO_UPDATE;
|
||||
switch (status) {
|
||||
case 'waiting': statusEnum = TargetUpdateStatus.WAITING; break;
|
||||
case 'started': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
|
||||
case 'in_progress': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
|
||||
case 'completed': statusEnum = TargetUpdateStatus.COMPLETED; break;
|
||||
case 'error': statusEnum = TargetUpdateStatus.ERROR; break;
|
||||
case 'no_update': statusEnum = TargetUpdateStatus.NO_UPDATE; break;
|
||||
case 'restarting': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
|
||||
default: statusEnum = TargetUpdateStatus.NO_UPDATE; break;
|
||||
}
|
||||
|
||||
stateSetters.setStm32FirmwareProgress(progress, statusEnum);
|
||||
//console.log(`📊 STM32: status=${status}, progress=${progress}, available=${updateAvailable}`);
|
||||
}
|
||||
|
||||
// P4 Webinterface Status
|
||||
if (targets.p4_webinterface) {
|
||||
const status = targets.p4_webinterface.status;
|
||||
const progress = targets.p4_webinterface.progress || 0;
|
||||
const updateAvailable = targets.p4_webinterface.update_available || false;
|
||||
|
||||
let statusEnum = TargetUpdateStatus.NO_UPDATE;
|
||||
switch (status) {
|
||||
case 'waiting': statusEnum = TargetUpdateStatus.WAITING; break;
|
||||
case 'started': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
|
||||
case 'in_progress': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
|
||||
case 'completed': statusEnum = TargetUpdateStatus.COMPLETED; break;
|
||||
case 'error': statusEnum = TargetUpdateStatus.ERROR; break;
|
||||
case 'no_update': statusEnum = TargetUpdateStatus.NO_UPDATE; break;
|
||||
case 'restarting': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
|
||||
default: statusEnum = TargetUpdateStatus.NO_UPDATE; break;
|
||||
}
|
||||
|
||||
stateSetters.setP4WebinterfaceProgress(progress, statusEnum);
|
||||
//console.log(`📊 P4 Webinterface: status=${status}, progress=${progress}, available=${updateAvailable}`);
|
||||
}
|
||||
|
||||
// P4 Firmware Status
|
||||
if (targets.p4_firmware) {
|
||||
const status = targets.p4_firmware.status;
|
||||
const progress = targets.p4_firmware.progress || 0;
|
||||
const updateAvailable = targets.p4_firmware.update_available || false;
|
||||
|
||||
let statusEnum = TargetUpdateStatus.NO_UPDATE;
|
||||
switch (status) {
|
||||
case 'waiting': statusEnum = TargetUpdateStatus.WAITING; break;
|
||||
case 'started': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
|
||||
case 'in_progress': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
|
||||
case 'completed': statusEnum = TargetUpdateStatus.COMPLETED; break;
|
||||
case 'error': statusEnum = TargetUpdateStatus.ERROR; break;
|
||||
case 'no_update': statusEnum = TargetUpdateStatus.NO_UPDATE; break;
|
||||
case 'restarting': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
|
||||
default: statusEnum = TargetUpdateStatus.NO_UPDATE; break;
|
||||
}
|
||||
|
||||
stateSetters.setP4FirmwareProgress(progress, statusEnum);
|
||||
//console.log(`📊 P4 Firmware: status=${status}, progress=${progress}, available=${updateAvailable}`);
|
||||
}
|
||||
|
||||
// C6 Status
|
||||
if (targets.c6) {
|
||||
const status = targets.c6.status;
|
||||
const progress = targets.c6.progress || 0;
|
||||
const updateAvailable = targets.c6.update_available || false;
|
||||
|
||||
let statusEnum = TargetUpdateStatus.NO_UPDATE;
|
||||
switch (status) {
|
||||
case 'waiting': statusEnum = TargetUpdateStatus.WAITING; break;
|
||||
case 'started': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
|
||||
case 'in_progress': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
|
||||
case 'completed': statusEnum = TargetUpdateStatus.COMPLETED; break;
|
||||
case 'error': statusEnum = TargetUpdateStatus.ERROR; break;
|
||||
case 'no_update': statusEnum = TargetUpdateStatus.NO_UPDATE; break;
|
||||
case 'restarting': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
|
||||
default: statusEnum = TargetUpdateStatus.NO_UPDATE; break;
|
||||
}
|
||||
|
||||
stateSetters.setC6FirmwareProgress(progress, statusEnum);
|
||||
//console.log(`📊 C6: status=${status}, progress=${progress}, available=${updateAvailable}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there's an ongoing update
|
||||
if (dataFromServer.update_in_progress) {
|
||||
//console.log("🔄 Update in progress detected, setting total_update_status to updating");
|
||||
stateSetters.setUpdateInProgress(true);
|
||||
stateSetters.setTotalUpdateStatus(TotalUpdateStatus.UPDATING);
|
||||
|
||||
// Use the total_update_status from backend if available
|
||||
if (dataFromServer.total_update_status !== undefined) {
|
||||
const totalStatus = dataFromServer.total_update_status;
|
||||
//console.log("📊 Backend total_update_status:", totalStatus);
|
||||
|
||||
switch (totalStatus) {
|
||||
case 0: // INITIAL
|
||||
stateSetters.setTotalUpdateStatus(TotalUpdateStatus.INITIAL);
|
||||
break;
|
||||
case 1: // STARTED
|
||||
stateSetters.setTotalUpdateStatus(TotalUpdateStatus.STARTED);
|
||||
break;
|
||||
case 2: // UPDATING
|
||||
stateSetters.setTotalUpdateStatus(TotalUpdateStatus.UPDATING);
|
||||
break;
|
||||
case 3: // SUCCESS
|
||||
stateSetters.setTotalUpdateStatus(TotalUpdateStatus.SUCCESS);
|
||||
break;
|
||||
case 4: // ERROR
|
||||
stateSetters.setTotalUpdateStatus(TotalUpdateStatus.ERROR);
|
||||
break;
|
||||
case 5: // RESTARTING
|
||||
stateSetters.setTotalUpdateStatus(TotalUpdateStatus.RESTARTING);
|
||||
break;
|
||||
default:
|
||||
console.warn("Unknown total_update_status:", totalStatus);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Open the updater modal, if it is not already open and no success/error popups are showing
|
||||
const isModalOpen = $isUpdaterModalOpen.get();
|
||||
const isSuccessPopupOpen = $isSuccessPopupOpen.get();
|
||||
const isErrorPopupOpen = $isErrorPopupOpen.get();
|
||||
/*console.log("🎯 Modal decision matrix:", {
|
||||
isModalOpen,
|
||||
totalUpdateStatus: currentUpdateState,
|
||||
isSuccessPopupOpen,
|
||||
isErrorPopupOpen,
|
||||
shouldOpen: !isModalOpen && !isSuccessPopupOpen && !isErrorPopupOpen && currentUpdateState !== 'success' && currentUpdateState !== 'restarting'
|
||||
});*/
|
||||
|
||||
// Don't open updater modal if success/error popups are open or we're in success/restarting state
|
||||
const shouldOpenModal = !isModalOpen && !isSuccessPopupOpen && !isErrorPopupOpen &&
|
||||
currentUpdateState !== 'success' && currentUpdateState !== 'restarting';
|
||||
if (shouldOpenModal) {
|
||||
//console.log("🚀 Opening updater modal - conditions met");
|
||||
stateSetters.setUpdaterModalOpen(true);
|
||||
} else {
|
||||
//console.log("🚫 Skipping updater modal - conditions not met");
|
||||
}
|
||||
} else {
|
||||
//console.log("✅ No update in progress, checking if all available updates are completed");
|
||||
|
||||
// Check if all targets with update_available: true are completed
|
||||
let allAvailableUpdatesCompleted = true;
|
||||
let hasAnyAvailableUpdates = false;
|
||||
|
||||
if (dataFromServer.targets.stm32 && dataFromServer.targets.stm32.update_available) {
|
||||
hasAnyAvailableUpdates = true;
|
||||
if (dataFromServer.targets.stm32.status !== 'completed') {
|
||||
allAvailableUpdatesCompleted = false;
|
||||
}
|
||||
}
|
||||
if (dataFromServer.targets.p4_webinterface && dataFromServer.targets.p4_webinterface.update_available) {
|
||||
hasAnyAvailableUpdates = true;
|
||||
if (dataFromServer.targets.p4_webinterface.status !== 'completed') {
|
||||
allAvailableUpdatesCompleted = false;
|
||||
}
|
||||
}
|
||||
if (dataFromServer.targets.p4_firmware && dataFromServer.targets.p4_firmware.update_available) {
|
||||
hasAnyAvailableUpdates = true;
|
||||
if (dataFromServer.targets.p4_firmware.status !== 'completed') {
|
||||
allAvailableUpdatesCompleted = false;
|
||||
}
|
||||
}
|
||||
if (dataFromServer.targets.c6 && dataFromServer.targets.c6.update_available) {
|
||||
hasAnyAvailableUpdates = true;
|
||||
if (dataFromServer.targets.c6.status !== 'completed') {
|
||||
allAvailableUpdatesCompleted = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasAnyAvailableUpdates && allAvailableUpdatesCompleted) {
|
||||
//console.log("🎉 All available updates completed successfully, setting total_update_status to success");
|
||||
stateSetters.setUpdateInProgress(false);
|
||||
stateSetters.setTotalUpdateStatus(TotalUpdateStatus.SUCCESS);
|
||||
} else if (!hasAnyAvailableUpdates) {
|
||||
//console.log("ℹ️ No updates available, resetting total_update_status to initial");
|
||||
stateSetters.setUpdateInProgress(false);
|
||||
if (currentUpdateState !== 'success' && currentUpdateState !== 'error' && currentUpdateState !== 'restarting') {
|
||||
stateSetters.setTotalUpdateStatus(TotalUpdateStatus.INITIAL);
|
||||
}
|
||||
} else {
|
||||
//console.log("⏳ Some available updates still in progress, keeping current total_update_status");
|
||||
}
|
||||
}
|
||||
|
||||
} else if (dataFromServer.message_type === "response_update_status") {
|
||||
console.log("📊 Update status response received:", dataFromServer);
|
||||
|
||||
// Check if we're currently in restarting state - if so, ignore status updates
|
||||
const currentUpdateState = $updateState.get() as any;
|
||||
if (currentUpdateState === 'restarting') {
|
||||
//console.log("🔄 Ignoring update status during RESTARTING state - countdown in progress");
|
||||
return;
|
||||
}
|
||||
|
||||
// Always process the complete status information, regardless of update_in_progress
|
||||
//console.log("🔄 Processing complete update status for all targets");
|
||||
|
||||
// Set progress and status for each update type if provided
|
||||
if (dataFromServer.p4_firmware_progress !== undefined) {
|
||||
const status = dataFromServer.p4_firmware_status || 0;
|
||||
stateSetters.setP4FirmwareProgress(dataFromServer.p4_firmware_progress, status);
|
||||
//console.log(`📊 P4 Firmware: progress=${dataFromServer.p4_firmware_progress}, status=${status}`);
|
||||
}
|
||||
if (dataFromServer.p4_webinterface_progress !== undefined) {
|
||||
const status = dataFromServer.p4_webinterface_status || 0;
|
||||
stateSetters.setP4WebinterfaceProgress(dataFromServer.p4_webinterface_progress, status);
|
||||
//console.log(`📊 P4 Webinterface: progress=${dataFromServer.p4_webinterface_progress}, status=${status}`);
|
||||
}
|
||||
if (dataFromServer.stm32_firmware_progress !== undefined) {
|
||||
const status = dataFromServer.stm32_firmware_status || 0;
|
||||
stateSetters.setStm32FirmwareProgress(dataFromServer.stm32_firmware_progress, status);
|
||||
//console.log(`📊 STM32 Firmware: progress=${dataFromServer.stm32_firmware_progress}, status=${status}`);
|
||||
}
|
||||
if (dataFromServer.c6_firmware_progress !== undefined) {
|
||||
const status = dataFromServer.c6_firmware_status || 0;
|
||||
stateSetters.setC6FirmwareProgress(dataFromServer.c6_firmware_progress, status);
|
||||
//console.log(`📊 C6 Firmware: progress=${dataFromServer.c6_firmware_progress}, status=${status}`);
|
||||
}
|
||||
|
||||
// Store updated_targets array from server for UpdaterModal
|
||||
if (dataFromServer.updated_targets && Array.isArray(dataFromServer.updated_targets)) {
|
||||
stateSetters.setUpdatedTargets(dataFromServer.updated_targets);
|
||||
//console.log("🎯 Updated targets:", dataFromServer.updated_targets);
|
||||
}
|
||||
|
||||
// Set current target if provided
|
||||
if (dataFromServer.current_target !== undefined) {
|
||||
stateSetters.setCurrentUpdateTargetIndex(dataFromServer.current_target);
|
||||
//console.log("🎯 Current target:", dataFromServer.current_target);
|
||||
}
|
||||
|
||||
// Check if there's an ongoing update
|
||||
if (dataFromServer.update_in_progress) {
|
||||
//console.log("🔄 Update in progress detected, setting state to updating");
|
||||
stateSetters.setUpdateInProgress(true);
|
||||
stateSetters.setUpdateState("updating");
|
||||
|
||||
// Open the updater modal, if it is not already open and no success/error popups are showing
|
||||
const isModalOpen = $isUpdaterModalOpen.get();
|
||||
const isSuccessPopupOpen = $isSuccessPopupOpen.get();
|
||||
const isErrorPopupOpen = $isErrorPopupOpen.get();
|
||||
/*console.log("🎯 Modal decision matrix:", {
|
||||
isModalOpen,
|
||||
updateState: currentUpdateState,
|
||||
isSuccessPopupOpen,
|
||||
isErrorPopupOpen,
|
||||
shouldOpen: !isModalOpen && !isSuccessPopupOpen && !isErrorPopupOpen && currentUpdateState !== 'success' && currentUpdateState !== 'restarting'
|
||||
});*/
|
||||
|
||||
// Don't open updater modal if success/error popups are open or we're in success/restarting state
|
||||
const shouldOpenModal = !isModalOpen && !isSuccessPopupOpen && !isErrorPopupOpen &&
|
||||
currentUpdateState !== 'success' && currentUpdateState !== 'restarting';
|
||||
if (shouldOpenModal) {
|
||||
//console.log("🚀 Opening updater modal - conditions met");
|
||||
stateSetters.setUpdaterModalOpen(true);
|
||||
} else {
|
||||
//console.log("🚫 Skipping updater modal - conditions not met");
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// Unbekannter Nachrichtentyp – korrektes Feld protokollieren
|
||||
console.warn(`[${new Date().toISOString()}] Unbekannter Nachrichtentyp: ${dataFromServer.message_type ?? 'undefined'}`);
|
||||
console.debug('Payload:', dataFromServer);
|
||||
}
|
||||
}
|
||||
|
||||
function onError(error: Event) {
|
||||
$wsConnectionStatus.set("Disconnected");
|
||||
//console.log("WebSocket error:", error);
|
||||
clearHeartbeat();
|
||||
scheduleReconnect();
|
||||
}
|
||||
|
||||
function onClose(event: CloseEvent) {
|
||||
$wsConnectionStatus.set("Disconnected");
|
||||
//console.log("WebSocket closed with code:", event.code);
|
||||
//console.log("Close reason:", event.reason);
|
||||
//console.log("Clean close:", event.wasClean);
|
||||
clearHeartbeat();
|
||||
if (event.code === 1000) {
|
||||
//console.log("Normal closure");
|
||||
}
|
||||
scheduleReconnect();
|
||||
}
|
||||
|
||||
const connectWebSocket = () => {
|
||||
// Clear any existing reconnect attempts
|
||||
clearReconnect();
|
||||
clearHeartbeat();
|
||||
|
||||
const wsAddress = `ws://hydrobuddy.local/ws`;
|
||||
//console.log(`Verbinde mit: ${wsAddress}`);
|
||||
if ($websocket.get() !== null) {
|
||||
($websocket.get() as WebSocket).close(1000);
|
||||
$websocket.set(null);
|
||||
}
|
||||
$websocket.set(new WebSocket(wsAddress));
|
||||
if ($websocket.get() !== null) {
|
||||
($websocket.get() as WebSocket).onopen = onOpen;
|
||||
|
||||
($websocket.get() as WebSocket).onmessage = onMessage;
|
||||
|
||||
($websocket.get() as WebSocket).onerror = onError;
|
||||
|
||||
($websocket.get() as WebSocket).onclose = onClose;
|
||||
}
|
||||
}
|
||||
|
||||
export const WebSocketProvider = () => {
|
||||
|
||||
// Auto-connect WebSocket at first render
|
||||
useEffect(() => {
|
||||
//console.log("Component mounted, starting WebSocket connection...");
|
||||
connectWebSocket();
|
||||
// Cleanup beim Unmounting
|
||||
return () => {
|
||||
clearHeartbeat();
|
||||
clearReconnect();
|
||||
($websocket.get() as WebSocket).close();
|
||||
$websocket.set(null);
|
||||
$wsConnectionStatus.set("Disconnected");
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
168
src/layouts/Layout.astro
Normal file
168
src/layouts/Layout.astro
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
---
|
||||
import Sidebar from "../components/Sidebar.astro";
|
||||
import GlobalModals from "../components/GlobalModals";
|
||||
import MemoryProgressBar from "../components/MemoryProgressBar";
|
||||
import "../styles/global.css";
|
||||
import Router from "../components/Router.astro";
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
}
|
||||
const { title = "Alpha Driver" } = Astro.props;
|
||||
|
||||
let currentTheme = "light";
|
||||
let sidebarOpen = false;
|
||||
---
|
||||
|
||||
<div class="drawer lg:drawer-open">
|
||||
<input
|
||||
id="drawer-toggle"
|
||||
type="checkbox"
|
||||
class="drawer-toggle"
|
||||
checked={sidebarOpen}
|
||||
/>
|
||||
|
||||
<div class="drawer-content flex flex-col">
|
||||
{/* Top Navigation Bar */}
|
||||
<div class="navbar bg-base-100 shadow-md">
|
||||
<div class="flex-none lg:hidden">
|
||||
<label for="drawer-toggle" class="btn btn-square btn-ghost">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="inline-block w-6 h-6 stroke-current"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="text-xl font-audiowide">Alpha Driver Webinterface</span>
|
||||
</div>
|
||||
<div class="flex-1 flex justify-center">
|
||||
<div class="w-80">
|
||||
<MemoryProgressBar showDetails={false} className="text-xs" client:load />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
{/* Theme select */}
|
||||
<select
|
||||
class="select select-bordered select-sm mr-2 theme-selector"
|
||||
id="theme-selector"
|
||||
value={currentTheme}
|
||||
>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="goggles1">Goggles</option>
|
||||
<option value="gogglesBW">Goggles B&W</option>
|
||||
<option value="emerald">Nature</option>
|
||||
<option value="dark-neon">Dark Neon</option>
|
||||
<option value="retro">Sepia</option>
|
||||
<option value="halloween">Chocolate</option>
|
||||
<option value="winter">Winter</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<main class="flex-1 p-4 overflow-x-auto" id="spa-main-content" >
|
||||
{/* Global Update Notification */}
|
||||
<div id="update-notification" class="alert alert-info mb-4 hidden">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
|
||||
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
|
||||
<path d="M12 9l0 3" />
|
||||
<path d="M12 15l.01 0" />
|
||||
</svg>
|
||||
<span>Software updates are available for your device.</span>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-success" onclick="if(typeof window.openUpdaterModal === 'function') window.openUpdaterModal()">
|
||||
View Updates
|
||||
</button>
|
||||
<button class="btn btn-sm btn-ghost" onclick="document.getElementById('update-notification').classList.add('hidden')">
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content will be loaded here by the router */}
|
||||
<Router />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div class="drawer-side z-10">
|
||||
<label for="drawer-toggle" aria-label="close sidebar" class="drawer-overlay"
|
||||
></label>
|
||||
<Sidebar />
|
||||
</div>
|
||||
|
||||
{/* Global Modals */}
|
||||
<GlobalModals client:load />
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Note: Update notification is now controlled by SecurityPage toggles
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// Type declaration for global function
|
||||
declare global {
|
||||
interface Window {
|
||||
handleNavigation: (path: string) => void;
|
||||
navigateTo?: (path: string) => void;
|
||||
}
|
||||
}
|
||||
|
||||
// Theme handling
|
||||
const themeSelector = document.getElementById(
|
||||
"theme-selector",
|
||||
) as HTMLSelectElement;
|
||||
if (themeSelector) {
|
||||
const savedTheme = localStorage.getItem("theme") || "light";
|
||||
themeSelector.value = savedTheme;
|
||||
document.documentElement.setAttribute("data-theme", savedTheme);
|
||||
|
||||
themeSelector.addEventListener("change", (e) => {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
const theme = target.value;
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
localStorage.setItem("theme", theme);
|
||||
});
|
||||
}
|
||||
|
||||
// Drawer handling
|
||||
const drawerToggle = document.getElementById(
|
||||
"drawer-toggle",
|
||||
) as HTMLInputElement;
|
||||
if (drawerToggle) {
|
||||
drawerToggle.addEventListener("change", (e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const checked = target.checked;
|
||||
drawerToggle.checked = checked;
|
||||
});
|
||||
}
|
||||
|
||||
// Navigation handling
|
||||
window.handleNavigation = function (path: string) {
|
||||
console.log("handleNavigation", path);
|
||||
|
||||
// Add vibration feedback on mobile for page navigation
|
||||
if ('vibrate' in navigator && window.innerWidth < 1024) {
|
||||
navigator.vibrate(25);
|
||||
}
|
||||
|
||||
if (drawerToggle) {
|
||||
drawerToggle.checked = false;
|
||||
}
|
||||
if (typeof window !== "undefined" && window.navigateTo) {
|
||||
window.navigateTo(path);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
25
src/pages/index.astro
Normal file
25
src/pages/index.astro
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
import { WebSocketProvider } from "../components/WebSocketProvider";
|
||||
import { TestbenchWebSocketProvider } from "../components/TestbenchWebsocketProvider";
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import Router from "../components/Router.astro";
|
||||
|
||||
---
|
||||
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Testbench Webinterface</title>
|
||||
<meta name="view-transition" content="same-origin" />
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<Layout title="Alpha Driver - Dashboard" />
|
||||
{/*<WebSocketProvider client:load />*/}
|
||||
<WebSocketProvider client:load />
|
||||
<TestbenchWebSocketProvider client:load />
|
||||
</body>
|
||||
</html>
|
||||
239
src/styles/global.css
Normal file
239
src/styles/global.css
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
@font-face {
|
||||
font-family: 'Varela Round';
|
||||
src: url('../assets/VarelaRound-Regular.woff2') format('woff2');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Audiowide';
|
||||
src: url('../assets/Audiowide-Regular.woff2') format('woff2');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Varela Round', sans-serif;
|
||||
}
|
||||
|
||||
.font-audiowide, .navbar span.text-xl {
|
||||
font-family: 'Audiowide', cursive;
|
||||
}
|
||||
|
||||
/* Responsive improvements */
|
||||
html, body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Faster drawer animations - 30% faster */
|
||||
.drawer-side {
|
||||
transition: transform 0.21s ease-out;
|
||||
}
|
||||
|
||||
/* Fix join button corner radius issues */
|
||||
.join > :not(:first-child):not(:last-child) {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.join > :first-child:not(:last-child) {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
.join > :last-child:not(:first-child) {
|
||||
border-top-left-radius: 0 !important;
|
||||
border-bottom-left-radius: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Mobile responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
|
||||
.stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.navbar span {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Fix horizontal button groups on mobile - but NOT tabs or joins */
|
||||
.join:not(.tabs):not(.btn-group) {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.join:not(.tabs):not(.btn-group) .btn {
|
||||
border-radius: 0.5rem !important;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.join:not(.tabs):not(.btn-group) .btn:first-child {
|
||||
border-radius: 0.5rem !important;
|
||||
}
|
||||
|
||||
.join:not(.tabs):not(.btn-group) .btn:last-child {
|
||||
border-radius: 0.5rem !important;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.text-2xl {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.theme-selector {
|
||||
width: auto;
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tap feedback for mobile */
|
||||
.btn:active,
|
||||
.menu a:active {
|
||||
transform: scale(0.98);
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
/* Goggles B&W theme improvements */
|
||||
[data-theme="gogglesBW"] {
|
||||
/* Better sidebar menu contrast */
|
||||
.menu li > * {
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.menu li > *:hover {
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.menu li > *.active {
|
||||
border-color: #ffffff;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Better button visibility with stronger outlines */
|
||||
.btn {
|
||||
border: 2px solid rgba(255, 255, 255, 0.4) !important;
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
box-shadow: 0 1px 3px rgba(255, 255, 255, 0.2) !important;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
border-color: rgba(255, 255, 255, 0.7) !important;
|
||||
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||
box-shadow: 0 2px 6px rgba(255, 255, 255, 0.3) !important;
|
||||
}
|
||||
|
||||
.btn-active, .btn.btn-active {
|
||||
border-color: #ffffff !important;
|
||||
background-color: rgba(255, 255, 255, 0.3) !important;
|
||||
color: #000000 !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
/* Join buttons specific styling */
|
||||
.join .btn {
|
||||
border: 2px solid rgba(255, 255, 255, 0.4) !important;
|
||||
background-color: rgba(255, 255, 255, 0.05) !important;
|
||||
}
|
||||
|
||||
.join .btn:hover {
|
||||
border-color: rgba(255, 255, 255, 0.7) !important;
|
||||
background-color: rgba(255, 255, 255, 0.15) !important;
|
||||
}
|
||||
|
||||
/* High contrast slider styles */
|
||||
.range {
|
||||
background-color: #444444;
|
||||
}
|
||||
|
||||
.range::-webkit-slider-thumb {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.range::-moz-range-thumb {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* Make progress bars and indicators more visible */
|
||||
.range-primary {
|
||||
--range-shdw: #ffffff;
|
||||
}
|
||||
|
||||
/* Target voltage and similar sliders */
|
||||
input[type="range"] {
|
||||
background-color: #444444;
|
||||
}
|
||||
|
||||
.progress::-webkit-progress-bar,
|
||||
.progress::-moz-progress-bar {
|
||||
background-color: #444444;
|
||||
}
|
||||
|
||||
.progress::-webkit-progress-value,
|
||||
.progress::-moz-progress-value {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* Ensure gradient charts use high contrast */
|
||||
.absolute.top-0.bottom-0.left-0.rounded-lg {
|
||||
background: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* High contrast slider styles for GogglesBW theme (keeping original) */
|
||||
[data-theme="gogglesBW"] .range {
|
||||
background-color: #444444;
|
||||
}
|
||||
|
||||
[data-theme="gogglesBW"] .range::-webkit-slider-thumb {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
[data-theme="gogglesBW"] .range::-moz-range-thumb {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* Make progress bars and indicators more visible */
|
||||
[data-theme="gogglesBW"] .range-primary {
|
||||
--range-shdw: #ffffff;
|
||||
}
|
||||
|
||||
/* Target voltage and similar sliders in control/dcdc pages */
|
||||
[data-theme="gogglesBW"] input[type="range"] {
|
||||
background-color: #444444;
|
||||
}
|
||||
|
||||
[data-theme="gogglesBW"] .progress::-webkit-progress-bar,
|
||||
[data-theme="gogglesBW"] .progress::-moz-progress-bar {
|
||||
background-color: #444444;
|
||||
}
|
||||
|
||||
[data-theme="gogglesBW"] .progress::-webkit-progress-value,
|
||||
[data-theme="gogglesBW"] .progress::-moz-progress-value {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* Ensure gradient charts use high contrast in GogglesBW theme */
|
||||
[data-theme="gogglesBW"] .absolute.top-0.bottom-0.left-0.rounded-lg {
|
||||
background: white !important;
|
||||
}
|
||||
89
tailwind.config.mjs
Normal file
89
tailwind.config.mjs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
'varela': ['Varela Round', 'sans-serif'],
|
||||
'audiowide': ['Audiowide', 'cursive'],
|
||||
},
|
||||
fontSize: {
|
||||
'xs': ['0.875rem', { lineHeight: '1.25rem' }], // 14px
|
||||
'sm': ['1rem', { lineHeight: '1.5rem' }], // 16px
|
||||
'base': ['1.125rem', { lineHeight: '1.75rem' }], // 18px
|
||||
'lg': ['1.25rem', { lineHeight: '1.75rem' }], // 20px
|
||||
'xl': ['1.5rem', { lineHeight: '2rem' }], // 24px
|
||||
'2xl': ['2rem', { lineHeight: '2.25rem' }], // 32px
|
||||
'3xl': ['2.5rem', { lineHeight: '2.75rem' }], // 40px
|
||||
'4xl': ['3rem', { lineHeight: '3.5rem' }], // 48px
|
||||
'5xl': ['3.75rem', { lineHeight: '1' }], // 60px
|
||||
'6xl': ['4.5rem', { lineHeight: '1' }], // 72px
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require('daisyui')],
|
||||
daisyui: {
|
||||
themes: ["light", "dark",
|
||||
{
|
||||
// Original goggle theme renamed to Goggles1 (colors visible through laser goggles)
|
||||
// Based on orange/amber laser glasses (ARG)
|
||||
"goggles1": {
|
||||
...require("daisyui/src/theming/themes")["lofi"],
|
||||
"primary": "#f59e0b", // amber (visible through most laser glasses)
|
||||
"primary-content": "#000000",
|
||||
"secondary": "#84cc16", // lime green (visible through many laser glasses)
|
||||
"secondary-content": "#000000",
|
||||
"accent": "#f8fafc", // very light blue/white
|
||||
"neutral": "#1e293b", // slate-800
|
||||
"base-100": "#0f172a", // slate-900
|
||||
"base-200": "#1e293b", // slate-800
|
||||
"base-300": "#334155", // slate-700
|
||||
"base-content": "#f59e0b", // amber text
|
||||
"info": "#84cc16", // lime
|
||||
"success": "#84cc16", // lime
|
||||
"warning": "#f59e0b", // amber
|
||||
"error": "#f8fafc", // very light blue/white instead of red
|
||||
},
|
||||
// Improved black and white goggle theme with better contrast
|
||||
"gogglesBW": {
|
||||
...require("daisyui/src/theming/themes")["light"],
|
||||
"primary": "#000000",
|
||||
"primary-content": "#ffffff",
|
||||
"secondary": "#555555",
|
||||
"secondary-content": "#ffffff",
|
||||
"accent": "#777777",
|
||||
"accent-content": "#ffffff",
|
||||
"neutral": "#222222",
|
||||
"neutral-content": "#ffffff",
|
||||
"base-100": "#000000",
|
||||
"base-200": "#222222",
|
||||
"base-300": "#444444",
|
||||
"base-content": "#ffffff",
|
||||
"info": "#aaaaaa",
|
||||
"info-content": "#000000",
|
||||
"success": "#eeeeee",
|
||||
"success-content": "#000000",
|
||||
"warning": "#cccccc",
|
||||
"warning-content": "#000000",
|
||||
"error": "#ffffff",
|
||||
"error-content": "#000000",
|
||||
}
|
||||
},
|
||||
"emerald",
|
||||
{
|
||||
"dark-neon": {
|
||||
...require("daisyui/src/theming/themes")["synthwave"],
|
||||
"primary": "#e779c1",
|
||||
"secondary": "#58c7f3",
|
||||
"accent": "#f3cc30",
|
||||
"neutral": "#20134e",
|
||||
"neutral-content": "#f9f7fd",
|
||||
"base-100": "#2d1b69",
|
||||
"base-200": "#392c79",
|
||||
"base-300": "#453a88",
|
||||
"base-content": "#f9f7fd",
|
||||
}
|
||||
},
|
||||
"retro", "halloween", "winter"],
|
||||
},
|
||||
}
|
||||
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [
|
||||
".astro/types.d.ts",
|
||||
"**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"dist"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue