<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Architectural Decision Log</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 30px;
}
h1 {
color: #333;
margin-bottom: 30px;
font-size: 28px;
}
.actions {
margin-bottom: 20px;
display: flex;
gap: 10px;
}
button {
padding: 10px 20px;
background: #0066cc;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
}
button:hover {
background: #0052a3;
}
button.secondary {
background: #6c757d;
}
button.secondary:hover {
background: #5a6268;
}
button.danger {
background: #dc3545;
}
button.danger:hover {
background: #c82333;
}
.table-container {
overflow-x: auto;
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background: #f8f9fa;
font-weight: 600;
color: #495057;
position: sticky;
top: 0;
}
tr:hover {
background: #f8f9fa;
}
.status-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.status-proposed {
background: #fff3cd;
color: #856404;
}
.status-approved {
background: #d4edda;
color: #155724;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.active {
display: flex;
}
.modal-content {
background: white;
padding: 30px;
border-radius: 8px;
max-width: 600px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.modal-header h2 {
font-size: 24px;
color: #333;
}
.close-btn {
background: none;
color: #999;
font-size: 28px;
padding: 0;
width: 30px;
height: 30px;
line-height: 1;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #495057;
}
input, textarea, select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
font-family: inherit;
}
textarea {
resize: vertical;
min-height: 100px;
}
.fact-sheet-input {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.fact-sheet-input input {
flex: 1;
}
.fact-sheet-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.fact-sheet-tag {
background: #e9ecef;
padding: 6px 12px;
border-radius: 16px;
font-size: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.fact-sheet-tag button {
background: none;
color: #dc3545;
padding: 0;
width: 16px;
height: 16px;
font-size: 14px;
line-height: 1;
}
.action-buttons {
display: flex;
gap: 8px;
white-space: nowrap;
}
.action-buttons button {
padding: 6px 12px;
font-size: 12px;
}
td.action-buttons {
vertical-align: middle;
}
.loading {
text-align: center;
padding: 40px;
color: #6c757d;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 12px;
border-radius: 4px;
margin-bottom: 20px;
}
.timestamp {
font-size: 12px;
color: #6c757d;
}
.search-container {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.search-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 15px;
}
.search-field {
display: flex;
flex-direction: column;
}
.search-field label {
font-size: 12px;
font-weight: 500;
color: #495057;
margin-bottom: 5px;
}
.search-field input,
.search-field select {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.search-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.search-actions button {
padding: 8px 16px;
font-size: 13px;
}
.search-info {
font-size: 14px;
color: #6c757d;
margin-bottom: 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>π Architectural Decision Log</h1>
<div id="error-message" class="error" style="display: none;"></div>
<!-- Search Section -->
<div class="search-container">
<div class="search-grid">
<div class="search-field">
<label for="search-author">π Author</label>
<input type="text" id="search-author" placeholder="Search by author...">
</div>
<div class="search-field">
<label for="search-title">π Title</label>
<input type="text" id="search-title" placeholder="Search by title...">
</div>
<div class="search-field">
<label for="search-decision">π Decision</label>
<input type="text" id="search-decision" placeholder="Search by decision...">
</div>
<div class="search-field">
<label for="search-factsheets">π Fact Sheets</label>
<input type="text" id="search-factsheets" placeholder="e.g., LEA, Customer...">
</div>
<div class="search-field">
<label for="search-status">π Status</label>
<select id="search-status">
<option value="">All Statuses</option>
<option value="Proposed">Proposed</option>
<option value="Approved">Approved</option>
</select>
</div>
</div>
<div class="search-actions">
<button onclick="performSearch()">π Search</button>
<button class="secondary" onclick="clearSearch()">Clear</button>
</div>
</div>
<div id="search-info" class="search-info" style="display: none;"></div>
<div class="actions">
<button onclick="showCreateModal()">β New Decision</button>
<button class="secondary" onclick="loadEntries()">π Refresh</button>
</div>
<div class="table-container">
<div id="loading" class="loading">Loading...</div>
<table id="entries-table" style="display: none;">
<thead>
<tr>
<th>ID</th>
<th>Created</th>
<th>Last Edited</th>
<th>Author</th>
<th>Title</th>
<th>Decision</th>
<th>Fact Sheets</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="entries-body"></tbody>
</table>
</div>
</div>
<!-- Create/Edit Modal -->
<div id="entry-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modal-title">New Decision</h2>
<button class="close-btn" onclick="closeModal()">Γ</button>
</div>
<form id="entry-form" onsubmit="handleSubmit(event)">
<input type="hidden" id="entry-id">
<div class="form-group">
<label for="author">Author *</label>
<input type="text" id="author" required>
</div>
<div class="form-group">
<label for="title">Title *</label>
<input type="text" id="title" required>
</div>
<div class="form-group">
<label for="decision">Decision *</label>
<textarea id="decision" required></textarea>
</div>
<div class="form-group">
<label for="fact-sheet-input">Fact Sheets (SAP/LeanIX)</label>
<div class="fact-sheet-input">
<input type="text" id="fact-sheet-input" placeholder="Enter fact sheet name">
<button type="button" onclick="addFactSheet()">Add</button>
</div>
<div id="fact-sheet-list" class="fact-sheet-list"></div>
</div>
<div class="form-group">
<label for="status">Status *</label>
<select id="status" required>
<option value="Proposed">Proposed</option>
<option value="Approved">Approved</option>
</select>
</div>
<div class="actions">
<button type="submit">Save</button>
<button type="button" class="secondary" onclick="closeModal()">Cancel</button>
</div>
</form>
</div>
</div>
<script>
const MCP_API_URL = 'http://localhost:5000/adl/entries';
const MCP_SEARCH_URL = 'http://localhost:5000/adl/search';
let factSheets = [];
let currentEntryId = null;
let isSearchActive = false;
// Load entries on page load
window.addEventListener('DOMContentLoaded', loadEntries);
async function loadEntries() {
isSearchActive = false;
document.getElementById('search-info').style.display = 'none';
try {
document.getElementById('loading').style.display = 'block';
document.getElementById('entries-table').style.display = 'none';
hideError();
const response = await fetch(MCP_API_URL, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
});
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Failed to load entries');
}
renderEntries(result.data);
document.getElementById('loading').style.display = 'none';
document.getElementById('entries-table').style.display = 'table';
} catch (error) {
showError('Failed to load entries: ' + error.message);
document.getElementById('loading').style.display = 'none';
}
}
async function performSearch() {
const author = document.getElementById('search-author').value.trim();
const title = document.getElementById('search-title').value.trim();
const decision = document.getElementById('search-decision').value.trim();
const factSheets = document.getElementById('search-factsheets').value.trim();
const status = document.getElementById('search-status').value;
// Build search criteria (only include non-empty fields)
const searchCriteria = {};
if (author) searchCriteria.author = author;
if (title) searchCriteria.title = title;
if (decision) searchCriteria.decision = decision;
if (factSheets) searchCriteria.factSheets = factSheets;
if (status) searchCriteria.status = status;
// If no criteria, just load all entries
if (Object.keys(searchCriteria).length === 0) {
loadEntries();
return;
}
try {
document.getElementById('loading').style.display = 'block';
document.getElementById('entries-table').style.display = 'none';
hideError();
const response = await fetch(MCP_SEARCH_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(searchCriteria)
});
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Search failed');
}
isSearchActive = true;
const searchInfo = document.getElementById('search-info');
searchInfo.textContent = `Found ${result.data.length} matching entries`;
searchInfo.style.display = 'block';
renderEntries(result.data);
document.getElementById('loading').style.display = 'none';
document.getElementById('entries-table').style.display = 'table';
} catch (error) {
showError('Search failed: ' + error.message);
document.getElementById('loading').style.display = 'none';
}
}
function clearSearch() {
document.getElementById('search-author').value = '';
document.getElementById('search-title').value = '';
document.getElementById('search-decision').value = '';
document.getElementById('search-factsheets').value = '';
document.getElementById('search-status').value = '';
loadEntries();
}
// Allow Enter key in search fields
document.addEventListener('DOMContentLoaded', () => {
const searchFields = ['search-author', 'search-title', 'search-decision', 'search-factsheets'];
searchFields.forEach(fieldId => {
const field = document.getElementById(fieldId);
if (field) {
field.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
performSearch();
}
});
}
});
});
function renderEntries(entries) {
const tbody = document.getElementById('entries-body');
tbody.innerHTML = '';
entries.forEach(entry => {
const row = document.createElement('tr');
row.innerHTML = `
<td><code>${entry.id.substring(0, 8)}</code></td>
<td class="timestamp">${formatDate(entry.created)}</td>
<td class="timestamp">${formatDate(entry.lastEdited)}</td>
<td>${escapeHtml(entry.author)}</td>
<td><strong>${escapeHtml(entry.title)}</strong></td>
<td>${escapeHtml(entry.decision.substring(0, 100))}${entry.decision.length > 100 ? '...' : ''}</td>
<td>${entry.factSheets.map(fs => `<span class="fact-sheet-tag">${escapeHtml(fs)}</span>`).join(' ')}</td>
<td><span class="status-badge status-${entry.status.toLowerCase()}">${entry.status}</span></td>
<td class="action-buttons">
<button onclick="showEditModal('${entry.id}')">Edit</button>
<button class="danger" onclick="deleteEntry('${entry.id}')">Delete</button>
</td>
`;
tbody.appendChild(row);
});
}
function showCreateModal() {
currentEntryId = null;
document.getElementById('modal-title').textContent = 'New Decision';
document.getElementById('entry-form').reset();
factSheets = [];
renderFactSheets();
document.getElementById('entry-modal').classList.add('active');
}
async function showEditModal(id) {
try {
const response = await fetch(`${MCP_API_URL}/${id}`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
});
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Failed to load entry');
}
const entry = result.data;
currentEntryId = entry.id;
document.getElementById('modal-title').textContent = 'Edit Decision';
document.getElementById('entry-id').value = entry.id;
document.getElementById('author').value = entry.author;
document.getElementById('title').value = entry.title;
document.getElementById('decision').value = entry.decision;
document.getElementById('status').value = entry.status;
factSheets = [...entry.factSheets];
renderFactSheets();
document.getElementById('entry-modal').classList.add('active');
} catch (error) {
showError('Failed to load entry: ' + error.message);
}
}
function closeModal() {
document.getElementById('entry-modal').classList.remove('active');
}
async function handleSubmit(event) {
event.preventDefault();
const author = document.getElementById('author').value;
const title = document.getElementById('title').value;
const decision = document.getElementById('decision').value;
const status = document.getElementById('status').value;
try {
if (currentEntryId) {
// Update
await fetch(`${MCP_API_URL}/${currentEntryId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ author, title, decision, factSheets, status })
});
} else {
// Create
await fetch(MCP_API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ author, title, decision, factSheets, status })
});
}
closeModal();
loadEntries();
} catch (error) {
showError('Failed to save entry: ' + error.message);
}
}
async function deleteEntry(id) {
if (!confirm('Are you sure you want to delete this entry?')) {
return;
}
try {
await fetch(`${MCP_API_URL}/${id}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' }
});
loadEntries();
} catch (error) {
showError('Failed to delete entry: ' + error.message);
}
}
function addFactSheet() {
const input = document.getElementById('fact-sheet-input');
const value = input.value.trim();
if (value && !factSheets.includes(value)) {
factSheets.push(value);
renderFactSheets();
input.value = '';
}
}
function removeFactSheet(index) {
factSheets.splice(index, 1);
renderFactSheets();
}
function renderFactSheets() {
const container = document.getElementById('fact-sheet-list');
container.innerHTML = factSheets.map((fs, index) => `
<div class="fact-sheet-tag">
${escapeHtml(fs)}
<button type="button" onclick="removeFactSheet(${index})">Γ</button>
</div>
`).join('');
}
function formatDate(isoString) {
const date = new Date(isoString);
return date.toLocaleString();
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function showError(message) {
const errorDiv = document.getElementById('error-message');
errorDiv.textContent = message;
errorDiv.style.display = 'block';
}
function hideError() {
document.getElementById('error-message').style.display = 'none';
}
// Allow Enter key to add fact sheets
document.getElementById('fact-sheet-input')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
addFactSheet();
}
});
</script>
</body>
</html>