Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
25a26d9
Implement Meridian event tasks and org task hub MVP
cursoragent Mar 30, 2026
49da665
chore(debug): add runtime crash instrumentation logs
cursoragent Mar 30, 2026
3fb3562
fix(club-dash): stabilize task hub wiring and nav after Tasks tab
cursoragent Mar 30, 2026
a8fc311
fix(frontend): stabilize club-dashboard against Chrome renderer crashes
cursoragent Mar 30, 2026
632fffc
Refine task UI with Linear-inspired visual language
cursoragent Mar 31, 2026
39b4025
Add org task hub kanban and create task popups
cursoragent Mar 31, 2026
9a4bb50
Add drag-and-drop between kanban columns
cursoragent Mar 31, 2026
4c691c3
Polish kanban drag state and optimistic updates
cursoragent Mar 31, 2026
f7e65b8
Fix kanban drag duplication during optimistic updates
cursoragent Mar 31, 2026
8d6019e
Resolve kanban drag hook dependency warning
cursoragent Mar 31, 2026
bada52f
debug(tasks): instrument kanban drag/drop state for duplication analysis
cursoragent Mar 31, 2026
13ed511
MER-177 feat(tasks-backend): add configurable board statuses and orde…
AZ0228 Apr 7, 2026
8da62a3
MER-177 feat(tasks-ui): add shared board and rich task detail workspa…
AZ0228 Apr 7, 2026
958e78b
MER-177 feat(task-surfaces): wire shared task board into Club and OIE…
AZ0228 Apr 7, 2026
96413e3
MER-177 feat(dashboard): add standard sidebar collapse toggle and tru…
AZ0228 Apr 7, 2026
3ef565d
MER-178: Add finance and org budget schemas plus org membership audit…
AZ0228 Apr 8, 2026
1d2db57
MER-178: Register budget, finance config, and audit models in getMode…
AZ0228 Apr 8, 2026
62f307d
MER-178: Implement org budget workflow service (CRUD, stages, revisio…
AZ0228 Apr 8, 2026
310d8b0
MER-178: Add atlas policy helper and org membership service for gover…
AZ0228 Apr 8, 2026
6d36f82
MER-178: Expose org-scoped budget API routes under /org-budgets.
AZ0228 Apr 8, 2026
f2c1687
MER-178: Extend org management routes for finance queue and governanc…
AZ0228 Apr 8, 2026
cb2d67e
MER-178: Wire budget and atlas permissions through org, role, and eve…
AZ0228 Apr 8, 2026
f416c9e
MER-178: Add budget route outcomes and unit tests for budget and atla…
AZ0228 Apr 8, 2026
b80284e
MER-178: Add shared budget UI (audit timeline, stage message popup) a…
AZ0228 Apr 8, 2026
5ecb367
MER-178: Atlas finance budget queue, governance approvals, and financ…
AZ0228 Apr 8, 2026
901238b
MER-178: ClubDash org switching, budget/governance/lifecycle settings…
AZ0228 Apr 8, 2026
9d202c6
MER-178: Fix dashboard content stacking and add elevated Popup overla…
AZ0228 Apr 8, 2026
48d667e
MER-178: Org admin modal and overview updates for governance and fina…
AZ0228 Apr 8, 2026
9d8204e
MER-179: Replace room schedule iframe with calendar popup view.
AZ0228 Apr 8, 2026
4c9f949
MER-179: Implement reservation preflight across event flows.
AZ0228 Apr 8, 2026
fa4e1a3
MER-178: Refactor Atlas org management into a richer admin workflow w…
AZ0228 Apr 13, 2026
f993b5f
MER-178: Add domain-level approval operations with pending queues, mo…
AZ0228 Apr 13, 2026
9b0199e
MER-178: Expand reservation preflight into operational tooling with c…
AZ0228 Apr 13, 2026
620575a
MER-178: Improve dashboard and event management UX with sidebar edge-…
AZ0228 Apr 13, 2026
03cf7ca
MER-178: Harden profile update handling with explicit validation, dup…
AZ0228 Apr 13, 2026
a421f25
Merge remote-tracking branch 'origin/main' into MER-178-Budget
AZ0228 Apr 15, 2026
d181fee
MER-178: align backend tenant services with request-scoped model conv…
AZ0228 Apr 15, 2026
abc3197
MER-178: deliver community organizer UX and onboarding management imp…
AZ0228 Apr 15, 2026
384c58e
chore(MER-178-Budget): sync events @ 5dbc9cb
AZ0228 Apr 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion backend/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const path = require('path');
const fs = require('fs');
const cookieParser = require('cookie-parser');
const multer = require('multer');
const passport = require('passport');
Expand Down Expand Up @@ -59,6 +60,25 @@ function createApp() {
app.use(passport.initialize());
app.use(express.urlencoded({ extended: true }));

if (process.env.NODE_ENV !== 'production') {
app.post('/api/_agent-debug-log', (req, res) => {
try {
const payload = req.body || {};
const line = JSON.stringify({
hypothesisId: payload.hypothesisId || 'unknown',
location: payload.location || 'unknown',
message: payload.message || 'unknown',
data: payload.data && typeof payload.data === 'object' ? payload.data : {},
timestamp: Number(payload.timestamp) || Date.now()
});
fs.appendFileSync('/opt/cursor/logs/debug.log', `${line}\n`);
return res.status(200).json({ success: true });
} catch (error) {
return res.status(500).json({ success: false, message: 'debug log write failed' });
}
});
}

app.use(async (req, res, next) => {
try {
// Debug logging to identify polling routes
Expand Down Expand Up @@ -223,8 +243,10 @@ function createApp() {
const orgRoutes = require('./routes/orgRoutes.js');
const orgRoleRoutes = require('./routes/orgRoleRoutes.js');
const orgManagementRoutes = require('./routes/orgManagementRoutes.js');
const orgBudgetRoutes = require('./routes/orgBudgetRoutes.js');
const orgInviteRoutes = require('./routes/orgInviteRoutes.js');
const orgMessageRoutes = require('./routes/orgMessageRoutes.js');
const taskManagementRoutes = require('./routes/taskManagementRoutes.js');
const roomRoutes = require('./routes/roomRoutes.js');
const adminRoutes = require('./routes/adminRoutes.js');
const eventsRoutes = require('./events/index.js');
Expand Down Expand Up @@ -258,9 +280,11 @@ function createApp() {
app.use(orgRoutes);
app.use('/org-roles', orgRoleRoutes);
app.use('/org-management', orgManagementRoutes);
app.use('/org-budgets', orgBudgetRoutes);
app.use('/org-invites', orgInviteRoutes);
app.use('/org-messages', orgMessageRoutes);
app.use('/org-event-management', orgEventManagementRoutes);
app.use('/org-event-management', taskManagementRoutes);
app.use('/admin', roomRoutes);
app.use(adminRoutes);
app.use(formRoutes);
Expand Down Expand Up @@ -304,6 +328,8 @@ function createApp() {
};

try {
const getModels = require('./services/getModelService');
const { Classroom } = getModels(req, 'Classroom');
// Upload image to S3
const s3Response = await s3.upload(s3Params).promise();
const imageUrl = s3Response.Location;
Expand All @@ -313,7 +339,7 @@ function createApp() {
{ name: classroomName },
{ image: imageUrl },
{ new: true, upsert: true }
);
).populate('building', 'name');

res.status(200).json({ message: 'Image uploaded and classroom updated.', classroom });
} catch (error) {
Expand Down
5 changes: 4 additions & 1 deletion backend/constants/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ const ORG_PERMISSIONS = {

// Advanced features
MANAGE_INTEGRATIONS: 'manage_integrations', // Can manage third-party integrations
ACCESS_ADVANCED_FEATURES: 'access_advanced_features' // Can access beta/advanced features
ACCESS_ADVANCED_FEATURES: 'access_advanced_features', // Can access beta/advanced features

/** Atlas CMS Phase 1: upload/version governance documents (constitution, etc.) */
MANAGE_GOVERNANCE: 'manage_governance'
};

// Event-specific permissions
Expand Down
15 changes: 15 additions & 0 deletions backend/middlewares/verifyToken.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,21 @@ const verifyToken = async (req, res, next) => {
try {
const decodedToken = jwt.verify(token, process.env.JWT_SECRET);
await resolveRequestUser(req, decodedToken);
if (req.user?.userId) {
try {
const { User } = getModels(req, 'User');
const accountUser = await User.findById(req.user.userId).select('accessSuspended').lean();
if (accountUser?.accessSuspended) {
return res.status(403).json({
success: false,
message: 'This account has been suspended.',
code: 'ACCOUNT_SUSPENDED',
});
}
} catch (checkErr) {
console.error('[verifyToken] accessSuspended check failed:', checkErr);
}
}
return next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
Expand Down
152 changes: 152 additions & 0 deletions backend/migrations/migrateClassroomBuildingRefs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/**
* One-shot migration: legacy Classroom.building (string) → ObjectId ref to Building.
* Uses the native driver on collection names so it stays reliable across schema changes.
*
* Deploy note: existing tenants still have string `building` values until this runs.
* With the ObjectId classroom schema, hydrate those docs before migration will throw CastError.
* Prefer running once via CLI *before* restarting app servers on this version:
* MIGRATION_SCHOOL=rpi node Meridian/backend/migrations/migrateClassroomBuildingRefs.js
* Optional: FORCE=1 to clear the per-tenant guard row and re-run.
*/

const mongoose = require('mongoose');
const { connectToDatabase } = require('../connectionsManager');

const MIGRATION_KEY = 'classroom_building_oid_ref';
const CLASSROOMS_COLL = 'classrooms1';
const BUILDINGS_COLL = 'buildings';
const RUNS_COLL = 'admin_migration_runs';

const DEFAULT_BUILDING_IMAGE = '/classrooms/default.png';
const DEFAULT_TIME = { start: 0, end: 24 * 60 };

function escapeRegex(s) {
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

async function findBuildingByName(buildings, name) {
const trimmed = String(name || '').trim();
if (!trimmed) return null;
let doc = await buildings.findOne({ name: trimmed });
if (doc) return doc;
doc = await buildings.findOne({ name: new RegExp(`^${escapeRegex(trimmed)}$`, 'i') });
return doc;
}

/**
* @param {import('mongoose').Connection} mongooseConn
* @param {{ force?: boolean }} [options]
*/
async function runMigrateClassroomBuildingRefs(mongooseConn, options = {}) {
const { force = false } = options;
const db = mongooseConn.db;
const runs = db.collection(RUNS_COLL);
const classrooms = db.collection(CLASSROOMS_COLL);
const buildings = db.collection(BUILDINGS_COLL);

if (!force) {
const prior = await runs.findOne({ key: MIGRATION_KEY });
if (prior) {
return {
skipped: true,
reason: 'already_run',
ranAt: prior.completedAt || null,
};
}
} else {
await runs.deleteMany({ key: MIGRATION_KEY });
}

const stringRooms = await classrooms
.find({
building: { $exists: true, $type: 'string', $nin: ['', null] },
})
.toArray();

const distinctNames = [
...new Set(stringRooms.map((d) => String(d.building).trim()).filter(Boolean)),
];

const buildingsCreated = [];
const buildingsReused = [];
const nameToId = new Map();

for (const name of distinctNames) {
const existing = await findBuildingByName(buildings, name);
if (existing) {
nameToId.set(name, existing._id);
buildingsReused.push(name);
continue;
}
const ins = await buildings.insertOne({
name,
image: DEFAULT_BUILDING_IMAGE,
time: DEFAULT_TIME,
});
nameToId.set(name, ins.insertedId);
buildingsCreated.push(name);
}

const bulkOps = stringRooms
.map((doc) => {
const name = String(doc.building).trim();
const bid = nameToId.get(name);
if (!bid) return null;
return {
updateOne: {
filter: { _id: doc._id, building: { $type: 'string' } },
update: { $set: { building: bid } },
},
};
})
.filter(Boolean);

let classroomsUpdated = 0;
if (bulkOps.length) {
const wr = await classrooms.bulkWrite(bulkOps, { ordered: false });
classroomsUpdated = wr.modifiedCount || 0;
}

await classrooms.updateMany({ building: '' }, { $unset: { building: '' } });

const summary = {
skipped: false,
distinctBuildingNames: distinctNames.length,
buildingsCreatedCount: buildingsCreated.length,
buildingsReusedCount: buildingsReused.length,
buildingsCreated,
classroomsUpdated,
};

await runs.insertOne({
key: MIGRATION_KEY,
completedAt: new Date(),
summary,
});

return summary;
}

module.exports = {
runMigrateClassroomBuildingRefs,
MIGRATION_KEY,
};

async function cliMain() {
const school = process.env.MIGRATION_SCHOOL || process.argv[2] || 'rpi';
const conn = await connectToDatabase(school);
try {
const out = await runMigrateClassroomBuildingRefs(conn, { force: process.env.FORCE === '1' });
console.log(JSON.stringify(out, null, 2));
} finally {
await conn.close().catch(() => {});
await mongoose.disconnect().catch(() => {});
}
}

if (require.main === module) {
cliMain().then(() => process.exit(0)).catch((e) => {
console.error(e);
process.exit(1);
});
}
59 changes: 59 additions & 0 deletions backend/migrations/seedDomainSpaceGovernance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env node
require('dotenv').config();

const mongoose = require('mongoose');
const { connectToDatabase } = require('../connectionsManager');
const getModels = require('../services/getModelService');

function getDefaultSpaceGovernance() {
return {
governingScope: {
kind: 'all_spaces',
buildingIds: [],
spaceIds: [],
spaceGroupIds: []
},
concernScope: {
kind: 'campus_wide',
buildingIds: [],
spaceIds: [],
spaceGroupIds: []
},
scopeMode: 'inclusive',
priorityRules: []
};
}

async function run() {
const school = process.env.MIGRATION_SCHOOL || process.argv[2] || 'rpi';
const db = await connectToDatabase(school);
const req = { db };
const { Domain } = getModels(req, 'Domain');

const result = await Domain.updateMany(
{
$or: [
{ spaceGovernance: { $exists: false } },
{ spaceGovernance: null }
]
},
{
$set: {
spaceGovernance: getDefaultSpaceGovernance()
}
}
);

console.log(
`[seedDomainSpaceGovernance] school=${school} matched=${result.matchedCount || 0} modified=${result.modifiedCount || 0}`
);
}

run()
.catch((error) => {
console.error('[seedDomainSpaceGovernance] failed', error);
process.exitCode = 1;
})
.finally(async () => {
await mongoose.disconnect();
});
Loading
Loading