ipc+mavweb: add revert/undo endpoint to void latest fact for a key
- New store.VoidLatestFact() method finds latest non-voided fact for a key and writes a void-marker row pointing at it (transactional). - New IPC method MethodRevertFact with CoreAPI.RevertFact interface, storeAPI adapter, server dispatch, and client proxy. - New HTTP endpoint POST /api/revert?key=<key> in mavweb. - History page adds a 'revert' button per non-voided fact row with JS confirmation and optimistic UI (marks row voided on success). - All existing store, IPC, and mavweb tests pass.
This commit is contained in:
+26
-2
@@ -4,7 +4,7 @@
|
||||
<meta name=viewport content="width=device-width,initial-scale=1">
|
||||
<title>maven · history</title>
|
||||
<style>
|
||||
body{font:14px system-ui;margin:1.5rem;max-width:64rem}
|
||||
body{font:14px system-ui;margin:1.5rem;max-width:70rem}
|
||||
h1{font-size:1.3rem;margin-bottom:.3rem}
|
||||
nav a{margin-right:1rem}
|
||||
table{border-collapse:collapse;width:100%;margin-top:.5rem}
|
||||
@@ -16,12 +16,18 @@
|
||||
.val{font-family:monospace;word-break:break-all;max-width:20rem}
|
||||
.src{font-size:.85rem;color:#666}
|
||||
.count{color:#888;font-size:.85rem;margin-top:.3rem}
|
||||
.revert-btn{font-size:.8rem;padding:.15rem .4rem;cursor:pointer}
|
||||
.revert-btn:disabled{opacity:.4;cursor:not-allowed}
|
||||
#msg{margin-top:.5rem;padding:.4rem;border-radius:.3rem;display:none}
|
||||
.msg-ok{background:#e6ffed;display:block!important}
|
||||
.msg-err{background:#ffe6e6;display:block!important}
|
||||
</style>
|
||||
<h1>maven · command history</h1>
|
||||
<nav><a href=/dash>dash</a> <a href=/tools>tools</a> <a href=/auth/passkey>passkey</a></nav>
|
||||
<p class=count>{{len .Facts}} facts shown (newest first)</p>
|
||||
<div id=msg></div>
|
||||
<table>
|
||||
<tr><th>when<th>kind<th>key<th>value<th>source<th>conf</tr>
|
||||
<tr><th>when<th>kind<th>key<th>value<th>source<th>conf<th></tr>
|
||||
{{range .Facts}}<tr{{if .VoidsID}} class=voided{{end}}>
|
||||
<td>{{if .VoidsID}}<span class=void-badge>⨯ voided</span>{{end}}{{.Ts.Format "2006-01-02 15:04"}}</td>
|
||||
<td>{{.Kind}}</td>
|
||||
@@ -29,5 +35,23 @@
|
||||
<td class=val>{{.Value}}</td>
|
||||
<td class=src>{{.Source}}</td>
|
||||
<td>{{printf "%.2f" .Confidence}}</td>
|
||||
<td>{{if not .VoidsID}}<button class=revert-btn onclick="revert('{{.Key}}',this)">revert</button>{{end}}</td>
|
||||
</tr>{{end}}
|
||||
</table>
|
||||
<script>
|
||||
const msg=document.getElementById('msg');
|
||||
function revert(key,btn){
|
||||
btn.disabled=true;
|
||||
fetch('/api/revert',{method:'POST',headers:{'content-type':'application/x-www-form-urlencoded'},body:'key='+encodeURIComponent(key)})
|
||||
.then(r=>r.json().then(d=>({ok:r.ok,data:d})))
|
||||
.then(({ok,data})=>{
|
||||
msg.className=ok?'msg-ok':'msg-err';
|
||||
msg.textContent=ok?'reverted ✓':(data.error||'revert failed');
|
||||
if(ok) btn.closest('tr').className='voided';
|
||||
}).catch(e=>{
|
||||
msg.className='msg-err';
|
||||
msg.textContent='error: '+e;
|
||||
btn.disabled=false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -133,6 +133,9 @@ func main() {
|
||||
mux.HandleFunc("/history", func(w http.ResponseWriter, r *http.Request) {
|
||||
handleHistory(w, r, core)
|
||||
})
|
||||
mux.HandleFunc("/api/revert", func(w http.ResponseWriter, r *http.Request) {
|
||||
handleRevert(w, r, core)
|
||||
})
|
||||
// ----- passkey (WebAuthn) endpoints -----
|
||||
// Wired when both -core and a configured origin are present. The origin
|
||||
// must match the browser's view of mavweb (e.g. https://maven.kvmx.ru).
|
||||
@@ -385,6 +388,36 @@ func handleHistory(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
|
||||
}
|
||||
}
|
||||
|
||||
func handleRevert(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "POST only", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if core == nil {
|
||||
http.Error(w, "revert disabled (no -core)", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
key := strings.TrimSpace(r.FormValue("key"))
|
||||
if key == "" {
|
||||
http.Error(w, "key required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
ctx := r.Context()
|
||||
newID, err := core.RevertFact(ctx, key)
|
||||
if err != nil {
|
||||
log.Printf("revert %q: %v", key, err)
|
||||
if errors.Is(err, ipc.ErrNoFact) {
|
||||
http.Error(w, "no fact to revert", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, "revert failed", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
log.Printf("reverted fact for key=%s, new_id=%d", key, newID)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{"reverted": true, "new_id": newID})
|
||||
}
|
||||
|
||||
// handleTools serves the enable surface (GET) and applies an enable (POST).
|
||||
// POST fields: name, cmd (space-separated argv), destructive (checkbox). cmd is
|
||||
// whitespace-split — argv with embedded spaces isn't supported (ponytail: no
|
||||
|
||||
Reference in New Issue
Block a user