Release v0.1.2
This commit is contained in:
commit
efde009faa
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
225
install.sh
225
install.sh
|
|
@ -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,46 +408,62 @@ $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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "NWA deployed:"
|
echo "NWA deployed:"
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
updateLabel(newTheme);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.error('themeSwitch or themeLabel element not found!');
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTheme(theme) {
|
const savedTheme = localStorage.getItem('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 configuredDefault =
|
||||||
const themeLabel = document.querySelector('label[for="themeSwitch"]');
|
nwaConfig?.APPEARANCE?.default_theme === 'light'
|
||||||
if (themeLabel) {
|
? 'light'
|
||||||
if (theme === 'dark') {
|
: 'dark';
|
||||||
themeLabel.innerHTML = '<i class="fas fa-moon me-2"></i>Dark Mode';
|
|
||||||
|
// Show whether the browser is using an override
|
||||||
|
// or the application-configured default.
|
||||||
|
if (savedTheme === 'light' || savedTheme === 'dark') {
|
||||||
|
themeSelect.value = savedTheme;
|
||||||
} else {
|
} else {
|
||||||
themeLabel.innerHTML = '<i class="fas fa-sun me-2"></i>Light Mode';
|
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.`;
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -368,63 +369,10 @@ 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,38 +380,68 @@ 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in New Issue