diff --git a/public/css/logs.css b/public/css/logs.css
index 761068f..a03a615 100644
--- a/public/css/logs.css
+++ b/public/css/logs.css
@@ -1,32 +1,75 @@
- body {
- font-family: monospace, monospace;
- margin: 20px;
- }
+body {
+ font-family: monospace, monospace;
+ margin: 20px;
+}
- table {
- border-collapse: collapse;
- width: 100%;
- }
+table {
+ border-collapse: collapse;
+ width: 100%;
+}
- th,
- td {
- border: 1px solid #ccc;
- padding: 8px;
- text-align: left;
- }
+th,
+td {
+ border: 1px solid #ccc;
+ padding: 8px;
+ text-align: left;
+}
- th {
- background: #eee;
- }
+th {
+ background: #eee;
+}
- textarea {
- width: 100%;
- height: 100px;
- font-family: monospace;
- white-space: pre-wrap;
- }
+textarea {
+ width: 100%;
+ height: 100px;
+ font-family: monospace;
+ white-space: pre-wrap;
+}
- select,
- button {
- margin: 5px 0;
- }
+select,
+button {
+ margin: 5px 0;
+}
+.pagination-container {
+ margin: 20px 0;
+ padding: 15px;
+ background: #f5f5f5;
+ border-radius: 5px;
+}
+
+.pagination-info {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 10px;
+}
+
+.pagination-controls {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+}
+
+.pagination-controls button {
+ padding: 5px 10px;
+ border: 1px solid #ddd;
+ background: white;
+ cursor: pointer;
+ border-radius: 3px;
+}
+
+.pagination-controls button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.pagination-controls button:not(:disabled):hover {
+ background: #e9e9e9;
+}
+
+.loading-indicator {
+ text-align: center;
+ padding: 20px;
+ color: #666;
+ font-style: italic;
+}
diff --git a/public/js/logs.js b/public/js/logs.js
index 2e0a92c..b11051e 100644
--- a/public/js/logs.js
+++ b/public/js/logs.js
@@ -1,46 +1,195 @@
-const form = document.getElementById("filterForm");
-const theadRow = document.getElementById("logsTableHeaderRow");
-const tbody = document.querySelector("#logsTable tbody");
+class LogsViewer {
+ constructor() {
+ this.form = document.getElementById("filterForm");
+ this.theadRow = document.getElementById("logsTableHeaderRow");
+ this.tbody = document.querySelector("#logsTable tbody");
+ this.currentPage = 1;
+ this.limit = 50;
+ this.columns = [];
+ this.isLoading = false;
+ this.hasMore = true;
+ this.totalPages = 0;
+ this.currentFilters = {};
-form.addEventListener("submit", async (e) => {
- e.preventDefault();
- theadRow.innerHTML = "";
- tbody.innerHTML = "";
- const params = new URLSearchParams(new FormData(form));
+ this.setupEventListeners();
+ this.setupPaginationControls();
+ }
- try {
- const res = await fetch("/logs", {
- method: "POST",
- headers: {
- "Content-Type": "application/x-www-form-urlencoded",
- Accept: "application/json",
- },
- body: params.toString(),
+ setupEventListeners() {
+ this.form.addEventListener("submit", (e) => {
+ e.preventDefault();
+ this.resetAndLoad();
});
- if (!res.ok) throw new Error(await res.text());
- const logs = await res.json();
+ // Continuous scrolling
+ window.addEventListener("scroll", () => {
+ if (this.isNearBottom() && !this.isLoading && this.hasMore) {
+ this.loadNextPage();
+ }
+ });
+ }
- if (logs.length === 0) {
- tbody.innerHTML = '
| No logs found |
';
- return;
+ setupPaginationControls() {
+ // Create pagination container
+ const paginationContainer = document.createElement("div");
+ paginationContainer.id = "paginationContainer";
+ paginationContainer.className = "pagination-container";
+ paginationContainer.innerHTML = `
+
+
+ Loading more logs...
+
+ `;
+
+ // Insert after the form
+ this.form.parentNode.insertBefore(
+ paginationContainer,
+ this.form.nextSibling
+ );
+
+ // Add pagination event listeners
+ document
+ .getElementById("firstPage")
+ .addEventListener("click", () => this.goToPage(1));
+ document
+ .getElementById("prevPage")
+ .addEventListener("click", () => this.goToPage(this.currentPage - 1));
+ document
+ .getElementById("nextPage")
+ .addEventListener("click", () => this.goToPage(this.currentPage + 1));
+ document
+ .getElementById("lastPage")
+ .addEventListener("click", () => this.goToPage(this.totalPages));
+
+ document.getElementById("pageInput").addEventListener("keypress", (e) => {
+ if (e.key === "Enter") {
+ const page = parseInt(e.target.value);
+ if (page >= 1 && page <= this.totalPages) {
+ this.goToPage(page);
+ }
+ }
+ });
+ }
+
+ isNearBottom() {
+ return (
+ window.innerHeight + window.scrollY >= document.body.offsetHeight - 1000
+ );
+ }
+
+ async resetAndLoad() {
+ this.currentPage = 1;
+ this.hasMore = true;
+ this.theadRow.innerHTML = "";
+ this.tbody.innerHTML = "";
+ this.columns = [];
+ this.currentFilters = new URLSearchParams(new FormData(this.form));
+ await this.loadLogs(false);
+ }
+
+ async loadNextPage() {
+ if (this.hasMore && !this.isLoading) {
+ this.currentPage++;
+ await this.loadLogs(true);
}
+ }
- const columnSet = new Set();
+ async goToPage(page) {
+ if (page >= 1 && page <= this.totalPages && page !== this.currentPage) {
+ this.currentPage = page;
+ this.theadRow.innerHTML = "";
+ this.tbody.innerHTML = "";
+ this.columns = [];
+ await this.loadLogs(false);
+ }
+ }
+
+ async loadLogs(append = false) {
+ if (this.isLoading) return;
+
+ this.isLoading = true;
+ this.showLoading();
+
+ const params = new URLSearchParams(this.currentFilters);
+ params.append("page", this.currentPage);
+ params.append("limit", this.limit);
+
+ try {
+ const res = await fetch("/logs", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ Accept: "application/json",
+ },
+ body: params.toString(),
+ });
+
+ if (!res.ok) throw new Error(await res.text());
+ const data = await res.json();
+
+ this.hasMore = data.pagination.hasMore;
+ this.totalPages = data.pagination.totalPages;
+
+ if (data.logs.length === 0 && !append) {
+ this.tbody.innerHTML = '| No logs found |
';
+ this.updatePaginationInfo(0, 0, 0);
+ return;
+ }
+
+ this.renderLogs(data.logs, append);
+ this.updatePaginationInfo(
+ data.pagination.page,
+ data.pagination.totalPages,
+ data.pagination.total
+ );
+ } catch (error) {
+ const errorMsg = `| Error loading logs: ${error.message} |
`;
+ if (append) {
+ this.tbody.innerHTML += errorMsg;
+ } else {
+ this.tbody.innerHTML = errorMsg;
+ }
+ } finally {
+ this.isLoading = false;
+ this.hideLoading();
+ }
+ }
+
+ renderLogs(logs, append = false) {
+ if (logs.length === 0) return;
+
+ // Build column set
+ const columnSet = new Set(this.columns);
for (const log of logs) {
Object.keys(log).forEach((key) => columnSet.add(key));
}
- const columns = Array.from(columnSet);
- for (const col of columns) {
- const th = document.createElement("th");
- th.textContent = col;
- theadRow.appendChild(th);
+ const newColumns = Array.from(columnSet);
+
+ // Update headers if new columns or not appending
+ if (!append || newColumns.length > this.columns.length) {
+ this.columns = newColumns;
+ this.theadRow.innerHTML = "";
+ for (const col of this.columns) {
+ const th = document.createElement("th");
+ th.textContent = col;
+ this.theadRow.appendChild(th);
+ }
}
+ // Add rows
for (const log of logs) {
const tr = document.createElement("tr");
- for (const col of columns) {
+ for (const col of this.columns) {
const td = document.createElement("td");
const value = log[col];
if (col === "stack" && typeof value === "string") {
@@ -52,9 +201,38 @@
}
tr.appendChild(td);
}
- tbody.appendChild(tr);
+ this.tbody.appendChild(tr);
}
- } catch (error) {
- tbody.innerHTML = `| Error loading logs: ${error.message} |
`;
}
+
+ updatePaginationInfo(page, totalPages, total) {
+ const pageInfo = document.getElementById("pageInfo");
+ const pageInput = document.getElementById("pageInput");
+ const firstBtn = document.getElementById("firstPage");
+ const prevBtn = document.getElementById("prevPage");
+ const nextBtn = document.getElementById("nextPage");
+ const lastBtn = document.getElementById("lastPage");
+
+ pageInfo.textContent = `Page ${page} of ${totalPages} (${total} total logs)`;
+ pageInput.value = page;
+ pageInput.max = totalPages;
+
+ firstBtn.disabled = page <= 1;
+ prevBtn.disabled = page <= 1;
+ nextBtn.disabled = page >= totalPages;
+ lastBtn.disabled = page >= totalPages;
+ }
+
+ showLoading() {
+ document.getElementById("loadingIndicator").style.display = "block";
+ }
+
+ hideLoading() {
+ document.getElementById("loadingIndicator").style.display = "none";
+ }
+}
+
+// Initialize when DOM is loaded
+document.addEventListener("DOMContentLoaded", () => {
+ new LogsViewer();
});
diff --git a/src/middleware/validateRequestIntegrity.js b/src/middleware/validateRequestIntegrity.js
index a40eb70..8c49d4b 100644
--- a/src/middleware/validateRequestIntegrity.js
+++ b/src/middleware/validateRequestIntegrity.js
@@ -1,32 +1,24 @@
-const HttpError = require("../utils/HttpError")
+const HttpError = require("../utils/HttpError");
module.exports = (req, res, next) => {
- const allowedMethods = ["GET", "POST"];
+ const allowedMethods = ["HEAD", "GET", "POST"];
const contentLength = parseInt(req.get("content-length") || "0", 10);
const contentType = req.headers["content-type"] || "";
const headerCount = Object.keys(req.headers).length;
if (!allowedMethods.includes(req.method)) {
- return next(
- new HttpError("Method Not Allowed", 405)
- );
+ return next(new HttpError(`Http Method '${req.method}' Not Allowed`, 405));
}
if (contentLength > 4096) {
- return next(
- new HttpError("Payload Too Large", 413)
- );
+ return next(new HttpError("Payload Too Large", 413));
}
if (contentType.includes("multipart/form-data")) {
- return next(
- new HttpError("File uploads are not allowed.", 400)
- );
+ return next(new HttpError("File uploads are not allowed.", 400));
}
if (headerCount > 100) {
- return next(
- new HttpError("Too many headers.", 400)
- );
+ return next(new HttpError("Too many headers.", 400));
}
next();
diff --git a/src/routes/index.js b/src/routes/index.js
index f2c856b..689d5ab 100644
--- a/src/routes/index.js
+++ b/src/routes/index.js
@@ -14,10 +14,14 @@
const pages = require("./pages");
const rssFeed = require("./rssFeed");
const logs = require("./logs");
+const { qualifyLink } = require("../utils/qualifyLinks");
router.get("/error", errorPage); // Landing page after error is logged
router.get("/favicon.ico", (req, res) => res.status(204).end());
+router.head("/healthcheck", (req, res) => {
+ res.sendStatus(200);
+});
router.use(logs);
@@ -47,9 +51,10 @@
router.get("/blog/:year/:month/:name", post);
-router.get("/", async (req, res) => {
- res.redirect(301, "/blog");
-});
+// router.get("/", (req, res) => {
+// console.log(qualifyLink("/blog"));
+// res.redirect(301, qualifyLink("/blog"));
+// });
router.use((req, res, next) => {
next(new HttpError(null, 404));
diff --git a/src/routes/logs.js b/src/routes/logs.js
index c6d205f..ddec110 100644
--- a/src/routes/logs.js
+++ b/src/routes/logs.js
@@ -10,15 +10,12 @@
const dbPath = path.resolve(__dirname, "../../data/logs.sqlite3");
if (!fs.existsSync(dbPath)) {
- // Create empty file to allow readonly open later
fs.closeSync(fs.openSync(dbPath, "w"));
- // Optionally initialize schema here or open writable once for setup
}
const db = new Database(dbPath, { readonly: true });
router.get("/logs", secured, (req, res) => {
- // res.render("pages/logs", { layout: "logs" });
res.renderWithBaseContext("pages/logs", {
showSidebar: false,
showFooter: false,
@@ -26,16 +23,15 @@
});
router.post("/logs", secured, (req, res) => {
- // const log_type = req.query.log_type || "*";
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;
if (log_level !== "*" && !allowedLevels.includes(log_level)) {
return res.status(400).json({ error: "Invalid log_level" });
}
- // if (log_type !== "*" && !allowedTypes.includes(log_type)) {
- // return res.status(400).json({ error: "Invalid log_type" });
- // }
const conditions = [];
const params = [];
@@ -50,19 +46,21 @@
params.push(date);
}
- // if (log_type !== "*") {
- // conditions.push(`EXISTS (
- // SELECT 1 FROM log_metadata m
- // JOIN keys k ON k.id = m.key_id
- // WHERE m.log_id = l.id AND k.key = 'type' AND m.value = ?
- // )`);
- // params.push(log_type);
- // }
-
const whereClause = conditions.length
? "WHERE " + conditions.join(" AND ")
: "";
+ // Get total count for pagination
+ const countQuery = `
+ SELECT COUNT(DISTINCT l.id) as total
+ FROM logs l
+ ${whereClause}
+ `;
+
+ const totalResult = db.prepare(countQuery).get(...params);
+ const total = totalResult.total;
+
+ // Get paginated results
const query = `
SELECT
l.id,
@@ -75,11 +73,11 @@
${whereClause}
GROUP BY l.id
ORDER BY l.timestamp DESC
- LIMIT 500
+ LIMIT ? OFFSET ?
`;
try {
- const rows = db.prepare(query).all(...params);
+ const rows = db.prepare(query).all(...params, limit, offset);
const logs = rows.map((row) => {
const meta = {};
@@ -103,7 +101,16 @@
};
});
- res.json(logs);
+ res.json({
+ logs,
+ pagination: {
+ page,
+ limit,
+ total,
+ totalPages: Math.ceil(total / limit),
+ hasMore: page < Math.ceil(total / limit),
+ },
+ });
} catch {
res.status(500).json({ error: "Failed to query logs" });
}
diff --git a/src/routes/pages.js b/src/routes/pages.js
index f5c6edb..0a72c7f 100644
--- a/src/routes/pages.js
+++ b/src/routes/pages.js
@@ -24,7 +24,6 @@
construction.register("/archive", "Archive");
// construction.register("/rss-feed.xml", "RSS Feed");
construction.register("/tags", "Tags");
-construction.register("/blog", "Blog");
construction.register("/about/blog", "About This Blog");
// construction.register("/contact", "Contact Me");
diff --git a/src/utils/logging.js b/src/utils/logging.js
index a7846ec..b6839bb 100644
--- a/src/utils/logging.js
+++ b/src/utils/logging.js
@@ -133,20 +133,7 @@
debug: (...args) =>
writeLog("DEBUG", logStreams.debug, console.debug, ...args),
};
-// // Winston logger
-// const winstonLogger = createLogger({
-// transports: [
-// buildTransport("info", "info"),
-// buildTransport("error", "error"),
-// buildTransport("warn", "warn"),
-// buildTransport("debug", "debug"),
-// buildTransport("notice", "notice"),
-// new transports.Console({
-// level: "debug",
-// format: format.combine(format.colorize(), format.simple()),
-// }),
-// ],
-// });
+
const winstonLogger = createLogger({
format: format.combine(
format.timestamp(),
@@ -178,13 +165,6 @@
})
),
}),
- // new transports.Console({
- // level: "debug", // or "warn"/"error"
- // format: format.combine(
- // format.colorize(),
- // format.printf(({ level, message }) => `[${level}] ${message}`)
- // ),
- // }),
sqliteTransport,
],