feat: integrate feedback module

This commit is contained in:
2026-02-08 21:48:55 +01:00
parent 453a603392
commit 7ec826dae3
48 changed files with 4413 additions and 1168 deletions

View File

@@ -0,0 +1,39 @@
import { createDirectus, rest, authentication, createPanel, readDashboards } from '@directus/sdk';
async function addStatusPanel() {
const url = 'http://localhost:8059';
const email = 'marc@mintel.me';
const password = 'Tim300493.';
console.log(`🚀 Adding Status Panel: ${url}`);
const client = createDirectus(url).with(authentication('json')).with(rest());
try {
await client.login(email, password);
console.log('✅ Authenticated');
const dashboards = await client.request(readDashboards({ filter: { name: { _eq: 'Feedback Operational Intelligence' } } }));
const db = dashboards[0];
if (db) {
await client.request(createPanel({
dashboard: db.id,
name: 'Dashboard Status: LIVE',
type: 'label',
width: 24, height: 2, position_x: 0, position_y: 24,
options: {
text: '### ✅ Dashboard Rendering Service Active\n\nIf you see this, the system is online and updated as of ' + new Date().toISOString()
}
}));
console.log('✅ Status Panel Added');
} else {
console.error('❌ Dashboard not found');
}
} catch (e: any) {
console.error('❌ Failed:');
console.error(e.message);
}
}
addStatusPanel();

54
scripts/cms-apply.sh Executable file
View File

@@ -0,0 +1,54 @@
#!/bin/bash
ENV=$1
REMOTE_HOST="root@alpha.mintel.me"
REMOTE_DIR="/home/deploy/sites/klz-cables.com"
if [ -z "$ENV" ]; then
echo "Usage: ./scripts/cms-apply.sh [local|testing|staging|production]"
exit 1
fi
PRJ_ID=$(jq -r .name package.json | sed 's/@mintel\///' | sed 's/\.com$//')
case $ENV in
local)
CONTAINER=$(docker compose ps -q directus)
if [ -z "$CONTAINER" ]; then
echo "❌ Local directus container not found."
exit 1
fi
echo "🚀 Applying schema locally..."
docker exec "$CONTAINER" npx directus schema apply /directus/schema/snapshot.yaml --yes
;;
testing|staging|production)
case $ENV in
testing) PROJECT_NAME="${PRJ_ID}-testing" ;;
staging) PROJECT_NAME="${PRJ_ID}-staging" ;;
production) PROJECT_NAME="${PRJ_ID}-prod" ;;
esac
echo "📤 Uploading snapshot to $ENV..."
scp ./directus/schema/snapshot.yaml "$REMOTE_HOST:$REMOTE_DIR/directus/schema/snapshot.yaml"
echo "🔍 Detecting remote container..."
REMOTE_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME ps -q directus")
if [ -z "$REMOTE_CONTAINER" ]; then
echo "❌ Remote container for $ENV not found."
exit 1
fi
echo "🚀 Applying schema to $ENV..."
ssh "$REMOTE_HOST" "docker exec $REMOTE_CONTAINER npx directus schema apply /directus/schema/snapshot.yaml --yes"
echo "🔄 Restarting Directus to clear cache..."
ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME restart directus"
;;
*)
echo "❌ Invalid environment."
exit 1
;;
esac
echo "✨ Schema apply complete!"

15
scripts/cms-snapshot.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
# Detect local container
LOCAL_CONTAINER=$(docker compose ps -q directus)
if [ -z "$LOCAL_CONTAINER" ]; then
echo "❌ Local directus container not found. Is it running?"
exit 1
fi
echo "📸 Creating schema snapshot..."
# Note: we save it to the mounted volume path inside the container
docker exec "$LOCAL_CONTAINER" npx directus schema snapshot /directus/schema/snapshot.yaml
echo "✅ Snapshot saved to ./directus/schema/snapshot.yaml"

33
scripts/container-fix.js Normal file
View File

@@ -0,0 +1,33 @@
const sqlite3 = require('sqlite3').verbose();
const db = new sqlite3.Database('/directus/database/data.db');
db.serialize(() => {
console.log('--- INTERNAL REPAIR START ---');
// 1. Grant to Public Policy
db.run(`INSERT INTO directus_permissions (collection, action, fields, permissions, validation, presets, policy)
VALUES ('visual_feedback', 'read', '["*"]', '{}', '{}', '{}', 'abf8a154-5b1c-4a46-ac9c-7300570f4f17')`, (err) => {
if (err) console.log('Public grant note:', err.message);
else console.log('✅ Public READ granted.');
});
// 2. Grant to Admin Policy
db.run(`INSERT INTO directus_permissions (collection, action, fields, permissions, validation, presets, policy)
VALUES ('visual_feedback', 'read', '["*"]', '{}', '{}', '{}', 'bed7c035-28f7-4a78-b11a-0dc0e7fc3cd4')`, (err) => {
if (err) console.log('Admin grant note:', err.message);
else console.log('✅ Admin READ granted.');
});
// 3. Mark collection as non-hidden
db.run(`UPDATE directus_collections SET hidden = 0, accountability = NULL WHERE collection = 'visual_feedback'`, (err) => {
if (err) console.log('Collection update error:', err.message);
else console.log('✅ Collection metadata cleared.');
});
db.all(`SELECT COUNT(*) as count FROM visual_feedback`, (err, rows) => {
if (err) console.log('Item count error:', err.message);
else console.log(`📊 Items in visual_feedback: ${rows[0].count}`);
});
});
db.close();

View File

@@ -0,0 +1,82 @@
import { createDirectus, rest, authentication, readDashboards, createDashboard, createPanel } from '@directus/sdk';
async function debugVariants() {
const url = 'http://localhost:8059';
const email = 'marc@mintel.me';
const password = 'Tim300493.';
console.log(`🚀 creating Debug Variants Dashboard: ${url}`);
const client = createDirectus(url).with(authentication('json')).with(rest());
try {
await client.login(email, password);
console.log('✅ Authenticated');
const dashboard = await client.request(createDashboard({
name: 'Debug List Variants',
icon: 'bug_report',
color: '#FF0000'
}));
// Variant 1: No Template
await client.request(createPanel({
dashboard: dashboard.id,
name: 'No Template',
type: 'list',
width: 8, height: 8, position_x: 0, position_y: 0,
options: {
collection: 'visual_feedback',
fields: ['text'],
limit: 5
}
}));
// Variant 2: Simple Template {{text}}
await client.request(createPanel({
dashboard: dashboard.id,
name: 'Simple {{text}}',
type: 'list',
width: 8, height: 8, position_x: 8, position_y: 0,
options: {
collection: 'visual_feedback',
template: '{{text}}',
limit: 5
}
}));
// Variant 3: Spaced Template {{ text }}
await client.request(createPanel({
dashboard: dashboard.id,
name: 'Spaced {{ text }}',
type: 'list',
width: 8, height: 8, position_x: 16, position_y: 0,
options: {
collection: 'visual_feedback',
template: '{{ text }}',
limit: 5
}
}));
// Variant 4: With fields array AND template
await client.request(createPanel({
dashboard: dashboard.id,
name: 'Fields + Template',
type: 'list',
width: 8, height: 8, position_x: 0, position_y: 8,
options: {
collection: 'visual_feedback',
fields: ['text', 'user_name'],
template: '{{user_name}}: {{text}}',
limit: 5
}
}));
console.log('✅ Debug Dashboard Created');
} catch (e: any) {
console.error('❌ Creation failed:');
console.error(e.message);
}
}
debugVariants();

View File

@@ -0,0 +1,42 @@
import { createDirectus, rest, authentication, readDashboards, createDashboard, createPanel } from '@directus/sdk';
async function debugLabelFallback() {
const url = 'http://localhost:8059';
const email = 'marc@mintel.me';
const password = 'Tim300493.';
console.log(`🚀 creating Debug Label Fallback Dashboard: ${url}`);
const client = createDirectus(url).with(authentication('json')).with(rest());
try {
await client.login(email, password);
console.log('✅ Authenticated');
const dashboard = await client.request(createDashboard({
name: 'Debug Label Fallback',
icon: 'label',
color: '#0000FF'
}));
// Variant 5: Label with Markdown (Static list simulation)
// Note: Label panels don't take a collection, they just render text.
// This confirms if we can at least show SOMETHING.
await client.request(createPanel({
dashboard: dashboard.id,
name: 'Label Fallback',
type: 'label',
width: 12, height: 10, position_x: 0, position_y: 0,
options: {
text: '### Recent Feedback\n\n- User: Test Message\n- User2: Another Message'
}
}));
console.log('✅ Debug Label Dashboard Created');
} catch (e: any) {
console.error('❌ Creation failed:');
console.error(e.message);
}
}
debugLabelFallback();

View File

@@ -0,0 +1,45 @@
import { createDirectus, rest, authentication, readDashboards, createPanel, readPanels } from '@directus/sdk';
async function createDefaultList() {
const url = 'http://localhost:8059';
const email = 'marc@mintel.me';
const password = 'Tim300493.';
console.log(`🚀 Creating Default List Panel: ${url}`);
const client = createDirectus(url).with(authentication('json')).with(rest());
try {
await client.login(email, password);
console.log('✅ Authenticated');
const dashboards = await client.request(readDashboards({ filter: { name: { _eq: 'Feedback Operational Intelligence' } } }));
const db = dashboards[0];
// Create a completely default list panel
const panel = await client.request(createPanel({
dashboard: db.id,
name: 'Debug Default List',
type: 'list',
width: 12,
height: 10,
position_x: 0,
position_y: 24, // below
options: {
collection: 'visual_feedback'
}
}));
console.log(`Created Debug Panel: ${panel.id}`);
console.log(`Options: ${JSON.stringify(panel.options, null, 2)}`);
// Let's read it back to see if Directus enriched it with defaults
const panels = await client.request(readPanels({ filter: { id: { _eq: panel.id } } }));
console.log(`Enriched Options: ${JSON.stringify(panels[0].options, null, 2)}`);
} catch (e: any) {
console.error('❌ Creation failed:');
console.error(e.message);
}
}
createDefaultList();

34
scripts/feedback.yaml Normal file
View File

@@ -0,0 +1,34 @@
version: 1
directus: 11.14.1
vendor: sqlite
collections:
- collection: visual_feedback
meta:
icon: feedback
display_template: "{{user_name}}: {{text}}"
accountability: null
schema:
name: visual_feedback
fields:
- collection: visual_feedback
field: id
type: integer
schema:
is_primary_key: true
has_auto_increment: true
- collection: visual_feedback
field: status
type: string
schema:
default_value: open
- collection: visual_feedback
field: user_name
type: string
- collection: visual_feedback
field: text
type: text
- collection: visual_feedback
field: date_created
type: timestamp
schema:
default_value: CURRENT_TIMESTAMP

60
scripts/final-fix.ts Normal file
View File

@@ -0,0 +1,60 @@
import { createDirectus, rest, staticToken, updateCollection, createPermission, readCollections, readPermissions, createDashboard, createPanel, createItems } from '@directus/sdk';
import { config } from '../lib/config';
async function finalFix() {
const url = 'http://localhost:8059';
const token = '59fb8f4c1a51b18fe28ad947f713914e';
const client = createDirectus(url).with(staticToken(token)).with(rest());
try {
console.log('--- 1. UPDATE COLLECTION ACCOUNTABILITY ---');
await client.request(updateCollection('visual_feedback', {
meta: { accountability: null }
} as any));
console.log('✅ Accountability set to null.');
console.log('\n--- 2. GRANT PUBLIC READ ---');
// Policy ID for Public is always 'abf8a154-5b1c-4a46-ac9c-7300570f4f17' in v11 bootstrap usually,
// but let's check first.
try {
await client.request(createPermission({
policy: 'abf8a154-5b1c-4a46-ac9c-7300570f4f17',
collection: 'visual_feedback',
action: 'read',
fields: ['*']
} as any));
console.log('✅ Public READ granted.');
} catch (e) {
console.log(' (Public READ might already exist)');
}
console.log('\n--- 3. RECREATE DASHBOARD ---');
const dash = await client.request(createDashboard({
name: 'Feedback Final',
icon: 'check_circle',
color: '#00FF00'
}));
console.log(`✅ Dashboard "Feedback Final" ID: ${dash.id}`);
await client.request(createPanel({
dashboard: dash.id,
name: 'Visible Feedbacks',
type: 'metric',
width: 12,
height: 6,
position_x: 1,
position_y: 1,
options: { collection: 'visual_feedback', function: 'count', field: 'id' }
} as any));
console.log('\n--- 4. VERIFY READ VIA TOKEN ---');
const items = await client.request(() => ({ path: '/items/visual_feedback', method: 'GET' }));
console.log(`✅ Items count via token: ${items.data.length}`);
} catch (e: any) {
console.error('❌ Final fix failed:', e);
if (e.errors) console.error(JSON.stringify(e.errors, null, 2));
}
}
finalFix();

View File

@@ -0,0 +1,45 @@
import { createDirectus, rest, authentication, readCollections, updateCollection } from '@directus/sdk';
async function checkCollectionConfig() {
const url = 'http://localhost:8059';
const email = 'marc@mintel.me';
const password = 'Tim300493.';
console.log(`🚀 Checking Collection Config: ${url}`);
const client = createDirectus(url).with(authentication('json')).with(rest());
try {
await client.login(email, password);
console.log('✅ Authenticated');
const collection = await client.request(readCollections());
const fb = collection.find(c => c.collection === 'visual_feedback');
if (fb) {
console.log(`Collection: ${fb.collection}`);
console.log(`Display Template: ${fb.meta?.display_template}`);
console.log(`Hidden: ${fb.meta?.hidden}`);
if (!fb.meta?.display_template) {
console.log('⚠️ Display Template is missing! Fixing it...');
await client.request(updateCollection('visual_feedback', {
meta: {
...fb.meta,
display_template: '{{text}}' // Set a sensible default
}
}));
console.log('✅ Display Template set to {{text}}');
} else {
console.log('✅ Display Template is already set.');
}
} else {
console.error('❌ Collection visual_feedback not found!');
}
} catch (e: any) {
console.error('❌ Check failed:');
console.error(e.message);
}
}
checkCollectionConfig();

View File

@@ -0,0 +1,50 @@
import { createDirectus, rest, authentication, readDashboards, readPanels, updatePanel } from '@directus/sdk';
async function fixListPanel() {
const url = 'http://localhost:8059';
const email = 'marc@mintel.me';
const password = 'Tim300493.';
console.log(`🚀 Fixing List Panel Template: ${url}`);
const client = createDirectus(url).with(authentication('json')).with(rest());
try {
await client.login(email, password);
console.log('✅ Authenticated');
const dashboards = await client.request(readDashboards({ filter: { name: { _eq: 'Feedback Operational Intelligence' } } }));
const db = dashboards[0];
if (!db) throw new Error('Dashboard not found');
const panels = await client.request(readPanels({
filter: { dashboard: { _eq: db.id }, type: { _eq: 'list' } }
}));
const listPanel = panels[0];
if (!listPanel) throw new Error('List panel not found');
console.log(`Found Panel: ${listPanel.id}`);
console.log(`Current Template: ${listPanel.options.template}`);
// Try a different syntax or simple field
// In some versions it's {{field}}, in others it might be just field field
// Let's try to set it to just {{text}} to see if basic interpolation works
// Or maybe it needs HTML?
console.log('Updating template to simple {{text}} ...');
await client.request(updatePanel(listPanel.id, {
options: {
...listPanel.options,
template: '{{text}}'
}
}));
console.log('✅ Panel updated');
} catch (e: any) {
console.error('❌ Fix failed:');
console.error(e.message);
}
}
fixListPanel();

View File

@@ -0,0 +1,38 @@
import { createDirectus, rest, authentication, readDashboards, readPanels } from '@directus/sdk';
async function inspectDashboards() {
const url = 'http://localhost:8059';
const email = 'marc@mintel.me';
const password = 'Tim300493.';
console.log(`🚀 Inspecting Dashboards: ${url}`);
const client = createDirectus(url).with(authentication('json')).with(rest());
try {
await client.login(email, password);
console.log('✅ Authenticated');
const dashboards = await client.request(readDashboards({ fields: ['*'] }));
console.log('\n--- DASHBOARDS ---');
for (const db of dashboards) {
console.log(`Dashboard: ${db.name} (${db.id})`);
const panels = await client.request(readPanels({
filter: { dashboard: { _eq: db.id } },
fields: ['*']
}));
console.log(' Panels:');
panels.forEach(p => {
console.log(` - [${p.type}] ${p.name}`);
console.log(` Options: ${JSON.stringify(p.options, null, 2)}`);
});
}
} catch (e: any) {
console.error('❌ Inspection failed:');
console.error(e.message);
}
}
inspectDashboards();

80
scripts/nuke-pave.ts Normal file
View File

@@ -0,0 +1,80 @@
import { createDirectus, rest, staticToken, deleteCollection, createCollection, createDashboard, createPanel, createItems, createPermission, readPolicies } from '@directus/sdk';
import { config } from '../lib/config';
async function nukeAndPaveV11() {
console.log('🚀 NUKE & PAVE: Feedback System v11...');
const url = 'http://localhost:8059';
const token = '59fb8f4c1a51b18fe28ad947f713914e';
const client = createDirectus(url).with(staticToken(token)).with(rest());
try {
console.log('🗑️ Deleting collections...');
try { await client.request(deleteCollection('visual_feedback_comments')); } catch (e) { }
try { await client.request(deleteCollection('visual_feedback')); } catch (e) { }
console.log('🏗️ Creating "visual_feedback" fresh...');
await client.request(createCollection({
collection: 'visual_feedback',
meta: { icon: 'feedback', display_template: '{{user_name}}: {{text}}' },
fields: [
{ field: 'id', type: 'uuid', schema: { is_primary_key: true } },
{ field: 'status', type: 'string', schema: { default_value: 'open' }, meta: { interface: 'select-dropdown' } },
{ field: 'url', type: 'string', meta: { interface: 'input' } },
{ field: 'selector', type: 'string', meta: { interface: 'input' } },
{ field: 'text', type: 'text', meta: { interface: 'input-multiline' } },
{ field: 'user_name', type: 'string', meta: { interface: 'input' } },
{ field: 'date_created', type: 'timestamp', schema: { default_value: 'NOW()' }, meta: { interface: 'datetime' } }
]
} as any));
console.log('🔐 Granting Permissions...');
const policies = await client.request(readPolicies());
const adminPolicy = policies.find(p => p.name === 'Administrator')?.id;
const publicPolicy = policies.find(p => p.name === '$t:public_label' || p.name === 'Public')?.id;
for (const policy of [adminPolicy, publicPolicy]) {
if (!policy) continue;
console.log(` - Granting to Policy: ${policy}...`);
await client.request(createPermission({
policy,
collection: 'visual_feedback',
action: 'read',
fields: ['*'],
permissions: {},
validation: {}
} as any));
}
console.log('💉 Injecting items...');
await client.request(createItems('visual_feedback', [
{ user_name: 'Antigravity', text: 'Nuke & Pave Success', status: 'open' }
]));
console.log('📊 Recreating Dashboard...');
const dash = await client.request(createDashboard({
name: 'Feedback Insights',
icon: 'analytics',
color: '#6644FF'
}));
await client.request(createPanel({
dashboard: dash.id,
name: 'Status',
type: 'metric',
width: 12,
height: 6,
position_x: 1,
position_y: 1,
options: { collection: 'visual_feedback', function: 'count', field: 'id' }
} as any));
console.log('✅ Nuke & Pave Complete!');
} catch (e: any) {
console.error('❌ Nuke failed:', e);
if (e.errors) console.error(JSON.stringify(e.errors, null, 2));
}
}
nukeAndPaveV11();

View File

@@ -0,0 +1,176 @@
import { createDirectus, rest, authentication, readDashboards, deleteDashboard, createDashboard, createPanel } from '@directus/sdk';
async function rebuildDashboards() {
const url = 'http://localhost:8059';
const email = 'marc@mintel.me';
const password = 'Tim300493.';
console.log(`🚀 Rebuilding Dashboards: ${url}`);
const client = createDirectus(url).with(authentication('json')).with(rest());
try {
await client.login(email, password);
console.log('✅ Authenticated');
// 1. Delete existing dashboard
const oldDashboards = await client.request(readDashboards());
for (const db of oldDashboards) {
console.log(`Deleting dashboard: ${db.name} (${db.id})`);
await client.request(deleteDashboard(db.id));
}
// 2. Create the "Intelligence" Dashboard
const dashboard = await client.request(createDashboard({
name: 'Feedback Operational Intelligence',
note: 'High-fidelity overview of user feedback and system status.',
icon: 'analytics',
color: '#000000'
}));
console.log(`Created Dashboard: ${dashboard.id}`);
// 3. Add Panels (Grid is 24 units wide)
// --- Row 1: Key Metrics ---
// Total
await client.request(createPanel({
dashboard: dashboard.id as string,
name: 'Total Submissions',
type: 'metric',
width: 6,
height: 4,
position_x: 0,
position_y: 0,
options: {
collection: 'visual_feedback',
function: 'count',
field: 'id',
color: '#666666',
icon: 'all_inbox'
}
}));
// Open
await client.request(createPanel({
dashboard: dashboard.id as string,
name: 'Pending Action',
type: 'metric',
width: 6,
height: 4,
position_x: 6,
position_y: 0,
options: {
collection: 'visual_feedback',
function: 'count',
field: 'id',
filter: { status: { _eq: 'open' } },
color: '#FF0000',
icon: 'warning'
}
}));
// Type: Bug
await client.request(createPanel({
dashboard: dashboard.id as string,
name: 'Bugs Reported',
type: 'metric',
width: 6,
height: 4,
position_x: 12,
position_y: 0,
options: {
collection: 'visual_feedback',
function: 'count',
field: 'id',
filter: { type: { _eq: 'bug' } },
color: '#E91E63',
icon: 'bug_report'
}
}));
// Type: Feature
await client.request(createPanel({
dashboard: dashboard.id as string,
name: 'Feature Requests',
type: 'metric',
width: 6,
height: 4,
position_x: 18,
position_y: 0,
options: {
collection: 'visual_feedback',
function: 'count',
field: 'id',
filter: { type: { _eq: 'feature' } },
color: '#4CAF50',
icon: 'lightbulb'
}
}));
// --- Row 2: Trends and Distribution ---
// Time series (Volume)
await client.request(createPanel({
dashboard: dashboard.id as string,
name: 'Feedback Volume (Last 30 Days)',
type: 'chart-timeseries',
width: 16,
height: 10,
position_x: 0,
position_y: 4,
options: {
collection: 'visual_feedback',
function: 'count',
field: 'id',
group: 'date_created',
interval: 'day',
show_marker: true,
color: '#000000'
}
}));
// Category distribution (Pie)
await client.request(createPanel({
dashboard: dashboard.id as string,
name: 'Type Distribution',
type: 'chart-pie',
width: 8,
height: 10,
position_x: 16,
position_y: 4,
options: {
collection: 'visual_feedback',
function: 'count',
field: 'id',
group: 'type',
donut: true,
show_labels: true
}
}));
// --- Row 3: Details ---
// Detailed List
await client.request(createPanel({
dashboard: dashboard.id as string,
name: 'Recent Feedback (High Priority)',
type: 'list',
width: 24,
height: 10,
position_x: 0,
position_y: 14,
options: {
collection: 'visual_feedback',
fields: ['user_name', 'type', 'text', 'status', 'date_created'],
sort: ['-date_created'],
limit: 10
}
}));
console.log('✅ Dashboard rebuilt successfully');
} catch (e: any) {
console.error('❌ Rebuild failed:');
console.error(e.message);
if (e.errors) console.error(JSON.stringify(e.errors, null, 2));
}
}
rebuildDashboards();

View File

@@ -0,0 +1,122 @@
import { createDirectus, rest, authentication, createCollection, createDashboard, createPanel, createItems, createPermission, readPolicies, readRoles, readUsers } from '@directus/sdk';
async function setupHardened() {
const url = 'http://localhost:8059';
const email = 'marc@mintel.me';
const password = 'Tim300493.';
console.log('🚀 v11 HARDENED SETUP START...');
const client = createDirectus(url)
.with(authentication('json'))
.with(rest());
try {
console.log('🔑 Authenticating...');
await client.login(email, password);
console.log('👤 Identifying IDs...');
const me = await client.request(readUsers({ filter: { email: { _eq: email } } }));
const adminUser = me[0];
const roles = await client.request(readRoles());
const adminRole = roles.find(r => r.name === 'Administrator');
const policies = await client.request(readPolicies());
const adminPolicy = policies.find(p => p.name === 'Administrator');
console.log(`- User: ${adminUser.id}`);
console.log(`- Role: ${adminRole?.id}`);
console.log(`- Policy: ${adminPolicy?.id}`);
if (adminPolicy && adminRole) {
console.log('🔗 Linking Role -> Policy...');
try {
await client.request(() => ({
path: '/access',
method: 'POST',
body: JSON.stringify({ role: adminRole.id, policy: adminPolicy.id })
}));
} catch (e) { }
console.log('🔗 Linking User -> Policy (individual)...');
try {
await client.request(() => ({
path: '/access',
method: 'POST',
body: JSON.stringify({ user: adminUser.id, policy: adminPolicy.id })
}));
} catch (e) { }
}
console.log('🏗️ Creating Collection "visual_feedback"...');
try {
await client.request(createCollection({
collection: 'visual_feedback',
meta: { icon: 'feedback', display_template: '{{user_name}}: {{text}}' },
fields: [
{ field: 'id', type: 'uuid', schema: { is_primary_key: true } },
{ field: 'status', type: 'string', schema: { default_value: 'open' }, meta: { interface: 'select-dropdown' } },
{ field: 'url', type: 'string' },
{ field: 'text', type: 'text' },
{ field: 'user_name', type: 'string' },
{ field: 'date_created', type: 'timestamp', schema: { default_value: 'NOW()' } }
]
} as any));
} catch (e) {
console.log(' (Collection might already exist)');
}
if (adminPolicy) {
console.log('🔐 Granting ALL permissions to Administrator Policy...');
for (const action of ['create', 'read', 'update', 'delete']) {
try {
await client.request(createPermission({
collection: 'visual_feedback',
action,
fields: ['*'],
policy: adminPolicy.id
} as any));
} catch (e) { }
}
}
console.log('💉 Injecting Demo Item...');
try {
await client.request(createItems('visual_feedback', [
{ user_name: 'Antigravity', text: 'v11 Recovery Successful', status: 'open' }
]));
} catch (e) { }
console.log('📊 Recreating Dashboard...');
const dash = await client.request(createDashboard({
name: 'Feedback Final',
icon: 'check_circle',
color: '#00FF00'
}));
await client.request(createPanel({
dashboard: dash.id,
name: 'Total Feedbacks',
type: 'metric',
width: 12,
height: 6,
position_x: 1,
position_y: 1,
options: { collection: 'visual_feedback', function: 'count', field: 'id' }
} as any));
console.log('✅ Setup Complete! Setting static token...');
await client.request(() => ({
path: `/users/${adminUser.id}`,
method: 'PATCH',
body: JSON.stringify({ token: '59fb8f4c1a51b18fe28ad947f713914e' })
}));
console.log('✨ ALL DONE.');
} catch (e: any) {
console.error('❌ CRITICAL FAILURE:', e);
if (e.errors) console.error(JSON.stringify(e.errors, null, 2));
}
}
setupHardened();

86
scripts/setup-feedback.ts Normal file
View File

@@ -0,0 +1,86 @@
import { createDirectus, rest, staticToken, createCollection, readCollections, createDashboard, createPanel, createItems, readDashboards, readPanels, createPermission, readPolicies } from '@directus/sdk';
import { config } from '../lib/config';
async function setupInfraFeedback() {
console.log('🚀 Setting up INFRA_FEEDBACK (Renamed for v11 Visibility)...');
const url = 'http://localhost:8059';
const token = '59fb8f4c1a51b18fe28ad947f713914e';
const client = createDirectus(url).with(staticToken(token)).with(rest());
try {
const collections = await client.request(readCollections());
const existing = collections.map(c => c.collection);
const COLL = 'infra_feedback';
if (!existing.includes(COLL)) {
console.log(`🏗️ Creating "${COLL}"...`);
await client.request(createCollection({
collection: COLL,
meta: { icon: 'feedback', display_template: '{{user_name}}: {{text}}' },
fields: [
{ field: 'id', type: 'integer', schema: { is_primary_key: true, has_auto_increment: true } },
{ field: 'status', type: 'string', schema: { default_value: 'open' }, meta: { interface: 'select-dropdown' } },
{ field: 'url', type: 'string' },
{ field: 'text', type: 'text' },
{ field: 'user_name', type: 'string' },
{ field: 'date_created', type: 'timestamp', schema: { default_value: 'NOW()' } }
]
} as any));
}
const policies = await client.request(readPolicies());
const adminPolicy = policies.find(p => p.name === 'Administrator')?.id;
const publicPolicy = policies.find(p => p.name === '$t:public_label' || p.name === 'Public')?.id;
for (const policy of [adminPolicy, publicPolicy]) {
if (!policy) continue;
console.log(`🔐 Granting permissions to ${policy}...`);
for (const action of ['create', 'read', 'update', 'delete']) {
try {
await client.request(createPermission({
policy,
collection: COLL,
action,
fields: ['*']
} as any));
} catch (e) { }
}
}
console.log('💉 Injecting test data...');
await client.request(createItems(COLL, [
{ user_name: 'Antigravity', text: 'Rename Test Success', status: 'open' }
]));
console.log('📊 Configuring Dashboard "Feedback OVERVIEW"...');
const dashboards = await client.request(readDashboards());
let dash = dashboards.find(d => d.name === 'Feedback OVERVIEW');
if (dash) await client.request(() => ({ path: `/dashboards/${dash.id}`, method: 'DELETE' }));
dash = await client.request(createDashboard({
name: 'Feedback OVERVIEW',
icon: 'visibility',
color: '#FFCC00'
}));
await client.request(createPanel({
dashboard: dash.id,
name: 'Table View',
type: 'list',
width: 24,
height: 12,
position_x: 1,
position_y: 1,
options: { collection: COLL, display_template: '{{user_name}}: {{text}}' }
} as any));
console.log('✅ Renamed Setup Complete! Dash: "Feedback OVERVIEW"');
} catch (error: any) {
console.error('❌ Rename setup failed:', error);
}
}
setupInfraFeedback();

229
scripts/test-auth.ts Normal file
View File

@@ -0,0 +1,229 @@
import { createDirectus, rest, authentication, readItems, readCollections } from '@directus/sdk';
import { config } from './config';
import { getServerAppServices } from './services/create-services.server';
const { url, adminEmail, password, token, proxyPath, internalUrl } = config.directus;
// Use internal URL if on server to bypass Gatekeeper/Auth
// Use proxy path in browser to stay on the same origin
const effectiveUrl =
typeof window === 'undefined'
? internalUrl || url
: typeof window !== 'undefined'
? `${window.location.origin}${proxyPath}`
: proxyPath;
const client = createDirectus(effectiveUrl).with(rest()).with(authentication());
/**
* Helper to determine if we should show detailed errors
*/
const shouldShowDevErrors = config.isTesting || config.isDevelopment;
/**
* Genericizes error messages for production/staging
*/
function formatError(error: any) {
if (shouldShowDevErrors) {
return error.errors?.[0]?.message || error.message || 'An unexpected error occurred.';
}
return 'A system error occurred. Our team has been notified.';
}
let authPromise: Promise<void> | null = null;
export async function ensureAuthenticated() {
if (token) {
client.setToken(token);
return;
}
// Check if we already have a valid session token in memory
const existingToken = await client.getToken();
if (existingToken) {
return;
}
if (adminEmail && password) {
if (authPromise) {
return authPromise;
}
authPromise = (async () => {
try {
client.setToken(null as any);
await client.login(adminEmail, password);
console.log(`✅ Directus: Authenticated successfully as ${adminEmail}`);
} catch (e: any) {
if (typeof window === 'undefined') {
getServerAppServices().errors.captureException(e, { part: 'directus_auth' });
}
console.error(`Failed to authenticate with Directus (${adminEmail}):`, e.message);
if (shouldShowDevErrors && e.errors) {
console.error('Directus Auth Details:', JSON.stringify(e.errors, null, 2));
}
// Clear the promise on failure (especially on invalid credentials)
// so we can retry on next request if credentials were updated
authPromise = null;
throw e;
}
})();
return authPromise;
} else if (shouldShowDevErrors && !adminEmail && !password && !token) {
console.warn('Directus: No token or admin credentials provided.');
}
}
/**
* Maps the new translation-based schema back to the application's Product interface
*/
function mapDirectusProduct(item: any, locale: string): any {
const langCode = locale === 'en' ? 'en-US' : 'de-DE';
const translation =
item.translations?.find((t: any) => t.languages_code === langCode) ||
item.translations?.[0] ||
{};
return {
id: item.id,
sku: item.sku,
title: translation.name || '',
description: translation.description || '',
content: translation.content || '',
technicalData: {
technicalItems: translation.technical_items || [],
voltageTables: translation.voltage_tables || [],
},
locale: locale,
// Use proxy URL for assets to avoid CORS and handle internal/external issues
data_sheet_url: item.data_sheet ? `${proxyPath}/assets/${item.data_sheet}` : null,
categories: (item.categories_link || [])
.map((c: any) => c.categories_id?.translations?.[0]?.name)
.filter(Boolean),
};
}
export async function getProducts(locale: string = 'de') {
await ensureAuthenticated();
try {
const items = await client.request(
readItems('products', {
fields: ['*', 'translations.*', 'categories_link.categories_id.translations.name'],
}),
);
return items.map((item) => mapDirectusProduct(item, locale));
} catch (error) {
if (typeof window === 'undefined') {
getServerAppServices().errors.captureException(error, { part: 'directus_get_products' });
}
console.error('Error fetching products:', error);
return [];
}
}
export async function getProductBySlug(slug: string, locale: string = 'de') {
await ensureAuthenticated();
const langCode = locale === 'en' ? 'en-US' : 'de-DE';
try {
const items = await client.request(
readItems('products', {
filter: {
translations: {
slug: { _eq: slug },
languages_code: { _eq: langCode },
},
},
fields: ['*', 'translations.*', 'categories_link.categories_id.translations.name'],
limit: 1,
}),
);
if (!items || items.length === 0) return null;
return mapDirectusProduct(items[0], locale);
} catch (error) {
if (typeof window === 'undefined') {
getServerAppServices().errors.captureException(error, {
part: 'directus_get_product_by_slug',
slug,
});
}
console.error(`Error fetching product ${slug}:`, error);
return null;
}
}
export async function checkHealth() {
try {
// 1. Connectivity & Auth Check
try {
await ensureAuthenticated();
await client.request(readCollections());
} catch (e: any) {
if (typeof window === 'undefined') {
getServerAppServices().errors.captureException(e, { part: 'directus_health_auth' });
}
console.error('Directus authentication failed during health check:', e);
return {
status: 'error',
message: shouldShowDevErrors
? 'Authentication failed. Check your DIRECTUS_ADMIN_EMAIL and DIRECTUS_ADMIN_PASSWORD.'
: 'CMS is currently unavailable due to an internal authentication error.',
code: 'AUTH_FAILED',
details: shouldShowDevErrors ? e.message : undefined,
};
}
// 2. Schema check (does the contact_submissions table exist?)
try {
await client.request(readItems('contact_submissions', { limit: 1 }));
} catch (e: any) {
if (typeof window === 'undefined') {
getServerAppServices().errors.captureException(e, { part: 'directus_health_schema' });
}
if (
e.message?.includes('does not exist') ||
e.code === 'INVALID_PAYLOAD' ||
e.status === 404
) {
return {
status: 'error',
message: shouldShowDevErrors
? `The "contact_submissions" collection is missing or inaccessible. Error: ${e.message || 'Unknown'}`
: 'Required data structures are currently unavailable.',
code: 'SCHEMA_MISSING',
};
}
return {
status: 'error',
message: shouldShowDevErrors
? `Schema error: ${e.errors?.[0]?.message || e.message || 'Unknown error'}`
: 'The data schema is currently misconfigured.',
code: 'SCHEMA_ERROR',
};
}
return { status: 'ok', message: 'Directus is reachable and responding.' };
} catch (error: any) {
if (typeof window === 'undefined') {
getServerAppServices().errors.captureException(error, { part: 'directus_health_critical' });
}
console.error('Directus health check failed with unexpected error:', error);
return {
status: 'error',
message: formatError(error),
code: error.code || 'UNKNOWN',
};
}
}
export default client;
(async () => {
try {
await ensureAuthenticated();
console.log('Auth test successful');
} catch (e) {
console.error('Auth test failed:', e.message);
}
})();