diff --git a/README.md b/README.md index c98d838..724154e 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ | **Author** | John Haverlack | | **Copyright** | 2026 John Haverlack | | **License** | MIT | -| **Version** | 0.1.1 | -| **Date** | 2026-07-04 | +| **Version** | 0.1.2 | +| **Date** | 2026-07-17 | ## Overview diff --git a/changelog.json b/changelog.json index 4b03ce3..350af87 100644 --- a/changelog.json +++ b/changelog.json @@ -1,5 +1,12 @@ { "releases": [ + { + "version": "0.1.2", + "date": "2026-07-17", + "maturity": "PROD", + "summary": "", + "notes": "" + }, { "version": "0.1.1", "date": "2026-07-04", diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 8303d91..d830950 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,11 +5,22 @@ | **Author** | John Haverlack | | **Copyright** | 2026 John Haverlack | | **License** | MIT | -| **Version** | 0.1.1 | -| **Date** | 2026-07-04 | +| **Version** | 0.1.2 | +| **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) +- Versioning Tool Updates - Fixing Version Branch Tools - Start v0.1.1 - Adding Workflow Docs @@ -19,10 +30,11 @@ ## v0.1.0 - 2026-06-28 (INIT) -- Fixing Version Branch Tools -- Start v0.1.1 -- Adding Workflow Docs -- WIP: Fixing installer -- Cleaning up Template -- Initial Commit Migrating NWA from SDL +- v0.1.2 +- App Updates from HaverLab +- Fixing Installer +- Updating Metadata +- WIP: Fixing Installer +- Fixing Deploy Dir +- Start v0.1.2 diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 2adf54a..cb3fe84 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -5,8 +5,8 @@ | **Author** | John Haverlack | | **Copyright** | 2026 John Haverlack | | **License** | MIT | -| **Version** | 0.1.1 | -| **Date** | 2026-07-04 | +| **Version** | 0.1.2 | +| **Date** | 2026-07-17 | ## Overview diff --git a/install.sh b/install.sh index 07f0633..f0bd849 100755 --- a/install.sh +++ b/install.sh @@ -8,39 +8,45 @@ set -euo pipefail resolve_dirs() { local json="$1" + local sitename="$2" 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 raw["$key"]="$val" done < <(jq -r '.DIRS | to_entries[] | "\(.key)=\(.value)"' "$json") - if [[ -z "${raw[SITE]:-}" ]]; then - echo "ERROR: DIRS.SITE is not defined in nwa-deploy-config.json" - exit 1 - fi - - resolved["SITE"]="${raw[SITE]/#\~/$HOME}" + # SITE is derived from metadata.json, not from deploy config. + resolved["SITE"]="$HOME/.$sitename" local changed=true while $changed; do changed=false for key in "${!raw[@]}"; do + [[ "$key" == "SITE" ]] && continue [[ -n "${resolved[$key]:-}" ]] && continue local val="${raw[$key]}" + for rkey in "${!resolved[@]}"; do val="${val/$rkey/${resolved[$rkey]:-}}" done - # if [[ "$val" != ** ]]; then - # resolved["$key"]="$val" - # changed=true - # fi - local unresolved=false 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 break fi @@ -54,6 +60,8 @@ resolve_dirs() { done for key in "${!raw[@]}"; do + [[ "$key" == "SITE" ]] && continue + [[ -z "${resolved[$key]:-}" ]] && { echo "ERROR: Unresolved DIR: $key=${raw[$key]}" exit 1 @@ -67,6 +75,8 @@ resolve_dirs() { done } + + use_systemd() { # Check if systemd user mode is available and service exists if systemctl --user status >/dev/null 2>&1; then @@ -270,15 +280,18 @@ fi # ------------------------------------------------------------ # 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_DATE="$(jq -r '.METADATA.version_date' "$META_FILE")" COPYRIGHT="$(jq -r '.METADATA.copyright' "$META_FILE")" AUTHOR="$(jq -r '.METADATA.author' "$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")" +USERNAME="$(id -un)" # ------------------------------------------------------------ # NodeJS detection @@ -291,12 +304,13 @@ NODE_PLATFORMS="${NODE_PLATFORMS%% }" echo "####################################" echo "Node Web App (NWA)" echo "####################################" +echo " Sitename: $SITENAME" echo " Version: $VERSION" echo " Version Date: $VERSION_DATE" echo " Copyright: (C) $COPYRIGHT" echo " Author: $AUTHOR" echo " License: $LICENSE" -echo " SITEpage: $SITEPAGE" +echo " Homepage: $HOMEPAGE" echo " Repository: $REPO" echo "" @@ -325,7 +339,8 @@ fi echo "Directories:" echo " ROOT: $ROOT" -resolve_dirs "$CONFIG_FILE" +# resolve_dirs "$CONFIG_FILE" +resolve_dirs "$CONFIG_FILE" "$SITENAME" # ------------------------------------------------------------ # Copy nwa to Prod Dir @@ -342,65 +357,31 @@ NWA="$VERSIONS/current/nwa" # ------------------------------------------------------------ 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 # ------------------------------------------------------------ -# 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 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" 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 # ------------------------------------------------------------ @@ -413,44 +394,6 @@ NODE_BIN="$NODEJS/current/bin/node" NPM_BIN="$NODEJS/current/bin/npm" # 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 @@ -465,46 +408,62 @@ $NPM_BIN install # ------------------------------------------------------------ # Copy Start / Stop Scripts # ------------------------------------------------------------ -mkdir -p "$SITE/scripts/" -cp "$NWA/scripts/start-nwa.sh" "$SITE/scripts/" -cp "$NWA/scripts/stop-nwa.sh" "$SITE/scripts/" -cp "$NWA/scripts/status-nwa.sh" "$SITE/scripts/" -chmod u+x "$SITE/scripts/"* - +# mkdir -p "$SITE/scripts/" +# cp "$NWA/scripts/start-nwa.sh" "$SITE/scripts/" +# cp "$NWA/scripts/stop-nwa.sh" "$SITE/scripts/" +# cp "$NWA/scripts/status-nwa.sh" "$SITE/scripts/" +# chmod u+x "$SITE/scripts/"* # ------------------------------------------------------------ -# Install Systemd Unit Files -# Copy NWA/current/scripts/nwa.service to ~/.config/systemd/user +# Install Systemd Unit File +# Copy SITENAME.service template to ~/.config/systemd/user/${SITENAME}.service # ------------------------------------------------------------ if use_systemd; then echo "" - echo "Installing SystemD Unit Files" + echo "Installing SystemD Unit File" - mkdir -p ~/.config/systemd/user - cp "$NWA/scripts/nwa.service" ~/.config/systemd/user/ + USER_SYSTEMD_DIR="$HOME/.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" - sleep 1 - systemctl --user daemon-reload --no-pager + if [[ ! -f "$SERVICE_TEMPLATE" ]]; then + echo "ERROR: systemd service template not found: $SERVICE_TEMPLATE" + exit 1 + 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 -# ------------------------------------------------------------ -# 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 "NWA deployed:" diff --git a/metadata.json b/metadata.json index 77f83e3..8cec6b2 100644 --- a/metadata.json +++ b/metadata.json @@ -1,10 +1,12 @@ { "METADATA": { "name": "nwa", + "project": "Node Web App", "description": "Node Web App", "abbr": "NWA", - "version": "0.1.1", - "version_date": "2026-07-04", + "icon": "fa-solid fa-dice-d20", + "version": "0.1.2", + "version_date": "2026-07-17", "maturity": "PROD", "author": "John Haverlack", "copyright": "2026 John Haverlack", diff --git a/nwa/app/modules/web/index.js b/nwa/app/modules/web/index.js index 9e6fd95..88cbf6f 100644 --- a/nwa/app/modules/web/index.js +++ b/nwa/app/modules/web/index.js @@ -27,6 +27,21 @@ app.get('/api/config', (req, res) => { 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 app.get('/api/nav/sidepanel', (req, res) => { const sideMenuPath = path.join( diff --git a/nwa/app/package.json b/nwa/app/package.json index 5a26259..2679da8 100644 --- a/nwa/app/package.json +++ b/nwa/app/package.json @@ -1,7 +1,7 @@ { "name": "nwa", - "version": "0.1.1", - "version_date": "2026-07-04", + "version": "0.1.2", + "version_date": "2026-07-17", "description": "Node Web App", "main": "index.js", "type": "module", @@ -23,25 +23,15 @@ "dependencies": { "@alenaksu/json-viewer": "^2.0.1", "@fortawesome/fontawesome-free": "^6.7.2", - "aedes": "^0.51.3", "bootstrap": "^5.3.7", "bootstrap-icons": "^1.13.1", - "chart.js": "^4.5.1", - "csv-parse": "^5.6.0", - "d3": "^7.9.0", + "csv-parse": "^7.0.1", "express": "^5.1.0", - "graph.js": "^1.21.1", - "highlight.js": "^11.9.0", - "html-to-image": "^1.11.13", "marked": "^15.0.11", "mathjax-full": "^3.2.1", - "mqtt": "^5.14.1", "node-fetch": "^3.3.2", "open": "^11.0.0", "serve-index": "^1.9.1", - "three": "^0.181.2", - "websocket-stream": "^5.5.2", - "ws": "^8.18.3", "yargs": "^18.0.0" } } diff --git a/nwa/html/conf/nav-sidepanel.json b/nwa/html/conf/nav-sidepanel.json index 0f41ead..edecf60 100644 --- a/nwa/html/conf/nav-sidepanel.json +++ b/nwa/html/conf/nav-sidepanel.json @@ -1,115 +1,56 @@ { - "sdl":{ - "title": "NWA", - "desc": "Node Web App", - "icon": "fa-solid fa-dice-d20", - "page": "index", - "enabled": true, - "items": [] + "SIDE_PANEL": { + "enabled": false }, - "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, - "items": [] - }, - "proj": { - "title": "Projects", - "desc": "Projects", - "icon": "fa-solid fa-layer-group", - "page": "proj", - "enabled": true, - "items": [ - { - "title": "Experiments", - "desc": "Experiments", - "icon": "fa-solid fa-flask", - "page": "expr", - "enabled": false - } - ] - }, - "jobs": { - "title": "Jobs", - "desc": "Jobs", - "icon": "fa-solid fa-person-digging", - "page": "jobs", - "enabled": false, - "items": [] - }, - "app": { - "title": "App", - "icon": "fa-solid fa-dice-d20", - "enabled": false, - "items": [ - { - "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 - } - ] + "NAV_ITEMS": { + "nwa":{ + "title": "NWA", + "desc": "Node Web App", + "icon": "fa-solid fa-dice-d20", + "page": "index", + "enabled": false, + "items": [] + }, + "proj": { + "title": "Projects", + "desc": "Projects", + "icon": "fa-solid fa-layer-group", + "page": "proj", + "enabled": false, + "items": [ + { + "title": "Project 1", + "desc": "Project 1", + "icon": "fa-solid fa-flask", + "page": "expr", + "enabled": false + } + ] + }, + "app": { + "title": "App", + "icon": "fa-solid fa-dice-d20", + "enabled": false, + "items": [ + { + "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 + } + ] + } } } diff --git a/nwa/html/index.html b/nwa/html/index.html index 7c66dfe..68f604a 100644 --- a/nwa/html/index.html +++ b/nwa/html/index.html @@ -22,7 +22,7 @@ - - - diff --git a/nwa/html/js/settings.js b/nwa/html/js/settings.js index 70a10f7..c90b47e 100644 --- a/nwa/html/js/settings.js +++ b/nwa/html/js/settings.js @@ -1,50 +1,91 @@ -console.log('Settings.js loaded'); +import { + loadNwaConfig, + resolveTheme +} from '/js/weblib.js'; -// Get current theme from localStorage -const currentTheme = localStorage.getItem('theme') || 'dark'; -console.log('Current theme:', currentTheme); +console.log('settings.js loaded'); -// Set the toggle to match current theme -const themeSwitch = document.getElementById('themeSwitch'); -const themeLabel = document.querySelector('label[for="themeSwitch"]'); +initializeSettings(); -console.log('Theme switch element found:', themeSwitch); +async function initializeSettings() { + let nwaConfig = {}; -if (themeSwitch && themeLabel) { - themeSwitch.checked = (currentTheme === 'dark'); - - // Update label based on current theme - updateLabel(currentTheme); - - console.log('Switch checked state:', themeSwitch.checked); - - // Listen for changes - themeSwitch.addEventListener('change', (e) => { - console.log('Switch toggled!', e.target.checked); - const newTheme = e.target.checked ? 'dark' : 'light'; - console.log('Setting new theme:', newTheme); - setTheme(newTheme); - updateLabel(newTheme); - }); -} else { - console.error('themeSwitch or themeLabel element not found!'); -} - -function setTheme(theme) { - console.log('setTheme called with:', theme); - document.documentElement.setAttribute('data-bs-theme', theme); - localStorage.setItem('theme', theme); - console.log('Theme set to:', theme); - console.log('HTML data-bs-theme attribute:', document.documentElement.getAttribute('data-bs-theme')); -} - -function updateLabel(theme) { - const themeLabel = document.querySelector('label[for="themeSwitch"]'); - if (themeLabel) { - if (theme === 'dark') { - themeLabel.innerHTML = 'Dark Mode'; - } else { - themeLabel.innerHTML = 'Light Mode'; - } + try { + nwaConfig = await loadNwaConfig(); + } catch (err) { + console.error('Failed to load NWA config:', err); } + + const themeSelect = + document.getElementById('themeSelect'); + + const themeDescription = + document.getElementById('themeDescription'); + + if (!themeSelect || !themeDescription) { + console.error('Theme settings elements not found'); + return; + } + + const savedTheme = localStorage.getItem('theme'); + + 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 + ); + }); +} + +function updateThemeDescription( + selectedTheme, + configuredDefault, + descriptionElement +) { + if (selectedTheme === 'default') { + descriptionElement.textContent = + `Using the application default: ${configuredDefault}.`; + return; + } + + descriptionElement.textContent = + `Using a browser-specific ${selectedTheme} theme override.`; } \ No newline at end of file diff --git a/nwa/html/js/weblib.js b/nwa/html/js/weblib.js index fc19627..afc955f 100644 --- a/nwa/html/js/weblib.js +++ b/nwa/html/js/weblib.js @@ -1,3 +1,4 @@ +// weblib.js // Dynamically include fragments async function includeFragment(id, url) { const res = await fetch(url); @@ -21,7 +22,9 @@ async function loadMarkdown(page) { ]); 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) ---- 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 function loadNav() { @@ -271,37 +303,6 @@ function loadNav() { navContainer.innerHTML = ''; 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 => { console.error('Failed to load nav:', err); @@ -368,102 +369,79 @@ function loadNavTabs() { - // =============================== +// =============================== // 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 = ``; -// toggleBtn.innerHTML = ''; -// } else { -// // toggleBtn.innerHTML = ``; -// toggleBtn.innerHTML = ''; -// } - -// 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 = ` -// // -// // `; - -// toggleBtn.innerHTML = ` -// -// `; -// }; -// } async function loadSidePanel() { 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 = ''; - } else { - toggleBtn.innerHTML = ''; - } - + 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(); + + 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 = + new URLSearchParams(window.location.search).get('page') || 'index'; + + const isCollapsed = + localStorage.getItem('sidepanel_collapsed') === 'true'; + + panel.classList.toggle('collapsed', isCollapsed); + + toggleBtn.innerHTML = ` + + `; + renderAllSidePanelSections(inner, sideNav, currentPage); + + toggleBtn.onclick = () => { + const collapsed = panel.classList.toggle('collapsed'); + + localStorage.setItem( + 'sidepanel_collapsed', + String(collapsed) + ); + + toggleBtn.innerHTML = ` + + `; + }; } catch (err) { console.error('Failed to load side panel:', err); + panel.hidden = true; inner.innerHTML = ''; } - - // Collapse / expand behavior - toggleBtn.onclick = () => { - const collapsed = panel.classList.toggle('collapsed'); - localStorage.setItem('sidepanel_collapsed', collapsed); - toggleBtn.innerHTML = ` - - `; - }; } + // ------------------------------- // Find matching section by page // ------------------------------- @@ -798,61 +776,6 @@ export async function getConfig() { 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 `${tag}`; - -// 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 ` -// ${t.name} -// `; -// } - - export async function getLocalTimestamp() { @@ -876,38 +799,100 @@ export async function getUTCTimestamp() { // =============================== // 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 () => { - // Apply saved theme (default to dark) - const savedTheme = localStorage.getItem('theme') || 'dark'; - document.documentElement.setAttribute('data-bs-theme', savedTheme); + const [configResult, nwaConfigResult] = + await Promise.allSettled([ + loadConfig(), + loadNwaConfig() + ]); - const config = await loadConfig(); - // console.log(`DEBUG: ${JSON.stringify(config)}`) + const config = + configResult.status === 'fulfilled' + ? configResult.value + : {}; - // const tags = await loadTags(); - // console.log(`DEBUG: ${JSON.stringify(tags)}`) + const nwaConfig = + 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('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 + loadMarkdown(page); + if (config?.package?.description) { document.title = config.package.description; - const descEl = document.querySelector('.app-description'); - if (descEl) descEl.innerHTML = config.package.description; + const descEl = + document.querySelector('.app-description'); + + if (descEl) { + descEl.innerHTML = config.package.description; + } } }); \ No newline at end of file diff --git a/nwa/html/md/changelog.md b/nwa/html/md/changelog.md index 8303d91..d830950 100644 --- a/nwa/html/md/changelog.md +++ b/nwa/html/md/changelog.md @@ -5,11 +5,22 @@ | **Author** | John Haverlack | | **Copyright** | 2026 John Haverlack | | **License** | MIT | -| **Version** | 0.1.1 | -| **Date** | 2026-07-04 | +| **Version** | 0.1.2 | +| **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) +- Versioning Tool Updates - Fixing Version Branch Tools - Start v0.1.1 - Adding Workflow Docs @@ -19,10 +30,11 @@ ## v0.1.0 - 2026-06-28 (INIT) -- Fixing Version Branch Tools -- Start v0.1.1 -- Adding Workflow Docs -- WIP: Fixing installer -- Cleaning up Template -- Initial Commit Migrating NWA from SDL +- v0.1.2 +- App Updates from HaverLab +- Fixing Installer +- Updating Metadata +- WIP: Fixing Installer +- Fixing Deploy Dir +- Start v0.1.2 diff --git a/nwa/html/md/settings.md b/nwa/html/md/settings.md index a88e16e..661ac95 100644 --- a/nwa/html/md/settings.md +++ b/nwa/html/md/settings.md @@ -6,19 +6,15 @@
Toggle between light and dark theme
-