- Fixed download icon size (1.25rem instead of huge black icons) - Uploaded all 12 PDFs to production server - Restored table of contents rendering for all documents - Fixed modal cards with proper CSS and event handlers - Replaced all docs-viewer.html links with docs.html - Added nginx redirect from /docs/* to /docs.html - Fixed duplicate headers in modal sections - Improved cache-busting with timestamp versioning All documentation features now working correctly: ✅ Card-based document viewer with modals ✅ PDF downloads with proper icons ✅ Table of contents navigation ✅ Consistent URL structure 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
216 lines
5.4 KiB
JavaScript
216 lines
5.4 KiB
JavaScript
/**
|
|
* GovernanceLog Model
|
|
* Audit trail for Tractatus governance actions
|
|
*/
|
|
|
|
const { ObjectId } = require('mongodb');
|
|
const { getCollection } = require('../utils/db.util');
|
|
|
|
class GovernanceLog {
|
|
/**
|
|
* Create governance log entry
|
|
*/
|
|
static async create(data) {
|
|
const collection = await getCollection('governance_logs');
|
|
|
|
const log = {
|
|
action: data.action, // BLOG_TOPIC_SUGGESTION, MEDIA_TRIAGE, etc.
|
|
user_id: data.user_id ? new ObjectId(data.user_id) : null,
|
|
user_email: data.user_email,
|
|
timestamp: data.timestamp || new Date(),
|
|
|
|
// Tractatus governance data
|
|
quadrant: data.quadrant || null, // STR/OPS/TAC/SYS/STO
|
|
boundary_check: data.boundary_check || null, // BoundaryEnforcer result
|
|
cross_reference: data.cross_reference || null, // CrossReferenceValidator result
|
|
pressure_level: data.pressure_level || null, // ContextPressureMonitor result
|
|
|
|
outcome: data.outcome, // ALLOWED/BLOCKED/QUEUED_FOR_APPROVAL
|
|
|
|
// Action details
|
|
details: data.details || {},
|
|
|
|
// If action was blocked
|
|
blocked_reason: data.blocked_reason || null,
|
|
blocked_section: data.blocked_section || null,
|
|
|
|
// Metadata
|
|
service: data.service || 'unknown',
|
|
environment: process.env.NODE_ENV || 'production',
|
|
ip_address: data.ip_address || null,
|
|
|
|
created_at: new Date()
|
|
};
|
|
|
|
const result = await collection.insertOne(log);
|
|
return { ...log, _id: result.insertedId };
|
|
}
|
|
|
|
/**
|
|
* Find logs by action type
|
|
*/
|
|
static async findByAction(action, options = {}) {
|
|
const collection = await getCollection('governance_logs');
|
|
const { limit = 50, skip = 0, startDate, endDate } = options;
|
|
|
|
const filter = { action };
|
|
|
|
if (startDate || endDate) {
|
|
filter.timestamp = {};
|
|
if (startDate) filter.timestamp.$gte = new Date(startDate);
|
|
if (endDate) filter.timestamp.$lte = new Date(endDate);
|
|
}
|
|
|
|
return await collection
|
|
.find(filter)
|
|
.sort({ timestamp: -1 })
|
|
.skip(skip)
|
|
.limit(limit)
|
|
.toArray();
|
|
}
|
|
|
|
/**
|
|
* Find logs by user
|
|
*/
|
|
static async findByUser(userId, options = {}) {
|
|
const collection = await getCollection('governance_logs');
|
|
const { limit = 50, skip = 0 } = options;
|
|
|
|
return await collection
|
|
.find({ user_id: new ObjectId(userId) })
|
|
.sort({ timestamp: -1 })
|
|
.skip(skip)
|
|
.limit(limit)
|
|
.toArray();
|
|
}
|
|
|
|
/**
|
|
* Find blocked actions
|
|
*/
|
|
static async findBlocked(options = {}) {
|
|
const collection = await getCollection('governance_logs');
|
|
const { limit = 50, skip = 0 } = options;
|
|
|
|
return await collection
|
|
.find({ outcome: 'BLOCKED' })
|
|
.sort({ timestamp: -1 })
|
|
.skip(skip)
|
|
.limit(limit)
|
|
.toArray();
|
|
}
|
|
|
|
/**
|
|
* Find by outcome
|
|
*/
|
|
static async findByOutcome(outcome, options = {}) {
|
|
const collection = await getCollection('governance_logs');
|
|
const { limit = 50, skip = 0 } = options;
|
|
|
|
return await collection
|
|
.find({ outcome })
|
|
.sort({ timestamp: -1 })
|
|
.skip(skip)
|
|
.limit(limit)
|
|
.toArray();
|
|
}
|
|
|
|
/**
|
|
* Find by quadrant
|
|
*/
|
|
static async findByQuadrant(quadrant, options = {}) {
|
|
const collection = await getCollection('governance_logs');
|
|
const { limit = 50, skip = 0 } = options;
|
|
|
|
return await collection
|
|
.find({ quadrant })
|
|
.sort({ timestamp: -1 })
|
|
.skip(skip)
|
|
.limit(limit)
|
|
.toArray();
|
|
}
|
|
|
|
/**
|
|
* Get statistics
|
|
*/
|
|
static async getStatistics(startDate, endDate) {
|
|
const collection = await getCollection('governance_logs');
|
|
|
|
const filter = {};
|
|
if (startDate || endDate) {
|
|
filter.timestamp = {};
|
|
if (startDate) filter.timestamp.$gte = new Date(startDate);
|
|
if (endDate) filter.timestamp.$lte = new Date(endDate);
|
|
}
|
|
|
|
const [totalLogs] = await collection.aggregate([
|
|
{ $match: filter },
|
|
{
|
|
$group: {
|
|
_id: null,
|
|
total: { $sum: 1 },
|
|
allowed: {
|
|
$sum: { $cond: [{ $eq: ['$outcome', 'ALLOWED'] }, 1, 0] }
|
|
},
|
|
blocked: {
|
|
$sum: { $cond: [{ $eq: ['$outcome', 'BLOCKED'] }, 1, 0] }
|
|
},
|
|
queued: {
|
|
$sum: { $cond: [{ $eq: ['$outcome', 'QUEUED_FOR_APPROVAL'] }, 1, 0] }
|
|
}
|
|
}
|
|
}
|
|
]).toArray();
|
|
|
|
const byQuadrant = await collection.aggregate([
|
|
{ $match: { ...filter, quadrant: { $ne: null } } },
|
|
{
|
|
$group: {
|
|
_id: '$quadrant',
|
|
count: { $sum: 1 }
|
|
}
|
|
}
|
|
]).toArray();
|
|
|
|
const byAction = await collection.aggregate([
|
|
{ $match: filter },
|
|
{
|
|
$group: {
|
|
_id: '$action',
|
|
count: { $sum: 1 }
|
|
}
|
|
},
|
|
{ $sort: { count: -1 } },
|
|
{ $limit: 10 }
|
|
]).toArray();
|
|
|
|
return {
|
|
summary: totalLogs[0] || {
|
|
total: 0,
|
|
allowed: 0,
|
|
blocked: 0,
|
|
queued: 0
|
|
},
|
|
by_quadrant: byQuadrant,
|
|
top_actions: byAction
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Delete old logs (retention policy)
|
|
* @param {number} days - Keep logs newer than this many days
|
|
*/
|
|
static async deleteOldLogs(days = 90) {
|
|
const collection = await getCollection('governance_logs');
|
|
|
|
const cutoffDate = new Date();
|
|
cutoffDate.setDate(cutoffDate.getDate() - days);
|
|
|
|
const result = await collection.deleteMany({
|
|
created_at: { $lt: cutoffDate }
|
|
});
|
|
|
|
return result.deletedCount;
|
|
}
|
|
}
|
|
|
|
module.exports = GovernanceLog;
|