@gafj/gafj 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/http/api.js CHANGED
@@ -81,6 +81,7 @@ route("POST", "/api/sources", async (c, p, b) => {
81
81
  const buffer = Buffer.from(need(b, "data_base64"), "base64");
82
82
  return onboarding.addSource(c.db, { candidate_id: c.candidate_id, batch_id: batch, filename: need(b, "filename"), buffer, now: c.now(), actor: ACTOR("upload") });
83
83
  });
84
+ route("DELETE", "/api/sources/:id", (c, p) => onboarding.removeSource(c.db, { candidate_id: c.candidate_id, source_document_id: p.id, now: c.now(), actor: ACTOR("remove_source") }));
84
85
  route("GET", "/api/sources/:id", (c, p) => { const s = onboarding.sourceText(c.db, p.id); if (s.candidate_id !== c.candidate_id) throw bad("unknown source document", 404); return { source_document_id: s.id, filename: s.filename, kind: s.kind, extractor: s.extractor, extractor_version: s.extractor_version, text: s.text }; });
85
86
  route("POST", "/api/onboarding/batches/:id/dedup", (c, p) => onboarding.suggestDedup(c.db, { candidate_id: c.candidate_id, batch_id: p.id, now: c.now(), actor: ACTOR("dedup") }));
86
87
  route("POST", "/api/kb/groups/:id/merge", (c, p, b) => onboarding.mergeGroup(c.db, { candidate_id: c.candidate_id, dedup_group_id: p.id, keep_id: b.keep_id, now: c.now(), actor: ACTOR("merge") }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gafj/gafj",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "GAF-J: a local campaign engine for job search. One SQLite file, one ingest door, your own AI subscription.",
5
5
  "homepage": "https://gaf-j.com",
6
6
  "repository": {
@@ -70,6 +70,24 @@ function listSources(db, candidate_id, batch_id) {
70
70
  .map((s) => ({ ...s, empty: s.chars < 20 }));
71
71
  }
72
72
 
73
+ /** Remove an uploaded file and everything that came only from it: its pending rows, its extraction runs and attempts. Confirmed records stay; they are the candidate's, not the file's. */
74
+ function removeSource(db, { candidate_id, source_document_id, now, actor }) {
75
+ return withTx(db, (d) => {
76
+ const s = d.prepare("SELECT id, filename FROM source_document WHERE id = ? AND candidate_id = ?").get(source_document_id, candidate_id);
77
+ if (!s) throw new NotFound("source document");
78
+ const pending = d.prepare("DELETE FROM pending_accomplishment WHERE source_document_id = ?").run(s.id).changes;
79
+ const runs = d.prepare("SELECT id FROM ai_run WHERE source_document_id = ?").all(s.id).map((r) => r.id);
80
+ for (const id of runs) {
81
+ d.prepare("UPDATE pending_accomplishment SET ai_attempt_id = NULL WHERE ai_attempt_id IN (SELECT id FROM ai_attempt WHERE ai_run_id = ?)").run(id);
82
+ d.prepare("DELETE FROM ai_attempt WHERE ai_run_id = ?").run(id);
83
+ d.prepare("DELETE FROM ai_run WHERE id = ?").run(id);
84
+ }
85
+ d.prepare("DELETE FROM source_document WHERE id = ?").run(s.id);
86
+ appendEvent(d, { candidate_id, entity: "source_document", entity_id: s.id, event: "removed", actor_type: actor.type, actor_ref: actor.ref, payload: { filename: s.filename, pending, runs: runs.length }, at: now });
87
+ return { source_document_id: s.id, pending_removed: pending, runs_removed: runs.length };
88
+ });
89
+ }
90
+
73
91
  function sourceText(db, source_document_id) {
74
92
  const s = db.prepare("SELECT * FROM source_document WHERE id = ?").get(source_document_id);
75
93
  if (!s) throw new NotFound("source document");
@@ -265,4 +283,4 @@ function batchView(db, candidate_id) {
265
283
  return { batch_id: batch.id, status: batch.status, created_at: batch.created_at, sources, pending, groups: [...new Set(pending.map((p) => p.dedup_group_id).filter(Boolean))] };
266
284
  }
267
285
 
268
- module.exports = { openBatch, setBatchStatus, addSource, listSources, sourceText, checkDraft, proposeFromSource, saveExtraction, suggestDedup, mergeGroup, keepSeparate, proposeStyle, batchView, MAX_ATTEMPTS };
286
+ module.exports = { openBatch, setBatchStatus, addSource, removeSource, listSources, sourceText, checkDraft, proposeFromSource, saveExtraction, suggestDedup, mergeGroup, keepSeparate, proposeStyle, batchView, MAX_ATTEMPTS };
package/ui/lib.js CHANGED
@@ -9,7 +9,7 @@ async function req(method, path, body) {
9
9
  if (!r.ok) { const e = new Error(data.error || r.statusText); e.status = r.status; e.allowed = data.allowed; e.detail = data.detail; throw e; }
10
10
  return data;
11
11
  }
12
- export const api = { get: (p) => req("GET", p), post: (p, b) => req("POST", p, b === undefined ? {} : b), put: (p, b) => req("PUT", p, b === undefined ? {} : b) };
12
+ export const api = { get: (p) => req("GET", p), post: (p, b) => req("POST", p, b === undefined ? {} : b), put: (p, b) => req("PUT", p, b === undefined ? {} : b), del: (p) => req("DELETE", p, {}) };
13
13
 
14
14
  export function fmtWhen(iso, tz) {
15
15
  if (!iso) return "";
package/ui/screens/kb.js CHANGED
@@ -58,6 +58,7 @@ export function Onboarding({ d, reload, setErr, route, compact }) {
58
58
  ${s.empty ? html`<span class="tag blocked">no text</span>` : html`<span class="muted small">${s.chars} chars</span>`}
59
59
  <span class="muted small">${s.pending} pending, ${s.runs} runs</span>
60
60
  ${!s.empty ? html`<button class="small" disabled=${running[s.source_document_id] === "running"} onClick=${() => extract(s)}>${running[s.source_document_id] === "running" ? "extracting" : direct ? "Extract" : "Extract (paste)"}</button>` : ""}
61
+ <button class="small danger" title="remove this file and anything proposed from it; confirmed records stay" onClick=${async () => { if (!confirm(`Remove ${s.filename}? Records already confirmed stay; pending rows from this file go.`)) return; try { await api.del(`/api/sources/${s.source_document_id}`); await reload(); } catch (e) { setErr(e.message); } }}>remove</button>
61
62
  ${running[s.source_document_id] && running[s.source_document_id] !== "running" ? html`<span class="small muted">${running[s.source_document_id]}</span>` : ""}</li>`)}
62
63
  ${b.sources.length ? "" : html`<li class="muted">upload a resume to start</li>`}</ul>` : ""}
63
64
  ${b && b.pending.length ? html`<div class="row"><button onClick=${() => post(`/api/onboarding/batches/${b.batch_id}/dedup`)}>Suggest duplicates</button><span class="small muted">same employer, overlapping dates, a shared figure; nothing merges without you</span></div>` : ""}