**Three Public Features:** - Media Inquiry System: Press/media can submit inquiries with AI triage (Phase 2) - Case Study Submissions: Community can submit real-world AI safety failures - Blog Curation: Admin-only topic suggestions with AI assistance (Phase 2) **Backend Implementation:** - Media routes/controller: /api/media/inquiries endpoints - Cases routes/controller: /api/cases/submit endpoints - Blog routes/controller: Already existed, documented - Human oversight: All submissions go to moderation queue - Tractatus boundaries: BoundaryEnforcer integration in blog controller **Frontend Forms:** - /media-inquiry.html: Public submission form for press/media - /case-submission.html: Public submission form for case studies - Full validation, error handling, success messages **Validation Middleware Updates:** - Support nested field validation (contact.email, submitter.name) - validateEmail(fieldPath) now parameterized - validateRequired() supports dot-notation paths **Phase 1 Status:** - AI triage: Manual (Phase 2 will add Claude API integration) - All submissions require human review and approval - Moderation queue operational - Admin dashboard endpoints ready **Files Added:** - public/media-inquiry.html - public/case-submission.html - src/controllers/media.controller.js - src/controllers/cases.controller.js - src/routes/media.routes.js - src/routes/cases.routes.js **Files Modified:** - src/routes/index.js (registered new routes) - src/routes/auth.routes.js (updated validateEmail call) - src/middleware/validation.middleware.js (nested field support) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
314 lines
7.2 KiB
JavaScript
314 lines
7.2 KiB
JavaScript
/**
|
|
* Media Inquiry Controller
|
|
* Press/media inquiry submission and AI triage
|
|
*/
|
|
|
|
const MediaInquiry = require('../models/MediaInquiry.model');
|
|
const ModerationQueue = require('../models/ModerationQueue.model');
|
|
const GovernanceLog = require('../models/GovernanceLog.model');
|
|
const BoundaryEnforcer = require('../services/BoundaryEnforcer.service');
|
|
const logger = require('../utils/logger.util');
|
|
|
|
/**
|
|
* Submit media inquiry (public)
|
|
* POST /api/media/inquiries
|
|
*
|
|
* Phase 1: Manual triage (no AI)
|
|
* Phase 2: Add AI triage with claudeAPI.triageMediaInquiry()
|
|
*/
|
|
async function submitInquiry(req, res) {
|
|
try {
|
|
const { contact, inquiry } = req.body;
|
|
|
|
// Validate required fields
|
|
if (!contact?.name || !contact?.email || !contact?.outlet) {
|
|
return res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: 'Missing required contact information'
|
|
});
|
|
}
|
|
|
|
if (!inquiry?.subject || !inquiry?.message) {
|
|
return res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: 'Missing required inquiry information'
|
|
});
|
|
}
|
|
|
|
logger.info(`Media inquiry submitted: ${contact.outlet} - ${inquiry.subject}`);
|
|
|
|
// Create inquiry (Phase 1: no AI triage yet)
|
|
const mediaInquiry = await MediaInquiry.create({
|
|
contact,
|
|
inquiry,
|
|
status: 'new',
|
|
ai_triage: {
|
|
urgency: 'medium', // Default, will be AI-assessed in Phase 2
|
|
topic_sensitivity: 'standard',
|
|
involves_values: false
|
|
}
|
|
});
|
|
|
|
// Add to moderation queue for human review
|
|
await ModerationQueue.create({
|
|
type: 'MEDIA_INQUIRY',
|
|
reference_collection: 'media_inquiries',
|
|
reference_id: mediaInquiry._id,
|
|
quadrant: 'OPERATIONAL',
|
|
data: {
|
|
contact,
|
|
inquiry
|
|
},
|
|
priority: 'medium',
|
|
status: 'PENDING_APPROVAL',
|
|
requires_human_approval: true,
|
|
human_required_reason: 'All media inquiries require human review and response'
|
|
});
|
|
|
|
logger.info(`Media inquiry created: ${mediaInquiry._id}`);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: 'Thank you for your inquiry. We will review and respond shortly.',
|
|
inquiry_id: mediaInquiry._id,
|
|
governance: {
|
|
human_review: true,
|
|
note: 'All media inquiries are reviewed by humans before response'
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Submit inquiry error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred while submitting your inquiry'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List all media inquiries (admin)
|
|
* GET /api/media/inquiries?status=new
|
|
*/
|
|
async function listInquiries(req, res) {
|
|
try {
|
|
const { status = 'new', limit = 20, skip = 0 } = req.query;
|
|
|
|
const inquiries = await MediaInquiry.findByStatus(status, {
|
|
limit: parseInt(limit),
|
|
skip: parseInt(skip)
|
|
});
|
|
|
|
const total = await MediaInquiry.countByStatus(status);
|
|
|
|
res.json({
|
|
success: true,
|
|
status,
|
|
inquiries,
|
|
pagination: {
|
|
total,
|
|
limit: parseInt(limit),
|
|
skip: parseInt(skip),
|
|
hasMore: parseInt(skip) + inquiries.length < total
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('List inquiries error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List urgent media inquiries (admin)
|
|
* GET /api/media/inquiries/urgent
|
|
*/
|
|
async function listUrgentInquiries(req, res) {
|
|
try {
|
|
const { limit = 10 } = req.query;
|
|
|
|
const inquiries = await MediaInquiry.findUrgent({
|
|
limit: parseInt(limit)
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
count: inquiries.length,
|
|
inquiries
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('List urgent inquiries error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get media inquiry by ID (admin)
|
|
* GET /api/media/inquiries/:id
|
|
*/
|
|
async function getInquiry(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const inquiry = await MediaInquiry.findById(id);
|
|
|
|
if (!inquiry) {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Media inquiry not found'
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
inquiry
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Get inquiry error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assign inquiry to user (admin)
|
|
* POST /api/media/inquiries/:id/assign
|
|
*/
|
|
async function assignInquiry(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { user_id } = req.body;
|
|
|
|
const userId = user_id || req.user._id;
|
|
|
|
const success = await MediaInquiry.assign(id, userId);
|
|
|
|
if (!success) {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Media inquiry not found'
|
|
});
|
|
}
|
|
|
|
logger.info(`Media inquiry ${id} assigned to ${userId} by ${req.user.email}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Inquiry assigned successfully'
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Assign inquiry error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Respond to inquiry (admin)
|
|
* POST /api/media/inquiries/:id/respond
|
|
*/
|
|
async function respondToInquiry(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { content } = req.body;
|
|
|
|
if (!content) {
|
|
return res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: 'Response content is required'
|
|
});
|
|
}
|
|
|
|
const inquiry = await MediaInquiry.findById(id);
|
|
|
|
if (!inquiry) {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Media inquiry not found'
|
|
});
|
|
}
|
|
|
|
const success = await MediaInquiry.respond(id, {
|
|
content,
|
|
responder: req.user.email
|
|
});
|
|
|
|
if (!success) {
|
|
return res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'Failed to update inquiry'
|
|
});
|
|
}
|
|
|
|
logger.info(`Media inquiry ${id} responded to by ${req.user.email}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Response recorded successfully',
|
|
note: 'Remember to send actual email to media contact separately'
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Respond to inquiry error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete media inquiry (admin)
|
|
* DELETE /api/media/inquiries/:id
|
|
*/
|
|
async function deleteInquiry(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const success = await MediaInquiry.delete(id);
|
|
|
|
if (!success) {
|
|
return res.status(404).json({
|
|
error: 'Not Found',
|
|
message: 'Media inquiry not found'
|
|
});
|
|
}
|
|
|
|
logger.info(`Media inquiry deleted: ${id} by ${req.user.email}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Inquiry deleted successfully'
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Delete inquiry error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
submitInquiry,
|
|
listInquiries,
|
|
listUrgentInquiries,
|
|
getInquiry,
|
|
assignInquiry,
|
|
respondToInquiry,
|
|
deleteInquiry
|
|
};
|