diff --git a/public/css/styles.css b/public/css/styles.css index 727dda4..653c4e8 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -56,10 +56,8 @@ margin-top: 0; margin-bottom: 0; display: flex; - gap: 1.5rem; /* Adjust this value to control spacing between links */ - /* If you want them centered within the nav */ + gap: 1.5rem; justify-content: center; - /* Allow items to wrap to the next line on smaller screens */ flex-wrap: wrap; } footer nav p { @@ -101,25 +99,66 @@ color: #ecf0f1; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } -header#site-header .container { +/* header#site-header .container { display: flex; flex-direction: column; align-items: flex-start; gap: 0.5rem; padding: 1rem 0; +} */ +header#site-header .container { + max-width: 800px; + margin: 0 auto; + padding: 1rem 1.5rem; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + position: relative; } -header#site-header .logo { +/* header#site-header .logo { + display: flex; + align-items: center; font-size: 1.8rem; font-weight: bolder; position: relative; +} */ +/* Logo section - positioned above nav */ +header#site-header .logo { + display: flex; + align-items: center; + font-size: 1.8rem; + font-weight: bolder; + position: relative; + text-decoration: none; + color: inherit; + gap: 1rem; + margin-left: 10px; + padding-left: 64px; /* Space for the icon */ } -header#site-header .logo a { +/* .site-title { + margin: 0 1rem; + white-space: nowrap; + flex-shrink: 0; + color: inherit; + text-decoration: none; + font-weight: bolder; + font-size: + 1.8rem; +} */ +header#site-header .site-title { color: inherit; text-decoration: none; position: relative; display: inline-block; } -header#site-header .logo a::after { +header#site-header .site-title, a { + color: inherit; + text-decoration: none; + position: relative; + display: inline-block; +} +header#site-header .site-title a::after { content: ""; position: absolute; left: 0; @@ -131,9 +170,77 @@ transform-origin: left; transition: transform 0.3s ease; } -header#site-header .logo a:hover::after { +header#site-header .site-title a:hover::after { transform: scaleX(1); } +/* Logo icon that acts as home link - spans from site title to nav */ +.nav-logo { + text-decoration: none; + color: inherit; + display: flex; + align-items: flex-end; + position: absolute; + left: 1rem; + top: 1rem; + bottom: 1rem; +} +/* Favicon styles */ +#favicon-preview { + width: 64px; + height: 64px; + border-radius: 4px; + background-color: #333; + display: inline-block; + vertical-align: middle; +} +.concept { + width: 64px; + height: 64px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} +.icon { + width: 64px; + height: 64px; + display: flex; + justify-content: center; + align-items: center; + font-size: 24px; + user-select: none; +} +/* Deep Dive Lambda */ +.deep-dive { + background-color: #0A192F; + border-radius: 8px; + color: #00CED1; + font-weight: 700; + box-shadow: + 0 0 4px #00CED1, + 0 0 12px #00CED1; +} +/* Ghost Lambda */ +.ghost { + background-color: #F0F8FFCC; + border-radius: 8px; + color: #008B8B; + font-weight: 800; + text-shadow: 0 0 1px rgba(0, 0, 0, 0.2); +} +/* Circle of Logic */ +.circle { + background-color: #1E90FF; + border-radius: 50%; + color: #E0FFFF; + font-weight: 600; +} +div.concept { + display: none; +} +div.concept#favicon1 { + display: inline-block; +} .layout { display: flex; max-width: 1200px; @@ -246,6 +353,25 @@ nav.site-nav .dropdown-content form { margin: 0; } +/* Navigation row with consistent left alignment */ +.nav-row { + display: flex; + align-items: center; + gap: 1rem; + width: 100%; + padding-left: 64px; /* Same as icon width + gap to align with site title */ +} +/* Logo icon that acts as home link - spans from site title to nav */ +.nav-logo { + text-decoration: none; + color: inherit; + display: flex; + align-items: flex-end; + position: absolute; + left: 1rem; + top: 1rem; + bottom: 1rem; +} /* Reset */ * { margin: 0; @@ -487,6 +613,12 @@ .sidebar { width: 100%; } + header#site-header .container { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + header#site-header nav.site-nav { flex-direction: column; gap: 0; @@ -508,6 +640,22 @@ header#site-header #siteNav.hide a { display: none; } + nav.site-nav { + flex-direction: column; + gap: 0; + width: 100%; + } + .nav-row { + flex-direction: column; + align-items: flex-start; + width: 100%; + padding-left: 0; + } + .nav-logo { + position: relative; + left: 0; + margin-bottom: 0.5rem; + } nav.site-nav ul { display: block; justify-content: center; diff --git a/public/favicon-test.html b/public/favicon-test.html new file mode 100644 index 0000000..c4f0473 --- /dev/null +++ b/public/favicon-test.html @@ -0,0 +1,367 @@ + + + + + + Header Layout Fix + + + + + + + + diff --git a/public/favicon.html b/public/favicon.html new file mode 100644 index 0000000..d7253dd --- /dev/null +++ b/public/favicon.html @@ -0,0 +1,95 @@ + + + + + +Lambda Favicon Concepts + + + + +
+
λ
+
Deep Dive Lambda
Dark navy background, glowing cyan λ
+
+ +
+
λ
+
Ghost Lambda
Light transparent background, solid cyan λ
+
+ +
+
λ
+
Circle of Logic
Blue circle, white/light cyan λ
+
+ + + diff --git a/public/header.html b/public/header.html new file mode 100644 index 0000000..401a18f --- /dev/null +++ b/public/header.html @@ -0,0 +1,360 @@ + + + + + + Background Pattern Options + + + +

Subtle Background Pattern Options

+

Each pattern uses low opacity (5-15%) to maintain readability while adding visual interest.

+ +
+
1. Geometric Grid Pattern
+
+
+
λ
+
Jason Poage
+
+ +
+
+ +
+
2. Dot Pattern
+
+
+
λ
+
Jason Poage
+
+ +
+
+ +
+
3. Lambda Watermarks
+
+
+
λ
+
Jason Poage
+
+ +
+
+ +
+
4. Hexagonal Tessellation
+
+
+
λ
+
Jason Poage
+
+ +
+
+ +
+
5. Circuit Board Pattern
+
+
+
λ
+
Jason Poage
+
+ +
+
+ +
+
6. Triangular Tessellation
+
+
+
λ
+
Jason Poage
+
+ +
+
+ +
+
7. Gradient Mesh Pattern
+
+
+
λ
+
Jason Poage
+
+ +
+
+ +
+
8. Topographical Lines
+
+
+
λ
+
Jason Poage
+
+ +
+
+ +
+
9. Polygon Shapes
+
+
+
λ
+
Jason Poage
+
+ +
+
+ +
+
10. Soft Bokeh Effect
+
+
+
λ
+
Jason Poage
+
+ +
+
+ + diff --git a/src/css/footer.css b/src/css/footer.css index cc264fe..8ac3094 100644 --- a/src/css/footer.css +++ b/src/css/footer.css @@ -49,10 +49,8 @@ margin-top: 0; margin-bottom: 0; display: flex; - gap: 1.5rem; /* Adjust this value to control spacing between links */ - /* If you want them centered within the nav */ + gap: 1.5rem; justify-content: center; - /* Allow items to wrap to the next line on smaller screens */ flex-wrap: wrap; } diff --git a/src/css/header.css b/src/css/header.css index 443b0c6..b0cd3b6 100644 --- a/src/css/header.css +++ b/src/css/header.css @@ -5,28 +5,71 @@ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } -header#site-header .container { +/* header#site-header .container { display: flex; flex-direction: column; align-items: flex-start; gap: 0.5rem; padding: 1rem 0; -} - -header#site-header .logo { - font-size: 1.8rem; - font-weight: bolder; +} */ +header#site-header .container { + max-width: 800px; + margin: 0 auto; + padding: 1rem 1.5rem; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; position: relative; } -header#site-header .logo a { +/* header#site-header .logo { + display: flex; + align-items: center; + font-size: 1.8rem; + font-weight: bolder; + position: relative; +} */ +/* Logo section - positioned above nav */ +header#site-header .logo { + display: flex; + align-items: center; + font-size: 1.8rem; + font-weight: bolder; + position: relative; + text-decoration: none; + color: inherit; + gap: 1rem; + margin-left: 10px; + padding-left: 64px; /* Space for the icon */ +} + + +/* .site-title { + margin: 0 1rem; + white-space: nowrap; + flex-shrink: 0; + color: inherit; + text-decoration: none; + font-weight: bolder; + font-size: + 1.8rem; +} */ +header#site-header .site-title { color: inherit; text-decoration: none; position: relative; display: inline-block; } -header#site-header .logo a::after { +header#site-header .site-title, a { + color: inherit; + text-decoration: none; + position: relative; + display: inline-block; +} + +header#site-header .site-title a::after { content: ""; position: absolute; left: 0; @@ -39,7 +82,73 @@ transition: transform 0.3s ease; } -header#site-header .logo a:hover::after { +header#site-header .site-title a:hover::after { transform: scaleX(1); } + + +/* Favicon styles */ +#favicon-preview { + width: 64px; + height: 64px; + border-radius: 4px; + background-color: #333; + display: inline-block; + vertical-align: middle; +} + +.concept { + width: 64px; + height: 64px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} + +.icon { + width: 64px; + height: 64px; + display: flex; + justify-content: center; + align-items: center; + font-size: 24px; + user-select: none; +} + +/* Deep Dive Lambda */ +.deep-dive { + background-color: #0A192F; + border-radius: 8px; + color: #00CED1; + font-weight: 700; + box-shadow: + 0 0 4px #00CED1, + 0 0 12px #00CED1; +} + +/* Ghost Lambda */ +.ghost { + background-color: #F0F8FFCC; + border-radius: 8px; + color: #008B8B; + font-weight: 800; + text-shadow: 0 0 1px rgba(0, 0, 0, 0.2); +} + +/* Circle of Logic */ +.circle { + background-color: #1E90FF; + border-radius: 50%; + color: #E0FFFF; + font-weight: 600; +} + +div.concept { + display: none; +} + +div.concept#favicon1 { + display: inline-block; +} diff --git a/src/css/nav.css b/src/css/nav.css index 86dfc58..1e2c034 100644 --- a/src/css/nav.css +++ b/src/css/nav.css @@ -85,3 +85,24 @@ nav.site-nav .dropdown-content form { margin: 0; } + +/* Navigation row with consistent left alignment */ +.nav-row { + display: flex; + align-items: center; + gap: 1rem; + width: 100%; + padding-left: 64px; /* Same as icon width + gap to align with site title */ +} + +/* Logo icon that acts as home link - spans from site title to nav */ +.nav-logo { + text-decoration: none; + color: inherit; + display: flex; + align-items: flex-end; + position: absolute; + left: 1rem; + top: 1rem; + bottom: 1rem; +} diff --git a/src/css/responsive.css b/src/css/responsive.css index b41fcad..c264a0b 100644 --- a/src/css/responsive.css +++ b/src/css/responsive.css @@ -57,6 +57,12 @@ .sidebar { width: 100%; } + header#site-header .container { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + header#site-header nav.site-nav { flex-direction: column; gap: 0; @@ -78,6 +84,22 @@ header#site-header #siteNav.hide a { display: none; } + nav.site-nav { + flex-direction: column; + gap: 0; + width: 100%; + } + .nav-row { + flex-direction: column; + align-items: flex-start; + width: 100%; + padding-left: 0; + } + .nav-logo { + position: relative; + left: 0; + margin-bottom: 0.5rem; + } nav.site-nav ul { display: block; justify-content: center; diff --git a/src/middleware/routesList.js b/src/middleware/routesList.js new file mode 100644 index 0000000..277b2e0 --- /dev/null +++ b/src/middleware/routesList.js @@ -0,0 +1,79 @@ +// src/middleware/routesList.js +let cachedRoutes = null; +let cachedApp = null; + +function getAllRoutePaths(app) { + const paths = new Set(); + + function extractPaths(stack, basePath = "") { + if (!stack) return; + + stack.forEach((layer) => { + if (layer.route) { + // Direct route + paths.add(basePath + layer.route.path); + } else if ( + layer.name === "router" && + layer.handle && + layer.handle.stack + ) { + // Router middleware - try to extract base path + let routerPath = ""; + + // Try to extract path from regexp + if (layer.regexp && layer.regexp.source) { + const match = layer.regexp.source.match(/^\^\\?\/?([^\\$?]+)/); + if (match && match[1]) { + routerPath = "/" + match[1].replace(/\\\//g, "/"); + } + } + + extractPaths(layer.handle.stack, basePath + routerPath); + } + }); + } + + if (app && app._router && app._router.stack) { + extractPaths(app._router.stack); + } + + return Array.from(paths).sort(); +} + +// Middleware to capture the app instance +function routesList(req, res, next) { + // Store app reference for later use + if (!cachedApp && req.app) { + cachedApp = req.app; + } + next(); +} + +// Function to get routes (called from the route handler) +function getRoutes() { + if (!cachedApp) { + return []; + } + + // Cache routes on first access + if (!cachedRoutes) { + cachedRoutes = getAllRoutePaths(cachedApp); + } + + return cachedRoutes; +} + +// Force refresh of cached routes +function refreshRoutes() { + cachedRoutes = null; + if (cachedApp) { + cachedRoutes = getAllRoutePaths(cachedApp); + } + return cachedRoutes || []; +} + +module.exports = { + routesList, + getRoutes, + refreshRoutes, +}; diff --git a/src/routes/index.js b/src/routes/index.js index 6902e98..f16b5dc 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -14,10 +14,13 @@ const post = require("./post"); const pages = require("./pages"); const rssFeed = require("./rssFeed"); -const logs = require("./logs"); +// const logs = require("./logs"); const { qualifyLink } = require("../utils/qualifyLinks"); const HttpError = require("../utils/HttpError"); +const securedMiddleware = require("../middleware/secured"); +const securedRoutes = require("./secured"); + router.get("/error", errorPage); // Landing page after error is logged router.get("/favicon.ico", (req, res) => res.status(204).end()); @@ -25,7 +28,6 @@ res.sendStatus(200); }); -router.use(logs); router.use(admin); router.post("/track", analytics); @@ -60,6 +62,44 @@ router.get("/blog/:year/:month/:name", post); +// function flattenRouterLayers(stack, acc = []) { +// for (const layer of stack) { +// acc.push(layer); +// const h = layer.handle; +// if (typeof h === "function") { +// if (h.stack && Array.isArray(h.stack)) { +// flattenRouterLayers(h.stack, acc); +// } else if (h.handle && h.handle.stack && Array.isArray(h.handle.stack)) { +// flattenRouterLayers(h.handle.stack, acc); +// } +// } +// } +// return acc; +// } + +// router.use((req, res) => { +// const rootStack = req.app._router?.stack || req.app.router?.stack; +// if (!rootStack) return res.sendStatus(500); +// const flat = flattenRouterLayers(rootStack); +// const routes = []; +// flat.forEach((l) => { +// if (l.route) { +// routes.push(l.route.path); +// } +// }); +// res.json(routes).send(200); +// }); + +// router.use((req, res) => { +// const appStack = req.app._router?.stack || req.app.router?.stack; +// if (!appStack) return res.sendStatus(500); +// const flatStack = flattenRouterStack(appStack); +// flatStack.forEach((layer) => { +// console.log(layer); +// }); +// res.sendStatus(200); +// }); + router.get("/", (req, res) => { console.log(qualifyLink("/blog")); res.redirect(301, qualifyLink("/blog")); diff --git a/src/routes/logs.js b/src/routes/logs.js deleted file mode 100644 index ce5a233..0000000 --- a/src/routes/logs.js +++ /dev/null @@ -1,150 +0,0 @@ -const express = require("express"); -const router = express.Router(); -const Database = require("better-sqlite3"); -const path = require("path"); -const fs = require("fs"); -const secured = require("../middleware/secured"); - -const allowedLevels = ["warn", "error", "info", "debug", "functions", "notice"]; - -const dbPath = path.resolve(__dirname, "../../data/logs.sqlite3"); - -if (!fs.existsSync(dbPath)) { - fs.closeSync(fs.openSync(dbPath, "w")); -} - -const db = new Database(dbPath, { readonly: true }); - -router.get("/logs", secured, (req, res) => { - res.renderWithBaseContext("pages/logs", { - showSidebar: false, - showFooter: false, - }); -}); - -router.post("/logs", secured, (req, res) => { - const start = process.hrtime.bigint(); - - const log_level = req.query.log_level || "*"; - const date = req.query.date || "*"; - const page = parseInt(req.query.page) || 1; - const limit = parseInt(req.query.limit) || 50; - const offset = (page - 1) * limit; - - const parseStart = process.hrtime.bigint(); - - if (log_level !== "*" && !allowedLevels.includes(log_level)) { - return res.status(400).json({ error: "Invalid log_level" }); - } - - const conditions = []; - const params = []; - - if (log_level !== "*") { - conditions.push("level = ?"); - params.push(log_level); - } - - if (date !== "*") { - conditions.push("date(timestamp) = ?"); - params.push(date); - } - - const whereClause = conditions.length - ? "WHERE " + conditions.join(" AND ") - : ""; - - const countStart = process.hrtime.bigint(); - - // Count query - simple and fast - const countQuery = `SELECT COUNT(*) as total FROM logs ${whereClause}`; - const totalResult = db.prepare(countQuery).get(...params); - const total = totalResult.total; - - const queryStart = process.hrtime.bigint(); - - // STEP 1: Get just the log records we need (fast!) - const logQuery = ` - SELECT id, timestamp, level - FROM logs - ${whereClause} - ORDER BY timestamp DESC - LIMIT ? OFFSET ? - `; - - try { - const logRows = db.prepare(logQuery).all(...params, limit, offset); - - if (logRows.length === 0) { - return res.json({ - logs: [], - pagination: { page, limit, total, totalPages: 0, hasMore: false }, - }); - } - - // STEP 2: Get metadata only for these specific logs - const logIds = logRows.map((row) => row.id); - const placeholders = logIds.map(() => "?").join(","); - - const metadataQuery = ` - SELECT - m.log_id, - k.key, - m.value - FROM log_metadata m - JOIN keys k ON k.id = m.key_id - WHERE m.log_id IN (${placeholders}) - `; - - const metadataRows = db.prepare(metadataQuery).all(...logIds); - - const mapStart = process.hrtime.bigint(); - - // STEP 3: Build metadata lookup map - const metadataMap = {}; - metadataRows.forEach((row) => { - if (!metadataMap[row.log_id]) { - metadataMap[row.log_id] = {}; - } - try { - metadataMap[row.log_id][row.key] = JSON.parse(row.value); - } catch { - metadataMap[row.log_id][row.key] = row.value; - } - }); - - // STEP 4: Combine logs with their metadata - const logs = logRows.map((row) => ({ - id: row.id, - timestamp: row.timestamp, - level: row.level, - ...(metadataMap[row.id] || {}), - })); - - const end = process.hrtime.bigint(); - - req.log.info("logs route timings", { - totalMs: Number(end - start) / 1e6, - parseMs: Number(parseStart - start) / 1e6, - countMs: Number(queryStart - countStart) / 1e6, - queryMs: Number(mapStart - queryStart) / 1e6, - mapMs: Number(end - mapStart) / 1e6, - }); - - res.json({ - logs, - pagination: { - page, - limit, - total, - totalPages: Math.ceil(total / limit), - hasMore: page < Math.ceil(total / limit), - }, - }); - } catch (error) { - console.error("Query error:", error); - res.status(500).json({ error: "Failed to query logs" }); - } -}); - -module.exports = router; diff --git a/src/routes/secured/filteredLogs.js b/src/routes/secured/filteredLogs.js new file mode 100644 index 0000000..6b76936 --- /dev/null +++ b/src/routes/secured/filteredLogs.js @@ -0,0 +1,92 @@ +const router = require("express").Router(); +const fs = require("fs"); + +const excludeIps = new Set(["192.168.1.50", "73.19.173.54"]); + +// Use this function to flatten the Express router stack +function flattenRouterLayers(stack, acc = []) { + for (const layer of stack) { + acc.push(layer); + const h = layer.handle; + if (typeof h === "function") { + if (h.stack && Array.isArray(h.stack)) { + flattenRouterLayers(h.stack, acc); + } else if (h.handle && h.handle.stack && Array.isArray(h.handle.stack)) { + flattenRouterLayers(h.handle.stack, acc); + } + } + } + return acc; +} + +// Collect excludeRoutes from Express router layers +function getExcludeRoutes(router) { + const rootStack = router.stack; + const flat = flattenRouterLayers(rootStack); + const routes = []; + for (const l of flat) { + if (l.route && l.route.path) { + routes.push(l.route.path); + } + } + return routes; +} + +function shouldExclude(ip, url, excludeRoutes) { + if (excludeIps.has(ip)) return true; + + for (const route of excludeRoutes) { + if ( + route.includes(":token") || + route.includes(":year") || + route.includes(":month") || + route.includes(":name") + ) { + const routePrefix = route.split(":")[0]; + if (url.startsWith(routePrefix)) return true; + } else { + if (url === route || url.startsWith(route)) return true; + } + } + return false; +} + +function parseLogLine(line) { + const parts = line.split(" "); + if (parts.length < 1) return null; + const ip = parts[0]; + + const match = line.match(/"([^"]*)"/); + if (!match) return null; + + const request = match[1].split(" "); + if (request.length < 2) return null; + + return { ip, url: request[1] }; +} + +// Route that returns filtered logs as plaintext +router.get("/filtered-logs", (req, res) => { + const excludeRoutes = getExcludeRoutes(req.app._router); + const logPath = "/var/log/nginx/access.log"; + + try { + const input = fs.readFileSync(logPath, "utf8"); + const lines = input.split("\n"); + const filtered = []; + + for (const line of lines) { + if (!line.trim()) continue; + const parsed = parseLogLine(line); + if (!parsed) continue; + + if (!shouldExclude(parsed.ip, parsed.url, excludeRoutes)) { + filtered.push(line); + } + } + + res.type("text/plain").status(200).send(filtered.join("\n")); + } catch { + res.sendStatus(500); + } +}); diff --git a/src/routes/secured/index.js b/src/routes/secured/index.js new file mode 100644 index 0000000..921f8e5 --- /dev/null +++ b/src/routes/secured/index.js @@ -0,0 +1,10 @@ +// src/routes/index.js +const express = require("express"); +const router = express.Router(); + +const logs = require("./logs"); + +router.use(logs); +// router.use(routesList); + +module.exports = router; diff --git a/src/routes/secured/logs.js b/src/routes/secured/logs.js new file mode 100644 index 0000000..06c6e34 --- /dev/null +++ b/src/routes/secured/logs.js @@ -0,0 +1,149 @@ +const express = require("express"); +const router = express.Router(); +const Database = require("better-sqlite3"); +const path = require("path"); +const fs = require("fs"); + +const allowedLevels = ["warn", "error", "info", "debug", "functions", "notice"]; + +const dbPath = path.resolve(__dirname, "../../../data/logs.sqlite3"); + +if (!fs.existsSync(dbPath)) { + fs.closeSync(fs.openSync(dbPath, "w")); +} + +const db = new Database(dbPath, { readonly: true }); + +router.get("/logs", (req, res) => { + res.renderWithBaseContext("pages/logs", { + showSidebar: false, + showFooter: false, + }); +}); + +router.post("/logs", (req, res) => { + const start = process.hrtime.bigint(); + + const log_level = req.query.log_level || "*"; + const date = req.query.date || "*"; + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 50; + const offset = (page - 1) * limit; + + const parseStart = process.hrtime.bigint(); + + if (log_level !== "*" && !allowedLevels.includes(log_level)) { + return res.status(400).json({ error: "Invalid log_level" }); + } + + const conditions = []; + const params = []; + + if (log_level !== "*") { + conditions.push("level = ?"); + params.push(log_level); + } + + if (date !== "*") { + conditions.push("date(timestamp) = ?"); + params.push(date); + } + + const whereClause = conditions.length + ? "WHERE " + conditions.join(" AND ") + : ""; + + const countStart = process.hrtime.bigint(); + + // Count query - simple and fast + const countQuery = `SELECT COUNT(*) as total FROM logs ${whereClause}`; + const totalResult = db.prepare(countQuery).get(...params); + const total = totalResult.total; + + const queryStart = process.hrtime.bigint(); + + // STEP 1: Get just the log records we need (fast!) + const logQuery = ` + SELECT id, timestamp, level + FROM logs + ${whereClause} + ORDER BY timestamp DESC + LIMIT ? OFFSET ? + `; + + try { + const logRows = db.prepare(logQuery).all(...params, limit, offset); + + if (logRows.length === 0) { + return res.json({ + logs: [], + pagination: { page, limit, total, totalPages: 0, hasMore: false }, + }); + } + + // STEP 2: Get metadata only for these specific logs + const logIds = logRows.map((row) => row.id); + const placeholders = logIds.map(() => "?").join(","); + + const metadataQuery = ` + SELECT + m.log_id, + k.key, + m.value + FROM log_metadata m + JOIN keys k ON k.id = m.key_id + WHERE m.log_id IN (${placeholders}) + `; + + const metadataRows = db.prepare(metadataQuery).all(...logIds); + + const mapStart = process.hrtime.bigint(); + + // STEP 3: Build metadata lookup map + const metadataMap = {}; + metadataRows.forEach((row) => { + if (!metadataMap[row.log_id]) { + metadataMap[row.log_id] = {}; + } + try { + metadataMap[row.log_id][row.key] = JSON.parse(row.value); + } catch { + metadataMap[row.log_id][row.key] = row.value; + } + }); + + // STEP 4: Combine logs with their metadata + const logs = logRows.map((row) => ({ + id: row.id, + timestamp: row.timestamp, + level: row.level, + ...(metadataMap[row.id] || {}), + })); + + const end = process.hrtime.bigint(); + + req.log.info("logs route timings", { + totalMs: Number(end - start) / 1e6, + parseMs: Number(parseStart - start) / 1e6, + countMs: Number(queryStart - countStart) / 1e6, + queryMs: Number(mapStart - queryStart) / 1e6, + mapMs: Number(end - mapStart) / 1e6, + }); + + res.json({ + logs, + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit), + hasMore: page < Math.ceil(total / limit), + }, + }); + } catch (error) { + console.error("Query error:", error); + res.status(500).json({ error: "Failed to query logs" }); + } +}); + +module.exports = router; diff --git a/src/routes/secured/routesList.js b/src/routes/secured/routesList.js new file mode 100644 index 0000000..68e4e8a --- /dev/null +++ b/src/routes/secured/routesList.js @@ -0,0 +1,39 @@ +// src/routes/secured/routesList.js +const express = require("express"); +const { getRoutes, refreshRoutes } = require("../../middleware/routesList"); + +const router = express.Router(); + +router.get("/routes", (req, res) => { + try { + const routes = getRoutes(); + res.json({ + count: routes.length, + routes: routes, + }); + } catch (error) { + res.status(500).json({ + error: "Failed to retrieve routes", + message: error.message, + }); + } +}); + +// Optional: endpoint to refresh the route cache +router.post("/routes/refresh", (req, res) => { + try { + const routes = refreshRoutes(); + res.json({ + message: "Routes refreshed", + count: routes.length, + routes: routes, + }); + } catch (error) { + res.status(500).json({ + error: "Failed to refresh routes", + message: error.message, + }); + } +}); + +module.exports = router; diff --git a/src/utils/baseContext.js b/src/utils/baseContext.js index f2875e5..7be3733 100644 --- a/src/utils/baseContext.js +++ b/src/utils/baseContext.js @@ -14,10 +14,11 @@ const filteredNavLinks = filterSecureLinks(navLinks, isAuthenticated); const qualifiedNavLinks = qualifyNavLinks(filteredNavLinks); const menu = await getPostsMenu(path.join(__dirname, "../../content/posts")); - + const siteOwner = process.env.SITE_OWNER; return Object.assign( { - siteOwner: process.env.SITE_OWNER, + title: `${siteOwner}'s Software Blog`, + siteOwner, originCountry: process.env.COUNTRY, hCaptchaKey: process.env.HCAPTCHA_KEY, navLinks: qualifiedNavLinks, diff --git a/src/views/partials/favicon.handlebars b/src/views/partials/favicon.handlebars new file mode 100644 index 0000000..f6ded27 --- /dev/null +++ b/src/views/partials/favicon.handlebars @@ -0,0 +1,93 @@ + +
+
+
λ
+ +
+ +
+
λ
+ +
+ +
+
λ
+ +
+
diff --git a/src/views/partials/headers.handlebars b/src/views/partials/headers.handlebars index 0b69c6b..3936273 100644 --- a/src/views/partials/headers.handlebars +++ b/src/views/partials/headers.handlebars @@ -1,10 +1,15 @@ -