diff --git a/public/css/style.css b/public/css/style.css
index 70a794e..2b93ef6 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -2695,4 +2695,72 @@ input:checked+.slider:before {
color: var(--accent-indigo);
background: rgba(99, 102, 241, 0.1);
border-color: var(--accent-indigo);
+}
+
+/* ---- Footer ---- */
+.site-footer {
+ margin-top: 40px;
+ padding: 30px 28px;
+ border-top: 1px solid var(--border-color);
+ background: rgba(10, 14, 26, 0.4);
+ backdrop-filter: blur(10px);
+ position: relative;
+ z-index: 10;
+}
+
+:root.light-theme .site-footer {
+ background: rgba(255, 255, 255, 0.4);
+}
+
+.footer-content {
+ max-width: 1600px;
+ margin: 0 auto;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 20px;
+}
+
+.copyright {
+ font-size: 0.88rem;
+ color: var(--text-muted);
+ font-weight: 500;
+}
+
+.filings {
+ display: flex;
+ align-items: center;
+ gap: 20px;
+}
+
+.filings a {
+ font-size: 0.82rem;
+ color: var(--text-muted);
+ text-decoration: none;
+ transition: color 0.2s;
+ display: flex;
+ align-items: center;
+}
+
+.filings a:hover {
+ color: var(--accent-indigo);
+}
+
+@media (max-width: 768px) {
+ .site-footer {
+ padding: 24px 16px;
+ margin-top: 20px;
+ }
+
+ .footer-content {
+ flex-direction: column;
+ text-align: center;
+ gap: 12px;
+ }
+
+ .filings {
+ flex-direction: column;
+ gap: 8px;
+ }
}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index bb91bb7..8875c80 100644
--- a/public/index.html
+++ b/public/index.html
@@ -354,6 +354,20 @@
+
+
+
@@ -467,6 +481,14 @@
+
+
+
+
+
+
+
+
diff --git a/public/js/app.js b/public/js/app.js
index bee5886..7a4552c 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -105,7 +105,14 @@
partitionSummary: document.getElementById('partitionSummary'),
partitionHeader: document.getElementById('partitionHeader'),
globeCard: document.getElementById('globeCard'),
- btnExpandGlobe: document.getElementById('btnExpandGlobe')
+ btnExpandGlobe: document.getElementById('btnExpandGlobe'),
+ // Footer & Filing
+ icpFilingInput: document.getElementById('icpFilingInput'),
+ psFilingInput: document.getElementById('psFilingInput'),
+ icpFilingDisplay: document.getElementById('icpFilingDisplay'),
+ psFilingDisplay: document.getElementById('psFilingDisplay'),
+ psFilingText: document.getElementById('psFilingText'),
+ copyrightYear: document.getElementById('copyrightYear')
};
// ---- State ----
@@ -146,6 +153,11 @@
updateGaugesTime();
setInterval(updateGaugesTime, 1000);
+ // Initial footer year
+ if (dom.copyrightYear) {
+ dom.copyrightYear.textContent = new Date().getFullYear();
+ }
+
// Initial theme check (localStorage handled after site settings load to ensure priority)
// Network chart
@@ -473,6 +485,8 @@
if (dom.defaultThemeInput) dom.defaultThemeInput.value = window.SITE_SETTINGS.default_theme || 'dark';
if (dom.show95BandwidthInput) dom.show95BandwidthInput.value = window.SITE_SETTINGS.show_95_bandwidth ? "1" : "0";
if (dom.p95TypeSelect) dom.p95TypeSelect.value = window.SITE_SETTINGS.p95_type || 'tx';
+ if (dom.icpFilingInput) dom.icpFilingInput.value = window.SITE_SETTINGS.icp_filing || '';
+ if (dom.psFilingInput) dom.psFilingInput.value = window.SITE_SETTINGS.ps_filing || '';
// Latency routes loaded separately in openSettings or on startup
}
@@ -1639,6 +1653,8 @@
networkChart.draw();
}
}
+ if (dom.icpFilingInput) dom.icpFilingInput.value = settings.icp_filing || '';
+ if (dom.psFilingInput) dom.psFilingInput.value = settings.ps_filing || '';
// Apply to UI
applySiteSettings(settings);
@@ -1726,6 +1742,25 @@
applyTheme(settings.default_theme);
}
}
+
+ // Filing info
+ if (dom.icpFilingDisplay) {
+ if (settings.icp_filing) {
+ dom.icpFilingDisplay.textContent = settings.icp_filing;
+ dom.icpFilingDisplay.style.display = 'inline-block';
+ } else {
+ dom.icpFilingDisplay.style.display = 'none';
+ }
+ }
+
+ if (dom.psFilingDisplay) {
+ if (settings.ps_filing) {
+ if (dom.psFilingText) dom.psFilingText.textContent = settings.ps_filing;
+ dom.psFilingDisplay.style.display = 'inline-block';
+ } else {
+ dom.psFilingDisplay.style.display = 'none';
+ }
+ }
}
async function saveSiteSettings() {
@@ -1741,7 +1776,9 @@
logo_url: dom.logoUrlInput.value.trim(),
default_theme: dom.defaultThemeInput.value,
show_95_bandwidth: dom.show95BandwidthInput.value === "1" ? 1 : 0,
- p95_type: dom.p95TypeSelect.value
+ p95_type: dom.p95TypeSelect.value,
+ icp_filing: dom.icpFilingInput ? dom.icpFilingInput.value : '',
+ ps_filing: dom.psFilingInput ? dom.psFilingInput.value : ''
};
// If user sets default to auto, we should clear their manual override or set it to auto
diff --git a/server/db-integrity-check.js b/server/db-integrity-check.js
index ae2d139..9527efd 100644
--- a/server/db-integrity-check.js
+++ b/server/db-integrity-check.js
@@ -87,6 +87,16 @@ async function checkAndFixDatabase() {
await db.query("ALTER TABLE site_settings ADD COLUMN latency_target VARCHAR(255) AFTER latency_dest");
console.log(`[Database Integrity] ✅ Column 'latency_target' added.`);
}
+ if (!columnNames.includes('icp_filing')) {
+ console.log(`[Database Integrity] ⚠️ Missing column 'icp_filing' in 'site_settings'. Adding it...`);
+ await db.query("ALTER TABLE site_settings ADD COLUMN icp_filing VARCHAR(255) AFTER latency_target");
+ console.log(`[Database Integrity] ✅ Column 'icp_filing' added.`);
+ }
+ if (!columnNames.includes('ps_filing')) {
+ console.log(`[Database Integrity] ⚠️ Missing column 'ps_filing' in 'site_settings'. Adding it...`);
+ await db.query("ALTER TABLE site_settings ADD COLUMN ps_filing VARCHAR(255) AFTER icp_filing");
+ console.log(`[Database Integrity] ✅ Column 'ps_filing' added.`);
+ }
} catch (err) {
console.error('[Database Integrity] ❌ Error checking integrity:', err.message);
}
@@ -132,6 +142,8 @@ async function createTable(tableName) {
latency_source VARCHAR(100),
latency_dest VARCHAR(100),
latency_target VARCHAR(255),
+ icp_filing VARCHAR(255),
+ ps_filing VARCHAR(255),
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`);
diff --git a/server/index.js b/server/index.js
index bdf1baf..8ab7f57 100644
--- a/server/index.js
+++ b/server/index.js
@@ -269,6 +269,8 @@ app.post('/api/setup/init', async (req, res) => {
latency_source VARCHAR(100),
latency_dest VARCHAR(100),
latency_target VARCHAR(255),
+ icp_filing VARCHAR(255),
+ ps_filing VARCHAR(255),
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`);
@@ -412,7 +414,9 @@ const serveIndex = async (req, res) => {
blackbox_source_id: null,
latency_source: null,
latency_dest: null,
- latency_target: null
+ latency_target: null,
+ icp_filing: null,
+ ps_filing: null
};
if (isDbInitialized) {
@@ -564,7 +568,9 @@ app.get('/api/settings', async (req, res) => {
blackbox_source_id: null,
latency_source: null,
latency_dest: null,
- latency_target: null
+ latency_target: null,
+ icp_filing: null,
+ ps_filing: null
});
}
res.json(rows[0]);
@@ -576,11 +582,11 @@ app.get('/api/settings', async (req, res) => {
// Update site settings
app.post('/api/settings', requireAuth, async (req, res) => {
- const { page_name, title, logo_url, default_theme, show_95_bandwidth, p95_type, blackbox_source_id, latency_source, latency_dest, latency_target } = req.body;
+ const { page_name, title, logo_url, default_theme, show_95_bandwidth, p95_type, blackbox_source_id, latency_source, latency_dest, latency_target, icp_filing, ps_filing } = req.body;
try {
await db.query(
- `INSERT INTO site_settings (id, page_name, title, logo_url, default_theme, show_95_bandwidth, p95_type, blackbox_source_id, latency_source, latency_dest, latency_target)
- VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `INSERT INTO site_settings (id, page_name, title, logo_url, default_theme, show_95_bandwidth, p95_type, blackbox_source_id, latency_source, latency_dest, latency_target, icp_filing, ps_filing)
+ VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
page_name = VALUES(page_name),
title = VALUES(title),
@@ -591,12 +597,15 @@ app.post('/api/settings', requireAuth, async (req, res) => {
blackbox_source_id = VALUES(blackbox_source_id),
latency_source = VALUES(latency_source),
latency_dest = VALUES(latency_dest),
- latency_target = VALUES(latency_target)`,
+ latency_target = VALUES(latency_target),
+ icp_filing = VALUES(icp_filing),
+ ps_filing = VALUES(ps_filing)`,
[
page_name, title, logo_url, default_theme,
show_95_bandwidth ? 1 : 0, p95_type || 'tx',
blackbox_source_id || null, latency_source || null,
- latency_dest || null, latency_target || null
+ latency_dest || null, latency_target || null,
+ icp_filing || null, ps_filing || null
]
);
res.json({ success: true });