Add public curator notes and U-shaped hall wall hang.

Paintings get editable curator notes with brass plates in the 3D hall, and visit order now uses the far/end wall between left and right.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila Khodjaef
2026-07-25 14:21:22 +03:00
co-authored by Cursor
parent 5ddc3fd7f0
commit bc8369e373
29 changed files with 648 additions and 70 deletions
+39
View File
@@ -836,6 +836,45 @@ app.patch('/api/paintings/:id/checkup-flags', requireCurator, async (req, res) =
}
});
// Update public curator notes on a painting
app.patch('/api/paintings/:id/curator-notes', requireCurator, async (req, res) => {
try {
const paintingId = parseInt(req.params.id, 10);
if (!Number.isFinite(paintingId)) {
return res.status(400).json({ error: 'Invalid painting id' });
}
if (typeof req.body?.curatorNotes !== 'string') {
return res.status(400).json({ error: 'curatorNotes must be a string' });
}
const curatorNotes = req.body.curatorNotes.trim();
const result = await pool.query(
`UPDATE paintings SET curator_notes = $2
WHERE id = $1
RETURNING curator_notes`,
[paintingId, curatorNotes]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Painting not found' });
}
res.json({ curatorNotes: result.rows[0].curator_notes ?? '' });
await logCuratorAction({
userId: req.curatorUser.id,
action: 'painting.update_curator_notes',
resourceType: 'painting',
resourceId: paintingId,
details: { length: curatorNotes.length },
req,
});
} catch (err) {
console.error('Curator notes update error:', err.message);
res.status(500).json({ error: 'Failed to update curator notes' });
}
});
// Painting detail with influences
app.get('/api/paintings/:id', async (req, res) => {
try {
+12 -5
View File
@@ -4,6 +4,17 @@ const pool = require('../db');
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
function resolveCookieSecure() {
const flag = process.env.SESSION_COOKIE_SECURE;
if (flag === '1' || flag === 'true') return true;
if (flag === '0' || flag === 'false') return false;
// Behind Keenetic/HTTPS termination: match the browser scheme via X-Forwarded-Proto.
if (process.env.TRUST_PROXY === '1' || process.env.TRUST_PROXY === 'true') {
return 'auto';
}
return false;
}
function createSessionMiddleware() {
const secret = process.env.SESSION_SECRET;
if (!secret) {
@@ -12,10 +23,6 @@ function createSessionMiddleware() {
);
}
const secureCookie =
process.env.SESSION_COOKIE_SECURE === '1' ||
process.env.SESSION_COOKIE_SECURE === 'true';
return session({
store: new pgSession({
pool,
@@ -28,7 +35,7 @@ function createSessionMiddleware() {
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: secureCookie,
secure: resolveCookieSecure(),
sameSite: 'lax',
maxAge: SEVEN_DAYS_MS,
},
+1
View File
@@ -16,6 +16,7 @@ const INCREMENTAL_MIGRATIONS = [
'migrate-sync-timestamps.sql',
'migrate-i18n.sql',
'migrate-tours.sql',
'migrate-curator-notes.sql',
];
async function bootstrapCurator() {
+11 -4
View File
@@ -38,7 +38,7 @@ router.post('/login', async (req, res) => {
try {
const { rows } = await pool.query(
`SELECT id, username, password_hash FROM users WHERE username = $1`,
`SELECT id, username, password_hash FROM users WHERE LOWER(username) = LOWER($1)`,
[username.trim()]
);
if (rows.length === 0) {
@@ -56,9 +56,16 @@ router.post('/login', async (req, res) => {
req.session.userId = user.id;
req.session.username = user.username;
res.json({
role: 'curator',
username: user.username,
// Ensure the store writes before the response finishes (proxy / HTTPS).
req.session.save((err) => {
if (err) {
console.error('Auth session save error:', err.message);
return res.status(500).json({ error: 'Login failed' });
}
res.json({
role: 'curator',
username: user.username,
});
});
} catch (err) {
console.error('Auth login error:', err.message);