Ferrell Synthetic Intelligence commited on
Commit
4ed7180
·
1 Parent(s): e7d4595

Add local community issue and discussion CRUD

Browse files
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: 'issues', item: { title, detail: 'Created locally from AIDE.' } })
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
- <input id="community-title" placeholder="New local item" style="width:100%;margin-top:7px;background:#09151d;border:1px solid #203342;color:#e4f2ef;padding:6px;font:9px ui-monospace,monospace">
66
- <button id="community-add" class="secondary" style="width:100%;margin-top:5px;padding:6px">ADD LOCAL ISSUE</button>
 
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