feat: statuts → tags, panneau fiche, personnages enrichis, icônes SVG
- Statut absorbé par les tags (migration serveur, couleurs réservées, pas d'exclusivité) - Dropdown Statut supprimé (frise, rédaction, modale) - Filtre par tag ajouté en page Rédaction - Panneau fiche sticky en Rédaction (résumé, intérêt narratif, implications, tags, personnages, commentaires) - Fiches personnages : champs Origines et influences + Backstory (migration NocoDB) - Liens scènes automatiques sur les fiches personnages - Markdown (marked.js + DOMPurify) sur les fiches personnages - Icônes SVG sidebar (étoile, frise, crayon, avatar, livre) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+894
-89
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,8 @@ const NOCODB_BASE_ID = process.env.NOCODB_BASE_ID || '';
|
||||
const NOCODB_TABLE_ID = process.env.NOCODB_TABLE_ID || '';
|
||||
const NOCODB_VISION_TABLE_ID = process.env.NOCODB_VISION_TABLE_ID || '';
|
||||
const NOCODB_CONTENU_TABLE_ID = process.env.NOCODB_CONTENU_TABLE_ID || '';
|
||||
const NOCODB_PERSONNAGES_TABLE_ID = process.env.NOCODB_PERSONNAGES_TABLE_ID || '';
|
||||
const NOCODB_RESSOURCES_TABLE_ID = process.env.NOCODB_RESSOURCES_TABLE_ID || '';
|
||||
|
||||
// Auth
|
||||
function makeToken(ts) {
|
||||
@@ -266,6 +268,119 @@ app.put('/api/contenu/:sceneId', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ── Personnages API ──
|
||||
|
||||
function personnagesProxy(tableIdVar) {
|
||||
return NOCODB_PERSONNAGES_TABLE_ID;
|
||||
}
|
||||
|
||||
app.get('/api/personnages', requireAuth, async (req, res) => {
|
||||
if (!NOCODB_PERSONNAGES_TABLE_ID) return res.json({ list: [] });
|
||||
try {
|
||||
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_PERSONNAGES_TABLE_ID}?limit=100&sort=Nom`;
|
||||
const r = await fetch(url, { headers: { 'xc-token': NOCODB_TOKEN } });
|
||||
res.json(await r.json());
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
app.post('/api/personnages', requireAuth, async (req, res) => {
|
||||
if (!NOCODB_PERSONNAGES_TABLE_ID) return res.status(400).json({ error: 'Table non configurée' });
|
||||
try {
|
||||
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_PERSONNAGES_TABLE_ID}`;
|
||||
const r = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(req.body),
|
||||
});
|
||||
res.status(r.status).json(await r.json());
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
app.patch('/api/personnages/:id', requireAuth, async (req, res) => {
|
||||
if (!NOCODB_PERSONNAGES_TABLE_ID) return res.status(400).json({ error: 'Table non configurée' });
|
||||
try {
|
||||
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_PERSONNAGES_TABLE_ID}/${req.params.id}`;
|
||||
const r = await fetch(url, {
|
||||
method: 'PATCH',
|
||||
headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(req.body),
|
||||
});
|
||||
res.status(r.status).json(await r.json());
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
app.delete('/api/personnages/:id', requireAuth, async (req, res) => {
|
||||
if (!NOCODB_PERSONNAGES_TABLE_ID) return res.status(400).json({ error: 'Table non configurée' });
|
||||
try {
|
||||
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_PERSONNAGES_TABLE_ID}/${req.params.id}`;
|
||||
const r = await fetch(url, { method: 'DELETE', headers: { 'xc-token': NOCODB_TOKEN } });
|
||||
res.status(r.status).json({ ok: true });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// ── Ressources API ──
|
||||
|
||||
app.get('/api/ressources', requireAuth, async (req, res) => {
|
||||
if (!NOCODB_RESSOURCES_TABLE_ID) return res.json({ list: [] });
|
||||
try {
|
||||
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_RESSOURCES_TABLE_ID}?limit=200&sort=-CreatedAt`;
|
||||
const r = await fetch(url, { headers: { 'xc-token': NOCODB_TOKEN } });
|
||||
res.json(await r.json());
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
app.post('/api/ressources', requireAuth, async (req, res) => {
|
||||
if (!NOCODB_RESSOURCES_TABLE_ID) return res.status(400).json({ error: 'Table non configurée' });
|
||||
try {
|
||||
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_RESSOURCES_TABLE_ID}`;
|
||||
const r = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(req.body),
|
||||
});
|
||||
res.status(r.status).json(await r.json());
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
app.patch('/api/ressources/:id', requireAuth, async (req, res) => {
|
||||
if (!NOCODB_RESSOURCES_TABLE_ID) return res.status(400).json({ error: 'Table non configurée' });
|
||||
try {
|
||||
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_RESSOURCES_TABLE_ID}/${req.params.id}`;
|
||||
const r = await fetch(url, {
|
||||
method: 'PATCH',
|
||||
headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(req.body),
|
||||
});
|
||||
res.status(r.status).json(await r.json());
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
app.delete('/api/ressources/:id', requireAuth, async (req, res) => {
|
||||
if (!NOCODB_RESSOURCES_TABLE_ID) return res.status(400).json({ error: 'Table non configurée' });
|
||||
try {
|
||||
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_RESSOURCES_TABLE_ID}/${req.params.id}`;
|
||||
const r = await fetch(url, { method: 'DELETE', headers: { 'xc-token': NOCODB_TOKEN } });
|
||||
res.status(r.status).json({ ok: true });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// Ressource image upload
|
||||
app.post('/api/ressource-upload', requireAuth, (req, res) => {
|
||||
try {
|
||||
const { dataUrl } = req.body;
|
||||
if (!dataUrl || !dataUrl.startsWith('data:image/')) return res.status(400).json({ error: 'Invalid image' });
|
||||
const matches = dataUrl.match(/^data:image\/(\w+);base64,(.+)$/);
|
||||
if (!matches) return res.status(400).json({ error: 'Invalid format' });
|
||||
const ext = matches[1] === 'jpeg' ? 'jpg' : matches[1];
|
||||
const buffer = Buffer.from(matches[2], 'base64');
|
||||
const uploadDir = path.join(__dirname, 'public', 'uploads');
|
||||
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
|
||||
const filename = `res-${Date.now()}.${ext}`;
|
||||
fs.writeFileSync(path.join(uploadDir, filename), buffer);
|
||||
res.json({ path: `/uploads/${filename}?t=${Date.now()}` });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// ── Hero image upload (file too large for NocoDB LongText) ──
|
||||
const fs = require('fs');
|
||||
|
||||
@@ -319,4 +434,101 @@ app.get('/', (req, res) => {
|
||||
// Static files (login.html is public, index.html is gated above)
|
||||
app.use(express.static(path.join(__dirname, 'public'), { index: false }));
|
||||
|
||||
app.listen(PORT, () => console.log(`TdB RdB — port ${PORT}`));
|
||||
// ── Startup migration: ensure Tags column exists ──
|
||||
async function ensureTagsColumn() {
|
||||
try {
|
||||
const url = `${NOCODB_URL}/api/v2/meta/tables/${NOCODB_TABLE_ID}`;
|
||||
const r = await fetch(url, { headers: { 'xc-token': NOCODB_TOKEN } });
|
||||
const table = await r.json();
|
||||
const hasTagsCol = (table.columns || []).some(c => c.title === 'Tags');
|
||||
if (!hasTagsCol) {
|
||||
const createUrl = `${NOCODB_URL}/api/v2/meta/tables/${NOCODB_TABLE_ID}/columns`;
|
||||
const cr = await fetch(createUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ column_name: 'Tags', title: 'Tags', uidt: 'LongText' }),
|
||||
});
|
||||
if (cr.ok) {
|
||||
console.log('Tags column created');
|
||||
// Migrate existing Type values to Tags
|
||||
const recUrl = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_TABLE_ID}?limit=200&fields=Id,Type`;
|
||||
const recR = await fetch(recUrl, { headers: { 'xc-token': NOCODB_TOKEN } });
|
||||
const recData = await recR.json();
|
||||
for (const rec of (recData.list || [])) {
|
||||
if (rec.Type) {
|
||||
const patchUrl = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_TABLE_ID}/${rec.Id}`;
|
||||
await fetch(patchUrl, {
|
||||
method: 'PATCH',
|
||||
headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ Tags: rec.Type }),
|
||||
});
|
||||
}
|
||||
}
|
||||
console.log('Type values migrated to Tags');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Tags migration error:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Startup migration: Statut → Tags (one-time) ──
|
||||
async function migrateStatutToTags() {
|
||||
try {
|
||||
const recUrl = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_TABLE_ID}?limit=200&fields=Id,Statut,Tags`;
|
||||
const recR = await fetch(recUrl, { headers: { 'xc-token': NOCODB_TOKEN } });
|
||||
const recData = await recR.json();
|
||||
let migrated = 0;
|
||||
for (const rec of (recData.list || [])) {
|
||||
if (rec.Statut) {
|
||||
const existingTags = (rec.Tags || '').split(',').map(t => t.trim()).filter(Boolean);
|
||||
if (!existingTags.includes(rec.Statut)) {
|
||||
existingTags.push(rec.Statut);
|
||||
}
|
||||
const patchUrl = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_TABLE_ID}/${rec.Id}`;
|
||||
await fetch(patchUrl, {
|
||||
method: 'PATCH',
|
||||
headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ Tags: existingTags.join(', '), Statut: null }),
|
||||
});
|
||||
migrated++;
|
||||
}
|
||||
}
|
||||
if (migrated > 0) console.log(`Statut migrated to Tags for ${migrated} records`);
|
||||
} catch (e) {
|
||||
console.error('Statut migration error:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Startup migration: ensure Personnages columns (Origines, Backstory) ──
|
||||
async function ensurePersonnagesColumns() {
|
||||
if (!NOCODB_PERSONNAGES_TABLE_ID) return;
|
||||
try {
|
||||
const url = `${NOCODB_URL}/api/v2/meta/tables/${NOCODB_PERSONNAGES_TABLE_ID}`;
|
||||
const r = await fetch(url, { headers: { 'xc-token': NOCODB_TOKEN } });
|
||||
const table = await r.json();
|
||||
const existing = (table.columns || []).map(c => c.title);
|
||||
for (const col of ['Origines', 'Backstory']) {
|
||||
if (!existing.includes(col)) {
|
||||
const createUrl = `${NOCODB_URL}/api/v2/meta/tables/${NOCODB_PERSONNAGES_TABLE_ID}/columns`;
|
||||
await fetch(createUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ column_name: col, title: col, uidt: 'LongText' }),
|
||||
});
|
||||
console.log(`Personnages column '${col}' created`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Personnages columns migration error:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`TdB RdB — port ${PORT}`);
|
||||
if (NOCODB_TOKEN) {
|
||||
ensureTagsColumn();
|
||||
migrateStatutToTags();
|
||||
ensurePersonnagesColumns();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user