tractatus/src/controllers/blog.controller.js
TheFlow 7b42067d09 feat: fix documentation system - cards, PDFs, TOC, and navigation
- 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>
2025-10-07 22:51:55 +13:00

451 lines
11 KiB
JavaScript

/**
* Blog Controller
* AI-curated blog with human oversight
*/
const BlogPost = require('../models/BlogPost.model');
const ModerationQueue = require('../models/ModerationQueue.model');
const GovernanceLog = require('../models/GovernanceLog.model');
const { markdownToHtml } = require('../utils/markdown.util');
const logger = require('../utils/logger.util');
const claudeAPI = require('../services/ClaudeAPI.service');
const BoundaryEnforcer = require('../services/BoundaryEnforcer.service');
/**
* List published blog posts (public)
* GET /api/blog
*/
async function listPublishedPosts(req, res) {
try {
const { limit = 10, skip = 0 } = req.query;
const posts = await BlogPost.findPublished({
limit: parseInt(limit),
skip: parseInt(skip)
});
const total = await BlogPost.countByStatus('published');
res.json({
success: true,
posts,
pagination: {
total,
limit: parseInt(limit),
skip: parseInt(skip),
hasMore: parseInt(skip) + posts.length < total
}
});
} catch (error) {
logger.error('List published posts error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
/**
* Get single blog post by slug (public, published only)
* GET /api/blog/:slug
*/
async function getPublishedPost(req, res) {
try {
const { slug } = req.params;
const post = await BlogPost.findBySlug(slug);
if (!post || post.status !== 'published') {
return res.status(404).json({
error: 'Not Found',
message: 'Blog post not found'
});
}
// Increment view count
await BlogPost.incrementViews(post._id);
res.json({
success: true,
post
});
} catch (error) {
logger.error('Get published post error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
/**
* List posts by status (admin only)
* GET /api/blog/admin/posts?status=draft
*/
async function listPostsByStatus(req, res) {
try {
const { status = 'draft', limit = 20, skip = 0 } = req.query;
const posts = await BlogPost.findByStatus(status, {
limit: parseInt(limit),
skip: parseInt(skip)
});
const total = await BlogPost.countByStatus(status);
res.json({
success: true,
status,
posts,
pagination: {
total,
limit: parseInt(limit),
skip: parseInt(skip)
}
});
} catch (error) {
logger.error('List posts by status error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
/**
* Get any post by ID (admin only)
* GET /api/blog/admin/:id
*/
async function getPostById(req, res) {
try {
const { id } = req.params;
const post = await BlogPost.findById(id);
if (!post) {
return res.status(404).json({
error: 'Not Found',
message: 'Blog post not found'
});
}
res.json({
success: true,
post
});
} catch (error) {
logger.error('Get post by ID error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
/**
* Create blog post (admin only)
* POST /api/blog
*/
async function createPost(req, res) {
try {
const { title, slug, content, excerpt, tags, author, tractatus_classification } = req.body;
// Convert markdown content to HTML if needed
const content_html = content.includes('# ') ? markdownToHtml(content) : content;
const post = await BlogPost.create({
title,
slug,
content: content_html,
excerpt,
tags,
author: {
...author,
name: author?.name || req.user.name || req.user.email
},
tractatus_classification,
status: 'draft'
});
logger.info(`Blog post created: ${slug} by ${req.user.email}`);
res.status(201).json({
success: true,
post
});
} catch (error) {
logger.error('Create post error:', error);
// Handle duplicate slug
if (error.code === 11000) {
return res.status(409).json({
error: 'Conflict',
message: 'A post with this slug already exists'
});
}
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
/**
* Update blog post (admin only)
* PUT /api/blog/:id
*/
async function updatePost(req, res) {
try {
const { id } = req.params;
const updates = { ...req.body };
// If content is updated and looks like markdown, convert to HTML
if (updates.content && updates.content.includes('# ')) {
updates.content = markdownToHtml(updates.content);
}
const success = await BlogPost.update(id, updates);
if (!success) {
return res.status(404).json({
error: 'Not Found',
message: 'Blog post not found'
});
}
const post = await BlogPost.findById(id);
logger.info(`Blog post updated: ${id} by ${req.user.email}`);
res.json({
success: true,
post
});
} catch (error) {
logger.error('Update post error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
/**
* Publish blog post (admin only)
* POST /api/blog/:id/publish
*/
async function publishPost(req, res) {
try {
const { id } = req.params;
const { review_notes } = req.body;
const post = await BlogPost.findById(id);
if (!post) {
return res.status(404).json({
error: 'Not Found',
message: 'Blog post not found'
});
}
if (post.status === 'published') {
return res.status(400).json({
error: 'Bad Request',
message: 'Post is already published'
});
}
// Update with review notes if provided
if (review_notes) {
await BlogPost.update(id, {
'moderation.review_notes': review_notes
});
}
// Publish the post
await BlogPost.publish(id, req.userId);
const updatedPost = await BlogPost.findById(id);
logger.info(`Blog post published: ${id} by ${req.user.email}`);
res.json({
success: true,
post: updatedPost,
message: 'Post published successfully'
});
} catch (error) {
logger.error('Publish post error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
/**
* Delete blog post (admin only)
* DELETE /api/blog/:id
*/
async function deletePost(req, res) {
try {
const { id } = req.params;
const success = await BlogPost.delete(id);
if (!success) {
return res.status(404).json({
error: 'Not Found',
message: 'Blog post not found'
});
}
logger.info(`Blog post deleted: ${id} by ${req.user.email}`);
res.json({
success: true,
message: 'Post deleted successfully'
});
} catch (error) {
logger.error('Delete post error:', error);
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
/**
* Suggest blog topics using AI (admin only)
* POST /api/blog/suggest-topics
*
* TRA-OPS-0002: AI suggests topics, human writes and approves posts
*/
async function suggestTopics(req, res) {
try {
const { audience, theme } = req.body;
// Validate audience
const validAudiences = ['researcher', 'implementer', 'advocate', 'general'];
if (!audience || !validAudiences.includes(audience)) {
return res.status(400).json({
error: 'Bad Request',
message: `Audience must be one of: ${validAudiences.join(', ')}`
});
}
logger.info(`Blog topic suggestion requested: audience=${audience}, theme=${theme || 'none'}`);
// 1. Boundary check (TRA-OPS-0002: Editorial decisions require human oversight)
const boundaryCheck = await BoundaryEnforcer.checkDecision({
decision: 'Suggest blog topics for editorial calendar',
context: 'AI provides suggestions, human makes final editorial decisions',
quadrant: 'OPERATIONAL',
action_type: 'content_suggestion'
});
// Log boundary check
await GovernanceLog.create({
action: 'BLOG_TOPIC_SUGGESTION',
user_id: req.user._id,
user_email: req.user.email,
timestamp: new Date(),
boundary_check: boundaryCheck,
outcome: boundaryCheck.allowed ? 'QUEUED_FOR_APPROVAL' : 'BLOCKED',
details: {
audience,
theme
}
});
if (!boundaryCheck.allowed) {
logger.warn(`Blog topic suggestion blocked by BoundaryEnforcer: ${boundaryCheck.section}`);
return res.status(403).json({
error: 'Boundary Violation',
message: boundaryCheck.reasoning,
section: boundaryCheck.section,
details: 'This action requires human judgment in values territory'
});
}
// 2. Claude API call for topic suggestions
const suggestions = await claudeAPI.generateBlogTopics(audience, theme);
logger.info(`Claude API returned ${suggestions.length} topic suggestions`);
// 3. Create moderation queue entry (human approval required)
const queueEntry = await ModerationQueue.create({
type: 'BLOG_TOPIC_SUGGESTION',
reference_collection: 'blog_posts',
data: {
audience,
theme,
suggestions,
requested_by: req.user.email
},
status: 'PENDING_APPROVAL',
ai_generated: true,
requires_human_approval: true,
created_by: req.user._id,
created_at: new Date(),
metadata: {
boundary_check: boundaryCheck,
governance_policy: 'TRA-OPS-0002'
}
});
logger.info(`Created moderation queue entry: ${queueEntry._id}`);
// 4. Return response (suggestions queued for human review)
res.json({
success: true,
message: 'Blog topic suggestions generated. Awaiting human review and approval.',
queue_id: queueEntry._id,
suggestions,
governance: {
policy: 'TRA-OPS-0002',
boundary_check: boundaryCheck,
requires_approval: true,
note: 'Topics are suggestions only. Human must write all blog posts.'
}
});
} catch (error) {
logger.error('Suggest topics error:', error);
// Handle Claude API errors specifically
if (error.message.includes('Claude API')) {
return res.status(502).json({
error: 'AI Service Error',
message: 'Failed to generate topic suggestions. Please try again.',
details: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
res.status(500).json({
error: 'Internal Server Error',
message: 'An error occurred'
});
}
}
module.exports = {
listPublishedPosts,
getPublishedPost,
listPostsByStatus,
getPostById,
createPost,
updatePost,
publishPost,
deletePost,
suggestTopics
};