tractatus/src/models/BlogPost.model.js
TheFlow 5557126f6a feat: eliminate all GitHub references from agenticgovernance.digital
- Created /source-code.html — sovereign hosting landing page explaining
  why we left GitHub, how to access the code, and the sovereignty model
- Navbar: GitHub link → Source Code link (desktop + mobile)
- Footer: GitHub link → Source Code link
- Docs sidebar: GitHub section → Source Code section with sovereign repo
- Implementer page: all repository links point to /source-code.html,
  clone instructions updated, CI/CD code example genericised
- FAQ: GitHub Discussions button → Contact Us with email icon
- FAQ content: all 4 locales (en/de/fr/mi) rewritten to remove
  GitHub Actions YAML, GitHub URLs, and GitHub-specific patterns
- faq.js fallback content: same changes as locale files
- agent-lightning integration page: updated to source-code.html
- Project model: example URL changed from GitHub to Codeberg
- All locale files updated: navbar.github → navbar.source_code,
  footer GitHub → source_code, FAQ button text updated in 4 languages

Zero GitHub references remain in any HTML, JS, or JSON file
(only github-dark.min.css theme name in highlight.js CDN reference).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 18:14:26 +13:00

202 lines
5.2 KiB
JavaScript

/**
* BlogPost Model
* AI-curated blog with human oversight
*/
const { ObjectId } = require('mongodb');
const { getCollection } = require('../utils/db.util');
class BlogPost {
/**
* Create a new blog post
*/
static async create(data) {
const collection = await getCollection('blog_posts');
const post = {
title: data.title,
slug: data.slug,
author: {
type: data.author?.type || 'human', // 'human' or 'ai_curated'
name: data.author?.name || 'John Stroh',
claude_version: data.author?.claude_version
},
content: data.content,
excerpt: data.excerpt,
featured_image: data.featured_image,
status: data.status || 'draft', // draft/pending/published/archived
moderation: {
ai_analysis: data.moderation?.ai_analysis,
human_reviewer: data.moderation?.human_reviewer,
review_notes: data.moderation?.review_notes,
approved_at: data.moderation?.approved_at
},
tractatus_classification: {
quadrant: data.tractatus_classification?.quadrant || 'OPERATIONAL',
values_sensitive: data.tractatus_classification?.values_sensitive || false,
requires_strategic_review: data.tractatus_classification?.requires_strategic_review || false
},
published_at: data.published_at,
tags: data.tags || [],
view_count: 0,
engagement: {
shares: 0,
comments: 0
},
presentation: data.presentation || null
};
const result = await collection.insertOne(post);
return { ...post, _id: result.insertedId };
}
/**
* Find post by ID
*/
static async findById(id) {
const collection = await getCollection('blog_posts');
return await collection.findOne({ _id: new ObjectId(id) });
}
/**
* Find post by slug
*/
static async findBySlug(slug) {
const collection = await getCollection('blog_posts');
return await collection.findOne({ slug });
}
/**
* Find published posts
*/
static async findPublished(options = {}) {
const collection = await getCollection('blog_posts');
const { limit = 10, skip = 0, sort = { published_at: -1 } } = options;
return await collection
.find({ status: 'published' })
.sort(sort)
.skip(skip)
.limit(limit)
.toArray();
}
/**
* Find posts by status
*/
static async findByStatus(status, options = {}) {
const collection = await getCollection('blog_posts');
const { limit = 20, skip = 0 } = options;
return await collection
.find({ status })
.sort({ _id: -1 })
.skip(skip)
.limit(limit)
.toArray();
}
/**
* Update post
*/
static async update(id, updates) {
const collection = await getCollection('blog_posts');
const result = await collection.updateOne(
{ _id: new ObjectId(id) },
{ $set: updates }
);
return result.modifiedCount > 0;
}
/**
* Publish post (change status + set published_at)
*/
static async publish(id, reviewerId) {
const collection = await getCollection('blog_posts');
const result = await collection.updateOne(
{ _id: new ObjectId(id) },
{
$set: {
status: 'published',
published_at: new Date(),
'moderation.human_reviewer': reviewerId,
'moderation.approved_at': new Date()
}
}
);
// Notify subscribers (async, non-blocking)
if (result.modifiedCount > 0) {
const post = await collection.findOne({ _id: new ObjectId(id) });
if (post) {
try {
const notifier = require('../services/blogpost-notifier.service');
notifier.notifySubscribers(post, 'published').catch(err => {
console.error('[BlogPost] Subscriber notification failed (non-blocking):', err.message);
});
} catch (_) { /* notifier not available — non-critical */ }
}
}
return result.modifiedCount > 0;
}
/**
* Increment view count
*/
static async incrementViews(id) {
const collection = await getCollection('blog_posts');
await collection.updateOne(
{ _id: new ObjectId(id) },
{ $inc: { view_count: 1 } }
);
}
/**
* Delete post
*/
static async delete(id) {
const collection = await getCollection('blog_posts');
const result = await collection.deleteOne({ _id: new ObjectId(id) });
return result.deletedCount > 0;
}
/**
* Count posts by status
*/
static async countByStatus(status) {
const collection = await getCollection('blog_posts');
return await collection.countDocuments({ status });
}
/**
* Get published posts (alias for findPublished, used by RSS controller)
*/
static async getPublished(options = {}) {
return await this.findPublished(options);
}
/**
* Get published posts filtered by tag
*/
static async getPublishedByTag(tag, options = {}) {
const collection = await getCollection('blog_posts');
const { limit = 10, skip = 0, sort = { published_at: -1 } } = options;
return await collection
.find({
status: 'published',
tags: tag
})
.sort(sort)
.skip(skip)
.limit(limit)
.toArray();
}
}
module.exports = BlogPost;