mirror of
https://github.com/myronblair/jarvis
synced 2026-07-28 08:43:00 -05:00
fix: address 8 code-review findings in KB topic manager
- Fix 1 (ORDER BY): use table alias t.id to force integer PK ordering; topic_id alias was causing alphabetical sort, breaking rotation offsets - Fix 2 (PDOException): wrap INSERT/UPDATE in try/catch in kb_topic_save; duplicate topic_id now returns clean JSON error instead of HTTP 500 - Fix 3 (XSS/onclick): DEL button passes only t.id; tmDelete looks up name from _topicsData, removing JSON.stringify from onclick attribute - Fix 4 (validation): topic_id must contain at least one [a-z0-9] after sanitization; "@@@" to "___" no longer passes required-field check - Fix 5 (stale logs): guard comment and log messages updated from 20h to 4h to match the actual 14400s threshold - Fix 6 (dedup): tmEsc() replaced with existing esc() throughout; removed duplicate function definition - Fix 7 (toggle rollback): tmToggle uses inline fetch to revert el.checked on API failure instead of leaving the UI in wrong state - Fix 8 (confirm): tmDelete uses openModal confirmation instead of confirm() dialog, consistent with project live-popup convention Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014p87VFec84hNaf2WpvmLrW
This commit is contained in:
@@ -69,24 +69,24 @@ function normalize_pattern(string $pat): string {
|
|||||||
return '/' . $pat . '/i';
|
return '/' . $pat . '/i';
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── daily guard: skip if already ran within last 20 hours ── */
|
/* ── run guard: skip if ran within last 4 hours ── */
|
||||||
/* Set JARVIS_FORCE_RUN=1 (env) or pass --force (argv) to bypass */
|
/* Set JARVIS_FORCE_RUN=1 (env) or pass --force (argv) to bypass */
|
||||||
$lastRun = JarvisDB::single(
|
$lastRun = JarvisDB::single(
|
||||||
"SELECT updated_at FROM kb_facts WHERE category='kb_generator' AND fact_key='last_run'"
|
"SELECT updated_at FROM kb_facts WHERE category='kb_generator' AND fact_key='last_run'"
|
||||||
);
|
);
|
||||||
$forceRun = !empty(getenv('JARVIS_FORCE_RUN')) || (isset($argv[1]) && $argv[1] === '--force');
|
$forceRun = !empty(getenv('JARVIS_FORCE_RUN')) || (isset($argv[1]) && $argv[1] === '--force');
|
||||||
if (!$forceRun && $lastRun && (time() - strtotime($lastRun['updated_at'])) < 14400) {
|
if (!$forceRun && $lastRun && (time() - strtotime($lastRun['updated_at'])) < 14400) {
|
||||||
log_line('Skipping – ran within last 20 hours (next run tomorrow 3am). Use --force to override.');
|
log_line('Skipping – ran within last 4 hours. Use --force to override.');
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
if ($forceRun) log_line('Force-run flag set — bypassing 20-hour guard.');
|
if ($forceRun) log_line('Force-run flag set — bypassing 4-hour guard.');
|
||||||
|
|
||||||
log_line('Starting daily KB intent generation run.');
|
log_line('Starting daily KB intent generation run.');
|
||||||
|
|
||||||
/* ── load active topics from database ── */
|
/* ── load active topics from database ── */
|
||||||
$BATCHES = JarvisDB::query(
|
$BATCHES = JarvisDB::query(
|
||||||
"SELECT topic_id AS id, category, topic_name AS topic, description AS `desc`
|
"SELECT t.topic_id AS id, t.category, t.topic_name AS topic, t.description AS `desc`
|
||||||
FROM kb_generator_topics WHERE active=1 ORDER BY id ASC"
|
FROM kb_generator_topics t WHERE t.active=1 ORDER BY t.id ASC"
|
||||||
);
|
);
|
||||||
if (empty($BATCHES)) {
|
if (empty($BATCHES)) {
|
||||||
log_line('ERROR: No active topics in kb_generator_topics table. Add topics via the JARVIS admin panel.');
|
log_line('ERROR: No active topics in kb_generator_topics table. Add topics via the JARVIS admin panel.');
|
||||||
|
|||||||
+43
-29
@@ -641,20 +641,26 @@ if ($action) {
|
|||||||
$name = trim($_POST['topic_name'] ?? '');
|
$name = trim($_POST['topic_name'] ?? '');
|
||||||
$desc = trim($_POST['description'] ?? '');
|
$desc = trim($_POST['description'] ?? '');
|
||||||
$act = (int)!empty($_POST['active']);
|
$act = (int)!empty($_POST['active']);
|
||||||
if (!$tid||!$cat||!$name||!$desc) bad('All fields required');
|
if (!$tid || !preg_match('/[a-z0-9]/', $tid) || !$cat || !$name || !$desc)
|
||||||
if ($id) {
|
bad('All fields required — topic_id must contain at least one letter or digit');
|
||||||
JarvisDB::execute(
|
try {
|
||||||
'UPDATE kb_generator_topics SET topic_id=?,category=?,topic_name=?,description=?,active=?,updated_at=NOW() WHERE id=?',
|
if ($id) {
|
||||||
[$tid,$cat,$name,$desc,$act,$id]
|
JarvisDB::execute(
|
||||||
);
|
'UPDATE kb_generator_topics SET topic_id=?,category=?,topic_name=?,description=?,active=?,updated_at=NOW() WHERE id=?',
|
||||||
j(['ok'=>true,'msg'=>'Topic updated']);
|
[$tid,$cat,$name,$desc,$act,$id]
|
||||||
} else {
|
);
|
||||||
JarvisDB::execute(
|
} else {
|
||||||
'INSERT INTO kb_generator_topics (topic_id,category,topic_name,description,active) VALUES (?,?,?,?,?)',
|
JarvisDB::execute(
|
||||||
[$tid,$cat,$name,$desc,$act]
|
'INSERT INTO kb_generator_topics (topic_id,category,topic_name,description,active) VALUES (?,?,?,?,?)',
|
||||||
);
|
[$tid,$cat,$name,$desc,$act]
|
||||||
j(['ok'=>true,'msg'=>'Topic created']);
|
);
|
||||||
|
}
|
||||||
|
} catch (\PDOException $e) {
|
||||||
|
bad(strpos($e->getMessage(), 'Duplicate entry') !== false
|
||||||
|
? 'topic_id already in use — choose a different slug'
|
||||||
|
: 'Database error');
|
||||||
}
|
}
|
||||||
|
j(['ok'=>true,'msg'=> $id ? 'Topic updated' : 'Topic created']);
|
||||||
|
|
||||||
case 'kb_topic_delete':
|
case 'kb_topic_delete':
|
||||||
$id = (int)($_POST['id'] ?? 0); if (!$id) bad('Missing id');
|
$id = (int)($_POST['id'] ?? 0); if (!$id) bad('Missing id');
|
||||||
@@ -2557,16 +2563,16 @@ function tmFilter() {
|
|||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>${rows.map(t => `
|
<tbody>${rows.map(t => `
|
||||||
<tr style="border-bottom:1px solid rgba(255,255,255,0.03);font-size:0.65rem">
|
<tr style="border-bottom:1px solid rgba(255,255,255,0.03);font-size:0.65rem">
|
||||||
<td style="padding:4px 6px;color:var(--text-dim);font-size:0.57rem">${tmEsc(t.topic_id)}</td>
|
<td style="padding:4px 6px;color:var(--text-dim);font-size:0.57rem">${esc(t.topic_id)}</td>
|
||||||
<td style="padding:4px 6px"><span style="background:var(--bg2);padding:1px 5px;border-radius:2px;font-size:0.57rem;white-space:nowrap">${tmEsc(t.category)}</span></td>
|
<td style="padding:4px 6px"><span style="background:var(--bg2);padding:1px 5px;border-radius:2px;font-size:0.57rem;white-space:nowrap">${esc(t.category)}</span></td>
|
||||||
<td style="padding:4px 6px;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${tmEsc(t.description)}">${tmEsc(t.topic_name)}</td>
|
<td style="padding:4px 6px;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(t.description)}">${esc(t.topic_name)}</td>
|
||||||
<td style="padding:4px 6px;text-align:center">
|
<td style="padding:4px 6px;text-align:center">
|
||||||
<input type="checkbox" ${t.active==1?'checked':''} onchange="tmToggle(${t.id},this)" style="cursor:pointer;accent-color:var(--green)">
|
<input type="checkbox" ${t.active==1?'checked':''} onchange="tmToggle(${t.id},this)" style="cursor:pointer;accent-color:var(--green)">
|
||||||
</td>
|
</td>
|
||||||
<td style="padding:4px 6px;text-align:center;color:var(--text-dim);font-size:0.6rem">${t.run_count||0}</td>
|
<td style="padding:4px 6px;text-align:center;color:var(--text-dim);font-size:0.6rem">${t.run_count||0}</td>
|
||||||
<td style="padding:4px 6px;text-align:right;white-space:nowrap">
|
<td style="padding:4px 6px;text-align:right;white-space:nowrap">
|
||||||
<button class="btn btn-sm btn-yellow" style="padding:2px 7px;font-size:0.58rem" onclick="tmEditTopic(${t.id})">EDIT</button>
|
<button class="btn btn-sm btn-yellow" style="padding:2px 7px;font-size:0.58rem" onclick="tmEditTopic(${t.id})">EDIT</button>
|
||||||
<button class="btn btn-sm btn-red" style="padding:2px 7px;font-size:0.58rem;margin-left:3px" onclick="tmDelete(${t.id},${JSON.stringify(t.topic_name)})">DEL</button>
|
<button class="btn btn-sm btn-red" style="padding:2px 7px;font-size:0.58rem;margin-left:3px" onclick="tmDelete(${t.id})">DEL</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>`).join('')}
|
</tr>`).join('')}
|
||||||
</tbody></table>`;
|
</tbody></table>`;
|
||||||
@@ -2630,24 +2636,32 @@ async function tmSaveTopic() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function tmToggle(id, el) {
|
async function tmToggle(id, el) {
|
||||||
apiPost('kb_topic_toggle', {id}, (res) => {
|
const prev = !el.checked;
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('action', 'kb_topic_toggle');
|
||||||
|
fd.append('id', id);
|
||||||
|
try {
|
||||||
|
const r = await fetch(location.href, {method:'POST', body:fd});
|
||||||
|
const d = await r.json();
|
||||||
|
if (d.error) { toast(d.error, 'err'); el.checked = prev; return; }
|
||||||
const t = _topicsData.find(x => x.id == id);
|
const t = _topicsData.find(x => x.id == id);
|
||||||
if (t) t.active = res.active ? 1 : 0;
|
if (t) t.active = d.active ? 1 : 0;
|
||||||
tmFilter();
|
tmFilter();
|
||||||
});
|
} catch(e) { toast('Toggle failed', 'err'); el.checked = prev; }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function tmDelete(id, name) {
|
async function tmDelete(id) {
|
||||||
if (!confirm('Delete topic "' + name + '"?\nThis cannot be undone.')) return;
|
const t = _topicsData.find(x => x.id == id);
|
||||||
apiPost('kb_topic_delete', {id}, async () => {
|
const name = t?.topic_name || 'this topic';
|
||||||
toast('Topic deleted', 'ok');
|
openModal('CONFIRM DELETE',
|
||||||
await tmLoadTopics();
|
`<div style="font-family:var(--mono);font-size:0.75rem;line-height:2;padding:4px 0">Delete topic:<br>` +
|
||||||
});
|
`<span style="color:var(--red)">${esc(name)}</span><br>` +
|
||||||
|
`<span style="color:var(--text-dim);font-size:0.65rem">This cannot be undone.</span></div>`,
|
||||||
|
() => { apiPost('kb_topic_delete', {id}, async () => { toast('Topic deleted', 'ok'); await openTopicsManager(); }); },
|
||||||
|
'DELETE');
|
||||||
}
|
}
|
||||||
|
|
||||||
function tmEsc(s) {
|
|
||||||
return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user