show answered question history on dashboard
This commit is contained in:
parent
411cf86632
commit
6ba4241a45
4 changed files with 67 additions and 3 deletions
|
|
@ -488,11 +488,10 @@
|
||||||
function renderQuestions(s) {
|
function renderQuestions(s) {
|
||||||
const root = $('questions-section');
|
const root = $('questions-section');
|
||||||
root.innerHTML = '';
|
root.innerHTML = '';
|
||||||
|
const fmt = (n) => new Date(n * 1000).toISOString().replace('T', ' ').slice(0, 19);
|
||||||
if (!s.questions || !s.questions.length) {
|
if (!s.questions || !s.questions.length) {
|
||||||
root.append(el('p', { class: 'empty' }, 'no pending questions'));
|
root.append(el('p', { class: 'empty' }, 'no pending questions'));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const fmt = (n) => new Date(n * 1000).toISOString().replace('T', ' ').slice(0, 19);
|
|
||||||
const ul = el('ul', { class: 'questions' });
|
const ul = el('ul', { class: 'questions' });
|
||||||
for (const q of s.questions) {
|
for (const q of s.questions) {
|
||||||
const li = el('li', { class: 'question' });
|
const li = el('li', { class: 'question' });
|
||||||
|
|
@ -576,7 +575,34 @@
|
||||||
li.append(cancelForm);
|
li.append(cancelForm);
|
||||||
ul.append(li);
|
ul.append(li);
|
||||||
}
|
}
|
||||||
root.append(ul);
|
if (s.questions && s.questions.length) root.append(ul);
|
||||||
|
|
||||||
|
// Answered question history
|
||||||
|
const hist = s.question_history || [];
|
||||||
|
if (hist.length) {
|
||||||
|
const details = el('details', { class: 'q-history', 'data-restore-key': 'q-history' });
|
||||||
|
details.append(el('summary', {}, '◆ answ3red (' + hist.length + ')'));
|
||||||
|
const hul = el('ul', { class: 'questions questions-answered' });
|
||||||
|
for (const q of hist) {
|
||||||
|
const li = el('li', { class: 'question question-answered' });
|
||||||
|
const head = el('div', { class: 'q-head' },
|
||||||
|
el('span', { class: 'msg-ts' }, fmt(q.answered_at)), ' ',
|
||||||
|
el('span', { class: 'msg-from' }, q.asker), ' ',
|
||||||
|
el('span', { class: 'msg-sep' }, 'asked:'),
|
||||||
|
);
|
||||||
|
li.append(
|
||||||
|
head,
|
||||||
|
el('div', { class: 'q-body' }, q.question),
|
||||||
|
el('div', { class: 'q-answer' },
|
||||||
|
el('span', { class: 'msg-sep' }, 'answer: '),
|
||||||
|
el('span', { class: 'q-answer-text' }, q.answer || '(none)'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
hul.append(li);
|
||||||
|
}
|
||||||
|
details.append(hul);
|
||||||
|
root.append(details);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderInbox(s) {
|
function renderInbox(s) {
|
||||||
|
|
|
||||||
|
|
@ -517,6 +517,24 @@ summary:hover { color: var(--purple); }
|
||||||
.qform .q-free input:focus { outline: 1px solid var(--amber); }
|
.qform .q-free input:focus { outline: 1px solid var(--amber); }
|
||||||
.qform button { align-self: flex-start; }
|
.qform button { align-self: flex-start; }
|
||||||
.qform-cancel { margin-top: 0.3em; }
|
.qform-cancel { margin-top: 0.3em; }
|
||||||
|
.q-history {
|
||||||
|
margin-top: 0.8em;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.4em 0.7em;
|
||||||
|
}
|
||||||
|
.q-history summary { cursor: pointer; color: var(--muted); font-size: 0.9em; user-select: none; }
|
||||||
|
.questions-answered {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
animation: none;
|
||||||
|
padding: 0;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
.question-answered { opacity: 0.7; }
|
||||||
|
.question-answered .q-body { color: var(--muted); margin-bottom: 0.15em; }
|
||||||
|
.q-answer { font-size: 0.9em; color: var(--green, #a6e3a1); padding: 0.1em 0 0.4em 0; }
|
||||||
|
.q-answer-text { font-style: italic; }
|
||||||
.inbox {
|
.inbox {
|
||||||
background: var(--bg-elev);
|
background: var(--bg-elev);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,8 @@ struct StateSnapshot {
|
||||||
/// we mark the row answered and fire `HelperEvent::OperatorAnswered`
|
/// we mark the row answered and fire `HelperEvent::OperatorAnswered`
|
||||||
/// into the manager's inbox.
|
/// into the manager's inbox.
|
||||||
questions: Vec<crate::operator_questions::OpQuestion>,
|
questions: Vec<crate::operator_questions::OpQuestion>,
|
||||||
|
/// Last 20 answered questions, newest-first.
|
||||||
|
question_history: Vec<crate::operator_questions::OpQuestion>,
|
||||||
/// State dirs (config history + claude creds + /state/ notes) that
|
/// State dirs (config history + claude creds + /state/ notes) that
|
||||||
/// survive after a destroy-without-purge. The operator can re-spawn
|
/// survive after a destroy-without-purge. The operator can re-spawn
|
||||||
/// with the same name to resume, or PURG3 to wipe them.
|
/// with the same name to resume, or PURG3 to wipe them.
|
||||||
|
|
@ -298,6 +300,8 @@ async fn api_state(headers: HeaderMap, State(state): State<AppState>) -> axum::J
|
||||||
.recent_for(hive_sh4re::OPERATOR_RECIPIENT, 50),
|
.recent_for(hive_sh4re::OPERATOR_RECIPIENT, 50),
|
||||||
);
|
);
|
||||||
let questions = log_default("questions.pending", state.coord.questions.pending());
|
let questions = log_default("questions.pending", state.coord.questions.pending());
|
||||||
|
let question_history =
|
||||||
|
log_default("questions.recent_answered", state.coord.questions.recent_answered(20));
|
||||||
|
|
||||||
axum::Json(StateSnapshot {
|
axum::Json(StateSnapshot {
|
||||||
hostname,
|
hostname,
|
||||||
|
|
@ -310,6 +314,7 @@ async fn api_state(headers: HeaderMap, State(state): State<AppState>) -> axum::J
|
||||||
meta_inputs: read_meta_inputs(),
|
meta_inputs: read_meta_inputs(),
|
||||||
operator_inbox,
|
operator_inbox,
|
||||||
questions,
|
questions,
|
||||||
|
question_history,
|
||||||
tombstones,
|
tombstones,
|
||||||
port_conflicts,
|
port_conflicts,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,21 @@ impl OperatorQuestions {
|
||||||
rows.collect::<rusqlite::Result<Vec<_>>>()
|
rows.collect::<rusqlite::Result<Vec<_>>>()
|
||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Last `limit` answered questions, newest-first.
|
||||||
|
pub fn recent_answered(&self, limit: u64) -> Result<Vec<OpQuestion>> {
|
||||||
|
let conn = self.conn.lock().unwrap();
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"SELECT id, asker, question, options_json, multi, asked_at, answered_at, answer, deadline_at
|
||||||
|
FROM operator_questions
|
||||||
|
WHERE answered_at IS NOT NULL
|
||||||
|
ORDER BY answered_at DESC
|
||||||
|
LIMIT ?1",
|
||||||
|
)?;
|
||||||
|
let rows = stmt.query_map(params![limit as i64], row_to_question)?;
|
||||||
|
rows.collect::<rusqlite::Result<Vec<_>>>()
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn row_to_question(row: &rusqlite::Row<'_>) -> rusqlite::Result<OpQuestion> {
|
fn row_to_question(row: &rusqlite::Row<'_>) -> rusqlite::Result<OpQuestion> {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue