Compare commits
No commits in common. "efde009faadb51912d017a1c3930f0865a28a67a" and "74e6e49f4b55523aea355de810527b247b1b7749" have entirely different histories.
efde009faa
...
74e6e49f4b
|
|
@ -5,8 +5,8 @@
|
||||||
| **Author** | John Haverlack |
|
| **Author** | John Haverlack |
|
||||||
| **Copyright** | 2026 John Haverlack |
|
| **Copyright** | 2026 John Haverlack |
|
||||||
| **License** | MIT |
|
| **License** | MIT |
|
||||||
| **Version** | 0.1.2 |
|
| **Version** | 0.1.0 |
|
||||||
| **Date** | 2026-07-17 |
|
| **Date** | 2026-06-08 |
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,5 @@
|
||||||
{
|
{
|
||||||
"releases": [
|
"releases": [
|
||||||
{
|
|
||||||
"version": "0.1.2",
|
|
||||||
"date": "2026-07-17",
|
|
||||||
"maturity": "PROD",
|
|
||||||
"summary": "",
|
|
||||||
"notes": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "0.1.1",
|
|
||||||
"date": "2026-07-04",
|
|
||||||
"maturity": "PROD",
|
|
||||||
"summary": "",
|
|
||||||
"notes": ""
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"date": "2026-06-28",
|
"date": "2026-06-28",
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,126 @@
|
||||||
# Node Web App (NWA)
|
# Software Defined Laboratory (SDL)
|
||||||
|
|
||||||
| Attribute | Value |
|
| Attribute | Value |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| **Author** | John Haverlack |
|
| **Author** | John Haverlack |
|
||||||
| **Copyright** | 2026 John Haverlack |
|
| **Copyright** | 2026 John Haverlack |
|
||||||
| **License** | MIT |
|
| **License** | MIT |
|
||||||
| **Version** | 0.1.2 |
|
| **Version** | 0.3.10 |
|
||||||
| **Date** | 2026-07-17 |
|
| **Date** | 2026-02-08 |
|
||||||
|
|
||||||
## v0.1.2 - 2026-07-17 (PROD)
|
## v0.3.10 - 2026-02-08 (ALPHA)
|
||||||
|
|
||||||
- v0.1.2
|
- Dash Staging Jobs
|
||||||
- App Updates from HaverLab
|
- Dash Full sdl_id
|
||||||
- Fixing Installer
|
- Updating main Web UI Page
|
||||||
- Updating Metadata
|
- Start v0.3.10
|
||||||
- WIP: Fixing Installer
|
|
||||||
- Fixing Deploy Dir
|
|
||||||
- Start v0.1.2
|
|
||||||
|
|
||||||
## v0.1.1 - 2026-07-04 (PROD)
|
## v0.3.9 - 2026-02-08 (ALPHA)
|
||||||
|
|
||||||
- Versioning Tool Updates
|
- v0.3.9
|
||||||
- Fixing Version Branch Tools
|
- Refactor Cluster Status Dash
|
||||||
- Start v0.1.1
|
- Refactor Cluster Status Dash
|
||||||
- Adding Workflow Docs
|
- System Columnn Display
|
||||||
- WIP: Fixing installer
|
- Start v0.3.9
|
||||||
- Cleaning up Template
|
|
||||||
- Initial Commit Migrating NWA from SDL
|
|
||||||
|
|
||||||
## v0.1.0 - 2026-06-28 (INIT)
|
## v0.3.8 - 2026-02-01 (ALPHA)
|
||||||
|
|
||||||
- v0.1.2
|
- v0.3.8
|
||||||
- App Updates from HaverLab
|
- Refactoing Worker Dashboard
|
||||||
- Fixing Installer
|
- Refactoing Worker Dashboard
|
||||||
- Updating Metadata
|
- Refactored Cluster Status
|
||||||
- WIP: Fixing Installer
|
- Refactor of telemetry format + hw detection
|
||||||
- Fixing Deploy Dir
|
- Switched Panel Icon to Bootstrap Icon
|
||||||
- Start v0.1.2
|
- Updating Worker Telemetry
|
||||||
|
- CP
|
||||||
|
- Fixing Wget/Curl Command Installer Port
|
||||||
|
- Fixing Wget/Curl Command Installer Port
|
||||||
|
- Fixing Wkr Start / Stop
|
||||||
|
- Update stop-sdl-wkr.sh
|
||||||
|
- WIP Install / Start / Stop
|
||||||
|
- WIP Install / Start /Stop
|
||||||
|
- Systemd Detectiion in Start/Stop/Status
|
||||||
|
- WIP: Start / Stop Scripts
|
||||||
|
- Non Systemd start stop scripts
|
||||||
|
- WIP: start stop scripts
|
||||||
|
- Updating Task List
|
||||||
|
- Adding Screeshot to README
|
||||||
|
- Start v0.3.8
|
||||||
|
|
||||||
|
## v0.3.7 - 2026-02-01 (ALPHA)
|
||||||
|
|
||||||
|
**Summary**
|
||||||
|
sdl-wkr Telemetry
|
||||||
|
|
||||||
|
- v0.3.7
|
||||||
|
- Fixing sdl-wkr RAM detection
|
||||||
|
- Start v0.3.7
|
||||||
|
|
||||||
|
## v0.3.6 - 2026-02-01 (ALPHA)
|
||||||
|
|
||||||
|
**Summary**
|
||||||
|
Light/Dark Mode, Wkr Install, Cluster Stats
|
||||||
|
|
||||||
|
- v0.3.6
|
||||||
|
- Adding Cluster Telemetry
|
||||||
|
- WIP: Worker Display
|
||||||
|
- Tweaks
|
||||||
|
- Start v0.3.6
|
||||||
|
- CP Updates
|
||||||
|
- Dash: Total Cluster Stats
|
||||||
|
- Publishing to cluster/workers
|
||||||
|
- Light/Dark Mode Toggle
|
||||||
|
- Added light/dark theme CSS
|
||||||
|
|
||||||
|
## v0.3.5 - 2026-02-01 (ALPHA)
|
||||||
|
|
||||||
|
- v0.3.5
|
||||||
|
- Fixing sdl-wkr install script
|
||||||
|
- Start v0.3.5
|
||||||
|
|
||||||
|
## v0.3.4 - 2026-01-31 (ALPHA)
|
||||||
|
|
||||||
|
- Working sdl-wkr self update
|
||||||
|
- Working sdl-wkr self update
|
||||||
|
- Start v0.3.4
|
||||||
|
|
||||||
|
## v0.3.3 - 2026-01-31 (ALPHA)
|
||||||
|
|
||||||
|
- WIP: sdl-wkr updates
|
||||||
|
- WIP: sdl-wkr updates
|
||||||
|
- Start v0.3.3
|
||||||
|
|
||||||
|
## v0.3.2 - 2026-01-31 (ALPHA)
|
||||||
|
|
||||||
|
- v0.3.2
|
||||||
|
- v0.3.2
|
||||||
|
- Squetching Logs
|
||||||
|
- WORKING: join/authz
|
||||||
|
- WIP: Join Worker
|
||||||
|
- WIP: Join Process
|
||||||
|
- sdl-wkr Cluster Join
|
||||||
|
- Fixing Service Module Render Bug
|
||||||
|
- Start v0.3.2
|
||||||
|
|
||||||
|
## v0.3.1 - 2026-01-31 (ALPHA)
|
||||||
|
|
||||||
|
**Summary**
|
||||||
|
README Cleanup
|
||||||
|
|
||||||
|
- v0.3.1
|
||||||
|
- Start v0.3.1
|
||||||
|
- Removing duplicate LICENSE file
|
||||||
|
- README Cleanup
|
||||||
|
|
||||||
|
## v0.3.0 - 2026-01-31 (ALPHA)
|
||||||
|
|
||||||
|
**Summary**
|
||||||
|
Initial Codebase
|
||||||
|
|
||||||
|
**Notes**
|
||||||
|
Initial GitHub Release
|
||||||
|
|
||||||
|
- Preparing GitHub Version
|
||||||
|
- Start v0.3.0
|
||||||
|
- Initial commit
|
||||||
|
|
||||||
|
|
|
||||||
309
docs/DESIGN.md
309
docs/DESIGN.md
|
|
@ -1,13 +1,314 @@
|
||||||
# Node Web (NWA)
|
# Software Defined Laboratory (SDL)
|
||||||
|
|
||||||
| Attribute | Value |
|
| Attribute | Value |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| **Author** | John Haverlack |
|
| **Author** | John Haverlack |
|
||||||
| **Copyright** | 2026 John Haverlack |
|
| **Copyright** | 2026 John Haverlack |
|
||||||
| **License** | MIT |
|
| **License** | MIT |
|
||||||
| **Version** | 0.1.2 |
|
| **Version** | 0.3.10 |
|
||||||
| **Date** | 2026-07-17 |
|
| **Date** | 2026-02-08 |
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
TBD
|
The Software Defined Laboratory (SDL) project provides a generalized distributed computing platform for managing parallel computational workflows across a cluster of heterogeneous distributed nodes. SDL is a minimalistic High Performance Compute (HPC) platform focused on extreme minimizaton of technical debt associated configuration, deployability, and maintenance of the cluster.
|
||||||
|
|
||||||
|
# Roadmap
|
||||||
|
|
||||||
|
## Prioritized Tasks
|
||||||
|
|
||||||
|
MVP - Minimum Viable Product Task List
|
||||||
|
|
||||||
|
**Core Infrastructure** ✅ (Complete)
|
||||||
|
- [x] sdl-mgr MQTT Broker
|
||||||
|
- [x] sdl-mgr UDP Beacon
|
||||||
|
- [x] sdl-mgr Web UI MQTT WebSocket Pub/Sub
|
||||||
|
- [x] sdl-wkr UDP Listener
|
||||||
|
- [x] SDL_ID Node ID
|
||||||
|
- [x] Version Updater Script
|
||||||
|
- [x] Deploy sdl-mgr to $SDL_HOME/sdl-mgr
|
||||||
|
- [x] Create SDL Worker Dist Build Process
|
||||||
|
- [x] Create SDL Worker Install to $SDL_HOME
|
||||||
|
- [x] SDL Install Script
|
||||||
|
- [x] Create SDL Worker Auto Update Process
|
||||||
|
- [x] Create Worker Join Process
|
||||||
|
- [x] Create Worker Telemetry Process
|
||||||
|
|
||||||
|
**Worker Monitoring & Telemetry** 🔄 (In Progress)
|
||||||
|
- [x] Worker Hardware Inventory Detection
|
||||||
|
- [x] Physical vs Logical CPU cores (hyperthreading)
|
||||||
|
- [x] Physical vs VM detection (hypervisor flag)
|
||||||
|
- [x] GPU VRAM capacity aggregation
|
||||||
|
- [x] Memory capacity (bytes → GB conversion)
|
||||||
|
- [x] Worker Real-time Load Status (pure Node.js)
|
||||||
|
- [x] CPU usage percentage (`os.cpus()` times)
|
||||||
|
- [x] RAM usage (total/free/used via `os.totalmem()/freemem()`)
|
||||||
|
- [x] GPU usage (exec-based: nvidia-smi, rocm-smi)
|
||||||
|
- [ ] Disk IO (optional: exec-based)
|
||||||
|
- [x] Worker Uptime Tracking
|
||||||
|
- [x] SDL process uptime (`process.uptime()`)
|
||||||
|
- [x] OS uptime (`os.uptime()`)
|
||||||
|
- [ ] Worker Display Improvements
|
||||||
|
- [x] Show OS distro + version in Platform column (e.g., "Debian 12 / x64")
|
||||||
|
- [x] Show system type (Physical 🖥️ / VM 💠)
|
||||||
|
- [ ] Show GPU VRAM in GPU column (e.g., "2 GPUs (48 GB)")
|
||||||
|
- [x] Show worker uptime in dashboard
|
||||||
|
- [x] Display last seen as elapsed time in dashboard, not date
|
||||||
|
- [ ] Track Username for sdl process
|
||||||
|
- [ ] Web UI
|
||||||
|
- [ ] Progresive Web App
|
||||||
|
- [ ] Editable Dashboard
|
||||||
|
- [x] Panel Icon
|
||||||
|
- [ ] Capacity Detection
|
||||||
|
- [ ] GPU VRAM capacity
|
||||||
|
- [ ] CPU capacity
|
||||||
|
- [ ] RAM capacity
|
||||||
|
- [ ] Network Negotiontion speed
|
||||||
|
- [ ] S3 (MinIO) storage capacity
|
||||||
|
- [ ] SW: Python, Rust, C++, etc
|
||||||
|
- [ ] PyTorch, Tensorflow, etc
|
||||||
|
- [ ] Ollamma, Models, etc
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Resource Management**
|
||||||
|
- [ ] CPU Reserve Capacity Allocation
|
||||||
|
- [ ] Config option: `reserve_cpus: N` (keep N cores for system)
|
||||||
|
- [ ] Track: `total`, `reserved`, `allocated`, `available`, `used`
|
||||||
|
- [ ] Display reserved capacity in dashboard
|
||||||
|
- [ ] Memory Reserve Capacity (optional, future)
|
||||||
|
- [ ] GPU Reserve Capacity (optional, future)
|
||||||
|
|
||||||
|
**Service Management**
|
||||||
|
- [x] Start/Stop shell scripts (for systems without systemd user services)
|
||||||
|
- [x] `start-sdl-mgr.sh` / `stop-sdl-mgr.sh`
|
||||||
|
- [x] `start-sdl-wkr.sh` / `stop-sdl-wkr.sh`
|
||||||
|
- [ ] Combined systemd service
|
||||||
|
- [ ] `sdl.service` to manage both sdl-mgr and sdl-wkr together
|
||||||
|
- [ ] Support for system-level (sudo) and user-level services
|
||||||
|
- [ ] SDL-MGR auto-installs SDL-WKR
|
||||||
|
- [ ] Manager node becomes worker by default
|
||||||
|
- [ ] Config flag to disable: `install_worker_on_manager: false
|
||||||
|
- [x] Pass HTTP Port to SDL-WKR Curl installer
|
||||||
|
- [ ] Update Install Summary
|
||||||
|
- [ ] Add Quite Mode to sdl-wkr install
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Data & Storage**
|
||||||
|
- [ ] Data Storage Organizational Structure
|
||||||
|
- [ ] Define directory hierarchy for tasks, results, logs
|
||||||
|
- [ ] Implement file naming conventions
|
||||||
|
- [ ] Add cleanup/retention policies
|
||||||
|
- [ ] MinIO S3 Storage Server Integration
|
||||||
|
- [ ] Install and configure MinIO
|
||||||
|
- [ ] S3 bucket structure for SDL data
|
||||||
|
- [ ] Worker access to S3 storage
|
||||||
|
- [ ] Web UI S3 browser integration
|
||||||
|
|
||||||
|
**Projects**
|
||||||
|
- [ ] Project Management Org Structure
|
||||||
|
- [ ] Define project metadata schema
|
||||||
|
- [ ] Project lifecycle states (draft, active, archived)
|
||||||
|
- [ ] Permission/access control model
|
||||||
|
- [ ] Experiment Organization under Org Structure
|
||||||
|
- [ ] Experiment templates
|
||||||
|
- [ ] Parameter tracking
|
||||||
|
- [ ] Result linking to experiments
|
||||||
|
- [ ] HPC Experiment Binary Development
|
||||||
|
- [ ] sdl-wkr Capablity detection (phyton, etc)
|
||||||
|
- [ ] Binary packaging format
|
||||||
|
- [ ] Dependency management
|
||||||
|
- [ ] Version control for binaries
|
||||||
|
|
||||||
|
**Dashboard Enhancements**
|
||||||
|
- [x] Dark/Light theme polish (CSS refinements)
|
||||||
|
- [ ] Real-time resource graphs (Chart.js)
|
||||||
|
- [ ] Worker status history timeline
|
||||||
|
- [x] Cluster health indicators
|
||||||
|
- [x] Alert/notification system for worker failures
|
||||||
|
- [ ] Add a Clock to the Dashboard (ISO 8601)
|
||||||
|
- [ ] WOrker Cards instead of table
|
||||||
|
- [ ] Search bar for workers, experiments, projects
|
||||||
|
|
||||||
|
|
||||||
|
**Testing & Documentation**
|
||||||
|
- [ ] Cross-platform testing (Debian, Ubuntu, Arch, Fedora, Win, OSx, BSD)
|
||||||
|
- [ ] Installation documentation
|
||||||
|
- [ ] Configuration examples
|
||||||
|
- [ ] Troubleshooting guide
|
||||||
|
- [ ] Validate IP change Stablity
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Design
|
||||||
|
|
||||||
|
## Design Goals
|
||||||
|
- Cross Platform (lnx, mac, win, bsd, rpi) Linux First, support for others later.
|
||||||
|
- Node.JS based Manager and Worker code bases
|
||||||
|
- Extreme Minimal OS Dependancies. sdl-mgr self hosts all the necessary code to deploy and coordinate a cluster. Including distrubution of Node binaries.
|
||||||
|
- Automate Update Workflows
|
||||||
|
- Zero Technical Debt (ideal)
|
||||||
|
- Sane handling of sdl-workers comming and going, being added or removed.
|
||||||
|
- Zero sdl-wkr Configuration (ideal) - Install > Run > Listen to UDP Bcast > Auto Join MQTT for cluster command and control.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Security considerations TLS, Auth, etc. are future scopes of work. At this stage we are focused on getting the manager and workers up and running as a Minimum Viable Product (MVP) for a cluster of SDL workers. But we are also thinking forward about security and cross platform support so those features will be easy to implement later without a major code refactor.
|
||||||
|
- The current assumption is that SDL runs in a segmented subnet with restricted access. And that we do not have malicious actors trying to compromise the network or poison the open MQTT broker.
|
||||||
|
|
||||||
|
|
||||||
|
# Architecture
|
||||||
|
|
||||||
|
Software Defined Laboratry (SDL)
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
### SDL Source Code Structure
|
||||||
|
```
|
||||||
|
../sdl
|
||||||
|
├── docs
|
||||||
|
├── sdl-mgr
|
||||||
|
│ ├── app
|
||||||
|
│ │ └── modules
|
||||||
|
│ │ ├── mqtt
|
||||||
|
│ │ ├── nwa-lib
|
||||||
|
│ │ ├── sdl-mgr
|
||||||
|
│ │ ├── template
|
||||||
|
│ │ └── web
|
||||||
|
│ ├── conf
|
||||||
|
│ │ └── modules
|
||||||
|
│ ├── html
|
||||||
|
│ │ ├── conf
|
||||||
|
│ │ ├── css
|
||||||
|
│ │ ├── img
|
||||||
|
│ │ ├── js
|
||||||
|
│ │ └── md
|
||||||
|
│ └── scripts
|
||||||
|
├── sdl-wkr
|
||||||
|
│ ├── app
|
||||||
|
│ │ └── modules
|
||||||
|
│ │ ├── nwa-lib
|
||||||
|
│ │ ├── sdl-wkr
|
||||||
|
│ │ └── template
|
||||||
|
│ ├── conf
|
||||||
|
│ │ └── modules
|
||||||
|
│ └── scripts
|
||||||
|
└── tools
|
||||||
|
```
|
||||||
|
|
||||||
|
### SDL Installation Structure
|
||||||
|
```
|
||||||
|
~/.sdl/
|
||||||
|
├── conf
|
||||||
|
├── data
|
||||||
|
│ ├── jobs
|
||||||
|
│ └── proj
|
||||||
|
├── dist
|
||||||
|
├── logs
|
||||||
|
├── nodejs
|
||||||
|
│ └── current -> /home/jehaverlack/.sdl/nodejs/node-v22.22.0-linux-x64
|
||||||
|
├── sdl-mgr
|
||||||
|
│ ├── current -> /home/jehaverlack/.sdl/sdl-mgr/sdl-mgr_0.2.7
|
||||||
|
│ └── sdl-mgr_0.2.7
|
||||||
|
│ ├── app
|
||||||
|
│ │ └── modules
|
||||||
|
│ │ ├── mqtt
|
||||||
|
│ │ ├── nwa-lib
|
||||||
|
│ │ ├── sdl-mgr
|
||||||
|
│ │ ├── template
|
||||||
|
│ │ └── web
|
||||||
|
│ ├── conf
|
||||||
|
│ │ └── modules
|
||||||
|
│ ├── html
|
||||||
|
│ │ ├── conf
|
||||||
|
│ │ ├── css
|
||||||
|
│ │ ├── img
|
||||||
|
│ │ ├── js
|
||||||
|
│ │ └── md
|
||||||
|
│ └── scripts
|
||||||
|
└── sdl-wkr
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### SDL Manager Install Script
|
||||||
|
|
||||||
|
```
|
||||||
|
$ git clone https://github.com/jehaverlack/sdl.git
|
||||||
|
$ cd sdl
|
||||||
|
$ ./install-sdl.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This will install both sdl-mgr and sdl-wkr on the Manager host.
|
||||||
|
|
||||||
|
### SDL Worker Install Script
|
||||||
|
|
||||||
|
Once the sdl-mgr is running, it broadcasts the install command over UDP. On your worker host, run the following command to listen for the install command on your worker.
|
||||||
|
|
||||||
|
```
|
||||||
|
nc -u -l -k 10101 | jq -r '.msg.sdl_wkr_install_cmd[]'
|
||||||
|
```
|
||||||
|
|
||||||
|
> **NOTE**: If you have a firewall, you must allow SDL Worker to listen on UDP port 10101.
|
||||||
|
|
||||||
|
The install command will look like the following, both curl and wget commands are supported.
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -s http://10.0.0.144:8081/dist/install-sdl-wkr.sh | bash -s 10.0.0.144:8081
|
||||||
|
```
|
||||||
|
|
||||||
|
### SDL Worker Auto Update Process
|
||||||
|
|
||||||
|
Once a worker is joined to a manager's MQTT broker, it will automatically update to the latest version of SDL deployed by the manager.
|
||||||
|
|
||||||
|
### Worker Join
|
||||||
|
|
||||||
|
- UDP Bcast
|
||||||
|
- MQTT Join
|
||||||
|
|
||||||
|
### Worker Telemetry
|
||||||
|
|
||||||
|
Workers publish telemetry to the manager MQTT broker. The manager will collect this telemetry and publish to the web UI over MQTT.
|
||||||
|
|
||||||
|
### SDL Storage
|
||||||
|
|
||||||
|
- MinIO S3 Storage Server
|
||||||
|
- this is a non node.js codebase
|
||||||
|
|
||||||
|
```
|
||||||
|
Project
|
||||||
|
└── Experiment
|
||||||
|
├── L0 Raw Results
|
||||||
|
└── L1 Analysis Results
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
sdl/
|
||||||
|
└── projects/
|
||||||
|
└── <project_id>/
|
||||||
|
└── experiments/
|
||||||
|
└── <experiment_id>/
|
||||||
|
└── runs/
|
||||||
|
└── <run_id>/
|
||||||
|
├── l0/
|
||||||
|
└── l1/
|
||||||
|
```
|
||||||
|
|
||||||
|
# Projects
|
||||||
|
- Need to define structure to manage projects.
|
||||||
|
- Projects will be managed with JSON files
|
||||||
|
- We web UI management Edit interface is nice but will come later. Just need to get things working first.
|
||||||
|
|
||||||
|
## Experiments
|
||||||
|
- Within each Project Scope experiments can be conducted (run) as a specific configuration or permuation of variable to test.
|
||||||
|
- Each experiment represents a set of parrallized jobs for the cluster to execute on.
|
||||||
|
- L0 Raw Results will be collected and stored in a run S3 Bucket.
|
||||||
|
- L1 Analysis Results will be collected and stored in a run S3 Bucket
|
||||||
|
- L1 Analysis represents an other set of parrallized jobs for the cluster to execute on.
|
||||||
|
|
||||||
|
### Run Engine (RE)
|
||||||
|
- Run Engines (RE): May be a Python, Rust, C++, etc. executable/script that is forked by the sdl-wkr Node.JS process.
|
||||||
|
- RE get input configuration from the local sdl-wkr process.
|
||||||
|
- RE communicate only with the sdl-wkr process.
|
||||||
|
- But RE processes may read and write direclty to S3
|
||||||
|
- the sdl-wkr process communicates with the SDL Manager (sdl-mgr) process to notify on status of jobs.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
119
docs/INSTALL.md
119
docs/INSTALL.md
|
|
@ -1,2 +1,119 @@
|
||||||
# Node Web App (NWA)
|
|
||||||
|
|
||||||
|
# Software Defined Laboratory (SDL)
|
||||||
|
|
||||||
|
> NOTE: This is a work in progress. SDL has only been tested on Debian based
|
||||||
|
> Linux systems. But has been designed to be cross platform. So running on
|
||||||
|
> other platforms should not be a heavy lift.
|
||||||
|
|
||||||
|
Other than basic OS tools, SDL downloads all dependencies idempotently and installs
|
||||||
|
everything into a single SDL home directory (~/.sdl).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SDL Manager (sdl-mgr)
|
||||||
|
|
||||||
|
#### Installation
|
||||||
|
|
||||||
|
```
|
||||||
|
git clone <SDL_REPO_URL>
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
cd sdl
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
./install-sdl.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Upgrading
|
||||||
|
|
||||||
|
```
|
||||||
|
cd sdl
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
git pull origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
./install-sdl.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SDL Worker (sdl-wkr)
|
||||||
|
|
||||||
|
#### Prerequisites
|
||||||
|
|
||||||
|
(Debian, Ubuntu, Linux Mint, Zorin OS, Raspbian)
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt -y install gawk coreutils curl jq grep sed tar unzip netcat-openbsd
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Installation
|
||||||
|
|
||||||
|
(Linux, OSX, Windows(WSL2))
|
||||||
|
|
||||||
|
Listen to UDP Broadcast on port 10101
|
||||||
|
- To get the **sld-wkr** curl or wget install command
|
||||||
|
|
||||||
|
```
|
||||||
|
nc -u -l -k 10101
|
||||||
|
```
|
||||||
|
|
||||||
|
OR
|
||||||
|
|
||||||
|
```
|
||||||
|
nc -u -l -k 10101 |jq
|
||||||
|
{
|
||||||
|
"ts": "2026-01-31T05:20:31.815Z",
|
||||||
|
"sdl_id": "906a027c-52f0-4cbb-b3e0-5eb84856d50a",
|
||||||
|
"role": "sdl-mgr",
|
||||||
|
"host": "haverlab-sld-mgr",
|
||||||
|
"type": "udp-beacon",
|
||||||
|
"msg": {
|
||||||
|
"info": {
|
||||||
|
"desc": "Software Defined Laboratory: Manager (sdl-mgr)",
|
||||||
|
"version": "0.2.10",
|
||||||
|
"copyright": "2026 John Haverlack"
|
||||||
|
},
|
||||||
|
"cluster": {
|
||||||
|
"id": "haverlab",
|
||||||
|
"name": "Haverlab",
|
||||||
|
"desc": "John Haverlack's Home Lab"
|
||||||
|
},
|
||||||
|
"mqtt": {
|
||||||
|
"host": "10.0.0.178",
|
||||||
|
"port": 1883
|
||||||
|
},
|
||||||
|
"web": {
|
||||||
|
"proto": "http",
|
||||||
|
"host": "10.0.0.178",
|
||||||
|
"port": 8081,
|
||||||
|
"api_config": "/api/config",
|
||||||
|
"config_url": "http://10.0.0.178:8081/api/config"
|
||||||
|
},
|
||||||
|
"sdl_wkr_install_cmd": {
|
||||||
|
"curl": "curl -s http://10.0.0.178:8081/dist/install-sdl-wkr.sh | bash -s 10.0.0.178",
|
||||||
|
"wget": "wget -O - http://10.0.0.178:8081/dist/install-sdl-wkr.sh | bash -s 10.0.0.178"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -s http://<SDL_MGR_IP>:<WEB_PORT>/dist/install-sdl-wkr.sh | bash -s <SDL_MGR_IP>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Upgrading
|
||||||
|
|
||||||
|
SDL Workers continuously monitor the SDL Manager’s advertised version via MQTT cluster status messages and will automatically re-install and restart when a newer version is detected.
|
||||||
|
|
||||||
|
Alternatively you can upgrade manually by rerunning the install-sdl-wkr.sh script.
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -s http://<SDL_MGR_IP>:<WEB_PORT>/dist/install-sdl-wkr.sh | bash -s <SDL_MGR_IP>
|
||||||
|
```
|
||||||
|
|
|
||||||
227
install.sh
227
install.sh
|
|
@ -8,45 +8,39 @@ set -euo pipefail
|
||||||
|
|
||||||
resolve_dirs() {
|
resolve_dirs() {
|
||||||
local json="$1"
|
local json="$1"
|
||||||
local sitename="$2"
|
|
||||||
declare -A raw resolved
|
declare -A raw resolved
|
||||||
|
|
||||||
if [[ -z "$sitename" || "$sitename" == "null" ]]; then
|
|
||||||
echo "ERROR: METADATA.name is not defined in metadata.json"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Basic safety: deployment directory name should be simple.
|
|
||||||
if [[ ! "$sitename" =~ ^[A-Za-z0-9._-]+$ ]]; then
|
|
||||||
echo "ERROR: Invalid METADATA.name for deploy directory: $sitename"
|
|
||||||
echo " Allowed characters: A-Z a-z 0-9 . _ -"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
while IFS='=' read -r key val; do
|
while IFS='=' read -r key val; do
|
||||||
raw["$key"]="$val"
|
raw["$key"]="$val"
|
||||||
done < <(jq -r '.DIRS | to_entries[] | "\(.key)=\(.value)"' "$json")
|
done < <(jq -r '.DIRS | to_entries[] | "\(.key)=\(.value)"' "$json")
|
||||||
|
|
||||||
# SITE is derived from metadata.json, not from deploy config.
|
if [[ -z "${raw[SITE]:-}" ]]; then
|
||||||
resolved["SITE"]="$HOME/.$sitename"
|
echo "ERROR: DIRS.SITE is not defined in nwa-deploy-config.json"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
resolved["SITE"]="${raw[SITE]/#\~/$HOME}"
|
||||||
|
|
||||||
local changed=true
|
local changed=true
|
||||||
while $changed; do
|
while $changed; do
|
||||||
changed=false
|
changed=false
|
||||||
for key in "${!raw[@]}"; do
|
for key in "${!raw[@]}"; do
|
||||||
[[ "$key" == "SITE" ]] && continue
|
|
||||||
[[ -n "${resolved[$key]:-}" ]] && continue
|
[[ -n "${resolved[$key]:-}" ]] && continue
|
||||||
|
|
||||||
local val="${raw[$key]}"
|
local val="${raw[$key]}"
|
||||||
|
|
||||||
for rkey in "${!resolved[@]}"; do
|
for rkey in "${!resolved[@]}"; do
|
||||||
val="${val/$rkey/${resolved[$rkey]:-}}"
|
val="${val/$rkey/${resolved[$rkey]:-}}"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# if [[ "$val" != ** ]]; then
|
||||||
|
# resolved["$key"]="$val"
|
||||||
|
# changed=true
|
||||||
|
# fi
|
||||||
|
|
||||||
local unresolved=false
|
local unresolved=false
|
||||||
|
|
||||||
for unresolved_key in "${!raw[@]}"; do
|
for unresolved_key in "${!raw[@]}"; do
|
||||||
if [[ "$unresolved_key" != "SITE" && -z "${resolved[$unresolved_key]:-}" && "$val" == *"$unresolved_key"* ]]; then
|
if [[ -z "${resolved[$unresolved_key]:-}" && "$val" == *"$unresolved_key"* ]]; then
|
||||||
unresolved=true
|
unresolved=true
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|
@ -60,8 +54,6 @@ resolve_dirs() {
|
||||||
done
|
done
|
||||||
|
|
||||||
for key in "${!raw[@]}"; do
|
for key in "${!raw[@]}"; do
|
||||||
[[ "$key" == "SITE" ]] && continue
|
|
||||||
|
|
||||||
[[ -z "${resolved[$key]:-}" ]] && {
|
[[ -z "${resolved[$key]:-}" ]] && {
|
||||||
echo "ERROR: Unresolved DIR: $key=${raw[$key]}"
|
echo "ERROR: Unresolved DIR: $key=${raw[$key]}"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -75,8 +67,6 @@ resolve_dirs() {
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
use_systemd() {
|
use_systemd() {
|
||||||
# Check if systemd user mode is available and service exists
|
# Check if systemd user mode is available and service exists
|
||||||
if systemctl --user status >/dev/null 2>&1; then
|
if systemctl --user status >/dev/null 2>&1; then
|
||||||
|
|
@ -280,18 +270,15 @@ fi
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# Setting variables
|
# Setting variables
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
SITENAME="$(jq -r '.METADATA.name' "$META_FILE")"
|
|
||||||
PROJECT="$(jq -r '.METADATA.project' "$META_FILE")"
|
|
||||||
DESCRIPTION="$(jq -r '.METADATA.description // .METADATA.name' "$META_FILE")"
|
|
||||||
VERSION="$(jq -r '.METADATA.version' "$META_FILE")"
|
VERSION="$(jq -r '.METADATA.version' "$META_FILE")"
|
||||||
VERSION_DATE="$(jq -r '.METADATA.version_date' "$META_FILE")"
|
VERSION_DATE="$(jq -r '.METADATA.version_date' "$META_FILE")"
|
||||||
COPYRIGHT="$(jq -r '.METADATA.copyright' "$META_FILE")"
|
COPYRIGHT="$(jq -r '.METADATA.copyright' "$META_FILE")"
|
||||||
AUTHOR="$(jq -r '.METADATA.author' "$META_FILE")"
|
AUTHOR="$(jq -r '.METADATA.author' "$META_FILE")"
|
||||||
LICENSE="$(jq -r '.METADATA.license' "$META_FILE")"
|
LICENSE="$(jq -r '.METADATA.license' "$META_FILE")"
|
||||||
HOMEPAGE="$(jq -r '.METADATA.homepage' "$META_FILE")"
|
SITEPAGE="$(jq -r '.METADATA.SITEpage' "$META_FILE")"
|
||||||
REPO="$(jq -r '.METADATA.git_repo' "$META_FILE")"
|
REPO="$(jq -r '.METADATA.git_repo' "$META_FILE")"
|
||||||
|
|
||||||
USERNAME="$(id -un)"
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# NodeJS detection
|
# NodeJS detection
|
||||||
|
|
@ -304,13 +291,12 @@ NODE_PLATFORMS="${NODE_PLATFORMS%% }"
|
||||||
echo "####################################"
|
echo "####################################"
|
||||||
echo "Node Web App (NWA)"
|
echo "Node Web App (NWA)"
|
||||||
echo "####################################"
|
echo "####################################"
|
||||||
echo " Sitename: $SITENAME"
|
|
||||||
echo " Version: $VERSION"
|
echo " Version: $VERSION"
|
||||||
echo " Version Date: $VERSION_DATE"
|
echo " Version Date: $VERSION_DATE"
|
||||||
echo " Copyright: (C) $COPYRIGHT"
|
echo " Copyright: (C) $COPYRIGHT"
|
||||||
echo " Author: $AUTHOR"
|
echo " Author: $AUTHOR"
|
||||||
echo " License: $LICENSE"
|
echo " License: $LICENSE"
|
||||||
echo " Homepage: $HOMEPAGE"
|
echo " SITEpage: $SITEPAGE"
|
||||||
echo " Repository: $REPO"
|
echo " Repository: $REPO"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
|
@ -339,8 +325,7 @@ fi
|
||||||
|
|
||||||
echo "Directories:"
|
echo "Directories:"
|
||||||
echo " ROOT: $ROOT"
|
echo " ROOT: $ROOT"
|
||||||
# resolve_dirs "$CONFIG_FILE"
|
resolve_dirs "$CONFIG_FILE"
|
||||||
resolve_dirs "$CONFIG_FILE" "$SITENAME"
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# Copy nwa to Prod Dir
|
# Copy nwa to Prod Dir
|
||||||
|
|
@ -357,31 +342,65 @@ NWA="$VERSIONS/current/nwa"
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
install_nodejs
|
install_nodejs
|
||||||
|
|
||||||
|
# echo ""
|
||||||
|
# echo "Fetching NodeJS Binaries to $DIST"
|
||||||
|
# SHASUM_URL="https://nodejs.org/dist/v${NODE_VERSION}/SHASUMS256.txt"
|
||||||
|
# curl -sSL "$SHASUM_URL" -o "$DIST/NODE_SHASUMS256.txt"
|
||||||
|
|
||||||
|
# # Download NodeJS tarball
|
||||||
|
|
||||||
|
# for np in $NODE_PLATFORMS; do
|
||||||
|
# IFS=":" read -r OS ARCH <<< "$np"
|
||||||
|
|
||||||
|
# case "$OS" in
|
||||||
|
# linux) EXT="tar.xz" ;;
|
||||||
|
# darwin) EXT="tar.gz" ;;
|
||||||
|
# win) EXT="zip" ;;
|
||||||
|
# *) echo "ERROR: Unknown NodeJS target OS: $OS"; exit 1 ;;
|
||||||
|
# esac
|
||||||
|
|
||||||
|
# PLATFORM="${OS}-${ARCH}"
|
||||||
|
# NODE_DIR="node-v${NODE_VERSION}-${PLATFORM}"
|
||||||
|
# NODE_TARBALL="${NODE_DIR}.$EXT"
|
||||||
|
# NODE_URL="https://nodejs.org/dist/v${NODE_VERSION}/${NODE_TARBALL}"
|
||||||
|
|
||||||
|
# echo " $np: $NODE_TARBALL"
|
||||||
|
|
||||||
|
# if [ ! -e "$DIST/$NODE_TARBALL" ]; then
|
||||||
|
|
||||||
|
# curl -sSL "$NODE_URL" -o "$DIST/$NODE_TARBALL"
|
||||||
|
|
||||||
|
# EXPECTED_SHA="$(grep " $NODE_TARBALL\$" "$DIST/NODE_SHASUMS256.txt" | awk '{print $1}')"
|
||||||
|
|
||||||
|
# if [[ -z "$EXPECTED_SHA" ]]; then
|
||||||
|
# echo "Error: SHA256 not found for $NODE_TARBALL"
|
||||||
|
# exit 1
|
||||||
|
# fi
|
||||||
|
|
||||||
|
# ACTUAL_SHA="$(sha256sum "$DIST/$NODE_TARBALL" | awk '{print $1}')"
|
||||||
|
|
||||||
|
# if [[ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]]; then
|
||||||
|
# echo "SHA mismatch $NODE_TARBALL"
|
||||||
|
# rm -f "$DIST/$NODE_TARBALL"
|
||||||
|
# exit 1
|
||||||
|
# fi
|
||||||
|
|
||||||
|
# echo "$EXPECTED_SHA $NODE_TARBALL" > "$DIST/$NODE_TARBALL.sha256"
|
||||||
|
# fi
|
||||||
|
# done
|
||||||
|
|
||||||
unset OS ARCH
|
unset OS ARCH
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# Copy NWA/current/conf/default-nwa-config.json to CONF/nwa-config.json
|
# Copy NWA/current/conf/efault-nwa-config.json to CONF/nwa-config.json
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
if [ ! -e "$CONF/nwa-config.json" ]; then
|
if [ ! -e "$CONF/nwa-config.json" ]; then
|
||||||
echo ""
|
echo ""
|
||||||
|
# echo "Copying: $NWA/current/conf/default-nwa-config.json to $CONF/nwa-config.json"
|
||||||
|
# cp "$NWA/current/conf/default-nwa-config.json" "$CONF/nwa-config.json"
|
||||||
echo "Copying: $NWA/conf/nwa-config.json to $CONF/nwa-config.json"
|
echo "Copying: $NWA/conf/nwa-config.json to $CONF/nwa-config.json"
|
||||||
rsync -av "$NWA/conf/nwa-config.json" "$CONF/nwa-config.json"
|
rsync -av "$NWA/conf/nwa-config.json" "$CONF/nwa-config.json"
|
||||||
|
|
||||||
echo "Updating site metadata in $CONF/nwa-config.json"
|
|
||||||
|
|
||||||
tmp_config="$(mktemp)"
|
|
||||||
jq \
|
|
||||||
--arg id "$SITENAME" \
|
|
||||||
--arg name "$PROJECT" \
|
|
||||||
--arg desc "$DESCRIPTION" \
|
|
||||||
'
|
|
||||||
.site.id = $id
|
|
||||||
| .site.name = $name
|
|
||||||
| .site.desc = $desc
|
|
||||||
' "$CONF/nwa-config.json" > "$tmp_config"
|
|
||||||
|
|
||||||
mv "$tmp_config" "$CONF/nwa-config.json"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|
@ -394,6 +413,44 @@ NODE_BIN="$NODEJS/current/bin/node"
|
||||||
NPM_BIN="$NODEJS/current/bin/npm"
|
NPM_BIN="$NODEJS/current/bin/npm"
|
||||||
# echo "NodeJS binary: $NODE_BIN"
|
# echo "NodeJS binary: $NODE_BIN"
|
||||||
|
|
||||||
|
# if [ -e "$NODE_BIN" ]; then
|
||||||
|
# NODE_BIN_VERSION="$("$NODE_BIN" --version)"
|
||||||
|
# # echo "NodeJS bin version: $NODE_BIN_VERSION"
|
||||||
|
# # echo "NodeJS version: v$NODE_VERSION"
|
||||||
|
# case "$OS_PLATFORM" in
|
||||||
|
# linux) EXT="tar.xz"; TAR_OPTS="-xJf" ;;
|
||||||
|
# darwin) EXT="tar.gz"; TAR_OPTS="-xzf" ;;
|
||||||
|
# win) EXT="zip" ;;
|
||||||
|
# *) echo "ERROR: Unsupported OS platform: $OS_PLATFORM"; exit 1 ;;
|
||||||
|
# esac
|
||||||
|
|
||||||
|
# if [ "$NODE_BIN_VERSION" != "v$NODE_VERSION" ]; then
|
||||||
|
# echo ""
|
||||||
|
# echo "Installing NodeJS: node-v${NODE_VERSION}-${OS_PLATFORM}-${OS_ARCH}"
|
||||||
|
|
||||||
|
# echo "Extracting: $DIST/node-v${NODE_VERSION}-${OS_PLATFORM}-${OS_ARCH}.$EXT"
|
||||||
|
# if [ $OS_PLATFORM == "win32" ]; then
|
||||||
|
# echo "ERROR: win32 host install not supported by this installer"
|
||||||
|
# exit 1
|
||||||
|
# # unzip "$DIST/node-v${NODE_VERSION}-${OS_PLATFORM}-${OS_ARCH}.$EXT" -d "$NODEJS"
|
||||||
|
# else
|
||||||
|
# tar $TAR_OPTS "$DIST/node-v${NODE_VERSION}-${OS_PLATFORM}-${OS_ARCH}.$EXT" -C "$NODEJS"
|
||||||
|
# fi
|
||||||
|
# # rm "$NODEJS/current"
|
||||||
|
# ln -sfn "$NODEJS/node-v${NODE_VERSION}-${OS_PLATFORM}-${OS_ARCH}" "$NODEJS/current"
|
||||||
|
# else
|
||||||
|
# echo ""
|
||||||
|
# echo "NodeJS $OS_PLATFORM-$OS_ARCH v$NODE_VERSION already installed"
|
||||||
|
# fi
|
||||||
|
# else
|
||||||
|
# echo ""
|
||||||
|
# echo "Installing NodeJS $NODE_VERSION"
|
||||||
|
|
||||||
|
# echo "Extracting: $DIST/node-v${NODE_VERSION}-${OS_PLATFORM}-${OS_ARCH}.tar.xz"
|
||||||
|
# tar -xJf "$DIST/node-v${NODE_VERSION}-${OS_PLATFORM}-${OS_ARCH}.tar.xz" -C "$NODEJS"
|
||||||
|
# ln -sfn "$NODEJS/node-v${NODE_VERSION}-${OS_PLATFORM}-${OS_ARCH}" "$NODEJS/current"
|
||||||
|
# fi
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# Install nwa Node Dependencies
|
# Install nwa Node Dependencies
|
||||||
|
|
@ -408,62 +465,46 @@ $NPM_BIN install
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# Copy Start / Stop Scripts
|
# Copy Start / Stop Scripts
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# mkdir -p "$SITE/scripts/"
|
mkdir -p "$SITE/scripts/"
|
||||||
# cp "$NWA/scripts/start-nwa.sh" "$SITE/scripts/"
|
cp "$NWA/scripts/start-nwa.sh" "$SITE/scripts/"
|
||||||
# cp "$NWA/scripts/stop-nwa.sh" "$SITE/scripts/"
|
cp "$NWA/scripts/stop-nwa.sh" "$SITE/scripts/"
|
||||||
# cp "$NWA/scripts/status-nwa.sh" "$SITE/scripts/"
|
cp "$NWA/scripts/status-nwa.sh" "$SITE/scripts/"
|
||||||
# chmod u+x "$SITE/scripts/"*
|
chmod u+x "$SITE/scripts/"*
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# Install Systemd Unit File
|
# Install Systemd Unit Files
|
||||||
# Copy SITENAME.service template to ~/.config/systemd/user/${SITENAME}.service
|
# Copy NWA/current/scripts/nwa.service to ~/.config/systemd/user
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
if use_systemd; then
|
if use_systemd; then
|
||||||
echo ""
|
echo ""
|
||||||
echo "Installing SystemD Unit File"
|
echo "Installing SystemD Unit Files"
|
||||||
|
|
||||||
USER_SYSTEMD_DIR="$HOME/.config/systemd/user"
|
mkdir -p ~/.config/systemd/user
|
||||||
SERVICE_NAME="${SITENAME}.service"
|
cp "$NWA/scripts/nwa.service" ~/.config/systemd/user/
|
||||||
SERVICE_TEMPLATE="$NWA/scripts/SITENAME.service"
|
|
||||||
SERVICE_FILE="$USER_SYSTEMD_DIR/$SERVICE_NAME"
|
|
||||||
|
|
||||||
if [[ ! -f "$SERVICE_TEMPLATE" ]]; then
|
"${SITE}/scripts/stop-nwa.sh"
|
||||||
echo "ERROR: systemd service template not found: $SERVICE_TEMPLATE"
|
sleep 1
|
||||||
exit 1
|
systemctl --user daemon-reload --no-pager
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$USER_SYSTEMD_DIR"
|
|
||||||
|
|
||||||
# Stop existing service if present
|
|
||||||
systemctl --user stop "$SERVICE_NAME" >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
# Optional: disable old hardcoded nwa.service if this install is now metadata-named
|
|
||||||
if [[ "$SERVICE_NAME" != "nwa.service" ]]; then
|
|
||||||
systemctl --user stop nwa.service >/dev/null 2>&1 || true
|
|
||||||
systemctl --user disable nwa.service >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Render template
|
|
||||||
sed \
|
|
||||||
-e "s#SITENAME#${SITENAME}#g" \
|
|
||||||
-e "s#PROJECT#${PROJECT}#g" \
|
|
||||||
-e "s#USERNAME#${USERNAME}#g" \
|
|
||||||
"$SERVICE_TEMPLATE" > "$SERVICE_FILE"
|
|
||||||
|
|
||||||
systemctl --user daemon-reload
|
|
||||||
systemd-analyze --user verify "$SERVICE_FILE"
|
|
||||||
|
|
||||||
systemctl --user enable "$SERVICE_NAME"
|
|
||||||
systemctl --user restart "$SERVICE_NAME"
|
|
||||||
|
|
||||||
echo "Installed systemd user service:"
|
|
||||||
echo " $SERVICE_NAME"
|
|
||||||
|
|
||||||
systemctl --user start "$SERVICE_NAME"
|
|
||||||
systemctl --user --no-pager status "$SERVICE_NAME"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Start NWA
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
"${SITE}/scripts/start-nwa.sh"
|
||||||
|
sleep 2
|
||||||
|
"${SITE}/scripts/status-nwa.sh"
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "ERROR: NWA failed to start"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "NWA deployed:"
|
echo "NWA deployed:"
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
{
|
{
|
||||||
"METADATA": {
|
"METADATA": {
|
||||||
"name": "nwa",
|
"name": "nwa",
|
||||||
"project": "Node Web App",
|
|
||||||
"description": "Node Web App",
|
"description": "Node Web App",
|
||||||
"abbr": "NWA",
|
"abbr": "NWA",
|
||||||
"icon": "fa-solid fa-dice-d20",
|
"version": "0.1.0",
|
||||||
"version": "0.1.2",
|
"version_date": "2026-06-28",
|
||||||
"version_date": "2026-07-17",
|
|
||||||
"maturity": "PROD",
|
"maturity": "PROD",
|
||||||
"author": "John Haverlack",
|
"author": "John Haverlack",
|
||||||
"copyright": "2026 John Haverlack",
|
"copyright": "2026 John Haverlack",
|
||||||
|
|
@ -17,7 +15,6 @@
|
||||||
},
|
},
|
||||||
"META_MAPS": {
|
"META_MAPS": {
|
||||||
"package.json": {
|
"package.json": {
|
||||||
"description": "description",
|
|
||||||
"version": "version",
|
"version": "version",
|
||||||
"version_date": "version_date",
|
"version_date": "version_date",
|
||||||
"author": "author",
|
"author": "author",
|
||||||
|
|
@ -38,6 +35,7 @@
|
||||||
"MANIFEST": [
|
"MANIFEST": [
|
||||||
"README.md",
|
"README.md",
|
||||||
"docs/DESIGN.md",
|
"docs/DESIGN.md",
|
||||||
"nwa/app/package.json"
|
"sdl-mgr/app/package.json",
|
||||||
|
"sdl-wkr/app/package.json"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,21 +27,6 @@ app.get('/api/config', (req, res) => {
|
||||||
log(module + ': WEBAPI: ' + JSON.stringify(webreqmeta))
|
log(module + ': WEBAPI: ' + JSON.stringify(webreqmeta))
|
||||||
});
|
});
|
||||||
|
|
||||||
// nwa Config as JSON
|
|
||||||
app.get('/api/nwaconf', (req, res) => {
|
|
||||||
const nwaconfPath = (path.join(config.dirs.html, 'conf', 'nwa.json'));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const nwaconfData = JSON.parse(fs.readFileSync(nwaconfPath, 'utf8'));
|
|
||||||
res.json(nwaconfData);
|
|
||||||
|
|
||||||
log(module + ': INFO: GET /api/nwaconf');
|
|
||||||
} catch (err) {
|
|
||||||
log(`ERROR: GET /api/nwaconf failed: ${err}`);
|
|
||||||
res.status(500).json({ error: 'Failed to load nwa.json' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Serve sidepanel nav as JSON
|
// Serve sidepanel nav as JSON
|
||||||
app.get('/api/nav/sidepanel', (req, res) => {
|
app.get('/api/nav/sidepanel', (req, res) => {
|
||||||
const sideMenuPath = path.join(
|
const sideMenuPath = path.join(
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "nwa",
|
"name": "nwa",
|
||||||
"version": "0.1.2",
|
"version": "0.1.0",
|
||||||
"version_date": "2026-07-17",
|
"version_date": "2026-02-08",
|
||||||
"description": "Node Web App",
|
"description": "Node Web App (NWA)",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -23,15 +23,25 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alenaksu/json-viewer": "^2.0.1",
|
"@alenaksu/json-viewer": "^2.0.1",
|
||||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||||
|
"aedes": "^0.51.3",
|
||||||
"bootstrap": "^5.3.7",
|
"bootstrap": "^5.3.7",
|
||||||
"bootstrap-icons": "^1.13.1",
|
"bootstrap-icons": "^1.13.1",
|
||||||
"csv-parse": "^7.0.1",
|
"chart.js": "^4.5.1",
|
||||||
|
"csv-parse": "^5.6.0",
|
||||||
|
"d3": "^7.9.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"graph.js": "^1.21.1",
|
||||||
|
"highlight.js": "^11.9.0",
|
||||||
|
"html-to-image": "^1.11.13",
|
||||||
"marked": "^15.0.11",
|
"marked": "^15.0.11",
|
||||||
"mathjax-full": "^3.2.1",
|
"mathjax-full": "^3.2.1",
|
||||||
|
"mqtt": "^5.14.1",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"open": "^11.0.0",
|
"open": "^11.0.0",
|
||||||
"serve-index": "^1.9.1",
|
"serve-index": "^1.9.1",
|
||||||
|
"three": "^0.181.2",
|
||||||
|
"websocket-stream": "^5.5.2",
|
||||||
|
"ws": "^8.18.3",
|
||||||
"yargs": "^18.0.0"
|
"yargs": "^18.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,56 +1,115 @@
|
||||||
{
|
{
|
||||||
"SIDE_PANEL": {
|
"sdl":{
|
||||||
"enabled": false
|
"title": "NWA",
|
||||||
|
"desc": "Node Web App",
|
||||||
|
"icon": "fa-solid fa-dice-d20",
|
||||||
|
"page": "index",
|
||||||
|
"enabled": true,
|
||||||
|
"items": []
|
||||||
},
|
},
|
||||||
"NAV_ITEMS": {
|
"dash": {
|
||||||
"nwa":{
|
"title": "Dash",
|
||||||
"title": "NWA",
|
"desc": "Dashboard",
|
||||||
"desc": "Node Web App",
|
"icon": "fa-solid fa-gauge",
|
||||||
"icon": "fa-solid fa-dice-d20",
|
"page": "dash",
|
||||||
"page": "index",
|
"enabled": false,
|
||||||
"enabled": false,
|
"items": []
|
||||||
"items": []
|
},
|
||||||
},
|
"wkrs": {
|
||||||
"proj": {
|
"title": "Workers",
|
||||||
"title": "Projects",
|
"desc": "Workers",
|
||||||
"desc": "Projects",
|
"icon": "fa-solid fa-server",
|
||||||
"icon": "fa-solid fa-layer-group",
|
"page": "wkrs",
|
||||||
"page": "proj",
|
"enabled": false,
|
||||||
"enabled": false,
|
"items": []
|
||||||
"items": [
|
},
|
||||||
{
|
"proj": {
|
||||||
"title": "Project 1",
|
"title": "Projects",
|
||||||
"desc": "Project 1",
|
"desc": "Projects",
|
||||||
"icon": "fa-solid fa-flask",
|
"icon": "fa-solid fa-layer-group",
|
||||||
"page": "expr",
|
"page": "proj",
|
||||||
"enabled": false
|
"enabled": true,
|
||||||
}
|
"items": [
|
||||||
]
|
{
|
||||||
},
|
"title": "Experiments",
|
||||||
"app": {
|
"desc": "Experiments",
|
||||||
"title": "App",
|
"icon": "fa-solid fa-flask",
|
||||||
"icon": "fa-solid fa-dice-d20",
|
"page": "expr",
|
||||||
"enabled": false,
|
"enabled": false
|
||||||
"items": [
|
}
|
||||||
{
|
]
|
||||||
"title": "About",
|
},
|
||||||
"icon": "fa-solid fa-circle-info",
|
"jobs": {
|
||||||
"page": "about",
|
"title": "Jobs",
|
||||||
"enabled": true
|
"desc": "Jobs",
|
||||||
},
|
"icon": "fa-solid fa-person-digging",
|
||||||
{
|
"page": "jobs",
|
||||||
"title": "API",
|
"enabled": false,
|
||||||
"icon": "fa-solid fa-code",
|
"items": []
|
||||||
"page": "api",
|
},
|
||||||
"enabled": true
|
"app": {
|
||||||
},
|
"title": "App",
|
||||||
{
|
"icon": "fa-solid fa-dice-d20",
|
||||||
"title": "Docs",
|
"enabled": false,
|
||||||
"icon": "fa-solid fa-book",
|
"items": [
|
||||||
"page": "docs",
|
{
|
||||||
"enabled": false
|
"title": "About",
|
||||||
}
|
"icon": "fa-solid fa-circle-info",
|
||||||
]
|
"page": "about",
|
||||||
}
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "API",
|
||||||
|
"icon": "fa-solid fa-code",
|
||||||
|
"page": "api",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Docs",
|
||||||
|
"icon": "fa-solid fa-book",
|
||||||
|
"page": "docs",
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"experiments": {
|
||||||
|
"title": "Experiments",
|
||||||
|
"icon": "fa-solid fa-flask",
|
||||||
|
"enabled": false,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"title": "Browse",
|
||||||
|
"icon": "fa-solid fa-table-list",
|
||||||
|
"page": "experiments",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "New Experiment",
|
||||||
|
"icon": "fa-solid fa-plus",
|
||||||
|
"page": "experiment-new",
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"runs": {
|
||||||
|
"title": "Runs",
|
||||||
|
"icon": "fa-solid fa-play",
|
||||||
|
"enabled": false,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"title": "Active Runs",
|
||||||
|
"icon": "fa-solid fa-spinner",
|
||||||
|
"page": "runs-active",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "History",
|
||||||
|
"icon": "fa-solid fa-clock-rotate-left",
|
||||||
|
"page": "runs-history",
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
<link rel="stylesheet" href="/css/nwa.css" />
|
<link rel="stylesheet" href="/css/nwa.css" />
|
||||||
<link rel="stylesheet" href="/css/custom-styles.css" />
|
<link rel="stylesheet" href="/css/custom-styles.css" />
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.MathJax = {
|
window.MathJax = {
|
||||||
tex: {
|
tex: {
|
||||||
inlineMath: [["$", "$"], ["\\(", "\\)"]],
|
inlineMath: [["$", "$"], ["\\(", "\\)"]],
|
||||||
|
|
@ -52,6 +52,8 @@
|
||||||
|
|
||||||
<script src="/json-viewer/json-viewer.bundle.js"></script>
|
<script src="/json-viewer/json-viewer.bundle.js"></script>
|
||||||
|
|
||||||
|
<script src="/mqttjs/mqtt.min.js"></script>
|
||||||
|
|
||||||
<script src="/html-to-image/html-to-image.js"></script>
|
<script src="/html-to-image/html-to-image.js"></script>
|
||||||
|
|
||||||
<script type="module" src="/js/weblib.js"></script>
|
<script type="module" src="/js/weblib.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -1,91 +1,50 @@
|
||||||
import {
|
console.log('Settings.js loaded');
|
||||||
loadNwaConfig,
|
|
||||||
resolveTheme
|
|
||||||
} from '/js/weblib.js';
|
|
||||||
|
|
||||||
console.log('settings.js loaded');
|
// Get current theme from localStorage
|
||||||
|
const currentTheme = localStorage.getItem('theme') || 'dark';
|
||||||
|
console.log('Current theme:', currentTheme);
|
||||||
|
|
||||||
initializeSettings();
|
// Set the toggle to match current theme
|
||||||
|
const themeSwitch = document.getElementById('themeSwitch');
|
||||||
|
const themeLabel = document.querySelector('label[for="themeSwitch"]');
|
||||||
|
|
||||||
async function initializeSettings() {
|
console.log('Theme switch element found:', themeSwitch);
|
||||||
let nwaConfig = {};
|
|
||||||
|
|
||||||
try {
|
if (themeSwitch && themeLabel) {
|
||||||
nwaConfig = await loadNwaConfig();
|
themeSwitch.checked = (currentTheme === 'dark');
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load NWA config:', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
const themeSelect =
|
// Update label based on current theme
|
||||||
document.getElementById('themeSelect');
|
updateLabel(currentTheme);
|
||||||
|
|
||||||
const themeDescription =
|
console.log('Switch checked state:', themeSwitch.checked);
|
||||||
document.getElementById('themeDescription');
|
|
||||||
|
|
||||||
if (!themeSelect || !themeDescription) {
|
// Listen for changes
|
||||||
console.error('Theme settings elements not found');
|
themeSwitch.addEventListener('change', (e) => {
|
||||||
return;
|
console.log('Switch toggled!', e.target.checked);
|
||||||
}
|
const newTheme = e.target.checked ? 'dark' : 'light';
|
||||||
|
console.log('Setting new theme:', newTheme);
|
||||||
const savedTheme = localStorage.getItem('theme');
|
setTheme(newTheme);
|
||||||
|
updateLabel(newTheme);
|
||||||
const configuredDefault =
|
|
||||||
nwaConfig?.APPEARANCE?.default_theme === 'light'
|
|
||||||
? 'light'
|
|
||||||
: 'dark';
|
|
||||||
|
|
||||||
// Show whether the browser is using an override
|
|
||||||
// or the application-configured default.
|
|
||||||
if (savedTheme === 'light' || savedTheme === 'dark') {
|
|
||||||
themeSelect.value = savedTheme;
|
|
||||||
} else {
|
|
||||||
themeSelect.value = 'default';
|
|
||||||
}
|
|
||||||
|
|
||||||
updateThemeDescription(
|
|
||||||
themeSelect.value,
|
|
||||||
configuredDefault,
|
|
||||||
themeDescription
|
|
||||||
);
|
|
||||||
|
|
||||||
themeSelect.addEventListener('change', event => {
|
|
||||||
const selectedTheme = event.target.value;
|
|
||||||
|
|
||||||
if (selectedTheme === 'default') {
|
|
||||||
localStorage.removeItem('theme');
|
|
||||||
|
|
||||||
document.documentElement.setAttribute(
|
|
||||||
'data-bs-theme',
|
|
||||||
configuredDefault
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
localStorage.setItem('theme', selectedTheme);
|
|
||||||
|
|
||||||
document.documentElement.setAttribute(
|
|
||||||
'data-bs-theme',
|
|
||||||
selectedTheme
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateThemeDescription(
|
|
||||||
selectedTheme,
|
|
||||||
configuredDefault,
|
|
||||||
themeDescription
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
console.error('themeSwitch or themeLabel element not found!');
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateThemeDescription(
|
function setTheme(theme) {
|
||||||
selectedTheme,
|
console.log('setTheme called with:', theme);
|
||||||
configuredDefault,
|
document.documentElement.setAttribute('data-bs-theme', theme);
|
||||||
descriptionElement
|
localStorage.setItem('theme', theme);
|
||||||
) {
|
console.log('Theme set to:', theme);
|
||||||
if (selectedTheme === 'default') {
|
console.log('HTML data-bs-theme attribute:', document.documentElement.getAttribute('data-bs-theme'));
|
||||||
descriptionElement.textContent =
|
}
|
||||||
`Using the application default: ${configuredDefault}.`;
|
|
||||||
return;
|
function updateLabel(theme) {
|
||||||
|
const themeLabel = document.querySelector('label[for="themeSwitch"]');
|
||||||
|
if (themeLabel) {
|
||||||
|
if (theme === 'dark') {
|
||||||
|
themeLabel.innerHTML = '<i class="fas fa-moon me-2"></i>Dark Mode';
|
||||||
|
} else {
|
||||||
|
themeLabel.innerHTML = '<i class="fas fa-sun me-2"></i>Light Mode';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
descriptionElement.textContent =
|
|
||||||
`Using a browser-specific ${selectedTheme} theme override.`;
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
// weblib.js
|
|
||||||
// Dynamically include fragments
|
// Dynamically include fragments
|
||||||
async function includeFragment(id, url) {
|
async function includeFragment(id, url) {
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
|
|
@ -22,9 +21,7 @@ async function loadMarkdown(page) {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const navItems = await navRes.json();
|
const navItems = await navRes.json();
|
||||||
// const sideNav = await sideNavRes.json();
|
const sideNav = await sideNavRes.json();
|
||||||
const sideNavConfig = await sideNavRes.json();
|
|
||||||
const sideNav = sideNavConfig.NAV_ITEMS || {};
|
|
||||||
|
|
||||||
// ---- Locate metadata (nav.json first, then nav-sidepanel.json) ----
|
// ---- Locate metadata (nav.json first, then nav-sidepanel.json) ----
|
||||||
let meta = null;
|
let meta = null;
|
||||||
|
|
@ -153,35 +150,6 @@ async function loadPageScript(page) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadNwaConfig() {
|
|
||||||
const res = await fetch('/api/nwaconf', {
|
|
||||||
cache: 'no-store'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to load NWA config: HTTP ${res.status}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveTheme(nwaConfig = {}) {
|
|
||||||
const savedTheme = localStorage.getItem('theme');
|
|
||||||
|
|
||||||
if (savedTheme === 'dark' || savedTheme === 'light') {
|
|
||||||
return savedTheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultTheme = nwaConfig?.APPEARANCE?.default_theme;
|
|
||||||
|
|
||||||
if (defaultTheme === 'dark' || defaultTheme === 'light') {
|
|
||||||
return defaultTheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'dark';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load Nav
|
// Load Nav
|
||||||
function loadNav() {
|
function loadNav() {
|
||||||
|
|
@ -303,6 +271,37 @@ function loadNav() {
|
||||||
navContainer.innerHTML = '';
|
navContainer.innerHTML = '';
|
||||||
navContainer.appendChild(navEl);
|
navContainer.appendChild(navEl);
|
||||||
|
|
||||||
|
// --------- MANUAL DROPDOWN HANDLERS ---------
|
||||||
|
// const dropdownToggles = navContainer.querySelectorAll('.nav-item.dropdown > .dropdown-toggle');
|
||||||
|
|
||||||
|
// dropdownToggles.forEach(toggle => {
|
||||||
|
// toggle.addEventListener('click', evt => {
|
||||||
|
// evt.preventDefault();
|
||||||
|
|
||||||
|
// const parentLi = toggle.parentElement;
|
||||||
|
// const menu = parentLi.querySelector('.dropdown-menu');
|
||||||
|
// const isShown = menu.classList.contains('show');
|
||||||
|
|
||||||
|
// // close all others
|
||||||
|
// navContainer.querySelectorAll('.dropdown-menu.show').forEach(openMenu => {
|
||||||
|
// openMenu.classList.remove('show');
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // toggle current
|
||||||
|
// if (!isShown) {
|
||||||
|
// menu.classList.add('show');
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // close dropdown when clicking outside nav
|
||||||
|
// document.addEventListener('click', evt => {
|
||||||
|
// if (!navContainer.contains(evt.target)) {
|
||||||
|
// navContainer.querySelectorAll('.dropdown-menu.show').forEach(openMenu => {
|
||||||
|
// openMenu.classList.remove('show');
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('Failed to load nav:', err);
|
console.error('Failed to load nav:', err);
|
||||||
|
|
@ -369,10 +368,63 @@ function loadNavTabs() {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
// Side Panel (layout-collapsing)
|
// Side Panel (layout-collapsing)
|
||||||
// ===============================
|
// ===============================
|
||||||
|
// async function loadSidePanel() {
|
||||||
|
// // console.log('DEBUG: loadSidePanel() called');
|
||||||
|
|
||||||
|
// const panel = document.getElementById('sidepanel');
|
||||||
|
// const inner = document.getElementById('sidepanel-inner');
|
||||||
|
// const toggleBtn = document.getElementById('sidepanel-toggle');
|
||||||
|
|
||||||
|
// if (!panel || !inner || !toggleBtn) return;
|
||||||
|
|
||||||
|
// const currentPage =
|
||||||
|
// new URLSearchParams(window.location.search).get('page') || 'index';
|
||||||
|
|
||||||
|
// const isCollapsed =
|
||||||
|
// localStorage.getItem('sidepanel_collapsed') === 'true';
|
||||||
|
|
||||||
|
// if (isCollapsed) {
|
||||||
|
// panel.classList.add('collapsed');
|
||||||
|
// // toggleBtn.innerHTML = `<i class="fa-solid fa-folder-plus"></i>`;
|
||||||
|
// toggleBtn.innerHTML = '<i class="bi bi-layout-sidebar"></i>';
|
||||||
|
// } else {
|
||||||
|
// // toggleBtn.innerHTML = `<i class="fa-solid fa-folder-minus"></i>`;
|
||||||
|
// toggleBtn.innerHTML = '<i class="bi bi-layout-sidebar-reverse"></i>';
|
||||||
|
// }
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// const res = await fetch('/api/nav/sidepanel');
|
||||||
|
// if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
|
||||||
|
// const sideNav = await res.json();
|
||||||
|
|
||||||
|
// renderAllSidePanelSections(inner, sideNav, currentPage);
|
||||||
|
// } catch (err) {
|
||||||
|
// console.error('Failed to load side panel:', err);
|
||||||
|
// inner.innerHTML = '';
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Collapse / expand behavior
|
||||||
|
// toggleBtn.onclick = () => {
|
||||||
|
// const collapsed = panel.classList.toggle('collapsed');
|
||||||
|
// localStorage.setItem('sidepanel_collapsed', collapsed);
|
||||||
|
|
||||||
|
// // toggleBtn.innerHTML = `
|
||||||
|
// // <i class="fa-solid ${
|
||||||
|
// // collapsed ? 'fa-folder-plus' : 'fa-folder-minus'
|
||||||
|
// // }"></i>
|
||||||
|
// // `;
|
||||||
|
|
||||||
|
// toggleBtn.innerHTML = `
|
||||||
|
// <i class="bi ${
|
||||||
|
// collapsed ? 'bi-layout-sidebar' : 'bi-layout-sidebar-reverse'
|
||||||
|
// }"></i>
|
||||||
|
// `;
|
||||||
|
// };
|
||||||
|
// }
|
||||||
async function loadSidePanel() {
|
async function loadSidePanel() {
|
||||||
const panel = document.getElementById('sidepanel');
|
const panel = document.getElementById('sidepanel');
|
||||||
const inner = document.getElementById('sidepanel-inner');
|
const inner = document.getElementById('sidepanel-inner');
|
||||||
|
|
@ -380,67 +432,37 @@ async function loadSidePanel() {
|
||||||
|
|
||||||
if (!panel || !inner || !toggleBtn) return;
|
if (!panel || !inner || !toggleBtn) return;
|
||||||
|
|
||||||
|
const currentPage =
|
||||||
|
new URLSearchParams(window.location.search).get('page') || 'index';
|
||||||
|
const isCollapsed =
|
||||||
|
localStorage.getItem('sidepanel_collapsed') === 'true';
|
||||||
|
|
||||||
|
if (isCollapsed) {
|
||||||
|
panel.classList.add('collapsed');
|
||||||
|
toggleBtn.innerHTML = '<i class="bi bi-layout-sidebar"></i>';
|
||||||
|
} else {
|
||||||
|
toggleBtn.innerHTML = '<i class="bi bi-layout-sidebar-reverse"></i>';
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/nav/sidepanel');
|
const res = await fetch('/api/nav/sidepanel');
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`); // ✅ Fixed template literal
|
||||||
if (!res.ok) {
|
const sideNav = await res.json();
|
||||||
throw new Error(`HTTP ${res.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sideNavConfig = await res.json();
|
|
||||||
|
|
||||||
// Disable the entire side panel
|
|
||||||
if (sideNavConfig.SIDE_PANEL?.enabled === false) {
|
|
||||||
panel.hidden = true;
|
|
||||||
inner.innerHTML = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
panel.hidden = false;
|
|
||||||
|
|
||||||
const sideNav = sideNavConfig.NAV_ITEMS || {};
|
|
||||||
|
|
||||||
const currentPage =
|
|
||||||
new URLSearchParams(window.location.search).get('page') || 'index';
|
|
||||||
|
|
||||||
const isCollapsed =
|
|
||||||
localStorage.getItem('sidepanel_collapsed') === 'true';
|
|
||||||
|
|
||||||
panel.classList.toggle('collapsed', isCollapsed);
|
|
||||||
|
|
||||||
toggleBtn.innerHTML = `
|
|
||||||
<i class="bi ${
|
|
||||||
isCollapsed
|
|
||||||
? 'bi-layout-sidebar'
|
|
||||||
: 'bi-layout-sidebar-reverse'
|
|
||||||
}"></i>
|
|
||||||
`;
|
|
||||||
|
|
||||||
renderAllSidePanelSections(inner, sideNav, currentPage);
|
renderAllSidePanelSections(inner, sideNav, currentPage);
|
||||||
|
|
||||||
toggleBtn.onclick = () => {
|
|
||||||
const collapsed = panel.classList.toggle('collapsed');
|
|
||||||
|
|
||||||
localStorage.setItem(
|
|
||||||
'sidepanel_collapsed',
|
|
||||||
String(collapsed)
|
|
||||||
);
|
|
||||||
|
|
||||||
toggleBtn.innerHTML = `
|
|
||||||
<i class="bi ${
|
|
||||||
collapsed
|
|
||||||
? 'bi-layout-sidebar'
|
|
||||||
: 'bi-layout-sidebar-reverse'
|
|
||||||
}"></i>
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load side panel:', err);
|
console.error('Failed to load side panel:', err);
|
||||||
panel.hidden = true;
|
|
||||||
inner.innerHTML = '';
|
inner.innerHTML = '';
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
// Collapse / expand behavior
|
||||||
|
toggleBtn.onclick = () => {
|
||||||
|
const collapsed = panel.classList.toggle('collapsed');
|
||||||
|
localStorage.setItem('sidepanel_collapsed', collapsed);
|
||||||
|
toggleBtn.innerHTML = `
|
||||||
|
<i class="bi ${collapsed ? 'bi-layout-sidebar' : 'bi-layout-sidebar-reverse'}"></i>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------
|
// -------------------------------
|
||||||
// Find matching section by page
|
// Find matching section by page
|
||||||
|
|
@ -776,6 +798,61 @@ export async function getConfig() {
|
||||||
return await res.json();
|
return await res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// export async function getConfig() {
|
||||||
|
// const cached = sessionStorage.getItem('ssl_config');
|
||||||
|
// if (!cached) {
|
||||||
|
// throw new Error('ssl_config not found in sessionStorage');
|
||||||
|
// }
|
||||||
|
// return JSON.parse(cached);
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// // Load tags
|
||||||
|
// export async function loadTags(forceRefresh = false) {
|
||||||
|
// if (!forceRefresh) {
|
||||||
|
// const cached = localStorage.getItem('app_tags');
|
||||||
|
// if (cached) {
|
||||||
|
// // console.log('Loaded tags from cache');
|
||||||
|
// return JSON.parse(cached);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const res = await fetch('/api/sql/query/tags'); // ✅ wait for fetch to resolve
|
||||||
|
// const tags = await res.json(); // ✅ wait for JSON parsing
|
||||||
|
// localStorage.setItem('app_tags', JSON.stringify(tags));
|
||||||
|
// // console.log('Fetched tags from backend');
|
||||||
|
// return tags;
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// export async function refreshTags() {
|
||||||
|
// const tags = await loadTags(true); // force refresh
|
||||||
|
// // console.log('Tags reloaded:', tags);
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// export async function tag_badge(tag, active = true) {
|
||||||
|
// const tags = JSON.parse(localStorage.getItem('app_tags')) || [];
|
||||||
|
// const t = tags.find(x => x.name === tag);
|
||||||
|
// if (!t) return `<span class="badge bg-warning" title="Tag not found">${tag}</span>`;
|
||||||
|
|
||||||
|
// const safeName = t.name.replace(/\W+/g, '_');
|
||||||
|
|
||||||
|
// const styleActive = `background-color:${t.color};border-color:${t.color};color:#fff;`;
|
||||||
|
// const styleInactive = `color:${t.color};background-color:#e9ecef;border:1px solid ${t.color};`;
|
||||||
|
// const style = active ? styleActive : styleInactive;
|
||||||
|
|
||||||
|
// return `<span class="tag_${safeName} badge rounded-pill tag-toggle"
|
||||||
|
// data-tag="${t.name}"
|
||||||
|
// data-active="${active}"
|
||||||
|
// style="${style}"
|
||||||
|
// title="${t.description}"
|
||||||
|
// onclick="toggleTag('${t.name.replace(/'/g, "\\'")}')">
|
||||||
|
// ${t.name}
|
||||||
|
// </span>`;
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export async function getLocalTimestamp() {
|
export async function getLocalTimestamp() {
|
||||||
|
|
@ -799,100 +876,38 @@ export async function getUTCTimestamp() {
|
||||||
// ===============================
|
// ===============================
|
||||||
// Load on page load
|
// Load on page load
|
||||||
// ===============================
|
// ===============================
|
||||||
// window.addEventListener('DOMContentLoaded', async () => {
|
|
||||||
// // Apply saved theme (default to dark)
|
|
||||||
// const savedTheme = localStorage.getItem('theme') || 'dark';
|
|
||||||
// document.documentElement.setAttribute('data-bs-theme', savedTheme);
|
|
||||||
|
|
||||||
// const config = await loadConfig();
|
|
||||||
// // console.log(`DEBUG: ${JSON.stringify(config)}`)
|
|
||||||
|
|
||||||
// // const tags = await loadTags();
|
|
||||||
// // console.log(`DEBUG: ${JSON.stringify(tags)}`)
|
|
||||||
|
|
||||||
// // Load fragments and layout
|
|
||||||
// includeFragment('banner', '/banner.html');
|
|
||||||
// // includeFragment('footer', '/footer.html');
|
|
||||||
|
|
||||||
// loadNav();
|
|
||||||
|
|
||||||
// loadSidePanel();
|
|
||||||
|
|
||||||
// loadFooter();
|
|
||||||
|
|
||||||
|
|
||||||
// // Load Markdown content
|
|
||||||
// const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
// const page = urlParams.get('page') || 'index';
|
|
||||||
// loadMarkdown(`${page}`);
|
|
||||||
|
|
||||||
// // Update title and app description
|
|
||||||
// if (config?.package?.description) {
|
|
||||||
// document.title = config.package.description;
|
|
||||||
|
|
||||||
// const descEl = document.querySelector('.app-description');
|
|
||||||
// if (descEl) descEl.innerHTML = config.package.description;
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
window.addEventListener('DOMContentLoaded', async () => {
|
window.addEventListener('DOMContentLoaded', async () => {
|
||||||
const [configResult, nwaConfigResult] =
|
// Apply saved theme (default to dark)
|
||||||
await Promise.allSettled([
|
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||||
loadConfig(),
|
document.documentElement.setAttribute('data-bs-theme', savedTheme);
|
||||||
loadNwaConfig()
|
|
||||||
]);
|
|
||||||
|
|
||||||
const config =
|
const config = await loadConfig();
|
||||||
configResult.status === 'fulfilled'
|
// console.log(`DEBUG: ${JSON.stringify(config)}`)
|
||||||
? configResult.value
|
|
||||||
: {};
|
|
||||||
|
|
||||||
const nwaConfig =
|
// const tags = await loadTags();
|
||||||
nwaConfigResult.status === 'fulfilled'
|
// console.log(`DEBUG: ${JSON.stringify(tags)}`)
|
||||||
? nwaConfigResult.value
|
|
||||||
: {};
|
|
||||||
|
|
||||||
if (configResult.status === 'rejected') {
|
|
||||||
console.error(
|
|
||||||
'Failed to load application config:',
|
|
||||||
configResult.reason
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nwaConfigResult.status === 'rejected') {
|
|
||||||
console.error(
|
|
||||||
'Failed to load NWA config:',
|
|
||||||
nwaConfigResult.reason
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Saved user preference takes precedence over configured default.
|
|
||||||
const theme = resolveTheme(nwaConfig);
|
|
||||||
|
|
||||||
document.documentElement.setAttribute(
|
|
||||||
'data-bs-theme',
|
|
||||||
theme
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// Load fragments and layout
|
||||||
includeFragment('banner', '/banner.html');
|
includeFragment('banner', '/banner.html');
|
||||||
|
// includeFragment('footer', '/footer.html');
|
||||||
|
|
||||||
loadNav();
|
loadNav();
|
||||||
|
|
||||||
loadSidePanel();
|
loadSidePanel();
|
||||||
|
|
||||||
loadFooter();
|
loadFooter();
|
||||||
|
|
||||||
|
|
||||||
|
// Load Markdown content
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const page = urlParams.get('page') || 'index';
|
const page = urlParams.get('page') || 'index';
|
||||||
|
loadMarkdown(`${page}`);
|
||||||
|
|
||||||
loadMarkdown(page);
|
// Update title and app description
|
||||||
|
|
||||||
if (config?.package?.description) {
|
if (config?.package?.description) {
|
||||||
document.title = config.package.description;
|
document.title = config.package.description;
|
||||||
|
|
||||||
const descEl =
|
const descEl = document.querySelector('.app-description');
|
||||||
document.querySelector('.app-description');
|
if (descEl) descEl.innerHTML = config.package.description;
|
||||||
|
|
||||||
if (descEl) {
|
|
||||||
descEl.innerHTML = config.package.description;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -1,40 +1,126 @@
|
||||||
# Node Web App (NWA)
|
# Software Defined Laboratory (SDL)
|
||||||
|
|
||||||
| Attribute | Value |
|
| Attribute | Value |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| **Author** | John Haverlack |
|
| **Author** | John Haverlack |
|
||||||
| **Copyright** | 2026 John Haverlack |
|
| **Copyright** | 2026 John Haverlack |
|
||||||
| **License** | MIT |
|
| **License** | MIT |
|
||||||
| **Version** | 0.1.2 |
|
| **Version** | 0.3.10 |
|
||||||
| **Date** | 2026-07-17 |
|
| **Date** | 2026-02-08 |
|
||||||
|
|
||||||
## v0.1.2 - 2026-07-17 (PROD)
|
## v0.3.10 - 2026-02-08 (ALPHA)
|
||||||
|
|
||||||
- v0.1.2
|
- Dash Staging Jobs
|
||||||
- App Updates from HaverLab
|
- Dash Full sdl_id
|
||||||
- Fixing Installer
|
- Updating main Web UI Page
|
||||||
- Updating Metadata
|
- Start v0.3.10
|
||||||
- WIP: Fixing Installer
|
|
||||||
- Fixing Deploy Dir
|
|
||||||
- Start v0.1.2
|
|
||||||
|
|
||||||
## v0.1.1 - 2026-07-04 (PROD)
|
## v0.3.9 - 2026-02-08 (ALPHA)
|
||||||
|
|
||||||
- Versioning Tool Updates
|
- v0.3.9
|
||||||
- Fixing Version Branch Tools
|
- Refactor Cluster Status Dash
|
||||||
- Start v0.1.1
|
- Refactor Cluster Status Dash
|
||||||
- Adding Workflow Docs
|
- System Columnn Display
|
||||||
- WIP: Fixing installer
|
- Start v0.3.9
|
||||||
- Cleaning up Template
|
|
||||||
- Initial Commit Migrating NWA from SDL
|
|
||||||
|
|
||||||
## v0.1.0 - 2026-06-28 (INIT)
|
## v0.3.8 - 2026-02-01 (ALPHA)
|
||||||
|
|
||||||
- v0.1.2
|
- v0.3.8
|
||||||
- App Updates from HaverLab
|
- Refactoing Worker Dashboard
|
||||||
- Fixing Installer
|
- Refactoing Worker Dashboard
|
||||||
- Updating Metadata
|
- Refactored Cluster Status
|
||||||
- WIP: Fixing Installer
|
- Refactor of telemetry format + hw detection
|
||||||
- Fixing Deploy Dir
|
- Switched Panel Icon to Bootstrap Icon
|
||||||
- Start v0.1.2
|
- Updating Worker Telemetry
|
||||||
|
- CP
|
||||||
|
- Fixing Wget/Curl Command Installer Port
|
||||||
|
- Fixing Wget/Curl Command Installer Port
|
||||||
|
- Fixing Wkr Start / Stop
|
||||||
|
- Update stop-sdl-wkr.sh
|
||||||
|
- WIP Install / Start / Stop
|
||||||
|
- WIP Install / Start /Stop
|
||||||
|
- Systemd Detectiion in Start/Stop/Status
|
||||||
|
- WIP: Start / Stop Scripts
|
||||||
|
- Non Systemd start stop scripts
|
||||||
|
- WIP: start stop scripts
|
||||||
|
- Updating Task List
|
||||||
|
- Adding Screeshot to README
|
||||||
|
- Start v0.3.8
|
||||||
|
|
||||||
|
## v0.3.7 - 2026-02-01 (ALPHA)
|
||||||
|
|
||||||
|
**Summary**
|
||||||
|
sdl-wkr Telemetry
|
||||||
|
|
||||||
|
- v0.3.7
|
||||||
|
- Fixing sdl-wkr RAM detection
|
||||||
|
- Start v0.3.7
|
||||||
|
|
||||||
|
## v0.3.6 - 2026-02-01 (ALPHA)
|
||||||
|
|
||||||
|
**Summary**
|
||||||
|
Light/Dark Mode, Wkr Install, Cluster Stats
|
||||||
|
|
||||||
|
- v0.3.6
|
||||||
|
- Adding Cluster Telemetry
|
||||||
|
- WIP: Worker Display
|
||||||
|
- Tweaks
|
||||||
|
- Start v0.3.6
|
||||||
|
- CP Updates
|
||||||
|
- Dash: Total Cluster Stats
|
||||||
|
- Publishing to cluster/workers
|
||||||
|
- Light/Dark Mode Toggle
|
||||||
|
- Added light/dark theme CSS
|
||||||
|
|
||||||
|
## v0.3.5 - 2026-02-01 (ALPHA)
|
||||||
|
|
||||||
|
- v0.3.5
|
||||||
|
- Fixing sdl-wkr install script
|
||||||
|
- Start v0.3.5
|
||||||
|
|
||||||
|
## v0.3.4 - 2026-01-31 (ALPHA)
|
||||||
|
|
||||||
|
- Working sdl-wkr self update
|
||||||
|
- Working sdl-wkr self update
|
||||||
|
- Start v0.3.4
|
||||||
|
|
||||||
|
## v0.3.3 - 2026-01-31 (ALPHA)
|
||||||
|
|
||||||
|
- WIP: sdl-wkr updates
|
||||||
|
- WIP: sdl-wkr updates
|
||||||
|
- Start v0.3.3
|
||||||
|
|
||||||
|
## v0.3.2 - 2026-01-31 (ALPHA)
|
||||||
|
|
||||||
|
- v0.3.2
|
||||||
|
- v0.3.2
|
||||||
|
- Squetching Logs
|
||||||
|
- WORKING: join/authz
|
||||||
|
- WIP: Join Worker
|
||||||
|
- WIP: Join Process
|
||||||
|
- sdl-wkr Cluster Join
|
||||||
|
- Fixing Service Module Render Bug
|
||||||
|
- Start v0.3.2
|
||||||
|
|
||||||
|
## v0.3.1 - 2026-01-31 (ALPHA)
|
||||||
|
|
||||||
|
**Summary**
|
||||||
|
README Cleanup
|
||||||
|
|
||||||
|
- v0.3.1
|
||||||
|
- Start v0.3.1
|
||||||
|
- Removing duplicate LICENSE file
|
||||||
|
- README Cleanup
|
||||||
|
|
||||||
|
## v0.3.0 - 2026-01-31 (ALPHA)
|
||||||
|
|
||||||
|
**Summary**
|
||||||
|
Initial Codebase
|
||||||
|
|
||||||
|
**Notes**
|
||||||
|
Initial GitHub Release
|
||||||
|
|
||||||
|
- Preparing GitHub Version
|
||||||
|
- Start v0.3.0
|
||||||
|
- Initial commit
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,19 @@
|
||||||
<h5 class="mb-0">Appearance</h5>
|
<h5 class="mb-0">Appearance</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<label for="themeSelect" class="form-label">
|
<div class="row align-items-center">
|
||||||
<i class="fas fa-palette me-2"></i>Theme
|
<div class="col-8">
|
||||||
</label>
|
<label for="themeSwitch" class="form-label mb-1">
|
||||||
<select class="form-select" id="themeSelect">
|
<i class="fas fa-moon me-2"></i>Dark Mode
|
||||||
<option value="default">Application Default</option>
|
</label>
|
||||||
<option value="light">Light</option>
|
<p class="text-muted small mb-0">Toggle between light and dark theme</p>
|
||||||
<option value="dark">Dark</option>
|
</div>
|
||||||
</select>
|
<div class="col-4 text-end">
|
||||||
<p class="text-muted small mt-2 mb-0" id="themeDescription"></p>
|
<div class="form-check form-switch d-inline-block">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" id="themeSwitch" checked>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=PROJECT
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
|
|
||||||
# Environment
|
|
||||||
Environment=NWA_HOME=%h/.SITENAME
|
|
||||||
Environment=NODE_ENV=production
|
|
||||||
|
|
||||||
# Paths
|
|
||||||
WorkingDirectory=%h/.SITENAME/versions/current/nwa/app
|
|
||||||
|
|
||||||
# Start command
|
|
||||||
ExecStart=%h/.SITENAME/nodejs/current/bin/node index.js
|
|
||||||
|
|
||||||
# Restart behavior
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=3
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
StandardOutput=append:%h/.SITENAME/logs/SITENAME.log
|
|
||||||
StandardError=append:%h/.SITENAME/logs/SITENAME.log
|
|
||||||
|
|
||||||
# Clean shutdown
|
|
||||||
KillSignal=SIGTERM
|
|
||||||
TimeoutStopSec=15
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=default.target
|
|
||||||
|
|
@ -129,6 +129,6 @@ while IFS='|' read -r version rdate maturity summary notes; do
|
||||||
echo "" >> "$OUT_FILE"
|
echo "" >> "$OUT_FILE"
|
||||||
done
|
done
|
||||||
|
|
||||||
cp docs/CHANGELOG.md nwa/html/md/changelog.md
|
cp docs/CHANGELOG.md sdl-mgr/html/md/changelog.md
|
||||||
|
|
||||||
echo "Changelog written to ${OUT_FILE}"
|
echo "Changelog written to ${OUT_FILE}"
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# metadata-update.sh - Update NWA metadata in package.json and Markdown files
|
# metadata-update.sh - Update SDL metadata in package.json and Markdown files
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
# Resolve paths
|
# Resolve paths
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
NWA_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
SDL_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
META_FILE="$NWA_ROOT/metadata.json"
|
META_FILE="$SDL_ROOT/metadata.json"
|
||||||
|
|
||||||
command -v jq >/dev/null || { echo "jq required"; exit 1; }
|
command -v jq >/dev/null || { echo "jq required"; exit 1; }
|
||||||
command -v sed >/dev/null || { echo "sed required"; exit 1; }
|
command -v sed >/dev/null || { echo "sed required"; exit 1; }
|
||||||
|
|
@ -26,34 +26,15 @@ escape_regex() {
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
# Update package.json
|
# Update package.json
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
# update_package_json() {
|
|
||||||
# local file="$1"
|
|
||||||
# local tmp
|
|
||||||
# tmp="$(mktemp)"
|
|
||||||
|
|
||||||
# jq \
|
|
||||||
# --slurpfile meta "$META_FILE" \
|
|
||||||
# '
|
|
||||||
# . as $pkg
|
|
||||||
# | ($meta.META_MAPS["package.json"] | to_entries) as $maps
|
|
||||||
# | reduce $maps[] as $m ($pkg;
|
|
||||||
# .[$m.key] = $meta.METADATA[$m.value]
|
|
||||||
# )
|
|
||||||
# ' "$file" > "$tmp"
|
|
||||||
|
|
||||||
# mv "$tmp" "$file"
|
|
||||||
# echo "Updated $file"
|
|
||||||
# }
|
|
||||||
update_package_json() {
|
update_package_json() {
|
||||||
local file="$1"
|
local file="$1"
|
||||||
local tmp
|
local tmp
|
||||||
tmp="$(mktemp)"
|
tmp="$(mktemp)"
|
||||||
|
|
||||||
jq \
|
jq \
|
||||||
--slurpfile meta_file "$META_FILE" \
|
--argfile meta "$META_FILE" \
|
||||||
'
|
'
|
||||||
($meta_file[0]) as $meta
|
. as $pkg
|
||||||
| . as $pkg
|
|
||||||
| ($meta.META_MAPS["package.json"] | to_entries) as $maps
|
| ($meta.META_MAPS["package.json"] | to_entries) as $maps
|
||||||
| reduce $maps[] as $m ($pkg;
|
| reduce $maps[] as $m ($pkg;
|
||||||
.[$m.key] = $meta.METADATA[$m.value]
|
.[$m.key] = $meta.METADATA[$m.value]
|
||||||
|
|
@ -87,7 +68,7 @@ update_markdown() {
|
||||||
# Main
|
# Main
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
jq -r '.MANIFEST[]' "$META_FILE" | while read -r rel_path; do
|
jq -r '.MANIFEST[]' "$META_FILE" | while read -r rel_path; do
|
||||||
file="$NWA_ROOT/$rel_path"
|
file="$SDL_ROOT/$rel_path"
|
||||||
|
|
||||||
if [[ ! -f "$file" ]]; then
|
if [[ ! -f "$file" ]]; then
|
||||||
echo "WARN: $rel_path not found, skipping"
|
echo "WARN: $rel_path not found, skipping"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue