Corrections on user counter and analytics to the kst4contest page

This commit is contained in:
Marc Froehlich
2026-09-12 03:55:27 +02:00
parent b23884bde1
commit 914a06cb7f
4 changed files with 47 additions and 7 deletions
+1
View File
@@ -185,6 +185,7 @@ Current website/deployment scripts and update-feed behaviour must be inspected b
- The counter endpoint disables its own access log and serves the public JSON with a one-hour public cache policy and `X-Content-Type-Options: nosniff`. - The counter endpoint disables its own access log and serves the public JSON with a one-hour public cache policy and `X-Content-Type-Options: nosniff`.
- GoAccess is the server-side source. A registry keeps stable site IDs, hostnames, current analytics-log paths, activation dates, public-counter switches and output targets separate for each project subdomain. The Country database is `/var/lib/GeoIP/GeoLite2-Country.mmdb`. A combined report uses only the registered project sites; `stats.hamradioonline.de` is excluded. - GoAccess is the server-side source. A registry keeps stable site IDs, hostnames, current analytics-log paths, activation dates, public-counter switches and output targets separate for each project subdomain. The Country database is `/var/lib/GeoIP/GeoLite2-Country.mmdb`. A combined report uses only the registered project sites; `stats.hamradioonline.de` is excluded.
- The regular generator passes each current analytics log and its optional uncompressed `.1` rotation directly to GoAccess and relies on the persistent GoAccess database for incremental processing. Logrotate therefore uses `delaycompress`. Older `.gz` rotations are not imported during regular runs, and missing Zlib support is an accepted, explicitly reported capability state for the GoAccess 1.8.1 production baseline. - The regular generator passes each current analytics log and its optional uncompressed `.1` rotation directly to GoAccess and relies on the persistent GoAccess database for incremental processing. Logrotate therefore uses `delaycompress`. Older `.gz` rotations are not imported during regular runs, and missing Zlib support is an accepted, explicitly reported capability state for the GoAccess 1.8.1 production baseline.
- GoAccess 1.8.1 exposes the Country panel as `geolocation` in JSON. Combined report jobs explicitly enable `VIRTUAL_HOSTS` and require the resulting `vhosts` panel; individual site jobs do not enable it. Missing required panels invalidate the complete staged run.
- Node.js 18.19.1 is the production runtime baseline. `--check` requires readable input files, prepared writable output directories and GoAccess built with GeoIP2/MMDB support. OpenSSL and absent Zlib support remain informational. Dry-runs use temporary state and never acquire the production lock. - Node.js 18.19.1 is the production runtime baseline. `--check` requires readable input files, prepared writable output directories and GoAccess built with GeoIP2/MMDB support. OpenSSL and absent Zlib support remain informational. Dry-runs use temporary state and never acquire the production lock.
- The generator runs as `hamradio-analytics`. The state root is mode `0711`; only explicitly prepared report and public-output directories are shared read-only with Nginx through the `www-data` group. GoAccess databases and public counter state remain private. No ACL support is assumed. - The generator runs as `hamradio-analytics`. The state root is mode `0711`; only explicitly prepared report and public-output directories are shared read-only with Nginx through the `www-data` group. GoAccess databases and public counter state remain private. No ACL support is assumed.
- The protected statistics vhost is enabled in two stages: an IPv4-only HTTP bootstrap obtains the certificate through `/snap/bin/certbot`, then the final configuration retains an IPv4 HTTP block for the webroot ACME challenge and permanently redirects all other HTTP requests to HTTPS. The HTTPS block uses the existing Certbot TLS options and redirects authenticated requests from `/` to `/combined/`. IPv6 remains disabled until the DNS AAAA record has been confirmed. - The protected statistics vhost is enabled in two stages: an IPv4-only HTTP bootstrap obtains the certificate through `/snap/bin/certbot`, then the final configuration retains an IPv4 HTTP block for the webroot ACME challenge and permanently redirects all other HTTP requests to HTTPS. The HTTPS block uses the existing Certbot TLS options and redirects authenticated requests from `/` to `/combined/`. IPv6 remains disabled until the DNS AAAA record has been confirmed.
+6
View File
@@ -262,6 +262,12 @@ day, requested pages, countries, HTTP status codes and virtual hosts. Host,
remote-user, referrer, keyphrase, operating-system, browser and other detailed remote-user, referrer, keyphrase, operating-system, browser and other detailed
panels are disabled. panels are disabled.
GoAccess 1.8.1 writes the Country panel under the JSON key `geolocation`.
Combined jobs explicitly pass `--enable-panel=VIRTUAL_HOSTS` and require the
resulting `vhosts` key. Site jobs do not enable that panel. The generator treats
either missing key as an invalid report rather than publishing incomplete
statistics.
The Country database is provided through the registry at The Country database is provided through the registry at
`/var/lib/GeoIP/GeoLite2-Country.mmdb`. A file whose name contains `City` is `/var/lib/GeoIP/GeoLite2-Country.mmdb`. A file whose name contains `City` is
rejected. Do not replace it with a City database merely because one happens to rejected. Do not replace it with a City database merely because one happens to
+9 -4
View File
@@ -450,9 +450,9 @@ function validateReport(report, combined) {
if (!report || typeof report !== "object" || Array.isArray(report)) { if (!report || typeof report !== "object" || Array.isArray(report)) {
throw new Error("GoAccess JSON report must be an object"); throw new Error("GoAccess JSON report must be an object");
} }
const requiredPanels = ["visitors", "requests", "status_codes", "geo_location"]; const requiredPanels = ["visitors", "requests", "status_codes", "geolocation"];
if (combined) { if (combined) {
requiredPanels.push("virtual_hosts"); requiredPanels.push("vhosts");
} }
if (!report.general || typeof report.general !== "object") { if (!report.general || typeof report.general !== "object") {
throw new Error("GoAccess JSON report has no general summary"); throw new Error("GoAccess JSON report has no general summary");
@@ -682,10 +682,15 @@ function prepareReport(job, context) {
const args = [ const args = [
...job.logs, ...job.logs,
"--no-global-config", "--no-global-config",
"--config-file", runConfig, "--config-file", runConfig
];
if (job.combined) {
args.push("--enable-panel=VIRTUAL_HOSTS");
}
args.push(
"--output", outputJson, "--output", outputJson,
"--output", outputHtml "--output", outputHtml
]; );
context.runGoAccess({ context.runGoAccess({
binary: context.goaccessBinary, binary: context.goaccessBinary,
args, args,
+31 -3
View File
@@ -7,7 +7,8 @@ const test = require("node:test");
const { const {
formatGoAccessCheck, formatGoAccessCheck,
generateReports, generateReports,
parseGoAccessVersion parseGoAccessVersion,
validateReport
} = require("../ops/analytics/generate-reports"); } = require("../ops/analytics/generate-reports");
const GOACCESS_WITHOUT_ZLIB = { const GOACCESS_WITHOUT_ZLIB = {
@@ -28,10 +29,10 @@ function goAccessReport(dailyVisits, combined = false) {
}, },
requests: { data: [] }, requests: { data: [] },
status_codes: { data: [] }, status_codes: { data: [] },
geo_location: { data: [] } geolocation: { data: [] }
}; };
if (combined) { if (combined) {
report.virtual_hosts = { data: [] }; report.vhosts = { data: [] };
} }
return report; return report;
} }
@@ -230,6 +231,9 @@ test("processes subdomains separately and together without publishing disabled c
calls[2].args.slice(0, 3), calls[2].args.slice(0, 3),
[...alphaLogs, ...bravoLogs] [...alphaLogs, ...bravoLogs]
); );
assert.equal(calls[0].args.includes("--enable-panel=VIRTUAL_HOSTS"), false);
assert.equal(calls[1].args.includes("--enable-panel=VIRTUAL_HOSTS"), false);
assert.equal(calls[2].args.includes("--enable-panel=VIRTUAL_HOSTS"), true);
assert.equal(calls.some(call => call.args.some(argument => argument.endsWith(".gz"))), false); assert.equal(calls.some(call => call.args.some(argument => argument.endsWith(".gz"))), false);
assert.equal(fs.existsSync(path.join( assert.equal(fs.existsSync(path.join(
testFixture.registry.stateDirectory, testFixture.registry.stateDirectory,
@@ -245,6 +249,30 @@ test("processes subdomains separately and together without publishing disabled c
} }
}); });
test("validates GoAccess 1.8.1 panel names strictly", () => {
const siteReport = goAccessReport({ "2026-09-11": 3 });
const combinedReport = goAccessReport({ "2026-09-11": 3 }, true);
assert.doesNotThrow(() => validateReport(siteReport, false));
assert.doesNotThrow(() => validateReport(combinedReport, true));
const missingGeolocation = goAccessReport({ "2026-09-11": 3 });
delete missingGeolocation.geolocation;
missingGeolocation.geo_location = { data: [] };
assert.throws(
() => validateReport(missingGeolocation, false),
/GoAccess JSON report has no geolocation panel/
);
const missingVhosts = goAccessReport({ "2026-09-11": 3 }, true);
delete missingVhosts.vhosts;
missingVhosts.virtual_hosts = { data: [] };
assert.throws(
() => validateReport(missingVhosts, true),
/GoAccess JSON report has no vhosts panel/
);
});
test("dry-run validates generated data without changing production paths", () => { test("dry-run validates generated data without changing production paths", () => {
const testFixture = fixture([{ id: "alpha", publicCounter: true }]); const testFixture = fixture([{ id: "alpha", publicCounter: true }]);
try { try {