Release v0.1.2

This commit is contained in:
John Haverlack 2026-07-17 19:06:12 -08:00
commit efde009faa
15 changed files with 512 additions and 523 deletions

View File

@ -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.1 | | **Version** | 0.1.2 |
| **Date** | 2026-07-04 | | **Date** | 2026-07-17 |
## Overview ## Overview

View File

@ -1,5 +1,12 @@
{ {
"releases": [ "releases": [
{
"version": "0.1.2",
"date": "2026-07-17",
"maturity": "PROD",
"summary": "",
"notes": ""
},
{ {
"version": "0.1.1", "version": "0.1.1",
"date": "2026-07-04", "date": "2026-07-04",

View File

@ -5,11 +5,22 @@
| **Author** | John Haverlack | | **Author** | John Haverlack |
| **Copyright** | 2026 John Haverlack | | **Copyright** | 2026 John Haverlack |
| **License** | MIT | | **License** | MIT |
| **Version** | 0.1.1 | | **Version** | 0.1.2 |
| **Date** | 2026-07-04 | | **Date** | 2026-07-17 |
## v0.1.2 - 2026-07-17 (PROD)
- v0.1.2
- App Updates from HaverLab
- Fixing Installer
- Updating Metadata
- WIP: Fixing Installer
- Fixing Deploy Dir
- Start v0.1.2
## v0.1.1 - 2026-07-04 (PROD) ## v0.1.1 - 2026-07-04 (PROD)
- Versioning Tool Updates
- Fixing Version Branch Tools - Fixing Version Branch Tools
- Start v0.1.1 - Start v0.1.1
- Adding Workflow Docs - Adding Workflow Docs
@ -19,10 +30,11 @@
## v0.1.0 - 2026-06-28 (INIT) ## v0.1.0 - 2026-06-28 (INIT)
- Fixing Version Branch Tools - v0.1.2
- Start v0.1.1 - App Updates from HaverLab
- Adding Workflow Docs - Fixing Installer
- WIP: Fixing installer - Updating Metadata
- Cleaning up Template - WIP: Fixing Installer
- Initial Commit Migrating NWA from SDL - Fixing Deploy Dir
- Start v0.1.2

View File

@ -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.1 | | **Version** | 0.1.2 |
| **Date** | 2026-07-04 | | **Date** | 2026-07-17 |
## Overview ## Overview

View File

@ -8,39 +8,45 @@ 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")
if [[ -z "${raw[SITE]:-}" ]]; then # SITE is derived from metadata.json, not from deploy config.
echo "ERROR: DIRS.SITE is not defined in nwa-deploy-config.json" resolved["SITE"]="$HOME/.$sitename"
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 [[ -z "${resolved[$unresolved_key]:-}" && "$val" == *"$unresolved_key"* ]]; then if [[ "$unresolved_key" != "SITE" && -z "${resolved[$unresolved_key]:-}" && "$val" == *"$unresolved_key"* ]]; then
unresolved=true unresolved=true
break break
fi fi
@ -54,6 +60,8 @@ 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
@ -67,6 +75,8 @@ 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
@ -270,15 +280,18 @@ 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")"
SITEPAGE="$(jq -r '.METADATA.SITEpage' "$META_FILE")" HOMEPAGE="$(jq -r '.METADATA.homepage' "$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
@ -291,12 +304,13 @@ 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 " SITEpage: $SITEPAGE" echo " Homepage: $HOMEPAGE"
echo " Repository: $REPO" echo " Repository: $REPO"
echo "" echo ""
@ -325,7 +339,8 @@ 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
@ -342,65 +357,31 @@ 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/efault-nwa-config.json to CONF/nwa-config.json # Copy NWA/current/conf/default-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
# ------------------------------------------------------------ # ------------------------------------------------------------
@ -413,44 +394,6 @@ 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
@ -465,45 +408,61 @@ $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 Files # Install Systemd Unit File
# Copy NWA/current/scripts/nwa.service to ~/.config/systemd/user # Copy SITENAME.service template to ~/.config/systemd/user/${SITENAME}.service
# ------------------------------------------------------------ # ------------------------------------------------------------
if use_systemd; then if use_systemd; then
echo "" echo ""
echo "Installing SystemD Unit Files" echo "Installing SystemD Unit File"
mkdir -p ~/.config/systemd/user USER_SYSTEMD_DIR="$HOME/.config/systemd/user"
cp "$NWA/scripts/nwa.service" ~/.config/systemd/user/ SERVICE_NAME="${SITENAME}.service"
SERVICE_TEMPLATE="$NWA/scripts/SITENAME.service"
SERVICE_FILE="$USER_SYSTEMD_DIR/$SERVICE_NAME"
"${SITE}/scripts/stop-nwa.sh" if [[ ! -f "$SERVICE_TEMPLATE" ]]; then
sleep 1 echo "ERROR: systemd service template not found: $SERVICE_TEMPLATE"
systemctl --user daemon-reload --no-pager
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 exit 1
fi 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
#
echo "" echo ""

View File

@ -1,10 +1,12 @@
{ {
"METADATA": { "METADATA": {
"name": "nwa", "name": "nwa",
"project": "Node Web App",
"description": "Node Web App", "description": "Node Web App",
"abbr": "NWA", "abbr": "NWA",
"version": "0.1.1", "icon": "fa-solid fa-dice-d20",
"version_date": "2026-07-04", "version": "0.1.2",
"version_date": "2026-07-17",
"maturity": "PROD", "maturity": "PROD",
"author": "John Haverlack", "author": "John Haverlack",
"copyright": "2026 John Haverlack", "copyright": "2026 John Haverlack",

View File

@ -27,6 +27,21 @@ 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(

View File

@ -1,7 +1,7 @@
{ {
"name": "nwa", "name": "nwa",
"version": "0.1.1", "version": "0.1.2",
"version_date": "2026-07-04", "version_date": "2026-07-17",
"description": "Node Web App", "description": "Node Web App",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
@ -23,25 +23,15 @@
"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",
"chart.js": "^4.5.1", "csv-parse": "^7.0.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"
} }
} }

View File

@ -1,25 +1,13 @@
{ {
"sdl":{ "SIDE_PANEL": {
"enabled": false
},
"NAV_ITEMS": {
"nwa":{
"title": "NWA", "title": "NWA",
"desc": "Node Web App", "desc": "Node Web App",
"icon": "fa-solid fa-dice-d20", "icon": "fa-solid fa-dice-d20",
"page": "index", "page": "index",
"enabled": true,
"items": []
},
"dash": {
"title": "Dash",
"desc": "Dashboard",
"icon": "fa-solid fa-gauge",
"page": "dash",
"enabled": false,
"items": []
},
"wkrs": {
"title": "Workers",
"desc": "Workers",
"icon": "fa-solid fa-server",
"page": "wkrs",
"enabled": false, "enabled": false,
"items": [] "items": []
}, },
@ -28,25 +16,17 @@
"desc": "Projects", "desc": "Projects",
"icon": "fa-solid fa-layer-group", "icon": "fa-solid fa-layer-group",
"page": "proj", "page": "proj",
"enabled": true, "enabled": false,
"items": [ "items": [
{ {
"title": "Experiments", "title": "Project 1",
"desc": "Experiments", "desc": "Project 1",
"icon": "fa-solid fa-flask", "icon": "fa-solid fa-flask",
"page": "expr", "page": "expr",
"enabled": false "enabled": false
} }
] ]
}, },
"jobs": {
"title": "Jobs",
"desc": "Jobs",
"icon": "fa-solid fa-person-digging",
"page": "jobs",
"enabled": false,
"items": []
},
"app": { "app": {
"title": "App", "title": "App",
"icon": "fa-solid fa-dice-d20", "icon": "fa-solid fa-dice-d20",
@ -71,45 +51,6 @@
"enabled": false "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
}
]
} }
} }

View File

@ -52,8 +52,6 @@
<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>

View File

@ -1,50 +1,91 @@
console.log('Settings.js loaded'); import {
loadNwaConfig,
resolveTheme
} from '/js/weblib.js';
// Get current theme from localStorage console.log('settings.js loaded');
const currentTheme = localStorage.getItem('theme') || 'dark';
console.log('Current theme:', currentTheme);
// Set the toggle to match current theme initializeSettings();
const themeSwitch = document.getElementById('themeSwitch');
const themeLabel = document.querySelector('label[for="themeSwitch"]');
console.log('Theme switch element found:', themeSwitch); async function initializeSettings() {
let nwaConfig = {};
if (themeSwitch && themeLabel) { try {
themeSwitch.checked = (currentTheme === 'dark'); nwaConfig = await loadNwaConfig();
} catch (err) {
console.error('Failed to load NWA config:', err);
}
// Update label based on current theme const themeSelect =
updateLabel(currentTheme); document.getElementById('themeSelect');
console.log('Switch checked state:', themeSwitch.checked); const themeDescription =
document.getElementById('themeDescription');
// Listen for changes if (!themeSelect || !themeDescription) {
themeSwitch.addEventListener('change', (e) => { console.error('Theme settings elements not found');
console.log('Switch toggled!', e.target.checked); return;
const newTheme = e.target.checked ? 'dark' : 'light'; }
console.log('Setting new theme:', newTheme);
setTheme(newTheme); const savedTheme = localStorage.getItem('theme');
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 setTheme(theme) { function updateThemeDescription(
console.log('setTheme called with:', theme); selectedTheme,
document.documentElement.setAttribute('data-bs-theme', theme); configuredDefault,
localStorage.setItem('theme', theme); descriptionElement
console.log('Theme set to:', theme); ) {
console.log('HTML data-bs-theme attribute:', document.documentElement.getAttribute('data-bs-theme')); if (selectedTheme === 'default') {
descriptionElement.textContent =
`Using the application default: ${configuredDefault}.`;
return;
} }
function updateLabel(theme) { descriptionElement.textContent =
const themeLabel = document.querySelector('label[for="themeSwitch"]'); `Using a browser-specific ${selectedTheme} theme override.`;
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';
}
}
} }

View File

@ -1,3 +1,4 @@
// 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);
@ -21,7 +22,9 @@ 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;
@ -150,6 +153,35 @@ 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() {
@ -271,37 +303,6 @@ 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);
@ -371,60 +372,7 @@ 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');
@ -432,37 +380,67 @@ async function loadSidePanel() {
if (!panel || !inner || !toggleBtn) return; if (!panel || !inner || !toggleBtn) return;
try {
const res = await fetch('/api/nav/sidepanel');
if (!res.ok) {
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 = const currentPage =
new URLSearchParams(window.location.search).get('page') || 'index'; new URLSearchParams(window.location.search).get('page') || 'index';
const isCollapsed = const isCollapsed =
localStorage.getItem('sidepanel_collapsed') === 'true'; localStorage.getItem('sidepanel_collapsed') === 'true';
if (isCollapsed) { panel.classList.toggle('collapsed', isCollapsed);
panel.classList.add('collapsed');
toggleBtn.innerHTML = '<i class="bi bi-layout-sidebar"></i>'; toggleBtn.innerHTML = `
} else { <i class="bi ${
toggleBtn.innerHTML = '<i class="bi bi-layout-sidebar-reverse"></i>'; isCollapsed
} ? 'bi-layout-sidebar'
: 'bi-layout-sidebar-reverse'
}"></i>
`;
try {
const res = await fetch('/api/nav/sidepanel');
if (!res.ok) throw new Error(`HTTP ${res.status}`); // ✅ Fixed template literal
const sideNav = await res.json();
renderAllSidePanelSections(inner, sideNav, currentPage); renderAllSidePanelSections(inner, sideNav, currentPage);
} catch (err) {
console.error('Failed to load side panel:', err);
inner.innerHTML = '';
}
// Collapse / expand behavior
toggleBtn.onclick = () => { toggleBtn.onclick = () => {
const collapsed = panel.classList.toggle('collapsed'); const collapsed = panel.classList.toggle('collapsed');
localStorage.setItem('sidepanel_collapsed', collapsed);
localStorage.setItem(
'sidepanel_collapsed',
String(collapsed)
);
toggleBtn.innerHTML = ` toggleBtn.innerHTML = `
<i class="bi ${collapsed ? 'bi-layout-sidebar' : 'bi-layout-sidebar-reverse'}"></i> <i class="bi ${
collapsed
? 'bi-layout-sidebar'
: 'bi-layout-sidebar-reverse'
}"></i>
`; `;
}; };
} catch (err) {
console.error('Failed to load side panel:', err);
panel.hidden = true;
inner.innerHTML = '';
} }
}
// ------------------------------- // -------------------------------
// Find matching section by page // Find matching section by page
@ -798,61 +776,6 @@ 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() {
@ -876,38 +799,100 @@ 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 () => {
// Apply saved theme (default to dark) const [configResult, nwaConfigResult] =
const savedTheme = localStorage.getItem('theme') || 'dark'; await Promise.allSettled([
document.documentElement.setAttribute('data-bs-theme', savedTheme); loadConfig(),
loadNwaConfig()
]);
const config = await loadConfig(); const config =
// console.log(`DEBUG: ${JSON.stringify(config)}`) configResult.status === 'fulfilled'
? configResult.value
: {};
// const tags = await loadTags(); const nwaConfig =
// console.log(`DEBUG: ${JSON.stringify(tags)}`) nwaConfigResult.status === 'fulfilled'
? 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}`);
// Update title and app description loadMarkdown(page);
if (config?.package?.description) { if (config?.package?.description) {
document.title = config.package.description; document.title = config.package.description;
const descEl = document.querySelector('.app-description'); const descEl =
if (descEl) descEl.innerHTML = config.package.description; document.querySelector('.app-description');
if (descEl) {
descEl.innerHTML = config.package.description;
}
} }
}); });

View File

@ -5,11 +5,22 @@
| **Author** | John Haverlack | | **Author** | John Haverlack |
| **Copyright** | 2026 John Haverlack | | **Copyright** | 2026 John Haverlack |
| **License** | MIT | | **License** | MIT |
| **Version** | 0.1.1 | | **Version** | 0.1.2 |
| **Date** | 2026-07-04 | | **Date** | 2026-07-17 |
## v0.1.2 - 2026-07-17 (PROD)
- v0.1.2
- App Updates from HaverLab
- Fixing Installer
- Updating Metadata
- WIP: Fixing Installer
- Fixing Deploy Dir
- Start v0.1.2
## v0.1.1 - 2026-07-04 (PROD) ## v0.1.1 - 2026-07-04 (PROD)
- Versioning Tool Updates
- Fixing Version Branch Tools - Fixing Version Branch Tools
- Start v0.1.1 - Start v0.1.1
- Adding Workflow Docs - Adding Workflow Docs
@ -19,10 +30,11 @@
## v0.1.0 - 2026-06-28 (INIT) ## v0.1.0 - 2026-06-28 (INIT)
- Fixing Version Branch Tools - v0.1.2
- Start v0.1.1 - App Updates from HaverLab
- Adding Workflow Docs - Fixing Installer
- WIP: Fixing installer - Updating Metadata
- Cleaning up Template - WIP: Fixing Installer
- Initial Commit Migrating NWA from SDL - Fixing Deploy Dir
- Start v0.1.2

View File

@ -6,19 +6,15 @@
<h5 class="mb-0">Appearance</h5> <h5 class="mb-0">Appearance</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row align-items-center"> <label for="themeSelect" class="form-label">
<div class="col-8"> <i class="fas fa-palette me-2"></i>Theme
<label for="themeSwitch" class="form-label mb-1">
<i class="fas fa-moon me-2"></i>Dark Mode
</label> </label>
<p class="text-muted small mb-0">Toggle between light and dark theme</p> <select class="form-select" id="themeSelect">
</div> <option value="default">Application Default</option>
<div class="col-4 text-end"> <option value="light">Light</option>
<div class="form-check form-switch d-inline-block"> <option value="dark">Dark</option>
<input class="form-check-input" type="checkbox" role="switch" id="themeSwitch" checked> </select>
</div> <p class="text-muted small mt-2 mb-0" id="themeDescription"></p>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,31 @@
[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