Ferrell Synthetic Intelligence commited on
Commit ·
4ed7180
1
Parent(s): e7d4595
Add local community issue and discussion CRUD
Browse files- app.js +10 -2
- community/store.mjs +25 -0
- daemon/server.mjs +9 -1
- daemon/test-community-store.mjs +4 -0
- index.html +3 -2
app.js
CHANGED
|
@@ -208,8 +208,16 @@ function renderCasefile() {
|
|
| 208 |
|
| 209 |
function renderCommunity(tab = 'projects') {
|
| 210 |
const entries = communityStore[tab] || [];
|
| 211 |
-
$('#community-feed').innerHTML = `<div style="color:#72ff9e;margin-bottom:5px">LOCAL CACHE / SYNC OFF</div>${entries.map(entry => `<div style="border-left:2px solid #b277ff;padding-left:5px;margin:5px 0"><b>${esc(entry.title)}</b><br>${esc(entry.detail)}<br><span style="color:#718994">${esc(entry.boundary || entry.status || 'local')}</span></div>`).join('')}`;
|
| 212 |
document.querySelectorAll('[data-community-tab]').forEach(button => button.style.color = button.dataset.communityTab === tab ? '#72ff9e' : '#718994');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
}
|
| 214 |
|
| 215 |
async function loadCommunity() {
|
|
@@ -226,7 +234,7 @@ async function addCommunityIssue() {
|
|
| 226 |
try {
|
| 227 |
const response = await fetch('http://127.0.0.1:4777/api/community/items', {
|
| 228 |
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
| 229 |
-
body: JSON.stringify({ type: '
|
| 230 |
});
|
| 231 |
if (!response.ok) throw new Error('daemon rejected item');
|
| 232 |
$('#community-title').value = '';
|
|
|
|
| 208 |
|
| 209 |
function renderCommunity(tab = 'projects') {
|
| 210 |
const entries = communityStore[tab] || [];
|
| 211 |
+
$('#community-feed').innerHTML = `<div style="color:#72ff9e;margin-bottom:5px">LOCAL CACHE / SYNC OFF</div>${entries.map((entry, index) => `<div style="border-left:2px solid #b277ff;padding-left:5px;margin:5px 0"><b>${esc(entry.title)}</b><br>${esc(entry.detail)}<br><span style="color:#718994">${esc(entry.boundary || entry.status || 'local')}</span> <button data-community-remove="${index}" style="color:#ff6d82;background:none;border:0;font:9px ui-monospace,monospace;cursor:pointer">REMOVE</button></div>`).join('')}`;
|
| 212 |
document.querySelectorAll('[data-community-tab]').forEach(button => button.style.color = button.dataset.communityTab === tab ? '#72ff9e' : '#718994');
|
| 213 |
+
document.querySelectorAll('[data-community-remove]').forEach(button => button.onclick = () => removeCommunityItem(tab, Number(button.dataset.communityRemove)));
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
async function removeCommunityItem(type, index) {
|
| 217 |
+
try {
|
| 218 |
+
await fetch('http://127.0.0.1:4777/api/community/items', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type, index }) });
|
| 219 |
+
await loadCommunity();
|
| 220 |
+
} catch (error) { appendLog('COMMUNITY', `Could not remove local item: ${error.message}`, 'warning'); }
|
| 221 |
}
|
| 222 |
|
| 223 |
async function loadCommunity() {
|
|
|
|
| 234 |
try {
|
| 235 |
const response = await fetch('http://127.0.0.1:4777/api/community/items', {
|
| 236 |
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
| 237 |
+
body: JSON.stringify({ type: $('#community-type').value, item: { title, detail: 'Created locally from AIDE.' } })
|
| 238 |
});
|
| 239 |
if (!response.ok) throw new Error('daemon rejected item');
|
| 240 |
$('#community-title').value = '';
|
community/store.mjs
CHANGED
|
@@ -29,4 +29,29 @@ export class CommunityStore {
|
|
| 29 |
await fs.rename(temporary, this.file);
|
| 30 |
return entry;
|
| 31 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
}
|
|
|
|
| 29 |
await fs.rename(temporary, this.file);
|
| 30 |
return entry;
|
| 31 |
}
|
| 32 |
+
|
| 33 |
+
async update(type, index, item) {
|
| 34 |
+
if (!TYPES.has(type) || !Number.isInteger(index) || !this.data[type][index]) throw new Error('community item is not addressable');
|
| 35 |
+
const current = this.data[type][index];
|
| 36 |
+
const title = String(item?.title ?? current.title).trim();
|
| 37 |
+
const detail = String(item?.detail ?? current.detail).trim();
|
| 38 |
+
if (!title || title.length > 160 || detail.length > 2000) throw new Error('invalid community item');
|
| 39 |
+
this.data[type][index] = { ...current, title, detail, updated_at: new Date().toISOString() };
|
| 40 |
+
await this.#save();
|
| 41 |
+
return this.data[type][index];
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
async remove(type, index) {
|
| 45 |
+
if (!TYPES.has(type) || !Number.isInteger(index) || !this.data[type][index]) throw new Error('community item is not addressable');
|
| 46 |
+
const [removed] = this.data[type].splice(index, 1);
|
| 47 |
+
await this.#save();
|
| 48 |
+
return removed;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
async #save() {
|
| 52 |
+
await fs.mkdir(path.dirname(this.file), { recursive: true });
|
| 53 |
+
const temporary = `${this.file}.tmp-${process.pid}`;
|
| 54 |
+
await fs.writeFile(temporary, `${JSON.stringify(this.data, null, 2)}\n`);
|
| 55 |
+
await fs.rename(temporary, this.file);
|
| 56 |
+
}
|
| 57 |
}
|
daemon/server.mjs
CHANGED
|
@@ -21,7 +21,7 @@ function json(response, status, body) {
|
|
| 21 |
'Content-Type': 'application/json; charset=utf-8',
|
| 22 |
'Content-Length': Buffer.byteLength(payload),
|
| 23 |
'Access-Control-Allow-Origin': 'http://127.0.0.1:4173',
|
| 24 |
-
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
| 25 |
'Access-Control-Allow-Headers': 'Content-Type'
|
| 26 |
});
|
| 27 |
response.end(payload);
|
|
@@ -79,6 +79,14 @@ const server = http.createServer(async (request, response) => {
|
|
| 79 |
const input = await body(request);
|
| 80 |
return json(response, 201, { item: await communityStore.add(input.type, input.item) });
|
| 81 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
if (request.method === 'POST' && request.url === '/api/models/start') {
|
| 83 |
return json(response, 200, await modelManager.start((await body(request)).id));
|
| 84 |
}
|
|
|
|
| 21 |
'Content-Type': 'application/json; charset=utf-8',
|
| 22 |
'Content-Length': Buffer.byteLength(payload),
|
| 23 |
'Access-Control-Allow-Origin': 'http://127.0.0.1:4173',
|
| 24 |
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
| 25 |
'Access-Control-Allow-Headers': 'Content-Type'
|
| 26 |
});
|
| 27 |
response.end(payload);
|
|
|
|
| 79 |
const input = await body(request);
|
| 80 |
return json(response, 201, { item: await communityStore.add(input.type, input.item) });
|
| 81 |
}
|
| 82 |
+
if (request.method === 'PUT' && request.url === '/api/community/items') {
|
| 83 |
+
const input = await body(request);
|
| 84 |
+
return json(response, 200, { item: await communityStore.update(input.type, input.index, input.item) });
|
| 85 |
+
}
|
| 86 |
+
if (request.method === 'DELETE' && request.url === '/api/community/items') {
|
| 87 |
+
const input = await body(request);
|
| 88 |
+
return json(response, 200, { item: await communityStore.remove(input.type, input.index) });
|
| 89 |
+
}
|
| 90 |
if (request.method === 'POST' && request.url === '/api/models/start') {
|
| 91 |
return json(response, 200, await modelManager.start((await body(request)).id));
|
| 92 |
}
|
daemon/test-community-store.mjs
CHANGED
|
@@ -12,6 +12,10 @@ const store = new CommunityStore(file);
|
|
| 12 |
await store.load();
|
| 13 |
await store.add('issues', { title: 'Local issue', detail: 'Keep this on device.' });
|
| 14 |
assert.equal(store.list().issues.length, 1);
|
|
|
|
|
|
|
| 15 |
assert.match(await readFile(file, 'utf8'), /Local issue/);
|
|
|
|
|
|
|
| 16 |
await assert.rejects(store.add('unknown', { title: 'bad' }), /not allowed/);
|
| 17 |
console.log('community store test passed');
|
|
|
|
| 12 |
await store.load();
|
| 13 |
await store.add('issues', { title: 'Local issue', detail: 'Keep this on device.' });
|
| 14 |
assert.equal(store.list().issues.length, 1);
|
| 15 |
+
await store.update('issues', 0, { detail: 'Updated locally.' });
|
| 16 |
+
assert.equal(store.list().issues[0].detail, 'Updated locally.');
|
| 17 |
assert.match(await readFile(file, 'utf8'), /Local issue/);
|
| 18 |
+
await store.remove('issues', 0);
|
| 19 |
+
assert.equal(store.list().issues.length, 0);
|
| 20 |
await assert.rejects(store.add('unknown', { title: 'bad' }), /not allowed/);
|
| 21 |
console.log('community store test passed');
|
index.html
CHANGED
|
@@ -62,8 +62,9 @@
|
|
| 62 |
<button data-community-tab="discussions">DISCUSSIONS</button>
|
| 63 |
<button data-community-tab="marketplace">MARKET</button>
|
| 64 |
</div>
|
| 65 |
-
<
|
| 66 |
-
<
|
|
|
|
| 67 |
<div id="community-feed" style="font:9px/1.6 ui-monospace,monospace;color:#9fb8ba;margin-top:7px"></div>
|
| 68 |
</aside>
|
| 69 |
|
|
|
|
| 62 |
<button data-community-tab="discussions">DISCUSSIONS</button>
|
| 63 |
<button data-community-tab="marketplace">MARKET</button>
|
| 64 |
</div>
|
| 65 |
+
<select id="community-type" style="width:100%;margin-top:7px;background:#09151d;border:1px solid #203342;color:#e4f2ef;padding:6px;font:9px ui-monospace,monospace"><option value="issues">ISSUE</option><option value="discussions">DISCUSSION</option></select>
|
| 66 |
+
<input id="community-title" placeholder="New local item" style="width:100%;margin-top:5px;background:#09151d;border:1px solid #203342;color:#e4f2ef;padding:6px;font:9px ui-monospace,monospace">
|
| 67 |
+
<button id="community-add" class="secondary" style="width:100%;margin-top:5px;padding:6px">ADD LOCAL ITEM</button>
|
| 68 |
<div id="community-feed" style="font:9px/1.6 ui-monospace,monospace;color:#9fb8ba;margin-top:7px"></div>
|
| 69 |
</aside>
|
| 70 |
|