App Updates from HaverLab
This commit is contained in:
parent
72845bb6e9
commit
1a74e741c5
|
|
@ -291,6 +291,7 @@ LICENSE="$(jq -r '.METADATA.license' "$META_FILE")"
|
|||
HOMEPAGE="$(jq -r '.METADATA.homepage' "$META_FILE")"
|
||||
REPO="$(jq -r '.METADATA.git_repo' "$META_FILE")"
|
||||
|
||||
USERNAME="$(id -un)"
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# NodeJS detection
|
||||
|
|
@ -446,6 +447,7 @@ if use_systemd; then
|
|||
sed \
|
||||
-e "s#SITENAME#${SITENAME}#g" \
|
||||
-e "s#PROJECT#${PROJECT}#g" \
|
||||
-e "s#USERNAME#${USERNAME}#g" \
|
||||
"$SERVICE_TEMPLATE" > "$SERVICE_FILE"
|
||||
|
||||
systemctl --user daemon-reload
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
<link rel="stylesheet" href="/css/nwa.css" />
|
||||
<link rel="stylesheet" href="/css/custom-styles.css" />
|
||||
|
||||
<script>
|
||||
<script>
|
||||
window.MathJax = {
|
||||
tex: {
|
||||
inlineMath: [["$", "$"], ["\\(", "\\)"]],
|
||||
|
|
@ -52,8 +52,6 @@
|
|||
|
||||
<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 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
|
||||
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 = '<i class="fas fa-moon me-2"></i>Dark Mode';
|
||||
} else {
|
||||
themeLabel.innerHTML = '<i class="fas fa-sun me-2"></i>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.`;
|
||||
}
|
||||
|
|
@ -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,63 +369,10 @@ 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 = `<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() {
|
||||
const panel = document.getElementById('sidepanel');
|
||||
const inner = document.getElementById('sidepanel-inner');
|
||||
|
|
@ -432,38 +380,68 @@ async function loadSidePanel() {
|
|||
|
||||
if (!panel || !inner || !toggleBtn) return;
|
||||
|
||||
const currentPage =
|
||||
new URLSearchParams(window.location.search).get('page') || 'index';
|
||||
const isCollapsed =
|
||||
localStorage.getItem('sidepanel_collapsed') === 'true';
|
||||
|
||||
if (isCollapsed) {
|
||||
panel.classList.add('collapsed');
|
||||
toggleBtn.innerHTML = '<i class="bi bi-layout-sidebar"></i>';
|
||||
} else {
|
||||
toggleBtn.innerHTML = '<i class="bi bi-layout-sidebar-reverse"></i>';
|
||||
}
|
||||
|
||||
try {
|
||||
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 = `
|
||||
<i class="bi ${
|
||||
isCollapsed
|
||||
? 'bi-layout-sidebar'
|
||||
: 'bi-layout-sidebar-reverse'
|
||||
}"></i>
|
||||
`;
|
||||
|
||||
renderAllSidePanelSections(inner, sideNav, currentPage);
|
||||
|
||||
toggleBtn.onclick = () => {
|
||||
const collapsed = panel.classList.toggle('collapsed');
|
||||
|
||||
localStorage.setItem(
|
||||
'sidepanel_collapsed',
|
||||
String(collapsed)
|
||||
);
|
||||
|
||||
toggleBtn.innerHTML = `
|
||||
<i class="bi ${
|
||||
collapsed
|
||||
? 'bi-layout-sidebar'
|
||||
: 'bi-layout-sidebar-reverse'
|
||||
}"></i>
|
||||
`;
|
||||
};
|
||||
} catch (err) {
|
||||
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 = `
|
||||
<i class="bi ${collapsed ? 'bi-layout-sidebar' : 'bi-layout-sidebar-reverse'}"></i>
|
||||
`;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// -------------------------------
|
||||
// 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 `<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() {
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -6,19 +6,15 @@
|
|||
<h5 class="mb-0">Appearance</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-8">
|
||||
<label for="themeSwitch" class="form-label mb-1">
|
||||
<i class="fas fa-moon me-2"></i>Dark Mode
|
||||
</label>
|
||||
<p class="text-muted small mb-0">Toggle between light and dark theme</p>
|
||||
</div>
|
||||
<div class="col-4 text-end">
|
||||
<div class="form-check form-switch d-inline-block">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="themeSwitch" checked>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label for="themeSelect" class="form-label">
|
||||
<i class="fas fa-palette me-2"></i>Theme
|
||||
</label>
|
||||
<select class="form-select" id="themeSelect">
|
||||
<option value="default">Application Default</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
</select>
|
||||
<p class="text-muted small mt-2 mb-0" id="themeDescription"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue