File size: 6,475 Bytes
87087c0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
// Drives settings.html against /api/config/* (see api/routers/config.py).
// Every mutation re-fetches its own section from the server afterward
// rather than guessing the new state locally -- the server is the one
// source of truth for what's actually in the YAML/txt files.

const KINDS_EDITABLE_HERE = new Set(["presidio_builtin", "regex"]);

document.querySelectorAll(".tab").forEach((tab) => {
  tab.addEventListener("click", () => {
    document.querySelectorAll(".tab").forEach((t) => t.classList.remove("active"));
    document.querySelectorAll(".panel").forEach((p) => (p.hidden = true));
    tab.classList.add("active");
    document.getElementById(tab.dataset.panel).hidden = false;
  });
});

init();

async function init() {
  await Promise.all([loadCategories(), loadColumns(), loadExcludeList()]);

  document.getElementById("category-form").addEventListener("submit", onAddCategory);
  document.getElementById("column-form").addEventListener("submit", onAddColumn);
  document.getElementById("exclude-form").addEventListener("submit", onAddExcludeTerm);
}

// ------------------------------------------------------------- categories

async function loadCategories() {
  const categories = await getJSON("/api/config/categories");
  const tbody = document.querySelector("#categories-table tbody");
  tbody.innerHTML = "";

  for (const c of categories) {
    const editable = KINDS_EDITABLE_HERE.has(c.kind);
    const row = document.createElement("tr");
    row.innerHTML = `
      <td>${escapeHtml(c.name)}</td>
      <td>${editable ? c.kind : `<span class="badge code-backed" title="Backed by a function in redactor/special_categories.py -- edit the YAML directly to remove it">${c.kind}</span>`}</td>
      <td>${c.score_threshold ?? "—"}</td>
      <td>${escapeHtml(c.fake_provider ?? "—")}</td>
      <td><button class="row-delete" ${editable ? "" : "disabled"} title="${editable ? "Remove this category" : "Code-backed category -- can't be removed here"}">&times;</button></td>
    `;
    if (editable) {
      row.querySelector(".row-delete").addEventListener("click", () => deleteCategory(c.name));
    }
    tbody.appendChild(row);
  }
}

async function onAddCategory(e) {
  e.preventDefault();
  const form = e.target;
  const payload = {
    name: form.name.value.trim().toUpperCase(),
    patterns: form.patterns.value.split("\n").map((p) => p.trim()).filter(Boolean),
    score_threshold: parseFloat(form.score_threshold.value),
    fake_provider: form.fake_provider.value.trim(),
  };
  try {
    await postJSON("/api/config/categories", payload);
    form.reset();
    form.closest("details").open = false;
    await loadCategories();
  } catch (err) {
    showError(err.message);
  }
}

async function deleteCategory(name) {
  try {
    await deleteJSON(`/api/config/categories/${encodeURIComponent(name)}`);
    await loadCategories();
  } catch (err) {
    showError(err.message);
  }
}

// --------------------------------------------------------- table layouts

async function loadColumns() {
  const rules = await getJSON("/api/config/structural-columns");
  const tbody = document.querySelector("#columns-table tbody");
  tbody.innerHTML = "";

  rules.forEach((rule, index) => {
    const row = document.createElement("tr");
    row.innerHTML = `
      <td>${escapeHtml(rule.column_header)}</td>
      <td>${escapeHtml(rule.category)}</td>
      <td>${rule.requires_any_of.map(escapeHtml).join(", ")}</td>
      <td><button class="row-delete" title="Remove this rule">&times;</button></td>
    `;
    row.querySelector(".row-delete").addEventListener("click", () => deleteColumn(index));
    tbody.appendChild(row);
  });
}

async function onAddColumn(e) {
  e.preventDefault();
  const form = e.target;
  const payload = {
    column_header: form.column_header.value.trim().toLowerCase(),
    category: form.category.value,
    requires_any_of: form.requires_any_of.value.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean),
  };
  try {
    await postJSON("/api/config/structural-columns", payload);
    form.reset();
    form.closest("details").open = false;
    await loadColumns();
  } catch (err) {
    showError(err.message);
  }
}

async function deleteColumn(index) {
  try {
    await deleteJSON(`/api/config/structural-columns/${index}`);
    await loadColumns();
  } catch (err) {
    showError(err.message);
  }
}

// ------------------------------------------------------------ exclude list

async function loadExcludeList() {
  const terms = await getJSON("/api/config/exclude-list");
  const list = document.getElementById("exclude-list");
  list.innerHTML = "";

  for (const term of terms) {
    const li = document.createElement("li");
    li.innerHTML = `<span>${escapeHtml(term)}</span><button title="Remove">&times;</button>`;
    li.querySelector("button").addEventListener("click", () => deleteExcludeTerm(term));
    list.appendChild(li);
  }
}

async function onAddExcludeTerm(e) {
  e.preventDefault();
  const form = e.target;
  try {
    await postJSON("/api/config/exclude-list", { term: form.term.value.trim() });
    form.reset();
    await loadExcludeList();
  } catch (err) {
    showError(err.message);
  }
}

async function deleteExcludeTerm(term) {
  try {
    await deleteJSON(`/api/config/exclude-list/${encodeURIComponent(term)}`);
    await loadExcludeList();
  } catch (err) {
    showError(err.message);
  }
}

// ------------------------------------------------------------------ utils

async function getJSON(url) {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`Failed to load ${url}`);
  return res.json();
}

async function postJSON(url, body) {
  const res = await fetch(url, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error(data.detail || "Request failed.");
  return data;
}

async function deleteJSON(url) {
  const res = await fetch(url, { method: "DELETE" });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error(data.detail || "Request failed.");
  return data;
}

function showError(message) {
  const box = document.getElementById("settings-error");
  box.textContent = message;
  box.hidden = false;
  setTimeout(() => (box.hidden = true), 4000);
}

function escapeHtml(s) {
  const div = document.createElement("div");
  div.textContent = s;
  return div.innerHTML;
}