tractatus/public/js/admin/project-editor.js
TheFlow d8f8829635 feat: eliminate all GitHub references from agenticgovernance.digital
- Created /source-code.html — sovereign hosting landing page explaining
  why we left GitHub, how to access the code, and the sovereignty model
- Navbar: GitHub link → Source Code link (desktop + mobile)
- Footer: GitHub link → Source Code link
- Docs sidebar: GitHub section → Source Code section with sovereign repo
- Implementer page: all repository links point to /source-code.html,
  clone instructions updated, CI/CD code example genericised
- FAQ: GitHub Discussions button → Contact Us with email icon
- FAQ content: all 4 locales (en/de/fr/mi) rewritten to remove
  GitHub Actions YAML, GitHub URLs, and GitHub-specific patterns
- faq.js fallback content: same changes as locale files
- agent-lightning integration page: updated to source-code.html
- Project model: example URL changed from GitHub to Codeberg
- All locale files updated: navbar.github → navbar.source_code,
  footer GitHub → source_code, FAQ button text updated in 4 languages

Zero GitHub references remain in any HTML, JS, or JSON file
(only github-dark.min.css theme name in highlight.js CDN reference).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 18:14:26 +13:00

783 lines
32 KiB
JavaScript

/**
* Project Editor Modal
* Handles creation, editing, viewing, and variable management for projects
*
* @class ProjectEditor
*/
class ProjectEditor {
constructor() {
this.mode = 'create'; // 'create', 'edit', 'view', 'variables'
this.projectId = null;
this.originalProject = null;
this.variables = [];
}
/**
* Open editor in create mode
*/
openCreate() {
this.mode = 'create';
this.projectId = null;
this.originalProject = null;
this.render();
this.attachEventListeners();
}
/**
* Open editor in edit mode
*/
async openEdit(projectId) {
this.mode = 'edit';
this.projectId = projectId;
try {
const response = await apiRequest(`/api/admin/projects/${projectId}`);
if (!response.success || !response.project) {
throw new Error('Failed to load project');
}
this.originalProject = response.project;
this.variables = response.variables || [];
this.render();
this.populateForm(response.project);
this.attachEventListeners();
} catch (error) {
console.error('Failed to load project:', error);
showToast('Failed to load project for editing', 'error');
}
}
/**
* Open editor in view mode (read-only)
*/
async openView(projectId) {
this.mode = 'view';
this.projectId = projectId;
try {
const response = await apiRequest(`/api/admin/projects/${projectId}`);
if (!response.success || !response.project) {
throw new Error('Failed to load project');
}
this.originalProject = response.project;
this.variables = response.variables || [];
this.renderViewMode(response.project);
} catch (error) {
console.error('Failed to load project:', error);
showToast('Failed to load project', 'error');
}
}
/**
* Open variables management mode
*/
async openVariables(projectId) {
this.mode = 'variables';
this.projectId = projectId;
try {
const [projectResponse, variablesResponse] = await Promise.all([
apiRequest(`/api/admin/projects/${projectId}`),
apiRequest(`/api/admin/projects/${projectId}/variables`)
]);
if (!projectResponse.success || !projectResponse.project) {
throw new Error('Failed to load project');
}
this.originalProject = projectResponse.project;
this.variables = variablesResponse.variables || [];
this.renderVariablesMode();
} catch (error) {
console.error('Failed to load project variables:', error);
showToast('Failed to load variables', 'error');
}
}
/**
* Render the editor modal
*/
render() {
const container = document.getElementById('modal-container');
const title = this.mode === 'create' ? 'Create New Project' : 'Edit Project';
container.innerHTML = `
<div id="project-editor-modal" class="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50 p-4">
<div class="bg-white rounded-lg shadow-xl max-w-3xl w-full max-h-[90vh] overflow-hidden flex flex-col">
<!-- Header -->
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<h3 class="text-lg font-medium text-gray-900">${title}</h3>
<button id="close-modal" class="text-gray-400 hover:text-gray-500">
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<!-- Body -->
<div class="flex-1 overflow-y-auto p-6">
<form id="project-form">
<div class="space-y-6">
<!-- Project ID -->
<div>
<label for="project-id" class="block text-sm font-medium text-gray-700 mb-1">
Project ID <span class="text-red-500">*</span>
</label>
<input
type="text"
id="project-id"
name="id"
placeholder="e.g., my-project, family-history"
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
${this.mode === 'edit' ? 'disabled' : 'required'}
>
<p class="mt-1 text-xs text-gray-500">Lowercase slug format (letters, numbers, hyphens only)</p>
</div>
<!-- Project Name -->
<div>
<label for="project-name" class="block text-sm font-medium text-gray-700 mb-1">
Project Name <span class="text-red-500">*</span>
</label>
<input
type="text"
id="project-name"
name="name"
placeholder="e.g., Family History Archive"
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
required
>
</div>
<!-- Description -->
<div>
<label for="project-description" class="block text-sm font-medium text-gray-700 mb-1">
Description
</label>
<textarea
id="project-description"
name="description"
rows="3"
placeholder="Brief description of the project..."
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
></textarea>
</div>
<!-- Tech Stack -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label for="tech-framework" class="block text-sm font-medium text-gray-700 mb-1">
Framework
</label>
<input
type="text"
id="tech-framework"
placeholder="e.g., Express.js"
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
>
</div>
<div>
<label for="tech-database" class="block text-sm font-medium text-gray-700 mb-1">
Database
</label>
<input
type="text"
id="tech-database"
placeholder="e.g., MongoDB"
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
>
</div>
<div>
<label for="tech-frontend" class="block text-sm font-medium text-gray-700 mb-1">
Frontend
</label>
<input
type="text"
id="tech-frontend"
placeholder="e.g., React"
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
>
</div>
</div>
<!-- Repository URL -->
<div>
<label for="repo-url" class="block text-sm font-medium text-gray-700 mb-1">
Repository URL
</label>
<input
type="url"
id="repo-url"
name="repositoryUrl"
placeholder="https://codeberg.org/org/repo"
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
>
</div>
<!-- Active Status -->
<div class="flex items-center">
<input
type="checkbox"
id="project-active"
name="active"
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
checked
>
<label for="project-active" class="ml-2 block text-sm text-gray-900">
Active
</label>
<p class="ml-2 text-xs text-gray-500">(Inactive projects are hidden from rule rendering)</p>
</div>
</div>
</form>
</div>
<!-- Footer -->
<div class="px-6 py-4 border-t border-gray-200 flex justify-end space-x-3">
<button id="cancel-btn" class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
Cancel
</button>
<button id="save-btn" class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700">
${this.mode === 'create' ? 'Create Project' : 'Save Changes'}
</button>
</div>
</div>
</div>
`;
}
/**
* Render view mode (read-only)
*/
renderViewMode(project) {
const container = document.getElementById('modal-container');
const techStack = project.techStack || {};
const metadata = project.metadata || {};
container.innerHTML = `
<div id="project-editor-modal" class="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50 p-4">
<div class="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
<!-- Header -->
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<div>
<h3 class="text-lg font-medium text-gray-900">${escapeHtml(project.name)}</h3>
<p class="text-sm text-gray-500 font-mono mt-1">${escapeHtml(project.id)}</p>
</div>
<button id="close-modal" class="text-gray-400 hover:text-gray-500">
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<!-- Body -->
<div class="flex-1 overflow-y-auto p-6">
<div class="space-y-6">
<!-- Status Badge -->
<div>
${project.active
? '<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800">Active</span>'
: '<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-800">Inactive</span>'
}
</div>
<!-- Description -->
${project.description ? `
<div>
<h4 class="text-sm font-medium text-gray-700 mb-2">Description</h4>
<p class="text-sm text-gray-900">${escapeHtml(project.description)}</p>
</div>
` : ''}
<!-- Tech Stack -->
${Object.keys(techStack).length > 0 ? `
<div>
<h4 class="text-sm font-medium text-gray-700 mb-2">Tech Stack</h4>
<div class="grid grid-cols-2 gap-4">
${techStack.framework ? `<div class="text-sm"><span class="font-medium">Framework:</span> ${escapeHtml(techStack.framework)}</div>` : ''}
${techStack.database ? `<div class="text-sm"><span class="font-medium">Database:</span> ${escapeHtml(techStack.database)}</div>` : ''}
${techStack.frontend ? `<div class="text-sm"><span class="font-medium">Frontend:</span> ${escapeHtml(techStack.frontend)}</div>` : ''}
${techStack.css ? `<div class="text-sm"><span class="font-medium">CSS:</span> ${escapeHtml(techStack.css)}</div>` : ''}
</div>
</div>
` : ''}
<!-- Repository -->
${project.repositoryUrl ? `
<div>
<h4 class="text-sm font-medium text-gray-700 mb-2">Repository</h4>
<a href="${escapeHtml(project.repositoryUrl)}" target="_blank" class="text-sm text-indigo-600 hover:text-indigo-700">
${escapeHtml(project.repositoryUrl)}
</a>
</div>
` : ''}
<!-- Variables -->
<div>
<div class="flex justify-between items-center mb-3">
<h4 class="text-sm font-medium text-gray-700">Variables (${this.variables.length})</h4>
<button data-action="openVariables" data-arg0="${project.id}" class="text-sm text-indigo-600 hover:text-indigo-700">
Manage Variables →
</button>
</div>
${this.variables.length > 0 ? `
<div class="border border-gray-200 rounded-md overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Value</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Category</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
${this.variables.slice(0, 5).map(v => `
<tr>
<td class="px-4 py-2 text-sm font-mono text-gray-900">${escapeHtml(v.variableName)}</td>
<td class="px-4 py-2 text-sm text-gray-600">${escapeHtml(v.value)}</td>
<td class="px-4 py-2 text-sm text-gray-500">${escapeHtml(v.category || 'other')}</td>
</tr>
`).join('')}
</tbody>
</table>
${this.variables.length > 5 ? `
<div class="px-4 py-2 bg-gray-50 text-xs text-gray-500 text-center">
Showing 5 of ${this.variables.length} variables
</div>
` : ''}
</div>
` : '<p class="text-sm text-gray-500 italic">No variables defined</p>'}
</div>
<!-- Metadata -->
<div class="text-xs text-gray-500 space-y-1">
<p>Created: ${new Date(project.createdAt).toLocaleString()}</p>
<p>Updated: ${new Date(project.updatedAt).toLocaleString()}</p>
</div>
</div>
</div>
<!-- Footer -->
<div class="px-6 py-4 border-t border-gray-200 flex justify-between">
<button data-action="openEdit" data-arg0="${project.id}" class="px-4 py-2 border border-indigo-300 rounded-md text-sm font-medium text-indigo-700 bg-indigo-50 hover:bg-indigo-100">
Edit Project
</button>
<button id="close-modal" class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
Close
</button>
</div>
</div>
</div>
`;
// Attach close handlers
document.getElementById('close-modal').addEventListener('click', () => this.close());
}
/**
* Render variables management mode
*/
renderVariablesMode() {
const container = document.getElementById('modal-container');
container.innerHTML = `
<div id="project-editor-modal" class="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50 p-4">
<div class="bg-white rounded-lg shadow-xl max-w-5xl w-full max-h-[90vh] overflow-hidden flex flex-col">
<!-- Header -->
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<div>
<h3 class="text-lg font-medium text-gray-900">Manage Variables</h3>
<p class="text-sm text-gray-500 mt-1">${escapeHtml(this.originalProject.name)} (${escapeHtml(this.originalProject.id)})</p>
</div>
<button id="close-modal" class="text-gray-400 hover:text-gray-500">
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<!-- Body -->
<div class="flex-1 overflow-y-auto p-6">
<div class="mb-4 flex justify-between items-center">
<p class="text-sm text-gray-600">${this.variables.length} variable${this.variables.length !== 1 ? 's' : ''} defined</p>
<button id="add-variable-btn" class="px-4 py-2 bg-indigo-600 text-white rounded-md text-sm font-medium hover:bg-indigo-700">
+ Add Variable
</button>
</div>
<div id="variables-list" class="space-y-3">
${this.variables.length > 0 ? this.variables.map(v => this.renderVariableCard(v)).join('') : `
<div class="text-center py-12 text-gray-500">
<p class="text-sm">No variables defined for this project.</p>
<p class="text-xs mt-2">Click "Add Variable" to create one.</p>
</div>
`}
</div>
</div>
<!-- Footer -->
<div class="px-6 py-4 border-t border-gray-200 flex justify-end">
<button id="close-modal" class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
Done
</button>
</div>
</div>
</div>
`;
// Attach event listeners
document.getElementById('close-modal').addEventListener('click', () => {
this.close();
// Refresh project list
if (window.loadProjects) window.loadProjects();
if (window.loadStatistics) window.loadStatistics();
});
document.getElementById('add-variable-btn').addEventListener('click', () => {
this.showVariableForm();
});
}
/**
* Render a single variable card
*/
renderVariableCard(variable) {
return `
<div class="border border-gray-200 rounded-md p-4 hover:border-indigo-300 transition-colors">
<div class="flex justify-between items-start">
<div class="flex-1">
<h5 class="text-sm font-medium font-mono text-gray-900">${escapeHtml(variable.variableName)}</h5>
<p class="text-sm text-gray-600 mt-1">${escapeHtml(variable.value)}</p>
${variable.description ? `<p class="text-xs text-gray-500 mt-1">${escapeHtml(variable.description)}</p>` : ''}
<div class="flex items-center space-x-3 mt-2">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800">
${escapeHtml(variable.category || 'other')}
</span>
<span class="text-xs text-gray-500">${escapeHtml(variable.dataType || 'string')}</span>
</div>
</div>
<div class="flex space-x-2 ml-4">
<button data-action="editVariable" data-arg0="${escapeHtml(variable.variableName)}" class="text-sm text-indigo-600 hover:text-indigo-700">
Edit
</button>
<button data-action="deleteVariable" data-arg0="${escapeHtml(variable.variableName)}" class="text-sm text-red-600 hover:text-red-700">
Delete
</button>
</div>
</div>
</div>
`;
}
/**
* Show variable form (add/edit)
*/
showVariableForm(variableName = null) {
const existingVariable = variableName ? this.variables.find(v => v.variableName === variableName) : null;
const isEdit = !!existingVariable;
const formHtml = `
<div class="border-t border-gray-200 mt-4 pt-4 bg-gray-50 rounded-md p-4">
<h4 class="text-sm font-medium text-gray-900 mb-4">${isEdit ? 'Edit' : 'Add'} Variable</h4>
<form id="variable-form" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
Variable Name <span class="text-red-500">*</span>
</label>
<input
type="text"
id="var-name"
placeholder="e.g., DB_NAME"
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 font-mono text-sm"
${isEdit ? 'readonly' : 'required'}
value="${isEdit ? escapeHtml(existingVariable.variableName) : ''}"
>
<p class="text-xs text-gray-500 mt-1">UPPER_SNAKE_CASE format</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
Value <span class="text-red-500">*</span>
</label>
<input
type="text"
id="var-value"
placeholder="e.g., my_database"
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm"
required
value="${isEdit ? escapeHtml(existingVariable.value) : ''}"
>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
<input
type="text"
id="var-description"
placeholder="What this variable represents..."
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm"
value="${isEdit && existingVariable.description ? escapeHtml(existingVariable.description) : ''}"
>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Category</label>
<select id="var-category" class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
<option value="other" ${isEdit && existingVariable.category === 'other' ? 'selected' : ''}>Other</option>
<option value="database" ${isEdit && existingVariable.category === 'database' ? 'selected' : ''}>Database</option>
<option value="security" ${isEdit && existingVariable.category === 'security' ? 'selected' : ''}>Security</option>
<option value="config" ${isEdit && existingVariable.category === 'config' ? 'selected' : ''}>Config</option>
<option value="path" ${isEdit && existingVariable.category === 'path' ? 'selected' : ''}>Path</option>
<option value="url" ${isEdit && existingVariable.category === 'url' ? 'selected' : ''}>URL</option>
<option value="port" ${isEdit && existingVariable.category === 'port' ? 'selected' : ''}>Port</option>
<option value="feature_flag" ${isEdit && existingVariable.category === 'feature_flag' ? 'selected' : ''}>Feature Flag</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Data Type</label>
<select id="var-datatype" class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
<option value="string" ${isEdit && existingVariable.dataType === 'string' ? 'selected' : ''}>String</option>
<option value="number" ${isEdit && existingVariable.dataType === 'number' ? 'selected' : ''}>Number</option>
<option value="boolean" ${isEdit && existingVariable.dataType === 'boolean' ? 'selected' : ''}>Boolean</option>
<option value="path" ${isEdit && existingVariable.dataType === 'path' ? 'selected' : ''}>Path</option>
<option value="url" ${isEdit && existingVariable.dataType === 'url' ? 'selected' : ''}>URL</option>
<option value="email" ${isEdit && existingVariable.dataType === 'email' ? 'selected' : ''}>Email</option>
</select>
</div>
</div>
<div class="flex justify-end space-x-2">
<button type="button" id="cancel-var-btn" class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
Cancel
</button>
<button type="submit" class="px-4 py-2 bg-indigo-600 text-white rounded-md text-sm font-medium hover:bg-indigo-700">
${isEdit ? 'Update' : 'Add'} Variable
</button>
</div>
</form>
</div>
`;
// Insert form
const container = document.querySelector('#variables-list');
const formContainer = document.createElement('div');
formContainer.id = 'variable-form-container';
formContainer.innerHTML = formHtml;
container.insertBefore(formContainer, container.firstChild);
// Attach event listeners
document.getElementById('variable-form').addEventListener('submit', async (e) => {
e.preventDefault();
await this.saveVariable(isEdit);
});
document.getElementById('cancel-var-btn').addEventListener('click', () => {
document.getElementById('variable-form-container').remove();
});
}
/**
* Save variable (create or update)
*/
async saveVariable(isEdit = false) {
const variableName = document.getElementById('var-name').value.trim();
const value = document.getElementById('var-value').value.trim();
const description = document.getElementById('var-description').value.trim();
const category = document.getElementById('var-category').value;
const dataType = document.getElementById('var-datatype').value;
if (!variableName || !value) {
showToast('Variable name and value are required', 'error');
return;
}
// Validate UPPER_SNAKE_CASE
if (!/^[A-Z][A-Z0-9_]*$/.test(variableName)) {
showToast('Variable name must be UPPER_SNAKE_CASE (e.g., DB_NAME)', 'error');
return;
}
try {
const response = await apiRequest(`/api/admin/projects/${this.projectId}/variables`, {
method: 'POST',
body: JSON.stringify({
variableName,
value,
description,
category,
dataType
})
});
if (response.success) {
showToast(`Variable ${isEdit ? 'updated' : 'created'} successfully`, 'success');
// Reload variables
const variablesResponse = await apiRequest(`/api/admin/projects/${this.projectId}/variables`);
this.variables = variablesResponse.variables || [];
// Re-render
this.renderVariablesMode();
} else {
showToast(response.message || 'Failed to save variable', 'error');
}
} catch (error) {
console.error('Failed to save variable:', error);
showToast('Failed to save variable', 'error');
}
}
/**
* Edit variable
*/
editVariable(variableName) {
// Remove any existing form first
const existingForm = document.getElementById('variable-form-container');
if (existingForm) existingForm.remove();
this.showVariableForm(variableName);
}
/**
* Delete variable
*/
async deleteVariable(variableName) {
if (!confirm(`Delete variable "${variableName}"?`)) {
return;
}
try {
const response = await apiRequest(`/api/admin/projects/${this.projectId}/variables/${variableName}`, {
method: 'DELETE'
});
if (response.success) {
showToast('Variable deleted successfully', 'success');
// Reload variables
const variablesResponse = await apiRequest(`/api/admin/projects/${this.projectId}/variables`);
this.variables = variablesResponse.variables || [];
// Re-render
this.renderVariablesMode();
} else {
showToast(response.message || 'Failed to delete variable', 'error');
}
} catch (error) {
console.error('Failed to delete variable:', error);
showToast('Failed to delete variable', 'error');
}
}
/**
* Populate form with project data (edit mode)
*/
populateForm(project) {
document.getElementById('project-id').value = project.id || '';
document.getElementById('project-name').value = project.name || '';
document.getElementById('project-description').value = project.description || '';
document.getElementById('project-active').checked = project.active !== false;
document.getElementById('repo-url').value = project.repositoryUrl || '';
if (project.techStack) {
document.getElementById('tech-framework').value = project.techStack.framework || '';
document.getElementById('tech-database').value = project.techStack.database || '';
document.getElementById('tech-frontend').value = project.techStack.frontend || '';
}
}
/**
* Attach event listeners
*/
attachEventListeners() {
document.getElementById('close-modal').addEventListener('click', () => this.close());
document.getElementById('cancel-btn').addEventListener('click', () => this.close());
document.getElementById('save-btn').addEventListener('click', () => this.submit());
}
/**
* Submit form
*/
async submit() {
const form = document.getElementById('project-form');
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const projectData = {
id: document.getElementById('project-id').value.trim(),
name: document.getElementById('project-name').value.trim(),
description: document.getElementById('project-description').value.trim(),
active: document.getElementById('project-active').checked,
repositoryUrl: document.getElementById('repo-url').value.trim() || null,
techStack: {
framework: document.getElementById('tech-framework').value.trim() || undefined,
database: document.getElementById('tech-database').value.trim() || undefined,
frontend: document.getElementById('tech-frontend').value.trim() || undefined
}
};
try {
let response;
if (this.mode === 'create') {
response = await apiRequest('/api/admin/projects', {
method: 'POST',
body: JSON.stringify(projectData)
});
} else {
response = await apiRequest(`/api/admin/projects/${this.projectId}`, {
method: 'PUT',
body: JSON.stringify(projectData)
});
}
if (response.success) {
showToast(`Project ${this.mode === 'create' ? 'created' : 'updated'} successfully`, 'success');
this.close();
// Refresh project list
if (window.loadProjects) window.loadProjects();
if (window.loadStatistics) window.loadStatistics();
} else {
showToast(response.message || 'Failed to save project', 'error');
}
} catch (error) {
console.error('Failed to save project:', error);
showToast('Failed to save project', 'error');
}
}
/**
* Close modal
*/
close() {
const container = document.getElementById('modal-container');
container.innerHTML = '';
}
}
// Utility function
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Create global instance
window.projectEditor = new ProjectEditor();
// Event delegation for data-action buttons (CSP compliance)
document.addEventListener('click', (e) => {
const button = e.target.closest('[data-action]');
if (!button) return;
const action = button.dataset.action;
const arg0 = button.dataset.arg0;
if (action === 'editVariable') {
window.projectEditor.editVariable(arg0);
} else if (action === 'deleteVariable') {
window.projectEditor.deleteVariable(arg0);
}
});