* Implement maintenance page with task and log display * Add backfill and metadata retry functionality * Integrate grid component for project display in thoughts page * Update types for maintenance tasks and logs * Enhance sidebar and shell for new maintenance navigation
This commit is contained in:
1 parent
b39cd3ba72
commit
927a118338
48 files changed
+2219
-859
No files matched your search
@@ -20,10 +20,26 @@ LDFLAGS := -s -w \
|
|||||||
-X $(BUILDINFO_PKG).Commit=$(COMMIT_SHA) \
|
-X $(BUILDINFO_PKG).Commit=$(COMMIT_SHA) \
|
||||||
-X $(BUILDINFO_PKG).BuildDate=$(BUILD_DATE)
|
-X $(BUILDINFO_PKG).BuildDate=$(BUILD_DATE)
|
||||||
|
|
||||||
.PHONY: all build clean migrate release-version test generate-migrations generate-models check-schema-drift build-cli ui-install ui-build ui-dev ui-check
|
.PHONY: all build clean migrate release-version test generate-migrations generate-models check-schema-drift build-cli ui-install ui-build ui-dev ui-check help
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Available targets:"
|
||||||
|
@echo " build Build server binary (includes UI build)"
|
||||||
|
@echo " build-cli Build CLI binary"
|
||||||
|
@echo " test Run all tests (includes UI check)"
|
||||||
|
@echo " clean Remove build artifacts"
|
||||||
|
@echo " migrate Run database migrations"
|
||||||
|
@echo " release-version Tag and push a new patch release (PATCH_INCREMENT=N)"
|
||||||
|
@echo " generate-migrations Generate SQL migration from DBML schema files"
|
||||||
|
@echo " generate-models Generate Go models from DBML schema"
|
||||||
|
@echo " check-schema-drift Verify generated migration matches current schema"
|
||||||
|
@echo " ui-install Install UI dependencies"
|
||||||
|
@echo " ui-build Build UI assets"
|
||||||
|
@echo " ui-dev Start UI dev server"
|
||||||
|
@echo " ui-check Run UI type checks"
|
||||||
|
|
||||||
build: ui-build
|
build: ui-build
|
||||||
@mkdir -p $(BIN_DIR)
|
@mkdir -p $(BIN_DIR)
|
||||||
go build -ldflags "$(LDFLAGS)" -o $(SERVER_BIN) $(CMD_SERVER)
|
go build -ldflags "$(LDFLAGS)" -o $(SERVER_BIN) $(CMD_SERVER)
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ The AMCS directory is used to store configuration and code for the Advanced Modu
|
|||||||
| `get_project_context` | Recent + semantic context for a project; uses explicit `project` or the active session project |
|
| `get_project_context` | Recent + semantic context for a project; uses explicit `project` or the active session project |
|
||||||
| `set_active_project` | Set session project scope; requires a stateful MCP session |
|
| `set_active_project` | Set session project scope; requires a stateful MCP session |
|
||||||
| `get_active_project` | Get current session project |
|
| `get_active_project` | Get current session project |
|
||||||
|
| `add_learning` | Create a curated learning record distinct from raw thoughts |
|
||||||
|
| `get_learning` | Retrieve a structured learning by ID |
|
||||||
|
| `list_learnings` | List structured learnings by project/category/area/status/priority/tag/query |
|
||||||
| `summarize_thoughts` | LLM prose summary over a filtered set |
|
| `summarize_thoughts` | LLM prose summary over a filtered set |
|
||||||
| `recall_context` | Semantic + recency context block for injection |
|
| `recall_context` | Semantic + recency context block for injection |
|
||||||
| `link_thoughts` | Create a typed relationship between thoughts |
|
| `link_thoughts` | Create a typed relationship between thoughts |
|
||||||
@@ -66,6 +69,17 @@ The AMCS directory is used to store configuration and code for the Advanced Modu
|
|||||||
| `describe_tools` | List all available MCP tools with names, descriptions, categories, and model-authored usage notes; call this at the start of a session to orient yourself |
|
| `describe_tools` | List all available MCP tools with names, descriptions, categories, and model-authored usage notes; call this at the start of a session to orient yourself |
|
||||||
| `annotate_tool` | Persist your own usage notes for a specific tool; notes are returned by `describe_tools` in future sessions |
|
| `annotate_tool` | Persist your own usage notes for a specific tool; notes are returned by `describe_tools` in future sessions |
|
||||||
|
|
||||||
|
## Learnings
|
||||||
|
|
||||||
|
Learnings are curated, structured memory records for durable insights you want to keep distinct from raw thoughts. Use them for normalized lessons, decisions, and evidence-backed findings that should be easy to retrieve and review over time.
|
||||||
|
|
||||||
|
Compared with `capture_thought`, learnings are more explicit and reviewable: they include a required `summary`, optional `details`, and structured fields like `category`, `area`, `status`, `priority`, `confidence`, and `tags`, plus optional links to a `project`, `related_thought_id`, or `related_skill_id`.
|
||||||
|
|
||||||
|
Use:
|
||||||
|
- `add_learning` to create a curated learning.
|
||||||
|
- `get_learning` to fetch one by ID.
|
||||||
|
- `list_learnings` to filter curated learnings across project and status dimensions.
|
||||||
|
|
||||||
## Self-Documenting Tools
|
## Self-Documenting Tools
|
||||||
|
|
||||||
AMCS includes a built-in tool directory that models can read and annotate.
|
AMCS includes a built-in tool directory that models can read and annotate.
|
||||||
@@ -598,6 +612,26 @@ Run the SQL migrations against a local database with:
|
|||||||
|
|
||||||
The web UI now lives in the top-level `ui/` module and is embedded into the Go binary at build time with `go:embed`.
|
The web UI now lives in the top-level `ui/` module and is embedded into the Go binary at build time with `go:embed`.
|
||||||
|
|
||||||
|
### Admin UI deployment model
|
||||||
|
|
||||||
|
AMCS uses a **lightweight embedded SPA panel** model:
|
||||||
|
|
||||||
|
- the Svelte admin app is compiled to static assets
|
||||||
|
- assets are embedded in the server binary and served from `/`
|
||||||
|
- backend APIs (`/api/status`, `/api/rs/*`, admin action routes, OAuth endpoints) stay on the same origin
|
||||||
|
- auth is enforced server-side for all sensitive API routes
|
||||||
|
|
||||||
|
This keeps deployment simple (single binary/container) while preserving SPA ergonomics for operator workflows.
|
||||||
|
|
||||||
|
### UI stack baseline
|
||||||
|
|
||||||
|
The admin frontend baseline is:
|
||||||
|
|
||||||
|
- Svelte 5 for the app shell and pages
|
||||||
|
- ResolveSpec-backed APIs for data access
|
||||||
|
- `@warkypublic/svelix` for admin UX components (including `GridlerFull` and form controllers)
|
||||||
|
- `@warkypublic/artemis-kit` as the default JavaScript tooling dependency baseline in `ui/package.json`
|
||||||
|
|
||||||
**Use `pnpm` for all UI work in this repo.**
|
**Use `pnpm` for all UI work in this repo.**
|
||||||
|
|
||||||
- `make build` — runs the real UI build first, then compiles the Go server
|
- `make build` — runs the real UI build first, then compiles the Go server
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.warky.dev/wdevs/amcs/internal/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
type adminActions struct {
|
||||||
|
backfill *tools.BackfillTool
|
||||||
|
retry *tools.EnrichmentRetryer
|
||||||
|
logger *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAdminActions(backfill *tools.BackfillTool, retry *tools.EnrichmentRetryer, logger *slog.Logger) *adminActions {
|
||||||
|
return &adminActions{
|
||||||
|
backfill: backfill,
|
||||||
|
retry: retry,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adminActions) backfillHandler() http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
w.Header().Set("Allow", http.MethodPost)
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var in tools.BackfillInput
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||||
|
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, out, err := a.backfill.Handle(r.Context(), nil, in)
|
||||||
|
if err != nil {
|
||||||
|
if a.logger != nil {
|
||||||
|
a.logger.Warn("admin backfill failed", slog.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(out)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *adminActions) retryMetadataHandler() http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
w.Header().Set("Allow", http.MethodPost)
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var in tools.RetryEnrichmentInput
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||||
|
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, out, err := a.retry.Handle(r.Context(), nil, in)
|
||||||
|
if err != nil {
|
||||||
|
if a.logger != nil {
|
||||||
|
a.logger.Warn("admin metadata retry failed", slog.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(out)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -191,6 +191,7 @@ func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *st
|
|||||||
filesTool := tools.NewFilesTool(db, activeProjects)
|
filesTool := tools.NewFilesTool(db, activeProjects)
|
||||||
enrichmentRetryer := tools.NewEnrichmentRetryer(context.Background(), db, bgMetadata, cfg.Capture, cfg.AI.Metadata.Timeout, activeProjects, logger)
|
enrichmentRetryer := tools.NewEnrichmentRetryer(context.Background(), db, bgMetadata, cfg.Capture, cfg.AI.Metadata.Timeout, activeProjects, logger)
|
||||||
backfillTool := tools.NewBackfillTool(db, bgEmbeddings, activeProjects, logger)
|
backfillTool := tools.NewBackfillTool(db, bgEmbeddings, activeProjects, logger)
|
||||||
|
adminActions := newAdminActions(backfillTool, enrichmentRetryer, logger)
|
||||||
|
|
||||||
toolSet := mcpserver.ToolSet{
|
toolSet := mcpserver.ToolSet{
|
||||||
Capture: tools.NewCaptureTool(db, embeddings, cfg.Capture, activeProjects, enrichmentRetryer, backfillTool),
|
Capture: tools.NewCaptureTool(db, embeddings, cfg.Capture, activeProjects, enrichmentRetryer, backfillTool),
|
||||||
@@ -236,6 +237,8 @@ func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *st
|
|||||||
mux.HandleFunc("/api/oauth/register", oauthRegisterHandler(dynClients, logger))
|
mux.HandleFunc("/api/oauth/register", oauthRegisterHandler(dynClients, logger))
|
||||||
mux.HandleFunc("/api/oauth/authorize", oauthAuthorizeHandler(dynClients, authCodes, logger))
|
mux.HandleFunc("/api/oauth/authorize", oauthAuthorizeHandler(dynClients, authCodes, logger))
|
||||||
mux.HandleFunc("/api/oauth/token", oauthTokenHandler(oauthRegistry, tokenStore, authCodes, logger))
|
mux.HandleFunc("/api/oauth/token", oauthTokenHandler(oauthRegistry, tokenStore, authCodes, logger))
|
||||||
|
mux.Handle("/api/admin/actions/backfill", authMiddleware(adminActions.backfillHandler()))
|
||||||
|
mux.Handle("/api/admin/actions/retry-metadata", authMiddleware(adminActions.retryMetadataHandler()))
|
||||||
mux.HandleFunc("/favicon.ico", serveFavicon)
|
mux.HandleFunc("/favicon.ico", serveFavicon)
|
||||||
mux.HandleFunc("/images/project.jpg", serveHomeImage)
|
mux.HandleFunc("/images/project.jpg", serveHomeImage)
|
||||||
mux.HandleFunc("/images/icon.png", serveIcon)
|
mux.HandleFunc("/images/icon.png", serveIcon)
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ func TestResolveSpecGuardAllowsSupportedMutations(t *testing.T) {
|
|||||||
entity string
|
entity string
|
||||||
operation string
|
operation string
|
||||||
}{
|
}{
|
||||||
|
{name: "learnings read", entity: "learnings", operation: "read"},
|
||||||
{name: "projects create", entity: "projects", operation: "create"},
|
{name: "projects create", entity: "projects", operation: "create"},
|
||||||
{name: "thoughts update", entity: "thoughts", operation: "update"},
|
{name: "thoughts update", entity: "thoughts", operation: "update"},
|
||||||
{name: "thoughts delete", entity: "thoughts", operation: "delete"},
|
{name: "thoughts delete", entity: "thoughts", operation: "delete"},
|
||||||
@@ -119,6 +120,13 @@ func TestResolveSpecGuardBlocksUnsupportedMutations(t *testing.T) {
|
|||||||
wantCode: http.StatusForbidden,
|
wantCode: http.StatusForbidden,
|
||||||
wantMessageIn: `operation "delete" is not allowed for public.stored_files`,
|
wantMessageIn: `operation "delete" is not allowed for public.stored_files`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "mutations blocked for learnings",
|
||||||
|
entity: "learnings",
|
||||||
|
operation: "delete",
|
||||||
|
wantCode: http.StatusForbidden,
|
||||||
|
wantMessageIn: `operation "delete" is not allowed for public.learnings`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "unknown operation is rejected",
|
name: "unknown operation is rejected",
|
||||||
entity: "projects",
|
entity: "projects",
|
||||||
@@ -152,3 +160,13 @@ func TestResolveSpecGuardBlocksUnsupportedMutations(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolveSpecModelsIncludeLearnings(t *testing.T) {
|
||||||
|
models := resolveSpecModels()
|
||||||
|
for _, model := range models {
|
||||||
|
if model.schema == "public" && model.entity == "learnings" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatal("resolveSpecModels() missing public.learnings")
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ type statusAPIResponse struct {
|
|||||||
TotalKnown int `json:"total_known"`
|
TotalKnown int `json:"total_known"`
|
||||||
ConnectedWindow string `json:"connected_window"`
|
ConnectedWindow string `json:"connected_window"`
|
||||||
Entries []auth.AccessSnapshot `json:"entries"`
|
Entries []auth.AccessSnapshot `json:"entries"`
|
||||||
|
Metrics auth.AccessMetrics `json:"metrics"`
|
||||||
OAuthEnabled bool `json:"oauth_enabled"`
|
OAuthEnabled bool `json:"oauth_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ func statusSnapshot(info buildinfo.Info, tracker *auth.AccessTracker, oauthEnabl
|
|||||||
TotalKnown: len(entries),
|
TotalKnown: len(entries),
|
||||||
ConnectedWindow: "last 10 minutes",
|
ConnectedWindow: "last 10 minutes",
|
||||||
Entries: entries,
|
Entries: entries,
|
||||||
|
Metrics: tracker.Metrics(20),
|
||||||
OAuthEnabled: oauthEnabled,
|
OAuthEnabled: oauthEnabled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,15 @@ func TestStatusSnapshotShowsTrackedAccess(t *testing.T) {
|
|||||||
if snapshot.Entries[0].KeyID != "client-a" || snapshot.Entries[0].LastPath != "/files" {
|
if snapshot.Entries[0].KeyID != "client-a" || snapshot.Entries[0].LastPath != "/files" {
|
||||||
t.Fatalf("entry = %+v, want keyID client-a and path /files", snapshot.Entries[0])
|
t.Fatalf("entry = %+v, want keyID client-a and path /files", snapshot.Entries[0])
|
||||||
}
|
}
|
||||||
|
if snapshot.Metrics.TotalRequests != 1 {
|
||||||
|
t.Fatalf("Metrics.TotalRequests = %d, want 1", snapshot.Metrics.TotalRequests)
|
||||||
|
}
|
||||||
|
if snapshot.Metrics.UniqueIPs != 1 {
|
||||||
|
t.Fatalf("Metrics.UniqueIPs = %d, want 1", snapshot.Metrics.UniqueIPs)
|
||||||
|
}
|
||||||
|
if snapshot.Metrics.UniqueAgents != 1 {
|
||||||
|
t.Fatalf("Metrics.UniqueAgents = %d, want 1", snapshot.Metrics.UniqueAgents)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStatusAPIHandlerReturnsJSON(t *testing.T) {
|
func TestStatusAPIHandlerReturnsJSON(t *testing.T) {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -18,10 +20,17 @@ type AccessSnapshot struct {
|
|||||||
type AccessTracker struct {
|
type AccessTracker struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
entries map[string]AccessSnapshot
|
entries map[string]AccessSnapshot
|
||||||
|
ipCounts map[string]int
|
||||||
|
agentCounts map[string]int
|
||||||
|
totalRequests int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAccessTracker() *AccessTracker {
|
func NewAccessTracker() *AccessTracker {
|
||||||
return &AccessTracker{entries: make(map[string]AccessSnapshot)}
|
return &AccessTracker{
|
||||||
|
entries: make(map[string]AccessSnapshot),
|
||||||
|
ipCounts: make(map[string]int),
|
||||||
|
agentCounts: make(map[string]int),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *AccessTracker) Record(keyID, path, remoteAddr, userAgent string, now time.Time) {
|
func (t *AccessTracker) Record(keyID, path, remoteAddr, userAgent string, now time.Time) {
|
||||||
@@ -32,14 +41,36 @@ func (t *AccessTracker) Record(keyID, path, remoteAddr, userAgent string, now ti
|
|||||||
t.mu.Lock()
|
t.mu.Lock()
|
||||||
defer t.mu.Unlock()
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
|
normalizedRemoteAddr := normalizeRemoteAddr(remoteAddr)
|
||||||
|
|
||||||
entry := t.entries[keyID]
|
entry := t.entries[keyID]
|
||||||
entry.KeyID = keyID
|
entry.KeyID = keyID
|
||||||
entry.LastPath = path
|
entry.LastPath = path
|
||||||
entry.RemoteAddr = remoteAddr
|
entry.RemoteAddr = normalizedRemoteAddr
|
||||||
entry.UserAgent = userAgent
|
entry.UserAgent = userAgent
|
||||||
entry.LastAccessedAt = now.UTC()
|
entry.LastAccessedAt = now.UTC()
|
||||||
entry.RequestCount++
|
entry.RequestCount++
|
||||||
t.entries[keyID] = entry
|
t.entries[keyID] = entry
|
||||||
|
t.totalRequests++
|
||||||
|
|
||||||
|
if normalizedRemoteAddr != "" {
|
||||||
|
t.ipCounts[normalizedRemoteAddr]++
|
||||||
|
}
|
||||||
|
if userAgent != "" {
|
||||||
|
t.agentCounts[userAgent]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeRemoteAddr(value string) string {
|
||||||
|
trimmed := strings.TrimSpace(value)
|
||||||
|
if trimmed == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
host, _, err := net.SplitHostPort(trimmed)
|
||||||
|
if err == nil {
|
||||||
|
return host
|
||||||
|
}
|
||||||
|
return trimmed
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *AccessTracker) Snapshot() []AccessSnapshot {
|
func (t *AccessTracker) Snapshot() []AccessSnapshot {
|
||||||
@@ -79,3 +110,55 @@ func (t *AccessTracker) ConnectedCount(now time.Time, window time.Duration) int
|
|||||||
}
|
}
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RequestAggregate struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
RequestCount int `json:"request_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccessMetrics struct {
|
||||||
|
TotalRequests int `json:"total_requests"`
|
||||||
|
UniquePrincipals int `json:"unique_principals"`
|
||||||
|
UniqueIPs int `json:"unique_ips"`
|
||||||
|
UniqueAgents int `json:"unique_agents"`
|
||||||
|
TopIPs []RequestAggregate `json:"top_ips"`
|
||||||
|
TopAgents []RequestAggregate `json:"top_agents"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *AccessTracker) Metrics(topN int) AccessMetrics {
|
||||||
|
if t == nil {
|
||||||
|
return AccessMetrics{}
|
||||||
|
}
|
||||||
|
if topN <= 0 {
|
||||||
|
topN = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
t.mu.RLock()
|
||||||
|
defer t.mu.RUnlock()
|
||||||
|
|
||||||
|
return AccessMetrics{
|
||||||
|
TotalRequests: t.totalRequests,
|
||||||
|
UniquePrincipals: len(t.entries),
|
||||||
|
UniqueIPs: len(t.ipCounts),
|
||||||
|
UniqueAgents: len(t.agentCounts),
|
||||||
|
TopIPs: topAggregates(t.ipCounts, topN),
|
||||||
|
TopAgents: topAggregates(t.agentCounts, topN),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func topAggregates(items map[string]int, topN int) []RequestAggregate {
|
||||||
|
out := make([]RequestAggregate, 0, len(items))
|
||||||
|
for key, count := range items {
|
||||||
|
out = append(out, RequestAggregate{Key: key, RequestCount: count})
|
||||||
|
}
|
||||||
|
sort.Slice(out, func(i, j int) bool {
|
||||||
|
if out[i].RequestCount == out[j].RequestCount {
|
||||||
|
return out[i].Key < out[j].Key
|
||||||
|
}
|
||||||
|
return out[i].RequestCount > out[j].RequestCount
|
||||||
|
})
|
||||||
|
if len(out) > topN {
|
||||||
|
out = out[:topN]
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -30,6 +30,9 @@ func TestAccessTrackerRecordAndSnapshot(t *testing.T) {
|
|||||||
if snap[0].UserAgent != "agent-a2" {
|
if snap[0].UserAgent != "agent-a2" {
|
||||||
t.Fatalf("snapshot[0].UserAgent = %q, want agent-a2", snap[0].UserAgent)
|
t.Fatalf("snapshot[0].UserAgent = %q, want agent-a2", snap[0].UserAgent)
|
||||||
}
|
}
|
||||||
|
if snap[0].RemoteAddr != "10.0.0.1" {
|
||||||
|
t.Fatalf("snapshot[0].RemoteAddr = %q, want 10.0.0.1", snap[0].RemoteAddr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAccessTrackerConnectedCount(t *testing.T) {
|
func TestAccessTrackerConnectedCount(t *testing.T) {
|
||||||
@@ -43,3 +46,42 @@ func TestAccessTrackerConnectedCount(t *testing.T) {
|
|||||||
t.Fatalf("ConnectedCount() = %d, want 1", got)
|
t.Fatalf("ConnectedCount() = %d, want 1", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAccessTrackerMetrics(t *testing.T) {
|
||||||
|
tracker := NewAccessTracker()
|
||||||
|
now := time.Date(2026, 4, 4, 12, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
tracker.Record("client-a", "/mcp", "10.0.0.1:1234", "agent-a", now)
|
||||||
|
tracker.Record("client-a", "/mcp", "10.0.0.1:1234", "agent-a", now.Add(1*time.Second))
|
||||||
|
tracker.Record("client-b", "/files", "10.0.0.2:5678", "agent-b", now.Add(2*time.Second))
|
||||||
|
tracker.Record("client-c", "/files", "10.0.0.2:5678", "agent-b", now.Add(3*time.Second))
|
||||||
|
|
||||||
|
metrics := tracker.Metrics(5)
|
||||||
|
if metrics.TotalRequests != 4 {
|
||||||
|
t.Fatalf("TotalRequests = %d, want 4", metrics.TotalRequests)
|
||||||
|
}
|
||||||
|
if metrics.UniquePrincipals != 3 {
|
||||||
|
t.Fatalf("UniquePrincipals = %d, want 3", metrics.UniquePrincipals)
|
||||||
|
}
|
||||||
|
if metrics.UniqueIPs != 2 {
|
||||||
|
t.Fatalf("UniqueIPs = %d, want 2", metrics.UniqueIPs)
|
||||||
|
}
|
||||||
|
if metrics.UniqueAgents != 2 {
|
||||||
|
t.Fatalf("UniqueAgents = %d, want 2", metrics.UniqueAgents)
|
||||||
|
}
|
||||||
|
if len(metrics.TopIPs) != 2 {
|
||||||
|
t.Fatalf("len(TopIPs) = %d, want 2", len(metrics.TopIPs))
|
||||||
|
}
|
||||||
|
if metrics.TopIPs[0].RequestCount != 2 || metrics.TopIPs[1].RequestCount != 2 {
|
||||||
|
t.Fatalf("TopIPs counts = %+v, want both counts to be 2", metrics.TopIPs)
|
||||||
|
}
|
||||||
|
if metrics.TopIPs[0].Key != "10.0.0.1" && metrics.TopIPs[0].Key != "10.0.0.2" {
|
||||||
|
t.Fatalf("TopIPs[0].Key = %q, want normalized IP", metrics.TopIPs[0].Key)
|
||||||
|
}
|
||||||
|
if len(metrics.TopAgents) != 2 {
|
||||||
|
t.Fatalf("len(TopAgents) = %d, want 2", len(metrics.TopAgents))
|
||||||
|
}
|
||||||
|
if metrics.TopAgents[0].RequestCount != 2 || metrics.TopAgents[1].RequestCount != 2 {
|
||||||
|
t.Fatalf("TopAgents counts = %+v, want both counts to be 2", metrics.TopAgents)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -105,6 +105,86 @@ func TestStreamableHTTPReturnsStructuredToolErrors(t *testing.T) {
|
|||||||
t.Fatalf("build_date = %#v, want %q", got["build_date"], "2026-03-31T00:00:00Z")
|
t.Fatalf("build_date = %#v, want %q", got["build_date"], "2026-03-31T00:00:00Z")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("add_learning_requires_summary", func(t *testing.T) {
|
||||||
|
_, err := cs.CallTool(context.Background(), &mcp.CallToolParams{
|
||||||
|
Name: "add_learning",
|
||||||
|
Arguments: map[string]any{},
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("CallTool(add_learning) error = nil, want error")
|
||||||
|
}
|
||||||
|
|
||||||
|
rpcErr, data := requireWireError(t, err)
|
||||||
|
if rpcErr.Code != jsonrpc.CodeInvalidParams {
|
||||||
|
t.Fatalf("add_learning code = %d, want %d", rpcErr.Code, jsonrpc.CodeInvalidParams)
|
||||||
|
}
|
||||||
|
if data.Type != mcperrors.TypeInvalidArguments {
|
||||||
|
t.Fatalf("add_learning data.type = %q, want %q", data.Type, mcperrors.TypeInvalidArguments)
|
||||||
|
}
|
||||||
|
if data.Field != "summary" {
|
||||||
|
t.Fatalf("add_learning data.field = %q, want %q", data.Field, "summary")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("get_learning_requires_id", func(t *testing.T) {
|
||||||
|
_, err := cs.CallTool(context.Background(), &mcp.CallToolParams{
|
||||||
|
Name: "get_learning",
|
||||||
|
Arguments: map[string]any{},
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("CallTool(get_learning) error = nil, want error")
|
||||||
|
}
|
||||||
|
|
||||||
|
rpcErr, data := requireWireError(t, err)
|
||||||
|
if rpcErr.Code != jsonrpc.CodeInvalidParams {
|
||||||
|
t.Fatalf("get_learning code = %d, want %d", rpcErr.Code, jsonrpc.CodeInvalidParams)
|
||||||
|
}
|
||||||
|
if data.Type != mcperrors.TypeInvalidArguments {
|
||||||
|
t.Fatalf("get_learning data.type = %q, want %q", data.Type, mcperrors.TypeInvalidArguments)
|
||||||
|
}
|
||||||
|
if data.Field != "id" {
|
||||||
|
t.Fatalf("get_learning data.field = %q, want %q", data.Field, "id")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("add_learning_unconfigured_returns_structured_error", func(t *testing.T) {
|
||||||
|
_, err := cs.CallTool(context.Background(), &mcp.CallToolParams{
|
||||||
|
Name: "add_learning",
|
||||||
|
Arguments: map[string]any{
|
||||||
|
"summary": "Learning with configured check",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("CallTool(add_learning) error = nil, want error")
|
||||||
|
}
|
||||||
|
|
||||||
|
rpcErr, data := requireWireError(t, err)
|
||||||
|
if rpcErr.Code != jsonrpc.CodeInvalidParams {
|
||||||
|
t.Fatalf("add_learning code = %d, want %d", rpcErr.Code, jsonrpc.CodeInvalidParams)
|
||||||
|
}
|
||||||
|
if data.Type != mcperrors.TypeInvalidInput {
|
||||||
|
t.Fatalf("add_learning data.type = %q, want %q", data.Type, mcperrors.TypeInvalidInput)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("list_learnings_unconfigured_returns_structured_error", func(t *testing.T) {
|
||||||
|
_, err := cs.CallTool(context.Background(), &mcp.CallToolParams{
|
||||||
|
Name: "list_learnings",
|
||||||
|
Arguments: map[string]any{},
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("CallTool(list_learnings) error = nil, want error")
|
||||||
|
}
|
||||||
|
|
||||||
|
rpcErr, data := requireWireError(t, err)
|
||||||
|
if rpcErr.Code != jsonrpc.CodeInvalidParams {
|
||||||
|
t.Fatalf("list_learnings code = %d, want %d", rpcErr.Code, jsonrpc.CodeInvalidParams)
|
||||||
|
}
|
||||||
|
if data.Type != mcperrors.TypeInvalidInput {
|
||||||
|
t.Fatalf("list_learnings data.type = %q, want %q", data.Type, mcperrors.TypeInvalidInput)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func streamableTestToolSet() ToolSet {
|
func streamableTestToolSet() ToolSet {
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ func (t *LearningsTool) Add(ctx context.Context, req *mcp.CallToolRequest, in Ad
|
|||||||
if summary == "" {
|
if summary == "" {
|
||||||
return nil, AddLearningOutput{}, errRequiredField("summary")
|
return nil, AddLearningOutput{}, errRequiredField("summary")
|
||||||
}
|
}
|
||||||
|
if err := t.ensureConfigured(); err != nil {
|
||||||
|
return nil, AddLearningOutput{}, err
|
||||||
|
}
|
||||||
|
|
||||||
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, false)
|
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -113,6 +116,10 @@ func (t *LearningsTool) Add(ctx context.Context, req *mcp.CallToolRequest, in Ad
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *LearningsTool) Get(ctx context.Context, _ *mcp.CallToolRequest, in GetLearningInput) (*mcp.CallToolResult, GetLearningOutput, error) {
|
func (t *LearningsTool) Get(ctx context.Context, _ *mcp.CallToolRequest, in GetLearningInput) (*mcp.CallToolResult, GetLearningOutput, error) {
|
||||||
|
if err := t.ensureConfigured(); err != nil {
|
||||||
|
return nil, GetLearningOutput{}, err
|
||||||
|
}
|
||||||
|
|
||||||
learning, err := t.store.GetLearning(ctx, in.ID)
|
learning, err := t.store.GetLearning(ctx, in.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, GetLearningOutput{}, err
|
return nil, GetLearningOutput{}, err
|
||||||
@@ -121,6 +128,10 @@ func (t *LearningsTool) Get(ctx context.Context, _ *mcp.CallToolRequest, in GetL
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *LearningsTool) List(ctx context.Context, req *mcp.CallToolRequest, in ListLearningsInput) (*mcp.CallToolResult, ListLearningsOutput, error) {
|
func (t *LearningsTool) List(ctx context.Context, req *mcp.CallToolRequest, in ListLearningsInput) (*mcp.CallToolResult, ListLearningsOutput, error) {
|
||||||
|
if err := t.ensureConfigured(); err != nil {
|
||||||
|
return nil, ListLearningsOutput{}, err
|
||||||
|
}
|
||||||
|
|
||||||
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, false)
|
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ListLearningsOutput{}, err
|
return nil, ListLearningsOutput{}, err
|
||||||
@@ -146,6 +157,13 @@ func (t *LearningsTool) List(ctx context.Context, req *mcp.CallToolRequest, in L
|
|||||||
return nil, ListLearningsOutput{Learnings: items}, nil
|
return nil, ListLearningsOutput{Learnings: items}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *LearningsTool) ensureConfigured() error {
|
||||||
|
if t == nil || t.store == nil {
|
||||||
|
return errInvalidInput("learnings tool is not configured")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func defaultString(value string, fallback string) string {
|
func defaultString(value string, fallback string) string {
|
||||||
if value == "" {
|
if value == "" {
|
||||||
return fallback
|
return fallback
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.warky.dev/wdevs/amcs/internal/mcperrors"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLearningsAddRequiresSummary(t *testing.T) {
|
||||||
|
tool := &LearningsTool{}
|
||||||
|
|
||||||
|
_, _, err := tool.Add(context.Background(), nil, AddLearningInput{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Add() error = nil, want error")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, data := requireRPCError(t, err)
|
||||||
|
if data.Field != "summary" {
|
||||||
|
t.Fatalf("Add() error field = %q, want %q", data.Field, "summary")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLearningsMethodsRequireConfiguredStore(t *testing.T) {
|
||||||
|
tool := &LearningsTool{}
|
||||||
|
|
||||||
|
t.Run("add", func(t *testing.T) {
|
||||||
|
_, _, err := tool.Add(context.Background(), nil, AddLearningInput{Summary: "Keep this"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Add() error = nil, want error")
|
||||||
|
}
|
||||||
|
_, data := requireRPCError(t, err)
|
||||||
|
if data.Type != mcperrors.TypeInvalidInput {
|
||||||
|
t.Fatalf("Add() data.type = %q, want %q", data.Type, mcperrors.TypeInvalidInput)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("get", func(t *testing.T) {
|
||||||
|
_, _, err := tool.Get(context.Background(), nil, GetLearningInput{ID: uuid.New()})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Get() error = nil, want error")
|
||||||
|
}
|
||||||
|
_, data := requireRPCError(t, err)
|
||||||
|
if data.Type != mcperrors.TypeInvalidInput {
|
||||||
|
t.Fatalf("Get() data.type = %q, want %q", data.Type, mcperrors.TypeInvalidInput)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("list", func(t *testing.T) {
|
||||||
|
_, _, err := tool.List(context.Background(), nil, ListLearningsInput{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("List() error = nil, want error")
|
||||||
|
}
|
||||||
|
_, data := requireRPCError(t, err)
|
||||||
|
if data.Type != mcperrors.TypeInvalidInput {
|
||||||
|
t.Fatalf("List() data.type = %q, want %q", data.Type, mcperrors.TypeInvalidInput)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeStringSliceTrimsDedupesAndDropsEmpties(t *testing.T) {
|
||||||
|
got := normalizeStringSlice([]string{" alpha ", "beta", "", "beta", "alpha"})
|
||||||
|
if len(got) != 2 {
|
||||||
|
t.Fatalf("normalizeStringSlice() len = %d, want 2", len(got))
|
||||||
|
}
|
||||||
|
if got[0] != "alpha" || got[1] != "beta" {
|
||||||
|
t.Fatalf("normalizeStringSlice() = %#v, want [alpha beta]", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-1
@@ -53,6 +53,7 @@ Do not abandon the project scope or retry without a project. The project simply
|
|||||||
- Use project memory for code decisions, architecture, TODOs, debugging findings, and context specific to the current repo or workstream.
|
- Use project memory for code decisions, architecture, TODOs, debugging findings, and context specific to the current repo or workstream.
|
||||||
- Before substantial work, always retrieve context with `get_project_context` or `recall_context` so prior decisions inform your approach.
|
- Before substantial work, always retrieve context with `get_project_context` or `recall_context` so prior decisions inform your approach.
|
||||||
- Save durable project facts with `capture_thought` after completing meaningful work.
|
- Save durable project facts with `capture_thought` after completing meaningful work.
|
||||||
|
- Use structured learnings for curated, reusable lessons that should remain distinct from raw thought capture.
|
||||||
- Use `save_file` or `upload_file` for project assets the memory should retain, such as screenshots, PDFs, audio notes, and other documents.
|
- Use `save_file` or `upload_file` for project assets the memory should retain, such as screenshots, PDFs, audio notes, and other documents.
|
||||||
- If the goal is to retain the artifact itself, store the file directly instead of first reading, transcribing, or summarizing its contents.
|
- If the goal is to retain the artifact itself, store the file directly instead of first reading, transcribing, or summarizing its contents.
|
||||||
- For binary files or files larger than 10 MB, call `upload_file` with `content_path` (absolute server-side path) first to get an `amcs://files/{id}` URI, then pass that URI to `save_file` as `content_uri` to link it to a thought. This avoids base64 encoding entirely.
|
- For binary files or files larger than 10 MB, call `upload_file` with `content_path` (absolute server-side path) first to get an `amcs://files/{id}` URI, then pass that URI to `save_file` as `content_uri` to link it to a thought. This avoids base64 encoding entirely.
|
||||||
@@ -64,6 +65,14 @@ Do not abandon the project scope or retry without a project. The project simply
|
|||||||
- Stored files and attachment metadata must not be sent to the metadata extraction client.
|
- Stored files and attachment metadata must not be sent to the metadata extraction client.
|
||||||
- Do not attach memory to the wrong project.
|
- Do not attach memory to the wrong project.
|
||||||
|
|
||||||
|
## Structured Learnings
|
||||||
|
|
||||||
|
- Learnings are curated memory records for durable insights, decisions, and evidence-backed findings.
|
||||||
|
- Prefer `capture_thought` for fast/raw notes during work; prefer learnings when the information is stable enough to normalize and track.
|
||||||
|
- Create learnings with `add_learning` (required: `summary`; optional: `details`, `category`, `area`, `status`, `priority`, `confidence`, `action_required`, `tags`, and related links).
|
||||||
|
- Retrieve one learning with `get_learning` and browse/filter with `list_learnings` (project/category/area/status/priority/tag/query).
|
||||||
|
- Keep learnings concise, specific, and non-duplicative; use `tags` and status fields so future retrieval is reliable.
|
||||||
|
|
||||||
## Global Notebook Rules
|
## Global Notebook Rules
|
||||||
|
|
||||||
- Use global memory only for information that is genuinely cross-project or not project-bound.
|
- Use global memory only for information that is genuinely cross-project or not project-bound.
|
||||||
@@ -100,4 +109,4 @@ Notes are returned by `describe_tools` in future sessions. Annotate whenever you
|
|||||||
|
|
||||||
## Short Operational Form
|
## Short Operational Form
|
||||||
|
|
||||||
At the start of every session, call `describe_tools` to read the full tool list and any accumulated usage notes. Use AMCS memory in project scope when the current work matches a known project; if no clear project matches, global notebook memory is allowed for non-project-specific information. At the start of every project session call `list_project_skills` and `list_project_guardrails` and apply what is returned; only create new skills or guardrails if none exist. If your MCP client does not preserve sessions across calls, pass `project` explicitly instead of relying on `set_active_project`. Store durable notes with `capture_thought`. For binary files or files larger than 10 MB, call `upload_file` with `content_path` to stage the file and get an `amcs://files/{id}` URI, then pass that URI to `save_file` as `content_uri` to link it to a thought. For small files, use `save_file` or `upload_file` with `content_base64` directly. Browse stored files with `list_files`, and load them with `load_file` only when their contents are needed. Stored files can also be read as raw binary via MCP resources at `amcs://files/{id}`. Never store project-specific memory globally when a matching project exists, and never store memory in the wrong project. If project matching is ambiguous, ask the user. If a tool returns `project_not_found`, call `create_project` with that name and retry — never drop the project scope. Whenever you discover a non-obvious tool behaviour, gotcha, or workflow pattern, record it with `annotate_tool` so future sessions benefit.
|
At the start of every session, call `describe_tools` to read the full tool list and any accumulated usage notes. Use AMCS memory in project scope when the current work matches a known project; if no clear project matches, global notebook memory is allowed for non-project-specific information. At the start of every project session call `list_project_skills` and `list_project_guardrails` and apply what is returned; only create new skills or guardrails if none exist. If your MCP client does not preserve sessions across calls, pass `project` explicitly instead of relying on `set_active_project`. Store raw/durable notes with `capture_thought`, and store curated durable lessons with `add_learning`. For binary files or files larger than 10 MB, call `upload_file` with `content_path` to stage the file and get an `amcs://files/{id}` URI, then pass that URI to `save_file` as `content_uri` to link it to a thought. For small files, use `save_file` or `upload_file` with `content_base64` directly. Browse stored files with `list_files`, and load them with `load_file` only when their contents are needed. Stored files can also be read as raw binary via MCP resources at `amcs://files/{id}`. Never store project-specific memory globally when a matching project exists, and never store memory in the wrong project. If project matching is ambiguous, ask the user. If a tool returns `project_not_found`, call `create_project` with that name and retry — never drop the project scope. Whenever you discover a non-obvious tool behaviour, gotcha, or workflow pattern, record it with `annotate_tool` so future sessions benefit.
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
create extension if not exists vector;
|
create extension if not exists vector;
|
||||||
create extension if not exists pgcrypto;
|
create extension if not exists pgcrypto;
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||||
@@ -17,6 +17,13 @@ CREATE SEQUENCE IF NOT EXISTS public.identity_projects_id
|
|||||||
START 1
|
START 1
|
||||||
CACHE 1;
|
CACHE 1;
|
||||||
|
|
||||||
|
CREATE SEQUENCE IF NOT EXISTS public.identity_thought_links_id
|
||||||
|
INCREMENT 1
|
||||||
|
MINVALUE 1
|
||||||
|
MAXVALUE 9223372036854775807
|
||||||
|
START 1
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
CREATE SEQUENCE IF NOT EXISTS public.identity_embeddings_id
|
CREATE SEQUENCE IF NOT EXISTS public.identity_embeddings_id
|
||||||
INCREMENT 1
|
INCREMENT 1
|
||||||
MINVALUE 1
|
MINVALUE 1
|
||||||
@@ -38,6 +45,20 @@ CREATE SEQUENCE IF NOT EXISTS public.identity_tool_annotations_id
|
|||||||
START 1
|
START 1
|
||||||
CACHE 1;
|
CACHE 1;
|
||||||
|
|
||||||
|
CREATE SEQUENCE IF NOT EXISTS public.identity_project_skills_id
|
||||||
|
INCREMENT 1
|
||||||
|
MINVALUE 1
|
||||||
|
MAXVALUE 9223372036854775807
|
||||||
|
START 1
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
CREATE SEQUENCE IF NOT EXISTS public.identity_project_guardrails_id
|
||||||
|
INCREMENT 1
|
||||||
|
MINVALUE 1
|
||||||
|
MAXVALUE 9223372036854775807
|
||||||
|
START 1
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
-- Tables for schema: public
|
-- Tables for schema: public
|
||||||
CREATE TABLE IF NOT EXISTS public.family_members (
|
CREATE TABLE IF NOT EXISTS public.family_members (
|
||||||
birth_date date,
|
birth_date date,
|
||||||
@@ -97,6 +118,7 @@ CREATE TABLE IF NOT EXISTS public.projects (
|
|||||||
CREATE TABLE IF NOT EXISTS public.thought_links (
|
CREATE TABLE IF NOT EXISTS public.thought_links (
|
||||||
created_at timestamptz DEFAULT now(),
|
created_at timestamptz DEFAULT now(),
|
||||||
from_id bigint NOT NULL,
|
from_id bigint NOT NULL,
|
||||||
|
id serial NOT NULL,
|
||||||
relation text NOT NULL,
|
relation text NOT NULL,
|
||||||
to_id bigint NOT NULL
|
to_id bigint NOT NULL
|
||||||
);
|
);
|
||||||
@@ -322,6 +344,7 @@ CREATE TABLE IF NOT EXISTS public.agent_guardrails (
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS public.project_skills (
|
CREATE TABLE IF NOT EXISTS public.project_skills (
|
||||||
created_at timestamptz NOT NULL DEFAULT now(),
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
id serial NOT NULL,
|
||||||
project_id uuid NOT NULL,
|
project_id uuid NOT NULL,
|
||||||
skill_id uuid NOT NULL
|
skill_id uuid NOT NULL
|
||||||
);
|
);
|
||||||
@@ -329,6 +352,7 @@ CREATE TABLE IF NOT EXISTS public.project_skills (
|
|||||||
CREATE TABLE IF NOT EXISTS public.project_guardrails (
|
CREATE TABLE IF NOT EXISTS public.project_guardrails (
|
||||||
created_at timestamptz NOT NULL DEFAULT now(),
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
guardrail_id uuid NOT NULL,
|
guardrail_id uuid NOT NULL,
|
||||||
|
id serial NOT NULL,
|
||||||
project_id uuid NOT NULL
|
project_id uuid NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -879,6 +903,19 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'thought_links'
|
||||||
|
AND column_name = 'id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE public.thought_links ADD COLUMN id serial NOT NULL;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
DO $$
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
IF NOT EXISTS (
|
IF NOT EXISTS (
|
||||||
@@ -3102,6 +3139,19 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'project_skills'
|
||||||
|
AND column_name = 'id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE public.project_skills ADD COLUMN id serial NOT NULL;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
DO $$
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
IF NOT EXISTS (
|
IF NOT EXISTS (
|
||||||
@@ -3154,6 +3204,19 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'project_guardrails'
|
||||||
|
AND column_name = 'id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE public.project_guardrails ADD COLUMN id serial NOT NULL;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
DO $$
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
IF NOT EXISTS (
|
IF NOT EXISTS (
|
||||||
@@ -3181,7 +3244,7 @@ BEGIN
|
|||||||
AND constraint_name IN ('family_members_pkey', 'public_family_members_pkey');
|
AND constraint_name IN ('family_members_pkey', 'public_family_members_pkey');
|
||||||
|
|
||||||
IF auto_pk_name IS NOT NULL THEN
|
IF auto_pk_name IS NOT NULL THEN
|
||||||
EXECUTE 'ALTER TABLE public.family_members DROP CONSTRAINT ' || quote_ident(auto_pk_name);
|
EXECUTE 'ALTER TABLE public.family_members DROP CONSTRAINT ' || quote_ident(auto_pk_name) || ' cascade';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Add named primary key if it doesn't exist
|
-- Add named primary key if it doesn't exist
|
||||||
@@ -3209,7 +3272,7 @@ BEGIN
|
|||||||
AND constraint_name IN ('activities_pkey', 'public_activities_pkey');
|
AND constraint_name IN ('activities_pkey', 'public_activities_pkey');
|
||||||
|
|
||||||
IF auto_pk_name IS NOT NULL THEN
|
IF auto_pk_name IS NOT NULL THEN
|
||||||
EXECUTE 'ALTER TABLE public.activities DROP CONSTRAINT ' || quote_ident(auto_pk_name);
|
EXECUTE 'ALTER TABLE public.activities DROP CONSTRAINT ' || quote_ident(auto_pk_name) || ' cascade';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Add named primary key if it doesn't exist
|
-- Add named primary key if it doesn't exist
|
||||||
@@ -3237,7 +3300,7 @@ BEGIN
|
|||||||
AND constraint_name IN ('important_dates_pkey', 'public_important_dates_pkey');
|
AND constraint_name IN ('important_dates_pkey', 'public_important_dates_pkey');
|
||||||
|
|
||||||
IF auto_pk_name IS NOT NULL THEN
|
IF auto_pk_name IS NOT NULL THEN
|
||||||
EXECUTE 'ALTER TABLE public.important_dates DROP CONSTRAINT ' || quote_ident(auto_pk_name);
|
EXECUTE 'ALTER TABLE public.important_dates DROP CONSTRAINT ' || quote_ident(auto_pk_name) || ' cascade';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Add named primary key if it doesn't exist
|
-- Add named primary key if it doesn't exist
|
||||||
@@ -3265,7 +3328,7 @@ BEGIN
|
|||||||
AND constraint_name IN ('thoughts_pkey', 'public_thoughts_pkey');
|
AND constraint_name IN ('thoughts_pkey', 'public_thoughts_pkey');
|
||||||
|
|
||||||
IF auto_pk_name IS NOT NULL THEN
|
IF auto_pk_name IS NOT NULL THEN
|
||||||
EXECUTE 'ALTER TABLE public.thoughts DROP CONSTRAINT ' || quote_ident(auto_pk_name);
|
EXECUTE 'ALTER TABLE public.thoughts DROP CONSTRAINT ' || quote_ident(auto_pk_name) || ' cascade';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Add named primary key if it doesn't exist
|
-- Add named primary key if it doesn't exist
|
||||||
@@ -3293,7 +3356,7 @@ BEGIN
|
|||||||
AND constraint_name IN ('projects_pkey', 'public_projects_pkey');
|
AND constraint_name IN ('projects_pkey', 'public_projects_pkey');
|
||||||
|
|
||||||
IF auto_pk_name IS NOT NULL THEN
|
IF auto_pk_name IS NOT NULL THEN
|
||||||
EXECUTE 'ALTER TABLE public.projects DROP CONSTRAINT ' || quote_ident(auto_pk_name);
|
EXECUTE 'ALTER TABLE public.projects DROP CONSTRAINT ' || quote_ident(auto_pk_name) || ' cascade';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Add named primary key if it doesn't exist
|
-- Add named primary key if it doesn't exist
|
||||||
@@ -3308,6 +3371,34 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
auto_pk_name text;
|
||||||
|
BEGIN
|
||||||
|
-- Drop auto-generated primary key if it exists
|
||||||
|
SELECT constraint_name INTO auto_pk_name
|
||||||
|
FROM information_schema.table_constraints
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'thought_links'
|
||||||
|
AND constraint_type = 'PRIMARY KEY'
|
||||||
|
AND constraint_name IN ('thought_links_pkey', 'public_thought_links_pkey');
|
||||||
|
|
||||||
|
IF auto_pk_name IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER TABLE public.thought_links DROP CONSTRAINT ' || quote_ident(auto_pk_name) || ' cascade';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Add named primary key if it doesn't exist
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.table_constraints
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'thought_links'
|
||||||
|
AND constraint_name = 'pk_public_thought_links'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE public.thought_links ADD CONSTRAINT pk_public_thought_links PRIMARY KEY (id);
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
DO $$
|
DO $$
|
||||||
DECLARE
|
DECLARE
|
||||||
auto_pk_name text;
|
auto_pk_name text;
|
||||||
@@ -3321,7 +3412,7 @@ BEGIN
|
|||||||
AND constraint_name IN ('embeddings_pkey', 'public_embeddings_pkey');
|
AND constraint_name IN ('embeddings_pkey', 'public_embeddings_pkey');
|
||||||
|
|
||||||
IF auto_pk_name IS NOT NULL THEN
|
IF auto_pk_name IS NOT NULL THEN
|
||||||
EXECUTE 'ALTER TABLE public.embeddings DROP CONSTRAINT ' || quote_ident(auto_pk_name);
|
EXECUTE 'ALTER TABLE public.embeddings DROP CONSTRAINT ' || quote_ident(auto_pk_name) || ' cascade';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Add named primary key if it doesn't exist
|
-- Add named primary key if it doesn't exist
|
||||||
@@ -3349,7 +3440,7 @@ BEGIN
|
|||||||
AND constraint_name IN ('professional_contacts_pkey', 'public_professional_contacts_pkey');
|
AND constraint_name IN ('professional_contacts_pkey', 'public_professional_contacts_pkey');
|
||||||
|
|
||||||
IF auto_pk_name IS NOT NULL THEN
|
IF auto_pk_name IS NOT NULL THEN
|
||||||
EXECUTE 'ALTER TABLE public.professional_contacts DROP CONSTRAINT ' || quote_ident(auto_pk_name);
|
EXECUTE 'ALTER TABLE public.professional_contacts DROP CONSTRAINT ' || quote_ident(auto_pk_name) || ' cascade';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Add named primary key if it doesn't exist
|
-- Add named primary key if it doesn't exist
|
||||||
@@ -3377,7 +3468,7 @@ BEGIN
|
|||||||
AND constraint_name IN ('contact_interactions_pkey', 'public_contact_interactions_pkey');
|
AND constraint_name IN ('contact_interactions_pkey', 'public_contact_interactions_pkey');
|
||||||
|
|
||||||
IF auto_pk_name IS NOT NULL THEN
|
IF auto_pk_name IS NOT NULL THEN
|
||||||
EXECUTE 'ALTER TABLE public.contact_interactions DROP CONSTRAINT ' || quote_ident(auto_pk_name);
|
EXECUTE 'ALTER TABLE public.contact_interactions DROP CONSTRAINT ' || quote_ident(auto_pk_name) || ' cascade';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Add named primary key if it doesn't exist
|
-- Add named primary key if it doesn't exist
|
||||||
@@ -3405,7 +3496,7 @@ BEGIN
|
|||||||
AND constraint_name IN ('opportunities_pkey', 'public_opportunities_pkey');
|
AND constraint_name IN ('opportunities_pkey', 'public_opportunities_pkey');
|
||||||
|
|
||||||
IF auto_pk_name IS NOT NULL THEN
|
IF auto_pk_name IS NOT NULL THEN
|
||||||
EXECUTE 'ALTER TABLE public.opportunities DROP CONSTRAINT ' || quote_ident(auto_pk_name);
|
EXECUTE 'ALTER TABLE public.opportunities DROP CONSTRAINT ' || quote_ident(auto_pk_name) || ' cascade';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Add named primary key if it doesn't exist
|
-- Add named primary key if it doesn't exist
|
||||||
@@ -3433,7 +3524,7 @@ BEGIN
|
|||||||
AND constraint_name IN ('stored_files_pkey', 'public_stored_files_pkey');
|
AND constraint_name IN ('stored_files_pkey', 'public_stored_files_pkey');
|
||||||
|
|
||||||
IF auto_pk_name IS NOT NULL THEN
|
IF auto_pk_name IS NOT NULL THEN
|
||||||
EXECUTE 'ALTER TABLE public.stored_files DROP CONSTRAINT ' || quote_ident(auto_pk_name);
|
EXECUTE 'ALTER TABLE public.stored_files DROP CONSTRAINT ' || quote_ident(auto_pk_name) || ' cascade';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Add named primary key if it doesn't exist
|
-- Add named primary key if it doesn't exist
|
||||||
@@ -3461,7 +3552,7 @@ BEGIN
|
|||||||
AND constraint_name IN ('household_items_pkey', 'public_household_items_pkey');
|
AND constraint_name IN ('household_items_pkey', 'public_household_items_pkey');
|
||||||
|
|
||||||
IF auto_pk_name IS NOT NULL THEN
|
IF auto_pk_name IS NOT NULL THEN
|
||||||
EXECUTE 'ALTER TABLE public.household_items DROP CONSTRAINT ' || quote_ident(auto_pk_name);
|
EXECUTE 'ALTER TABLE public.household_items DROP CONSTRAINT ' || quote_ident(auto_pk_name) || ' cascade';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Add named primary key if it doesn't exist
|
-- Add named primary key if it doesn't exist
|
||||||
@@ -3489,7 +3580,7 @@ BEGIN
|
|||||||
AND constraint_name IN ('household_vendors_pkey', 'public_household_vendors_pkey');
|
AND constraint_name IN ('household_vendors_pkey', 'public_household_vendors_pkey');
|
||||||
|
|
||||||
IF auto_pk_name IS NOT NULL THEN
|
IF auto_pk_name IS NOT NULL THEN
|
||||||
EXECUTE 'ALTER TABLE public.household_vendors DROP CONSTRAINT ' || quote_ident(auto_pk_name);
|
EXECUTE 'ALTER TABLE public.household_vendors DROP CONSTRAINT ' || quote_ident(auto_pk_name) || ' cascade';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Add named primary key if it doesn't exist
|
-- Add named primary key if it doesn't exist
|
||||||
@@ -3517,7 +3608,7 @@ BEGIN
|
|||||||
AND constraint_name IN ('maintenance_tasks_pkey', 'public_maintenance_tasks_pkey');
|
AND constraint_name IN ('maintenance_tasks_pkey', 'public_maintenance_tasks_pkey');
|
||||||
|
|
||||||
IF auto_pk_name IS NOT NULL THEN
|
IF auto_pk_name IS NOT NULL THEN
|
||||||
EXECUTE 'ALTER TABLE public.maintenance_tasks DROP CONSTRAINT ' || quote_ident(auto_pk_name);
|
EXECUTE 'ALTER TABLE public.maintenance_tasks DROP CONSTRAINT ' || quote_ident(auto_pk_name) || ' cascade';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Add named primary key if it doesn't exist
|
-- Add named primary key if it doesn't exist
|
||||||
@@ -3545,7 +3636,7 @@ BEGIN
|
|||||||
AND constraint_name IN ('maintenance_logs_pkey', 'public_maintenance_logs_pkey');
|
AND constraint_name IN ('maintenance_logs_pkey', 'public_maintenance_logs_pkey');
|
||||||
|
|
||||||
IF auto_pk_name IS NOT NULL THEN
|
IF auto_pk_name IS NOT NULL THEN
|
||||||
EXECUTE 'ALTER TABLE public.maintenance_logs DROP CONSTRAINT ' || quote_ident(auto_pk_name);
|
EXECUTE 'ALTER TABLE public.maintenance_logs DROP CONSTRAINT ' || quote_ident(auto_pk_name) || ' cascade';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Add named primary key if it doesn't exist
|
-- Add named primary key if it doesn't exist
|
||||||
@@ -3573,7 +3664,7 @@ BEGIN
|
|||||||
AND constraint_name IN ('recipes_pkey', 'public_recipes_pkey');
|
AND constraint_name IN ('recipes_pkey', 'public_recipes_pkey');
|
||||||
|
|
||||||
IF auto_pk_name IS NOT NULL THEN
|
IF auto_pk_name IS NOT NULL THEN
|
||||||
EXECUTE 'ALTER TABLE public.recipes DROP CONSTRAINT ' || quote_ident(auto_pk_name);
|
EXECUTE 'ALTER TABLE public.recipes DROP CONSTRAINT ' || quote_ident(auto_pk_name) || ' cascade';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Add named primary key if it doesn't exist
|
-- Add named primary key if it doesn't exist
|
||||||
@@ -3601,7 +3692,7 @@ BEGIN
|
|||||||
AND constraint_name IN ('meal_plans_pkey', 'public_meal_plans_pkey');
|
AND constraint_name IN ('meal_plans_pkey', 'public_meal_plans_pkey');
|
||||||
|
|
||||||
IF auto_pk_name IS NOT NULL THEN
|
IF auto_pk_name IS NOT NULL THEN
|
||||||
EXECUTE 'ALTER TABLE public.meal_plans DROP CONSTRAINT ' || quote_ident(auto_pk_name);
|
EXECUTE 'ALTER TABLE public.meal_plans DROP CONSTRAINT ' || quote_ident(auto_pk_name) || ' cascade';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Add named primary key if it doesn't exist
|
-- Add named primary key if it doesn't exist
|
||||||
@@ -3629,7 +3720,7 @@ BEGIN
|
|||||||
AND constraint_name IN ('shopping_lists_pkey', 'public_shopping_lists_pkey');
|
AND constraint_name IN ('shopping_lists_pkey', 'public_shopping_lists_pkey');
|
||||||
|
|
||||||
IF auto_pk_name IS NOT NULL THEN
|
IF auto_pk_name IS NOT NULL THEN
|
||||||
EXECUTE 'ALTER TABLE public.shopping_lists DROP CONSTRAINT ' || quote_ident(auto_pk_name);
|
EXECUTE 'ALTER TABLE public.shopping_lists DROP CONSTRAINT ' || quote_ident(auto_pk_name) || ' cascade';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Add named primary key if it doesn't exist
|
-- Add named primary key if it doesn't exist
|
||||||
@@ -3657,7 +3748,7 @@ BEGIN
|
|||||||
AND constraint_name IN ('chat_histories_pkey', 'public_chat_histories_pkey');
|
AND constraint_name IN ('chat_histories_pkey', 'public_chat_histories_pkey');
|
||||||
|
|
||||||
IF auto_pk_name IS NOT NULL THEN
|
IF auto_pk_name IS NOT NULL THEN
|
||||||
EXECUTE 'ALTER TABLE public.chat_histories DROP CONSTRAINT ' || quote_ident(auto_pk_name);
|
EXECUTE 'ALTER TABLE public.chat_histories DROP CONSTRAINT ' || quote_ident(auto_pk_name) || ' cascade';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Add named primary key if it doesn't exist
|
-- Add named primary key if it doesn't exist
|
||||||
@@ -3685,7 +3776,7 @@ BEGIN
|
|||||||
AND constraint_name IN ('tool_annotations_pkey', 'public_tool_annotations_pkey');
|
AND constraint_name IN ('tool_annotations_pkey', 'public_tool_annotations_pkey');
|
||||||
|
|
||||||
IF auto_pk_name IS NOT NULL THEN
|
IF auto_pk_name IS NOT NULL THEN
|
||||||
EXECUTE 'ALTER TABLE public.tool_annotations DROP CONSTRAINT ' || quote_ident(auto_pk_name);
|
EXECUTE 'ALTER TABLE public.tool_annotations DROP CONSTRAINT ' || quote_ident(auto_pk_name) || ' cascade';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Add named primary key if it doesn't exist
|
-- Add named primary key if it doesn't exist
|
||||||
@@ -3713,7 +3804,7 @@ BEGIN
|
|||||||
AND constraint_name IN ('learnings_pkey', 'public_learnings_pkey');
|
AND constraint_name IN ('learnings_pkey', 'public_learnings_pkey');
|
||||||
|
|
||||||
IF auto_pk_name IS NOT NULL THEN
|
IF auto_pk_name IS NOT NULL THEN
|
||||||
EXECUTE 'ALTER TABLE public.learnings DROP CONSTRAINT ' || quote_ident(auto_pk_name);
|
EXECUTE 'ALTER TABLE public.learnings DROP CONSTRAINT ' || quote_ident(auto_pk_name) || ' cascade';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Add named primary key if it doesn't exist
|
-- Add named primary key if it doesn't exist
|
||||||
@@ -3741,7 +3832,7 @@ BEGIN
|
|||||||
AND constraint_name IN ('agent_skills_pkey', 'public_agent_skills_pkey');
|
AND constraint_name IN ('agent_skills_pkey', 'public_agent_skills_pkey');
|
||||||
|
|
||||||
IF auto_pk_name IS NOT NULL THEN
|
IF auto_pk_name IS NOT NULL THEN
|
||||||
EXECUTE 'ALTER TABLE public.agent_skills DROP CONSTRAINT ' || quote_ident(auto_pk_name);
|
EXECUTE 'ALTER TABLE public.agent_skills DROP CONSTRAINT ' || quote_ident(auto_pk_name) || ' cascade';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Add named primary key if it doesn't exist
|
-- Add named primary key if it doesn't exist
|
||||||
@@ -3769,7 +3860,7 @@ BEGIN
|
|||||||
AND constraint_name IN ('agent_guardrails_pkey', 'public_agent_guardrails_pkey');
|
AND constraint_name IN ('agent_guardrails_pkey', 'public_agent_guardrails_pkey');
|
||||||
|
|
||||||
IF auto_pk_name IS NOT NULL THEN
|
IF auto_pk_name IS NOT NULL THEN
|
||||||
EXECUTE 'ALTER TABLE public.agent_guardrails DROP CONSTRAINT ' || quote_ident(auto_pk_name);
|
EXECUTE 'ALTER TABLE public.agent_guardrails DROP CONSTRAINT ' || quote_ident(auto_pk_name) || ' cascade';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Add named primary key if it doesn't exist
|
-- Add named primary key if it doesn't exist
|
||||||
@@ -3784,6 +3875,62 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
auto_pk_name text;
|
||||||
|
BEGIN
|
||||||
|
-- Drop auto-generated primary key if it exists
|
||||||
|
SELECT constraint_name INTO auto_pk_name
|
||||||
|
FROM information_schema.table_constraints
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'project_skills'
|
||||||
|
AND constraint_type = 'PRIMARY KEY'
|
||||||
|
AND constraint_name IN ('project_skills_pkey', 'public_project_skills_pkey');
|
||||||
|
|
||||||
|
IF auto_pk_name IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER TABLE public.project_skills DROP CONSTRAINT ' || quote_ident(auto_pk_name) || ' cascade';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Add named primary key if it doesn't exist
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.table_constraints
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'project_skills'
|
||||||
|
AND constraint_name = 'pk_public_project_skills'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE public.project_skills ADD CONSTRAINT pk_public_project_skills PRIMARY KEY (id);
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
auto_pk_name text;
|
||||||
|
BEGIN
|
||||||
|
-- Drop auto-generated primary key if it exists
|
||||||
|
SELECT constraint_name INTO auto_pk_name
|
||||||
|
FROM information_schema.table_constraints
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'project_guardrails'
|
||||||
|
AND constraint_type = 'PRIMARY KEY'
|
||||||
|
AND constraint_name IN ('project_guardrails_pkey', 'public_project_guardrails_pkey');
|
||||||
|
|
||||||
|
IF auto_pk_name IS NOT NULL THEN
|
||||||
|
EXECUTE 'ALTER TABLE public.project_guardrails DROP CONSTRAINT ' || quote_ident(auto_pk_name) || ' cascade';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Add named primary key if it doesn't exist
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.table_constraints
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'project_guardrails'
|
||||||
|
AND constraint_name = 'pk_public_project_guardrails'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE public.project_guardrails ADD CONSTRAINT pk_public_project_guardrails PRIMARY KEY (id);
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
-- Indexes for schema: public
|
-- Indexes for schema: public
|
||||||
CREATE INDEX IF NOT EXISTS idx_activities_start_date_end_date
|
CREATE INDEX IF NOT EXISTS idx_activities_start_date_end_date
|
||||||
ON public.activities USING btree (start_date, end_date);
|
ON public.activities USING btree (start_date, end_date);
|
||||||
@@ -4327,6 +4474,25 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
DO $$
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
m_cnt bigint;
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_class c
|
||||||
|
INNER JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
|
WHERE c.relname = 'identity_thought_links_id'
|
||||||
|
AND n.nspname = 'public'
|
||||||
|
AND c.relkind = 'S'
|
||||||
|
) THEN
|
||||||
|
SELECT COALESCE(MAX(id), 0) + 1
|
||||||
|
FROM public.thought_links
|
||||||
|
INTO m_cnt;
|
||||||
|
|
||||||
|
PERFORM setval('public.identity_thought_links_id'::regclass, m_cnt);
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
DO $$
|
||||||
DECLARE
|
DECLARE
|
||||||
m_cnt bigint;
|
m_cnt bigint;
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -4383,6 +4549,44 @@ BEGIN
|
|||||||
END IF;
|
END IF;
|
||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
m_cnt bigint;
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_class c
|
||||||
|
INNER JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
|
WHERE c.relname = 'identity_project_skills_id'
|
||||||
|
AND n.nspname = 'public'
|
||||||
|
AND c.relkind = 'S'
|
||||||
|
) THEN
|
||||||
|
SELECT COALESCE(MAX(id), 0) + 1
|
||||||
|
FROM public.project_skills
|
||||||
|
INTO m_cnt;
|
||||||
|
|
||||||
|
PERFORM setval('public.identity_project_skills_id'::regclass, m_cnt);
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
m_cnt bigint;
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_class c
|
||||||
|
INNER JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
|
WHERE c.relname = 'identity_project_guardrails_id'
|
||||||
|
AND n.nspname = 'public'
|
||||||
|
AND c.relkind = 'S'
|
||||||
|
) THEN
|
||||||
|
SELECT COALESCE(MAX(id), 0) + 1
|
||||||
|
FROM public.project_guardrails
|
||||||
|
INTO m_cnt;
|
||||||
|
|
||||||
|
PERFORM setval('public.identity_project_guardrails_id'::regclass, m_cnt);
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
-- Comments for schema: public
|
-- Comments for schema: public
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -40,3 +40,16 @@ GRANT ALL ON TABLE public.project_guardrails TO amcs;
|
|||||||
GRANT ALL ON TABLE public.chat_histories TO amcs;
|
GRANT ALL ON TABLE public.chat_histories TO amcs;
|
||||||
|
|
||||||
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO amcs;
|
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO amcs;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
r RECORD;
|
||||||
|
BEGIN
|
||||||
|
FOR r IN
|
||||||
|
SELECT tablename
|
||||||
|
FROM pg_tables
|
||||||
|
WHERE schemaname = 'public' -- Change 'public' to your schema name
|
||||||
|
LOOP
|
||||||
|
EXECUTE format('ALTER TABLE public.%I OWNER TO amcs', r.tablename);
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+10
-10
@@ -11,22 +11,22 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
||||||
"@tailwindcss/vite": "^4.1.4",
|
"@tailwindcss/vite": "^4.2.4",
|
||||||
"@types/node": "^24.5.2",
|
"@types/node": "^25.6.0",
|
||||||
"svelte": "^5.28.2",
|
"svelte": "^5.55.5",
|
||||||
"svelte-check": "^4.1.6",
|
"svelte-check": "^4.4.6",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.2.4",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^6.0.3",
|
||||||
"vite": "^6.3.2"
|
"vite": "^8.0.10"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/svelte": "^10.49.0",
|
"@sentry/svelte": "^10.50.0",
|
||||||
"@skeletonlabs/skeleton": "^4.15.2",
|
"@skeletonlabs/skeleton": "^4.15.2",
|
||||||
"@skeletonlabs/skeleton-svelte": "^4.15.2",
|
"@skeletonlabs/skeleton-svelte": "^4.15.2",
|
||||||
"@tanstack/svelte-virtual": "^3.13.24",
|
"@tanstack/svelte-virtual": "^3.13.24",
|
||||||
"@warkypublic/artemis-kit": "file:../../artemis-kit",
|
"@warkypublic/artemis-kit": "file:../../artemis-kit",
|
||||||
"@warkypublic/resolvespec-js": "^1.0.1",
|
"@warkypublic/resolvespec-js": "^1.0.1",
|
||||||
"@warkypublic/svelix": "^0.1.31"
|
"@warkypublic/svelix": "^0.1.39"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Generated
+361
-673
File diff suppressed because it is too large.
Load diff
+21
-1
@@ -111,7 +111,27 @@
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Status request failed with ${response.status}`);
|
throw new Error(`Status request failed with ${response.status}`);
|
||||||
}
|
}
|
||||||
data = (await response.json()) as StatusResponse;
|
const raw = (await response.json()) as Partial<StatusResponse> | null;
|
||||||
|
data = {
|
||||||
|
title: raw?.title ?? 'AMCS',
|
||||||
|
description: raw?.description ?? '',
|
||||||
|
version: raw?.version ?? 'unknown',
|
||||||
|
build_date: raw?.build_date ?? 'unknown',
|
||||||
|
commit: raw?.commit ?? 'unknown',
|
||||||
|
connected_count: raw?.connected_count ?? 0,
|
||||||
|
total_known: raw?.total_known ?? 0,
|
||||||
|
connected_window: raw?.connected_window ?? 'last 10 minutes',
|
||||||
|
oauth_enabled: !!raw?.oauth_enabled,
|
||||||
|
entries: Array.isArray(raw?.entries) ? raw.entries : [],
|
||||||
|
metrics: {
|
||||||
|
total_requests: raw?.metrics?.total_requests ?? 0,
|
||||||
|
unique_principals: raw?.metrics?.unique_principals ?? 0,
|
||||||
|
unique_ips: raw?.metrics?.unique_ips ?? 0,
|
||||||
|
unique_agents: raw?.metrics?.unique_agents ?? 0,
|
||||||
|
top_ips: Array.isArray(raw?.metrics?.top_ips) ? raw.metrics.top_ips : [],
|
||||||
|
top_agents: Array.isArray(raw?.metrics?.top_agents) ? raw.metrics.top_agents : []
|
||||||
|
}
|
||||||
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err instanceof Error ? err.message : 'Failed to load status';
|
error = err instanceof Error ? err.message : 'Failed to load status';
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
+114
-10
@@ -94,20 +94,21 @@ async function rsCall<T>(
|
|||||||
|
|
||||||
function rsReadMany<T>(
|
function rsReadMany<T>(
|
||||||
entity: string,
|
entity: string,
|
||||||
options?: { filters?: ResolveSpecFilter[]; limit?: number; sort?: { column: string; direction: 'asc' | 'desc' }[] }
|
options?: { filters?: ResolveSpecFilter[]; limit?: number; offset?: number; sort?: { column: string; direction: 'asc' | 'desc' }[] }
|
||||||
): Promise<T[]> {
|
): Promise<T[]> {
|
||||||
return rsCall<T[]>(`/api/rs/public/${entity}`, 'read', {
|
return rsCall<T[] | null>(`/api/rs/public/${entity}`, 'read', {
|
||||||
options: {
|
options: {
|
||||||
...(options?.filters?.length ? { filters: options.filters } : {}),
|
...(options?.filters?.length ? { filters: options.filters } : {}),
|
||||||
...(options?.sort?.length ? { sort: options.sort } : {}),
|
...(options?.sort?.length ? { sort: options.sort } : {}),
|
||||||
...(options?.limit ? { limit: options.limit } : {})
|
...(options?.limit ? { limit: options.limit } : {}),
|
||||||
|
...(options?.offset ? { offset: options.offset } : {})
|
||||||
}
|
}
|
||||||
});
|
}).then((rows) => (Array.isArray(rows) ? rows : []));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const api = {
|
export const api = {
|
||||||
projects: {
|
projects: {
|
||||||
list: async () => {
|
list: async (params?: { limit?: number; offset?: number; q?: string }) => {
|
||||||
type ProjectRow = {
|
type ProjectRow = {
|
||||||
guid: string;
|
guid: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -117,7 +118,12 @@ export const api = {
|
|||||||
thought_count?: number;
|
thought_count?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const projects = await rsCall<ProjectRow[]>('/api/rs/public/projects', 'read', {
|
const filters: ResolveSpecFilter[] = [];
|
||||||
|
if (params?.q) {
|
||||||
|
filters.push({ column: 'name', operator: 'ilike', value: `%${params.q}%` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectRows = await rsCall<ProjectRow[] | null>('/api/rs/public/projects', 'read', {
|
||||||
options: {
|
options: {
|
||||||
columns: ['guid', 'name', 'description', 'created_at', 'last_active_at'],
|
columns: ['guid', 'name', 'description', 'created_at', 'last_active_at'],
|
||||||
computedColumns: [
|
computedColumns: [
|
||||||
@@ -126,11 +132,14 @@ export const api = {
|
|||||||
expression: 'COALESCE((SELECT COUNT(*) FROM public.thoughts t WHERE t.project_id = projects.guid), 0)'
|
expression: 'COALESCE((SELECT COUNT(*) FROM public.thoughts t WHERE t.project_id = projects.guid), 0)'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
...(filters.length ? { filters } : {}),
|
||||||
sort: [{ column: 'created_at', direction: 'desc' }],
|
sort: [{ column: 'created_at', direction: 'desc' }],
|
||||||
limit: 500
|
limit: params?.limit ?? 100,
|
||||||
|
offset: params?.offset ?? 0
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const projects = Array.isArray(projectRows) ? projectRows : [];
|
||||||
return projects.map((project) => ({
|
return projects.map((project) => ({
|
||||||
id: project.guid,
|
id: project.guid,
|
||||||
name: project.name,
|
name: project.name,
|
||||||
@@ -146,7 +155,7 @@ export const api = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
thoughts: {
|
thoughts: {
|
||||||
list: (params: { q?: string; project_id?: string; limit?: number; include_archived?: boolean }) => {
|
list: (params: { q?: string; project_id?: string; limit?: number; offset?: number; include_archived?: boolean }) => {
|
||||||
const filters: ResolveSpecFilter[] = [];
|
const filters: ResolveSpecFilter[] = [];
|
||||||
if (params.q) {
|
if (params.q) {
|
||||||
filters.push({ column: 'content', operator: 'ilike', value: `%${params.q}%` });
|
filters.push({ column: 'content', operator: 'ilike', value: `%${params.q}%` });
|
||||||
@@ -160,10 +169,65 @@ export const api = {
|
|||||||
return rsReadMany<import('./types').Thought>('thoughts', {
|
return rsReadMany<import('./types').Thought>('thoughts', {
|
||||||
filters,
|
filters,
|
||||||
limit: params.limit ?? 100,
|
limit: params.limit ?? 100,
|
||||||
|
...(params.offset !== undefined ? { offset: params.offset } : {}),
|
||||||
sort: [{ column: 'created_at', direction: 'desc' }]
|
sort: [{ column: 'created_at', direction: 'desc' }]
|
||||||
});
|
}).then((rows) =>
|
||||||
|
rows.map((row) => ({
|
||||||
|
...row,
|
||||||
|
content: typeof row.content === 'string' ? row.content : '',
|
||||||
|
metadata: {
|
||||||
|
...(row.metadata ?? {}),
|
||||||
|
people: row.metadata?.people ?? [],
|
||||||
|
action_items: row.metadata?.action_items ?? [],
|
||||||
|
dates_mentioned: row.metadata?.dates_mentioned ?? [],
|
||||||
|
topics: row.metadata?.topics ?? [],
|
||||||
|
type: row.metadata?.type ?? '',
|
||||||
|
source: row.metadata?.source ?? '',
|
||||||
|
metadata_status: row.metadata?.metadata_status ?? ''
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
);
|
||||||
},
|
},
|
||||||
get: (id: string) => rsCall<import('./types').Thought>(`/api/rs/public/thoughts/${id}`, 'read'),
|
links: async (thoughtID: string) => {
|
||||||
|
const numericID = Number.parseInt(thoughtID, 10);
|
||||||
|
if (Number.isNaN(numericID)) return [];
|
||||||
|
|
||||||
|
const [outbound, inbound] = await Promise.all([
|
||||||
|
rsReadMany<import('./types').ThoughtLink>('thought_links', {
|
||||||
|
filters: [{ column: 'from_id', operator: 'eq', value: numericID }],
|
||||||
|
limit: 200,
|
||||||
|
sort: [{ column: 'created_at', direction: 'desc' }]
|
||||||
|
}),
|
||||||
|
rsReadMany<import('./types').ThoughtLink>('thought_links', {
|
||||||
|
filters: [{ column: 'to_id', operator: 'eq', value: numericID }],
|
||||||
|
limit: 200,
|
||||||
|
sort: [{ column: 'created_at', direction: 'desc' }]
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
const byID = new Map<number, import('./types').ThoughtLink>();
|
||||||
|
for (const link of outbound) byID.set(link.id, link);
|
||||||
|
for (const link of inbound) byID.set(link.id, link);
|
||||||
|
|
||||||
|
return Array.from(byID.values()).sort(
|
||||||
|
(a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
get: (id: string) =>
|
||||||
|
rsCall<import('./types').Thought>(`/api/rs/public/thoughts/${id}`, 'read').then((row) => ({
|
||||||
|
...row,
|
||||||
|
content: typeof row.content === 'string' ? row.content : '',
|
||||||
|
metadata: {
|
||||||
|
...(row.metadata ?? {}),
|
||||||
|
people: row.metadata?.people ?? [],
|
||||||
|
action_items: row.metadata?.action_items ?? [],
|
||||||
|
dates_mentioned: row.metadata?.dates_mentioned ?? [],
|
||||||
|
topics: row.metadata?.topics ?? [],
|
||||||
|
type: row.metadata?.type ?? '',
|
||||||
|
source: row.metadata?.source ?? '',
|
||||||
|
metadata_status: row.metadata?.metadata_status ?? ''
|
||||||
|
}
|
||||||
|
})),
|
||||||
delete: (id: string) => rsCall<void>(`/api/rs/public/thoughts/${id}`, 'delete'),
|
delete: (id: string) => rsCall<void>(`/api/rs/public/thoughts/${id}`, 'delete'),
|
||||||
archive: (id: string) =>
|
archive: (id: string) =>
|
||||||
rsCall<void>(`/api/rs/public/thoughts/${id}`, 'update', {
|
rsCall<void>(`/api/rs/public/thoughts/${id}`, 'update', {
|
||||||
@@ -209,6 +273,46 @@ export const api = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
maintenance: {
|
||||||
|
tasks: () =>
|
||||||
|
rsReadMany<import('./types').MaintenanceTask>('maintenance_tasks', {
|
||||||
|
limit: 200,
|
||||||
|
sort: [{ column: 'next_due', direction: 'asc' }]
|
||||||
|
}),
|
||||||
|
logs: () =>
|
||||||
|
rsReadMany<import('./types').MaintenanceLog>('maintenance_logs', {
|
||||||
|
limit: 200,
|
||||||
|
sort: [{ column: 'completed_at', direction: 'desc' }]
|
||||||
|
}),
|
||||||
|
runBackfill: (input?: {
|
||||||
|
project?: string;
|
||||||
|
limit?: number;
|
||||||
|
include_archived?: boolean;
|
||||||
|
older_than_days?: number;
|
||||||
|
dry_run?: boolean;
|
||||||
|
}) =>
|
||||||
|
post<import('./types').BackfillResult>('/api/admin/actions/backfill', {
|
||||||
|
limit: input?.limit ?? 100,
|
||||||
|
project: input?.project,
|
||||||
|
include_archived: input?.include_archived ?? false,
|
||||||
|
older_than_days: input?.older_than_days ?? 0,
|
||||||
|
dry_run: input?.dry_run ?? false
|
||||||
|
}),
|
||||||
|
runRetryMetadata: (input?: {
|
||||||
|
project?: string;
|
||||||
|
limit?: number;
|
||||||
|
include_archived?: boolean;
|
||||||
|
older_than_days?: number;
|
||||||
|
dry_run?: boolean;
|
||||||
|
}) =>
|
||||||
|
post<import('./types').MetadataRetryResult>('/api/admin/actions/retry-metadata', {
|
||||||
|
limit: input?.limit ?? 100,
|
||||||
|
project: input?.project,
|
||||||
|
include_archived: input?.include_archived ?? false,
|
||||||
|
older_than_days: input?.older_than_days ?? 1,
|
||||||
|
dry_run: input?.dry_run ?? false
|
||||||
|
})
|
||||||
|
},
|
||||||
stats: async () => {
|
stats: async () => {
|
||||||
type StatsThoughtRow = {
|
type StatsThoughtRow = {
|
||||||
metadata?: {
|
metadata?: {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
<thead class="bg-white/5 text-xs uppercase tracking-[0.2em] text-slate-500">
|
<thead class="bg-white/5 text-xs uppercase tracking-[0.2em] text-slate-500">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-4 py-3 font-medium">Principal</th>
|
<th class="px-4 py-3 font-medium">Principal</th>
|
||||||
|
<th class="px-4 py-3 font-medium">IP</th>
|
||||||
<th class="px-4 py-3 font-medium">Last accessed</th>
|
<th class="px-4 py-3 font-medium">Last accessed</th>
|
||||||
<th class="px-4 py-3 font-medium">Last path</th>
|
<th class="px-4 py-3 font-medium">Last path</th>
|
||||||
<th class="px-4 py-3 font-medium">Agent</th>
|
<th class="px-4 py-3 font-medium">Agent</th>
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
{#each entries as entry}
|
{#each entries as entry}
|
||||||
<tr class="hover:bg-white/[0.03]">
|
<tr class="hover:bg-white/[0.03]">
|
||||||
<td class="px-4 py-3 align-top"><code class="rounded bg-white/5 px-2 py-1 font-mono text-xs text-cyan-100">{entry.key_id}</code></td>
|
<td class="px-4 py-3 align-top"><code class="rounded bg-white/5 px-2 py-1 font-mono text-xs text-cyan-100">{entry.key_id}</code></td>
|
||||||
|
<td class="px-4 py-3 align-top"><code class="text-xs text-slate-200">{entry.remote_addr || '—'}</code></td>
|
||||||
<td class="px-4 py-3 align-top text-slate-200">{formatDate(entry.last_accessed_at)}</td>
|
<td class="px-4 py-3 align-top text-slate-200">{formatDate(entry.last_accessed_at)}</td>
|
||||||
<td class="px-4 py-3 align-top"><code class="text-slate-100">{entry.last_path}</code></td>
|
<td class="px-4 py-3 align-top"><code class="text-slate-100">{entry.last_path}</code></td>
|
||||||
<td class="max-w-[16rem] truncate px-4 py-3 align-top text-xs text-slate-400">{entry.user_agent ?? '—'}</td>
|
<td class="max-w-[16rem] truncate px-4 py-3 align-top text-xs text-slate-400">{entry.user_agent ?? '—'}</td>
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { RequestAggregate } from '../../types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
entries,
|
||||||
|
emptyLabel
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
entries: RequestAggregate[];
|
||||||
|
emptyLabel: string;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="rounded-3xl border border-white/10 bg-slate-900/80 p-6 shadow-xl shadow-slate-950/20 sm:p-8">
|
||||||
|
<h3 class="text-xl font-semibold text-white">{title}</h3>
|
||||||
|
{#if entries.length === 0}
|
||||||
|
<div class="mt-6 rounded-2xl border border-dashed border-white/10 bg-slate-950/40 px-4 py-10 text-center text-slate-500">
|
||||||
|
{emptyLabel}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="mt-6 overflow-hidden rounded-2xl border border-white/10">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-white/10 text-left text-sm text-slate-300">
|
||||||
|
<thead class="bg-white/5 text-xs uppercase tracking-[0.2em] text-slate-500">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 font-medium">Value</th>
|
||||||
|
<th class="px-4 py-3 text-right font-medium">Requests</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-white/5 bg-slate-950/30">
|
||||||
|
{#each entries as entry}
|
||||||
|
<tr class="hover:bg-white/[0.03]">
|
||||||
|
<td class="max-w-[40rem] truncate px-4 py-3 align-top"><code class="text-xs text-slate-200">{entry.key}</code></td>
|
||||||
|
<td class="px-4 py-3 align-top text-right font-semibold text-white">{entry.request_count}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { StatusResponse } from '../../types';
|
import type { StatusResponse } from '../../types';
|
||||||
import AccessTable from './AccessTable.svelte';
|
import AccessTable from './AccessTable.svelte';
|
||||||
|
import ConnectionBreakdown from './ConnectionBreakdown.svelte';
|
||||||
import StatusCards from './StatusCards.svelte';
|
import StatusCards from './StatusCards.svelte';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -47,3 +48,18 @@
|
|||||||
{#if data && data.entries.length > 0}
|
{#if data && data.entries.length > 0}
|
||||||
<AccessTable entries={data.entries} />
|
<AccessTable entries={data.entries} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if data}
|
||||||
|
<div class="mt-6 grid gap-6 xl:grid-cols-2">
|
||||||
|
<ConnectionBreakdown
|
||||||
|
title="Requests By IP Address"
|
||||||
|
entries={data.metrics.top_ips}
|
||||||
|
emptyLabel="No connection requests recorded yet."
|
||||||
|
/>
|
||||||
|
<ConnectionBreakdown
|
||||||
|
title="Requests By User Agent"
|
||||||
|
entries={data.metrics.top_agents}
|
||||||
|
emptyLabel="No user agents recorded yet."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
const { data }: { data: StatusResponse } = $props();
|
const { data }: { data: StatusResponse } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mt-6 grid gap-4 sm:grid-cols-3">
|
<div class="mt-6 grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
<div class="rounded-2xl border border-white/10 bg-white/5 p-5">
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-5">
|
||||||
<p class="text-sm uppercase tracking-[0.2em] text-slate-400">Connected users</p>
|
<p class="text-sm uppercase tracking-[0.2em] text-slate-400">Connected users</p>
|
||||||
<p class="mt-2 text-3xl font-semibold text-white">{data.connected_count}</p>
|
<p class="mt-2 text-3xl font-semibold text-white">{data.connected_count}</p>
|
||||||
@@ -13,8 +13,26 @@
|
|||||||
<p class="text-sm uppercase tracking-[0.2em] text-slate-400">Known principals</p>
|
<p class="text-sm uppercase tracking-[0.2em] text-slate-400">Known principals</p>
|
||||||
<p class="mt-2 text-3xl font-semibold text-white">{data.total_known}</p>
|
<p class="mt-2 text-3xl font-semibold text-white">{data.total_known}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-5">
|
||||||
|
<p class="text-sm uppercase tracking-[0.2em] text-slate-400">Total requests</p>
|
||||||
|
<p class="mt-2 text-3xl font-semibold text-white">{data.metrics.total_requests}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-5">
|
||||||
|
<p class="text-sm uppercase tracking-[0.2em] text-slate-400">Unique IPs</p>
|
||||||
|
<p class="mt-2 text-3xl font-semibold text-white">{data.metrics.unique_ips}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-5">
|
||||||
|
<p class="text-sm uppercase tracking-[0.2em] text-slate-400">Unique agents</p>
|
||||||
|
<p class="mt-2 text-3xl font-semibold text-white">{data.metrics.unique_agents}</p>
|
||||||
|
</div>
|
||||||
<div class="rounded-2xl border border-white/10 bg-white/5 p-5">
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-5">
|
||||||
<p class="text-sm uppercase tracking-[0.2em] text-slate-400">Version</p>
|
<p class="text-sm uppercase tracking-[0.2em] text-slate-400">Version</p>
|
||||||
<p class="mt-2 break-all text-2xl font-semibold text-white">{data.version}</p>
|
<p class="mt-2 break-all text-2xl font-semibold text-white">{data.version}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 rounded-2xl border border-white/10 bg-slate-950/40 p-4 text-xs text-slate-400">
|
||||||
|
<p><span class="text-slate-200">Build date:</span> {data.build_date}</p>
|
||||||
|
<p class="mt-1"><span class="text-slate-200">Commit:</span> {data.commit}</p>
|
||||||
|
<p class="mt-1"><span class="text-slate-200">OAuth enabled:</span> {data.oauth_enabled ? 'yes' : 'no'}</p>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { GridlerFull, type GridlerColumn } from "@warkypublic/svelix";
|
||||||
|
import { GlobalStateStore } from "../../shellState";
|
||||||
|
import { adminGridTheme } from "../../gridTheme";
|
||||||
|
import type { Learning } from "../../types";
|
||||||
|
|
||||||
|
let selectedLearning = $state<Learning | null>(null);
|
||||||
|
let gridTotal = $state<number | null>(null);
|
||||||
|
|
||||||
|
const learningsDataSourceOptions = {
|
||||||
|
url: "/api/rs",
|
||||||
|
authToken: GlobalStateStore.getState().session.authToken,
|
||||||
|
schema: "public",
|
||||||
|
entity: "learnings",
|
||||||
|
uniqueID: "id",
|
||||||
|
sort: [{ column: "created_at", direction: "desc" }],
|
||||||
|
} as unknown as {
|
||||||
|
url: string;
|
||||||
|
authToken?: string;
|
||||||
|
schema: string;
|
||||||
|
entity: string;
|
||||||
|
uniqueID: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: GridlerColumn[] = [
|
||||||
|
{ id: "summary", title: "Summary", dataKey: "summary", width: 420 },
|
||||||
|
{ id: "category", title: "Category", dataKey: "category", width: 140 },
|
||||||
|
{ id: "area", title: "Area", dataKey: "area", width: 140 },
|
||||||
|
{ id: "status", title: "Status", dataKey: "status", width: 130 },
|
||||||
|
{ id: "priority", title: "Priority", dataKey: "priority", width: 120 },
|
||||||
|
{
|
||||||
|
id: "action_required",
|
||||||
|
title: "Action",
|
||||||
|
dataKey: "action_required",
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "created_at",
|
||||||
|
title: "Created",
|
||||||
|
dataKey: "created_at",
|
||||||
|
width: 210,
|
||||||
|
format: "datetime",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function normalizeLearning(rowData: Record<string, unknown>): Learning {
|
||||||
|
return {
|
||||||
|
id: String(rowData.id ?? ""),
|
||||||
|
summary: typeof rowData.summary === "string" ? rowData.summary : "",
|
||||||
|
details: typeof rowData.details === "string" ? rowData.details : "",
|
||||||
|
category: typeof rowData.category === "string" ? rowData.category : "",
|
||||||
|
area: typeof rowData.area === "string" ? rowData.area : "",
|
||||||
|
status: typeof rowData.status === "string" ? rowData.status : "",
|
||||||
|
priority: typeof rowData.priority === "string" ? rowData.priority : "",
|
||||||
|
confidence:
|
||||||
|
typeof rowData.confidence === "string" ? rowData.confidence : "",
|
||||||
|
action_required: Boolean(rowData.action_required),
|
||||||
|
source_type:
|
||||||
|
typeof rowData.source_type === "string" ? rowData.source_type : undefined,
|
||||||
|
source_ref:
|
||||||
|
typeof rowData.source_ref === "string" ? rowData.source_ref : undefined,
|
||||||
|
tags: typeof rowData.tags === "string" ? rowData.tags : undefined,
|
||||||
|
project_id:
|
||||||
|
typeof rowData.project_id === "string" ? rowData.project_id : undefined,
|
||||||
|
related_thought_id:
|
||||||
|
typeof rowData.related_thought_id === "string"
|
||||||
|
? rowData.related_thought_id
|
||||||
|
: undefined,
|
||||||
|
related_skill_id:
|
||||||
|
typeof rowData.related_skill_id === "string"
|
||||||
|
? rowData.related_skill_id
|
||||||
|
: undefined,
|
||||||
|
reviewed_at:
|
||||||
|
typeof rowData.reviewed_at === "string" ? rowData.reviewed_at : undefined,
|
||||||
|
reviewed_by:
|
||||||
|
typeof rowData.reviewed_by === "string" ? rowData.reviewed_by : undefined,
|
||||||
|
created_at: String(rowData.created_at ?? ""),
|
||||||
|
updated_at: String(rowData.updated_at ?? ""),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRowClick(
|
||||||
|
_row: number,
|
||||||
|
rowData: Record<string, unknown> | undefined,
|
||||||
|
) {
|
||||||
|
if (!rowData) {
|
||||||
|
selectedLearning = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedLearning = normalizeLearning(rowData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGridEvent(
|
||||||
|
type: string,
|
||||||
|
_item?: unknown,
|
||||||
|
_column?: unknown,
|
||||||
|
_coords?: unknown,
|
||||||
|
detail?: Record<string, unknown>,
|
||||||
|
) {
|
||||||
|
if (type !== "page_loaded" && type !== "load") return;
|
||||||
|
const total = detail?.total;
|
||||||
|
if (typeof total === "number") gridTotal = total;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(value?: string): string {
|
||||||
|
if (!value) return "—";
|
||||||
|
return new Date(value).toLocaleString();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="space-y-4 w-full">
|
||||||
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-semibold text-white">Learnings</h2>
|
||||||
|
<p class="mt-1 text-sm text-slate-400">
|
||||||
|
{#if gridTotal === null}
|
||||||
|
Server-backed grid
|
||||||
|
{:else}
|
||||||
|
{gridTotal} result{gridTotal !== 1 ? "s" : ""}
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-4 xl:grid-cols-[1.6fr_1fr]">
|
||||||
|
<div class="rounded-2xl border border-white/10 bg-slate-950/30 p-3">
|
||||||
|
<GridlerFull
|
||||||
|
{columns}
|
||||||
|
theme={adminGridTheme}
|
||||||
|
rowMarkers="number"
|
||||||
|
height={560}
|
||||||
|
width="100%"
|
||||||
|
pageSize={40}
|
||||||
|
dataSource="resolvespec"
|
||||||
|
dataSourceOptions={learningsDataSourceOptions}
|
||||||
|
serverSideSearch={true}
|
||||||
|
searchColumns={["summary", "details", "category", "area", "status"]}
|
||||||
|
{onGridEvent}
|
||||||
|
{onRowClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside class="rounded-2xl border border-white/10 bg-slate-900/70 p-4">
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<h3 class="text-sm font-semibold text-white">Learning Inspector</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !selectedLearning}
|
||||||
|
<p class="mt-3 text-sm text-slate-500">
|
||||||
|
Select a learning row to inspect details and metadata.
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<div class="mt-3 space-y-3 text-sm text-slate-300">
|
||||||
|
<p class="text-slate-200">{selectedLearning.summary}</p>
|
||||||
|
<div class="rounded-xl border border-white/10 bg-white/5 p-3 space-y-1">
|
||||||
|
<p>
|
||||||
|
<strong class="text-slate-100">Category:</strong>
|
||||||
|
{selectedLearning.category || "—"}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong class="text-slate-100">Area:</strong>
|
||||||
|
{selectedLearning.area || "—"}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong class="text-slate-100">Status:</strong>
|
||||||
|
{selectedLearning.status || "—"}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong class="text-slate-100">Priority:</strong>
|
||||||
|
{selectedLearning.priority || "—"}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong class="text-slate-100">Confidence:</strong>
|
||||||
|
{selectedLearning.confidence || "—"}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong class="text-slate-100">Action Required:</strong>
|
||||||
|
{selectedLearning.action_required ? "yes" : "no"}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong class="text-slate-100">Created:</strong>
|
||||||
|
{formatDate(selectedLearning.created_at)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong class="text-slate-100">Updated:</strong>
|
||||||
|
{formatDate(selectedLearning.updated_at)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong class="text-slate-100">Reviewed:</strong>
|
||||||
|
{formatDate(selectedLearning.reviewed_at)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong class="text-slate-100">Reviewed By:</strong>
|
||||||
|
{selectedLearning.reviewed_by || "—"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-white/10 bg-white/5 p-3">
|
||||||
|
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">Details</p>
|
||||||
|
<p class="mt-2 whitespace-pre-wrap text-slate-300">
|
||||||
|
{selectedLearning.details || "No details provided."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-white/10 bg-white/5 p-3">
|
||||||
|
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">Source</p>
|
||||||
|
<p class="mt-2 text-slate-300">
|
||||||
|
{selectedLearning.source_type || "—"}
|
||||||
|
{#if selectedLearning.source_ref}
|
||||||
|
· {selectedLearning.source_ref}
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 text-slate-400">Tags: {selectedLearning.tags || "—"}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { api } from '../../api';
|
||||||
|
import type { BackfillResult, MaintenanceLog, MaintenanceTask, MetadataRetryResult } from '../../types';
|
||||||
|
|
||||||
|
let tasks = $state<MaintenanceTask[]>([]);
|
||||||
|
let logs = $state<MaintenanceLog[]>([]);
|
||||||
|
let loading = $state(true);
|
||||||
|
let error = $state('');
|
||||||
|
let busyAction = $state<'backfill' | 'retry' | null>(null);
|
||||||
|
let actionError = $state('');
|
||||||
|
let actionMessage = $state('');
|
||||||
|
let dryRun = $state(true);
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading = true;
|
||||||
|
error = '';
|
||||||
|
try {
|
||||||
|
const [taskRows, logRows] = await Promise.all([api.maintenance.tasks(), api.maintenance.logs()]);
|
||||||
|
tasks = taskRows;
|
||||||
|
logs = logRows;
|
||||||
|
} catch (e) {
|
||||||
|
error = e instanceof Error ? e.message : 'Failed to load maintenance data';
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(value?: string): string {
|
||||||
|
if (!value) return '—';
|
||||||
|
return new Date(value).toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCost(value?: number): string {
|
||||||
|
if (value === undefined || value === null) return '—';
|
||||||
|
return `$${value.toFixed(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeBackfill(result: BackfillResult): string {
|
||||||
|
return `Backfill (${result.dry_run ? 'dry-run' : 'run'}) scanned ${result.scanned}, embedded ${result.embedded}, failed ${result.failed}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeRetry(result: MetadataRetryResult): string {
|
||||||
|
return `Metadata retry (${result.dry_run ? 'dry-run' : 'run'}) scanned ${result.scanned}, retried ${result.retried}, updated ${result.updated}, failed ${result.failed}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runBackfill() {
|
||||||
|
busyAction = 'backfill';
|
||||||
|
actionError = '';
|
||||||
|
actionMessage = '';
|
||||||
|
try {
|
||||||
|
const result = await api.maintenance.runBackfill({ dry_run: dryRun });
|
||||||
|
actionMessage = summarizeBackfill(result);
|
||||||
|
} catch (e) {
|
||||||
|
actionError = e instanceof Error ? e.message : 'Backfill failed';
|
||||||
|
} finally {
|
||||||
|
busyAction = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runRetryMetadata() {
|
||||||
|
busyAction = 'retry';
|
||||||
|
actionError = '';
|
||||||
|
actionMessage = '';
|
||||||
|
try {
|
||||||
|
const result = await api.maintenance.runRetryMetadata({ dry_run: dryRun });
|
||||||
|
actionMessage = summarizeRetry(result);
|
||||||
|
} catch (e) {
|
||||||
|
actionError = e instanceof Error ? e.message : 'Retry failed';
|
||||||
|
} finally {
|
||||||
|
busyAction = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(load);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="flex items-end justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-semibold text-white">Maintenance</h2>
|
||||||
|
<p class="mt-1 text-sm text-slate-400">Operational state and safe maintenance actions.</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-sm text-slate-200 transition hover:bg-white/10"
|
||||||
|
onclick={load}
|
||||||
|
>Refresh</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="rounded-2xl border border-white/10 bg-slate-900/70 p-4">
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<label class="flex items-center gap-2 text-sm text-slate-300">
|
||||||
|
<input type="checkbox" class="accent-cyan-400" bind:checked={dryRun} />
|
||||||
|
Dry run
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
class="rounded-xl border border-cyan-300/30 bg-cyan-400/10 px-4 py-2 text-sm font-medium text-cyan-100 transition hover:bg-cyan-400/20 disabled:opacity-50"
|
||||||
|
onclick={runBackfill}
|
||||||
|
disabled={busyAction !== null}
|
||||||
|
>{busyAction === 'backfill' ? 'Running backfill…' : 'Run Backfill'}</button>
|
||||||
|
<button
|
||||||
|
class="rounded-xl border border-amber-300/30 bg-amber-400/10 px-4 py-2 text-sm font-medium text-amber-100 transition hover:bg-amber-400/20 disabled:opacity-50"
|
||||||
|
onclick={runRetryMetadata}
|
||||||
|
disabled={busyAction !== null}
|
||||||
|
>{busyAction === 'retry' ? 'Running retry…' : 'Run Metadata Retry'}</button>
|
||||||
|
</div>
|
||||||
|
{#if actionError}
|
||||||
|
<p class="mt-3 text-sm text-rose-300">{actionError}</p>
|
||||||
|
{/if}
|
||||||
|
{#if actionMessage}
|
||||||
|
<p class="mt-3 text-sm text-emerald-300">{actionMessage}</p>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<div class="rounded-2xl border border-dashed border-white/10 bg-slate-950/40 py-12 text-center text-slate-400">Loading…</div>
|
||||||
|
{:else if error}
|
||||||
|
<div class="rounded-2xl border border-rose-400/30 bg-rose-400/10 px-4 py-4 text-sm text-rose-100">{error}</div>
|
||||||
|
{:else}
|
||||||
|
<section class="space-y-4">
|
||||||
|
<h3 class="text-lg font-semibold text-white">Tasks ({tasks.length})</h3>
|
||||||
|
{#if tasks.length === 0}
|
||||||
|
<div class="rounded-2xl border border-dashed border-white/10 bg-slate-950/40 py-10 text-center text-slate-500">No maintenance tasks.</div>
|
||||||
|
{:else}
|
||||||
|
<div class="overflow-hidden rounded-2xl border border-white/10">
|
||||||
|
<table class="min-w-full divide-y divide-white/10 text-sm text-slate-300">
|
||||||
|
<thead class="bg-white/5 text-xs uppercase tracking-[0.18em] text-slate-500">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left font-medium">Task</th>
|
||||||
|
<th class="px-4 py-3 text-left font-medium">Category</th>
|
||||||
|
<th class="px-4 py-3 text-left font-medium">Priority</th>
|
||||||
|
<th class="px-4 py-3 text-right font-medium">Next Due</th>
|
||||||
|
<th class="px-4 py-3 text-right font-medium">Last Completed</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-white/5 bg-slate-950/30">
|
||||||
|
{#each tasks as task}
|
||||||
|
<tr class="hover:bg-white/[0.03]">
|
||||||
|
<td class="px-4 py-3 font-medium text-white">{task.name}</td>
|
||||||
|
<td class="px-4 py-3 text-slate-400">{task.category || '—'}</td>
|
||||||
|
<td class="px-4 py-3 text-slate-300">{task.priority || 'medium'}</td>
|
||||||
|
<td class="px-4 py-3 text-right text-slate-300">{formatDate(task.next_due)}</td>
|
||||||
|
<td class="px-4 py-3 text-right text-slate-400">{formatDate(task.last_completed)}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="space-y-4">
|
||||||
|
<h3 class="text-lg font-semibold text-white">Recent Logs ({logs.length})</h3>
|
||||||
|
{#if logs.length === 0}
|
||||||
|
<div class="rounded-2xl border border-dashed border-white/10 bg-slate-950/40 py-10 text-center text-slate-500">No maintenance logs.</div>
|
||||||
|
{:else}
|
||||||
|
<div class="overflow-hidden rounded-2xl border border-white/10">
|
||||||
|
<table class="min-w-full divide-y divide-white/10 text-sm text-slate-300">
|
||||||
|
<thead class="bg-white/5 text-xs uppercase tracking-[0.18em] text-slate-500">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left font-medium">Completed</th>
|
||||||
|
<th class="px-4 py-3 text-left font-medium">Task ID</th>
|
||||||
|
<th class="px-4 py-3 text-left font-medium">By</th>
|
||||||
|
<th class="px-4 py-3 text-right font-medium">Cost</th>
|
||||||
|
<th class="px-4 py-3 text-left font-medium">Notes</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-white/5 bg-slate-950/30">
|
||||||
|
{#each logs as log}
|
||||||
|
<tr class="hover:bg-white/[0.03]">
|
||||||
|
<td class="px-4 py-3 text-slate-300">{formatDate(log.completed_at)}</td>
|
||||||
|
<td class="px-4 py-3"><code class="text-xs text-cyan-100">{log.task_id}</code></td>
|
||||||
|
<td class="px-4 py-3 text-slate-400">{log.performed_by || '—'}</td>
|
||||||
|
<td class="px-4 py-3 text-right tabular-nums text-slate-300">{formatCost(log.cost)}</td>
|
||||||
|
<td class="max-w-sm truncate px-4 py-3 text-slate-400">{log.notes || '—'}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import { GridlerFull, TextInputCtrl, type GridlerColumn } from '@warkypublic/svelix';
|
||||||
import { api } from '../../api';
|
import { api } from '../../api';
|
||||||
|
import { GlobalStateStore } from '../../shellState';
|
||||||
|
import { adminGridTheme } from '../../gridTheme';
|
||||||
import type { ProjectSummary } from '../../types';
|
import type { ProjectSummary } from '../../types';
|
||||||
|
|
||||||
let projects = $state<ProjectSummary[]>([]);
|
let projects = $state<ProjectSummary[]>([]);
|
||||||
@@ -11,12 +14,39 @@
|
|||||||
let newName = $state('');
|
let newName = $state('');
|
||||||
let newDesc = $state('');
|
let newDesc = $state('');
|
||||||
let createError = $state('');
|
let createError = $state('');
|
||||||
|
let selectedProject = $state<ProjectSummary | null>(null);
|
||||||
|
const projectDataSourceOptions = {
|
||||||
|
url: '/api/rs',
|
||||||
|
authToken: GlobalStateStore.getState().session.authToken,
|
||||||
|
schema: 'public',
|
||||||
|
entity: 'projects',
|
||||||
|
uniqueID: 'id',
|
||||||
|
hotfields: ['id', 'guid'],
|
||||||
|
sort: [{ column: 'created_at', direction: 'desc' }]
|
||||||
|
} as unknown as {
|
||||||
|
url: string;
|
||||||
|
authToken?: string;
|
||||||
|
schema: string;
|
||||||
|
entity: string;
|
||||||
|
uniqueID: string;
|
||||||
|
hotfields: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: GridlerColumn[] = [
|
||||||
|
{ id: 'name', title: 'Name', dataKey: 'name', width: 260 },
|
||||||
|
{ id: 'description', title: 'Description', dataKey: 'description', width: 360 },
|
||||||
|
{ id: 'last_active_at', title: 'Last Active', dataKey: 'last_active_at', width: 200, format: 'datetime' },
|
||||||
|
{ id: 'created_at', title: 'Created', dataKey: 'created_at', width: 200, format: 'datetime' }
|
||||||
|
];
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading = true;
|
loading = true;
|
||||||
error = '';
|
error = '';
|
||||||
try {
|
try {
|
||||||
projects = await api.projects.list();
|
projects = await api.projects.list();
|
||||||
|
if (selectedProject) {
|
||||||
|
selectedProject = projects.find((project) => project.id === selectedProject?.id) ?? null;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e instanceof Error ? e.message : 'Failed to load projects';
|
error = e instanceof Error ? e.message : 'Failed to load projects';
|
||||||
} finally {
|
} finally {
|
||||||
@@ -41,8 +71,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(value: string) {
|
function onProjectRowClick(_row: number, rowData: Record<string, unknown> | undefined) {
|
||||||
return new Date(value).toLocaleDateString();
|
if (!rowData) {
|
||||||
|
selectedProject = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = String(rowData.guid ?? rowData.id ?? '');
|
||||||
|
selectedProject = {
|
||||||
|
id,
|
||||||
|
name: String(rowData.name ?? ''),
|
||||||
|
description: String(rowData.description ?? ''),
|
||||||
|
created_at: String(rowData.created_at ?? ''),
|
||||||
|
last_active_at: String(rowData.last_active_at ?? ''),
|
||||||
|
thought_count: Number(rowData.thought_count ?? 0)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(load);
|
onMount(load);
|
||||||
@@ -70,16 +112,15 @@
|
|||||||
<div class="rounded-2xl border border-cyan-400/20 bg-slate-900 p-5">
|
<div class="rounded-2xl border border-cyan-400/20 bg-slate-900 p-5">
|
||||||
<h3 class="text-sm font-semibold text-white">Create project</h3>
|
<h3 class="text-sm font-semibold text-white">Create project</h3>
|
||||||
<div class="mt-3 space-y-3">
|
<div class="mt-3 space-y-3">
|
||||||
<input
|
<TextInputCtrl
|
||||||
type="text"
|
label="Project name"
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-sm text-white placeholder-slate-500 focus:border-cyan-400/40 focus:outline-none"
|
required
|
||||||
bind:value={newName}
|
bind:value={newName}
|
||||||
/>
|
/>
|
||||||
<input
|
<TextInputCtrl
|
||||||
type="text"
|
label="Description"
|
||||||
placeholder="Description (optional)"
|
placeholder="Description (optional)"
|
||||||
class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-sm text-white placeholder-slate-500 focus:border-cyan-400/40 focus:outline-none"
|
|
||||||
bind:value={newDesc}
|
bind:value={newDesc}
|
||||||
/>
|
/>
|
||||||
{#if createError}<p class="text-xs text-rose-300">{createError}</p>{/if}
|
{#if createError}<p class="text-xs text-rose-300">{createError}</p>{/if}
|
||||||
@@ -109,29 +150,25 @@
|
|||||||
No projects yet.
|
No projects yet.
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="overflow-hidden rounded-2xl border border-white/10">
|
<div class="rounded-2xl border border-white/10 bg-slate-950/30 p-3">
|
||||||
<table class="min-w-full divide-y divide-white/10 text-sm text-slate-300">
|
<GridlerFull
|
||||||
<thead class="bg-white/5 text-xs uppercase tracking-[0.18em] text-slate-500">
|
{columns}
|
||||||
<tr>
|
theme={adminGridTheme}
|
||||||
<th class="px-4 py-3 text-left font-medium">Name</th>
|
rowMarkers="number"
|
||||||
<th class="px-4 py-3 text-left font-medium">Description</th>
|
height={420}
|
||||||
<th class="px-4 py-3 text-right font-medium">Thoughts</th>
|
dataSource="resolvespec"
|
||||||
<th class="px-4 py-3 text-right font-medium">Last active</th>
|
dataSourceOptions={projectDataSourceOptions}
|
||||||
<th class="px-4 py-3 text-right font-medium">Created</th>
|
serverSideSearch={true}
|
||||||
</tr>
|
searchColumns={['name', 'description']}
|
||||||
</thead>
|
onRowClick={onProjectRowClick}
|
||||||
<tbody class="divide-y divide-white/5 bg-slate-950/30">
|
/>
|
||||||
{#each projects as p}
|
</div>
|
||||||
<tr class="hover:bg-white/[0.03]">
|
{#if selectedProject}
|
||||||
<td class="px-4 py-3 font-medium text-white">{p.name}</td>
|
<div class="rounded-2xl border border-white/10 bg-slate-900/70 p-4">
|
||||||
<td class="max-w-xs truncate px-4 py-3 text-slate-400">{p.description || '—'}</td>
|
<h3 class="text-sm font-semibold text-white">Selected project</h3>
|
||||||
<td class="px-4 py-3 text-right tabular-nums text-slate-200">{p.thought_count}</td>
|
<p class="mt-2 text-sm text-slate-300"><strong class="text-slate-100">{selectedProject.name}</strong> · {selectedProject.thought_count} thoughts</p>
|
||||||
<td class="px-4 py-3 text-right text-slate-400">{formatDate(p.last_active_at)}</td>
|
<p class="mt-1 text-sm text-slate-400">{selectedProject.description || 'No description.'}</p>
|
||||||
<td class="px-4 py-3 text-right text-slate-400">{formatDate(p.created_at)}</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
import type { ShellPage, StatusResponse } from '../../types';
|
import type { ShellPage, StatusResponse } from '../../types';
|
||||||
import FilesPage from '../files/FilesPage.svelte';
|
import FilesPage from '../files/FilesPage.svelte';
|
||||||
import GuardrailsPage from '../guardrails/GuardrailsPage.svelte';
|
import GuardrailsPage from '../guardrails/GuardrailsPage.svelte';
|
||||||
|
import LearningsPage from '../learnings/LearningsPage.svelte';
|
||||||
|
import MaintenancePage from '../maintenance/MaintenancePage.svelte';
|
||||||
import DashboardPage from '../dashboard/DashboardPage.svelte';
|
import DashboardPage from '../dashboard/DashboardPage.svelte';
|
||||||
import ProjectsPage from '../projects/ProjectsPage.svelte';
|
import ProjectsPage from '../projects/ProjectsPage.svelte';
|
||||||
import SkillsPage from '../skills/SkillsPage.svelte';
|
import SkillsPage from '../skills/SkillsPage.svelte';
|
||||||
@@ -37,12 +39,16 @@
|
|||||||
<ProjectsPage />
|
<ProjectsPage />
|
||||||
{:else if currentPage === 'thoughts'}
|
{:else if currentPage === 'thoughts'}
|
||||||
<ThoughtsPage />
|
<ThoughtsPage />
|
||||||
|
{:else if currentPage === 'learnings'}
|
||||||
|
<LearningsPage />
|
||||||
{:else if currentPage === 'skills'}
|
{:else if currentPage === 'skills'}
|
||||||
<SkillsPage />
|
<SkillsPage />
|
||||||
{:else if currentPage === 'guardrails'}
|
{:else if currentPage === 'guardrails'}
|
||||||
<GuardrailsPage />
|
<GuardrailsPage />
|
||||||
{:else if currentPage === 'files'}
|
{:else if currentPage === 'files'}
|
||||||
<FilesPage />
|
<FilesPage />
|
||||||
|
{:else if currentPage === 'maintenance'}
|
||||||
|
<MaintenancePage />
|
||||||
{/if}
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
@@ -15,9 +15,11 @@
|
|||||||
{ id: 'dashboard', label: 'Dashboard', description: 'System overview and status.' },
|
{ id: 'dashboard', label: 'Dashboard', description: 'System overview and status.' },
|
||||||
{ id: 'projects', label: 'Projects', description: 'Browse and manage projects.' },
|
{ id: 'projects', label: 'Projects', description: 'Browse and manage projects.' },
|
||||||
{ id: 'thoughts', label: 'Thoughts', description: 'Search and inspect thoughts.' },
|
{ id: 'thoughts', label: 'Thoughts', description: 'Search and inspect thoughts.' },
|
||||||
|
{ id: 'learnings', label: 'Learnings', description: 'Curated insights and outcomes.' },
|
||||||
{ id: 'skills', label: 'Skills', description: 'Agent skill registry.' },
|
{ id: 'skills', label: 'Skills', description: 'Agent skill registry.' },
|
||||||
{ id: 'guardrails', label: 'Guardrails', description: 'Agent guardrail registry.' },
|
{ id: 'guardrails', label: 'Guardrails', description: 'Agent guardrail registry.' },
|
||||||
{ id: 'files', label: 'Files', description: 'Stored file inventory.' }
|
{ id: 'files', label: 'Files', description: 'Stored file inventory.' },
|
||||||
|
{ id: 'maintenance', label: 'Maintenance', description: 'Task state and upkeep actions.' }
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,165 +1,389 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import {
|
||||||
import { api } from '../../api';
|
GridlerFull,
|
||||||
import type { Thought, SearchResult } from '../../types';
|
type GridColumnFilters,
|
||||||
|
type GridlerColumn,
|
||||||
|
} from "@warkypublic/svelix";
|
||||||
|
import { api } from "../../api";
|
||||||
|
import { GlobalStateStore } from "../../shellState";
|
||||||
|
import { adminGridTheme } from "../../gridTheme";
|
||||||
|
import type { StoredFile, Thought, ThoughtLink } from "../../types";
|
||||||
|
|
||||||
type Row = Thought | SearchResult;
|
|
||||||
|
|
||||||
let rows = $state<Row[]>([]);
|
|
||||||
let loading = $state(true);
|
|
||||||
let error = $state('');
|
|
||||||
let query = $state('');
|
|
||||||
let includeArchived = $state(false);
|
let includeArchived = $state(false);
|
||||||
let actionBusy = $state<string | null>(null);
|
let actionBusy = $state<string | null>(null);
|
||||||
let actionError = $state('');
|
let actionError = $state("");
|
||||||
|
let inspectorBusy = $state(false);
|
||||||
|
let selectedThought = $state<Thought | null>(null);
|
||||||
|
let relatedLinks = $state<ThoughtLink[]>([]);
|
||||||
|
let relatedFiles = $state<StoredFile[]>([]);
|
||||||
|
let gridTotal = $state<number | null>(null);
|
||||||
|
const thoughtsDataSourceOptions = {
|
||||||
|
url: "/api/rs",
|
||||||
|
authToken: GlobalStateStore.getState().session.authToken,
|
||||||
|
schema: "public",
|
||||||
|
entity: "thoughts",
|
||||||
|
uniqueID: "id",
|
||||||
|
hotfields: ["guid", "metadata", "project_id", "archived_at"],
|
||||||
|
sort: [{ column: "created_at", direction: "desc" }],
|
||||||
|
} as unknown as {
|
||||||
|
url: string;
|
||||||
|
authToken?: string;
|
||||||
|
schema: string;
|
||||||
|
entity: string;
|
||||||
|
uniqueID: string;
|
||||||
|
hotfields: string[];
|
||||||
|
};
|
||||||
|
|
||||||
let searchTimer: ReturnType<typeof setTimeout>;
|
const columns: GridlerColumn[] = [
|
||||||
|
{ id: "id", title: "ID", dataKey: "id", width: 90, format: "number" },
|
||||||
|
{ id: "content", title: "Content", dataKey: "content", width: 520 },
|
||||||
|
{
|
||||||
|
id: "created_at",
|
||||||
|
title: "Created",
|
||||||
|
dataKey: "created_at",
|
||||||
|
width: 210,
|
||||||
|
format: "datetime",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "updated_at",
|
||||||
|
title: "Updated",
|
||||||
|
dataKey: "updated_at",
|
||||||
|
width: 210,
|
||||||
|
format: "datetime",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "archived_at",
|
||||||
|
title: "Archived",
|
||||||
|
dataKey: "archived_at",
|
||||||
|
width: 210,
|
||||||
|
format: "datetime",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
async function load() {
|
const baseFilters = $derived<GridColumnFilters>(
|
||||||
loading = true;
|
includeArchived
|
||||||
error = '';
|
? {}
|
||||||
try {
|
: {
|
||||||
rows = await api.thoughts.list({ q: query || undefined, include_archived: includeArchived || undefined, limit: 100 }) as Row[];
|
archived_at: {
|
||||||
} catch (e) {
|
value: "",
|
||||||
error = e instanceof Error ? e.message : 'Failed to load thoughts';
|
op: "isNull",
|
||||||
} finally {
|
},
|
||||||
loading = false;
|
},
|
||||||
}
|
);
|
||||||
}
|
|
||||||
|
|
||||||
function onQueryInput() {
|
|
||||||
clearTimeout(searchTimer);
|
|
||||||
searchTimer = setTimeout(load, 350);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function archive(id: string) {
|
async function archive(id: string) {
|
||||||
actionBusy = id;
|
actionBusy = id;
|
||||||
actionError = '';
|
actionError = "";
|
||||||
try {
|
try {
|
||||||
await api.thoughts.archive(id);
|
await api.thoughts.archive(id);
|
||||||
await load();
|
if (selectedThought?.id === id)
|
||||||
|
selectedThought.archived_at = new Date().toISOString();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
actionError = e instanceof Error ? e.message : 'Archive failed';
|
actionError = e instanceof Error ? e.message : "Archive failed";
|
||||||
} finally {
|
} finally {
|
||||||
actionBusy = null;
|
actionBusy = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function remove(id: string) {
|
async function remove(id: string) {
|
||||||
if (!confirm('Permanently delete this thought?')) return;
|
if (!confirm("Permanently delete this thought?")) return;
|
||||||
actionBusy = id;
|
actionBusy = id;
|
||||||
actionError = '';
|
actionError = "";
|
||||||
try {
|
try {
|
||||||
await api.thoughts.delete(id);
|
await api.thoughts.delete(id);
|
||||||
await load();
|
if (selectedThought?.id === id) {
|
||||||
|
selectedThought = null;
|
||||||
|
relatedLinks = [];
|
||||||
|
relatedFiles = [];
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
actionError = e instanceof Error ? e.message : 'Delete failed';
|
actionError = e instanceof Error ? e.message : "Delete failed";
|
||||||
} finally {
|
} finally {
|
||||||
actionBusy = null;
|
actionBusy = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isArchived(row: Row): boolean {
|
function normalizeThought(rowData: Record<string, unknown>): Thought {
|
||||||
return 'archived_at' in row && !!row.archived_at;
|
const metadataObj = (rowData.metadata ?? {}) as Record<string, unknown>;
|
||||||
|
return {
|
||||||
|
id: String(rowData.id ?? ""),
|
||||||
|
guid: rowData.guid ? String(rowData.guid) : undefined,
|
||||||
|
content: typeof rowData.content === "string" ? rowData.content : "",
|
||||||
|
project_id: rowData.project_id ? String(rowData.project_id) : undefined,
|
||||||
|
archived_at: rowData.archived_at
|
||||||
|
? String(rowData.archived_at)
|
||||||
|
: undefined,
|
||||||
|
created_at: String(rowData.created_at ?? ""),
|
||||||
|
updated_at: String(rowData.updated_at ?? ""),
|
||||||
|
metadata: {
|
||||||
|
people: Array.isArray(metadataObj.people)
|
||||||
|
? (metadataObj.people as string[])
|
||||||
|
: [],
|
||||||
|
action_items: Array.isArray(metadataObj.action_items)
|
||||||
|
? (metadataObj.action_items as string[])
|
||||||
|
: [],
|
||||||
|
dates_mentioned: Array.isArray(metadataObj.dates_mentioned)
|
||||||
|
? (metadataObj.dates_mentioned as string[])
|
||||||
|
: [],
|
||||||
|
topics: Array.isArray(metadataObj.topics)
|
||||||
|
? (metadataObj.topics as string[])
|
||||||
|
: [],
|
||||||
|
type: typeof metadataObj.type === "string" ? metadataObj.type : "",
|
||||||
|
source:
|
||||||
|
typeof metadataObj.source === "string" ? metadataObj.source : "",
|
||||||
|
metadata_status:
|
||||||
|
typeof metadataObj.metadata_status === "string"
|
||||||
|
? metadataObj.metadata_status
|
||||||
|
: "",
|
||||||
|
metadata_error:
|
||||||
|
typeof metadataObj.metadata_error === "string"
|
||||||
|
? metadataObj.metadata_error
|
||||||
|
: undefined,
|
||||||
|
attachments: Array.isArray(metadataObj.attachments)
|
||||||
|
? (metadataObj.attachments as Thought["metadata"]["attachments"])
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function content(row: Row): string {
|
async function inspectThought(row: Thought | null) {
|
||||||
return row.content.length > 120 ? row.content.slice(0, 120) + '…' : row.content;
|
selectedThought = row;
|
||||||
|
relatedLinks = [];
|
||||||
|
relatedFiles = [];
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
|
inspectorBusy = true;
|
||||||
|
try {
|
||||||
|
const thoughtRef = row.guid ?? row.id;
|
||||||
|
const [links, files] = await Promise.all([
|
||||||
|
api.thoughts.links(row.id),
|
||||||
|
api.files.list({ thought_id: thoughtRef }),
|
||||||
|
]);
|
||||||
|
relatedLinks = links;
|
||||||
|
relatedFiles = files;
|
||||||
|
} finally {
|
||||||
|
inspectorBusy = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(value: string) {
|
function onRowClick(
|
||||||
|
_row: number,
|
||||||
|
rowData: Record<string, unknown> | undefined,
|
||||||
|
) {
|
||||||
|
if (!rowData) {
|
||||||
|
void inspectThought(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void inspectThought(normalizeThought(rowData));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGridEvent(
|
||||||
|
type: string,
|
||||||
|
_item?: unknown,
|
||||||
|
_column?: unknown,
|
||||||
|
_coords?: unknown,
|
||||||
|
detail?: Record<string, unknown>,
|
||||||
|
) {
|
||||||
|
if (type !== "page_loaded" && type !== "load") return;
|
||||||
|
const total = detail?.total;
|
||||||
|
if (typeof total === "number") gridTotal = total;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(value?: string) {
|
||||||
|
if (!value) return "—";
|
||||||
return new Date(value).toLocaleString();
|
return new Date(value).toLocaleString();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(load);
|
function isSelectedArchived(): boolean {
|
||||||
|
return !!selectedThought?.archived_at;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4 w-full">
|
||||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-2xl font-semibold text-white">Thoughts</h2>
|
<h2 class="text-2xl font-semibold text-white">Thoughts</h2>
|
||||||
<p class="mt-1 text-sm text-slate-400">{rows.length} result{rows.length !== 1 ? 's' : ''}</p>
|
<p class="mt-1 text-sm text-slate-400">
|
||||||
|
{#if gridTotal === null}
|
||||||
|
Server-backed grid
|
||||||
|
{:else}
|
||||||
|
{gridTotal} result{gridTotal !== 1 ? "s" : ""}
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<label class="flex items-center gap-2 text-sm text-slate-400">
|
<label class="flex items-center gap-2 text-sm text-slate-400">
|
||||||
<input type="checkbox" class="accent-cyan-400" bind:checked={includeArchived} onchange={load} />
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="accent-cyan-400"
|
||||||
|
bind:checked={includeArchived}
|
||||||
|
/>
|
||||||
Archived
|
Archived
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
class="rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-sm text-slate-200 transition hover:bg-white/10"
|
class="rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-sm text-slate-200 transition hover:bg-white/10"
|
||||||
onclick={load}
|
onclick={() => {
|
||||||
>Refresh</button>
|
selectedThought = null;
|
||||||
|
relatedLinks = [];
|
||||||
|
relatedFiles = [];
|
||||||
|
}}>Refresh</button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
|
||||||
type="search"
|
|
||||||
placeholder="Search thoughts…"
|
|
||||||
class="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-2.5 text-sm text-white placeholder-slate-500 focus:border-cyan-400/40 focus:outline-none"
|
|
||||||
bind:value={query}
|
|
||||||
oninput={onQueryInput}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if actionError}
|
{#if actionError}
|
||||||
<p class="text-sm text-rose-300">{actionError}</p>
|
<p class="text-sm text-rose-300">{actionError}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if loading}
|
<div class="grid gap-4 xl:grid-cols-[1.6fr_1fr]">
|
||||||
<div class="rounded-2xl border border-dashed border-white/10 bg-slate-950/40 py-12 text-center text-slate-400">Loading…</div>
|
<div class="rounded-2xl border border-white/10 bg-slate-950/30 p-3">
|
||||||
{:else if error}
|
<GridlerFull
|
||||||
<div class="rounded-2xl border border-rose-400/30 bg-rose-400/10 px-4 py-4 text-sm text-rose-100">{error}</div>
|
{columns}
|
||||||
{:else if rows.length === 0}
|
theme={adminGridTheme}
|
||||||
<div class="rounded-2xl border border-dashed border-white/10 bg-slate-950/40 py-12 text-center text-slate-500">No thoughts found.</div>
|
rowMarkers="number"
|
||||||
|
height={560}
|
||||||
|
width="100%"
|
||||||
|
pageSize={40}
|
||||||
|
dataSource="resolvespec"
|
||||||
|
dataSourceOptions={thoughtsDataSourceOptions}
|
||||||
|
serverSideSearch={true}
|
||||||
|
searchColumns={["content"]}
|
||||||
|
filters={baseFilters}
|
||||||
|
{onGridEvent}
|
||||||
|
{onRowClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside class="rounded-2xl border border-white/10 bg-slate-900/70 p-4">
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<h3 class="text-sm font-semibold text-white">Thought Inspector</h3>
|
||||||
|
{#if selectedThought}
|
||||||
|
<div class="flex gap-2">
|
||||||
|
{#if !isSelectedArchived()}
|
||||||
|
<button
|
||||||
|
class="text-xs text-slate-300 hover:text-white"
|
||||||
|
onclick={() => {
|
||||||
|
if (selectedThought) void archive(selectedThought.id);
|
||||||
|
}}
|
||||||
|
disabled={actionBusy === selectedThought?.id}>Archive</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
<button
|
||||||
|
class="text-xs text-rose-300 hover:text-rose-200"
|
||||||
|
onclick={() => {
|
||||||
|
if (selectedThought) void remove(selectedThought.id);
|
||||||
|
}}
|
||||||
|
disabled={actionBusy === selectedThought?.id}>Delete</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !selectedThought}
|
||||||
|
<p class="mt-3 text-sm text-slate-500">
|
||||||
|
Select a thought row to inspect metadata, links, and file records.
|
||||||
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="overflow-hidden rounded-2xl border border-white/10">
|
<div class="mt-3 space-y-3 text-sm text-slate-300">
|
||||||
<table class="min-w-full divide-y divide-white/10 text-sm text-slate-300">
|
<p class="text-slate-200">{selectedThought.content}</p>
|
||||||
<thead class="bg-white/5 text-xs uppercase tracking-[0.18em] text-slate-500">
|
<div class="rounded-xl border border-white/10 bg-white/5 p-3">
|
||||||
<tr>
|
<p>
|
||||||
<th class="px-4 py-3 text-left font-medium">Content</th>
|
<strong class="text-slate-100">Type:</strong>
|
||||||
<th class="px-4 py-3 text-left font-medium">Type</th>
|
{selectedThought.metadata.type || "—"}
|
||||||
<th class="px-4 py-3 text-left font-medium">Status</th>
|
</p>
|
||||||
<th class="px-4 py-3 text-right font-medium">Created</th>
|
<p>
|
||||||
<th class="px-4 py-3 text-right font-medium">Actions</th>
|
<strong class="text-slate-100">Status:</strong>
|
||||||
</tr>
|
{selectedThought.metadata.metadata_status || "active"}
|
||||||
</thead>
|
</p>
|
||||||
<tbody class="divide-y divide-white/5 bg-slate-950/30">
|
{#if selectedThought.metadata.metadata_error}
|
||||||
{#each rows as row}
|
<p class="mt-1 text-rose-300">
|
||||||
<tr class={`hover:bg-white/[0.03] ${isArchived(row) ? 'opacity-50' : ''}`}>
|
<strong>Error:</strong>
|
||||||
<td class="max-w-sm px-4 py-3 align-top">
|
{selectedThought.metadata.metadata_error}
|
||||||
<p class="text-white">{content(row)}</p>
|
</p>
|
||||||
{#if row.metadata.topics?.length}
|
|
||||||
<p class="mt-1 text-xs text-slate-500">{row.metadata.topics.slice(0,3).join(', ')}</p>
|
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
<p class="mt-1">
|
||||||
<td class="px-4 py-3 align-top">
|
<strong class="text-slate-100">Created:</strong>
|
||||||
<span class="rounded-full border border-white/10 bg-white/5 px-2 py-0.5 text-xs text-slate-300">
|
{formatDate(selectedThought.created_at)}
|
||||||
{row.metadata.type || '—'}
|
</p>
|
||||||
</span>
|
<p>
|
||||||
</td>
|
<strong class="text-slate-100">Updated:</strong>
|
||||||
<td class="px-4 py-3 align-top text-xs text-slate-400">
|
{formatDate(selectedThought.updated_at)}
|
||||||
{isArchived(row) ? 'archived' : (row.metadata.metadata_status || 'active')}
|
</p>
|
||||||
</td>
|
|
||||||
<td class="px-4 py-3 align-top text-right text-slate-400">{formatDate(row.created_at)}</td>
|
|
||||||
<td class="px-4 py-3 align-top text-right">
|
|
||||||
<div class="flex justify-end gap-2">
|
|
||||||
{#if !isArchived(row)}
|
|
||||||
<button
|
|
||||||
class="text-xs text-slate-400 underline-offset-2 hover:text-slate-200"
|
|
||||||
onclick={() => archive(row.id)}
|
|
||||||
disabled={actionBusy === row.id}
|
|
||||||
>Archive</button>
|
|
||||||
{/if}
|
|
||||||
<button
|
|
||||||
class="text-xs text-rose-400 underline-offset-2 hover:text-rose-300"
|
|
||||||
onclick={() => remove(row.id)}
|
|
||||||
disabled={actionBusy === row.id}
|
|
||||||
>Delete</button>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
|
||||||
</tr>
|
<div>
|
||||||
|
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">
|
||||||
|
Related Links ({relatedLinks.length})
|
||||||
|
</p>
|
||||||
|
{#if inspectorBusy}
|
||||||
|
<p class="mt-2 text-xs text-slate-500">Loading links…</p>
|
||||||
|
{:else if relatedLinks.length === 0}
|
||||||
|
<p class="mt-2 text-xs text-slate-500">No related links.</p>
|
||||||
|
{:else}
|
||||||
|
<ul class="mt-2 space-y-1 text-xs">
|
||||||
|
{#each relatedLinks as link}
|
||||||
|
<li
|
||||||
|
class="rounded-lg border border-white/10 bg-white/5 px-2 py-1"
|
||||||
|
>
|
||||||
|
<code class="text-cyan-100">{link.from_id}</code> →
|
||||||
|
<code class="text-cyan-100">{link.to_id}</code>
|
||||||
|
· {link.relation}
|
||||||
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</ul>
|
||||||
</table>
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">
|
||||||
|
Attachments ({selectedThought.metadata.attachments?.length ?? 0})
|
||||||
|
</p>
|
||||||
|
{#if (selectedThought.metadata.attachments?.length ?? 0) === 0}
|
||||||
|
<p class="mt-2 text-xs text-slate-500">No attachment metadata.</p>
|
||||||
|
{:else}
|
||||||
|
<ul class="mt-2 space-y-1 text-xs">
|
||||||
|
{#each selectedThought.metadata.attachments ?? [] as attachment}
|
||||||
|
<li
|
||||||
|
class="rounded-lg border border-white/10 bg-white/5 px-2 py-1"
|
||||||
|
>
|
||||||
|
{attachment.name} · {attachment.media_type} · {attachment.size_bytes}
|
||||||
|
bytes
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">
|
||||||
|
Stored File Records ({relatedFiles.length})
|
||||||
|
</p>
|
||||||
|
{#if inspectorBusy}
|
||||||
|
<p class="mt-2 text-xs text-slate-500">Loading files…</p>
|
||||||
|
{:else if relatedFiles.length === 0}
|
||||||
|
<p class="mt-2 text-xs text-slate-500">
|
||||||
|
No stored files linked to this thought.
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<ul class="mt-2 space-y-1 text-xs">
|
||||||
|
{#each relatedFiles as file}
|
||||||
|
<li
|
||||||
|
class="rounded-lg border border-white/10 bg-white/5 px-2 py-1"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={`/files/${file.id}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
class="text-cyan-300 hover:text-cyan-200">{file.name}</a
|
||||||
|
>
|
||||||
|
· {file.media_type}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type { GridlerTheme } from '@warkypublic/svelix';
|
||||||
|
|
||||||
|
export const adminGridTheme: Partial<GridlerTheme> = {
|
||||||
|
accentColor: '#22d3ee',
|
||||||
|
accentFg: '#06232b',
|
||||||
|
bgCell: '#020617',
|
||||||
|
bgHeader: '#0f172a',
|
||||||
|
bgHeaderHasFocus: '#164e63',
|
||||||
|
bgSearchResult: '#083344',
|
||||||
|
borderColor: '#1e293b',
|
||||||
|
foreground: '#cbd5e1',
|
||||||
|
fontFamily: 'Inter, system-ui, sans-serif',
|
||||||
|
fontSize: 13,
|
||||||
|
headerFontSize: 12,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
cellHorizontalPadding: 10,
|
||||||
|
cellVerticalPadding: 8
|
||||||
|
};
|
||||||
+99
-1
@@ -2,10 +2,25 @@ export type AccessEntry = {
|
|||||||
key_id: string;
|
key_id: string;
|
||||||
last_accessed_at: string;
|
last_accessed_at: string;
|
||||||
last_path: string;
|
last_path: string;
|
||||||
|
remote_addr: string;
|
||||||
user_agent: string;
|
user_agent: string;
|
||||||
request_count: number;
|
request_count: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type RequestAggregate = {
|
||||||
|
key: string;
|
||||||
|
request_count: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AccessMetrics = {
|
||||||
|
total_requests: number;
|
||||||
|
unique_principals: number;
|
||||||
|
unique_ips: number;
|
||||||
|
unique_agents: number;
|
||||||
|
top_ips: RequestAggregate[];
|
||||||
|
top_agents: RequestAggregate[];
|
||||||
|
};
|
||||||
|
|
||||||
export type StatusResponse = {
|
export type StatusResponse = {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
@@ -17,6 +32,7 @@ export type StatusResponse = {
|
|||||||
connected_window: string;
|
connected_window: string;
|
||||||
oauth_enabled: boolean;
|
oauth_enabled: boolean;
|
||||||
entries: AccessEntry[];
|
entries: AccessEntry[];
|
||||||
|
metrics: AccessMetrics;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NavItem = {
|
export type NavItem = {
|
||||||
@@ -26,7 +42,7 @@ export type NavItem = {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ShellPage = 'dashboard' | 'projects' | 'thoughts' | 'skills' | 'guardrails' | 'files';
|
export type ShellPage = 'dashboard' | 'projects' | 'thoughts' | 'learnings' | 'skills' | 'guardrails' | 'files' | 'maintenance';
|
||||||
|
|
||||||
export type Project = {
|
export type Project = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -49,10 +65,21 @@ export type ThoughtMetadata = {
|
|||||||
source: string;
|
source: string;
|
||||||
metadata_status: string;
|
metadata_status: string;
|
||||||
metadata_error?: string;
|
metadata_error?: string;
|
||||||
|
attachments?: ThoughtAttachment[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ThoughtAttachment = {
|
||||||
|
file_id: string;
|
||||||
|
name: string;
|
||||||
|
media_type: string;
|
||||||
|
kind?: string;
|
||||||
|
size_bytes: number;
|
||||||
|
sha256?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Thought = {
|
export type Thought = {
|
||||||
id: string;
|
id: string;
|
||||||
|
guid?: string;
|
||||||
content: string;
|
content: string;
|
||||||
metadata: ThoughtMetadata;
|
metadata: ThoughtMetadata;
|
||||||
project_id?: string;
|
project_id?: string;
|
||||||
@@ -61,6 +88,14 @@ export type Thought = {
|
|||||||
updated_at: string;
|
updated_at: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ThoughtLink = {
|
||||||
|
id: number;
|
||||||
|
from_id: number;
|
||||||
|
to_id: number;
|
||||||
|
relation: string;
|
||||||
|
created_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type SearchResult = {
|
export type SearchResult = {
|
||||||
id: string;
|
id: string;
|
||||||
content: string;
|
content: string;
|
||||||
@@ -109,3 +144,66 @@ export type ThoughtStats = {
|
|||||||
top_topics: { key: string; count: number }[];
|
top_topics: { key: string; count: number }[];
|
||||||
top_people: { key: string; count: number }[];
|
top_people: { key: string; count: number }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Learning = {
|
||||||
|
id: string;
|
||||||
|
summary: string;
|
||||||
|
details: string;
|
||||||
|
category: string;
|
||||||
|
area: string;
|
||||||
|
status: string;
|
||||||
|
priority: string;
|
||||||
|
confidence: string;
|
||||||
|
action_required: boolean;
|
||||||
|
source_type?: string;
|
||||||
|
source_ref?: string;
|
||||||
|
tags?: string;
|
||||||
|
project_id?: string;
|
||||||
|
related_thought_id?: string;
|
||||||
|
related_skill_id?: string;
|
||||||
|
reviewed_at?: string;
|
||||||
|
reviewed_by?: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MaintenanceTask = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
category?: string;
|
||||||
|
priority: string;
|
||||||
|
frequency_days?: number;
|
||||||
|
next_due?: string;
|
||||||
|
last_completed?: string;
|
||||||
|
notes?: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MaintenanceLog = {
|
||||||
|
id: string;
|
||||||
|
task_id: string;
|
||||||
|
completed_at: string;
|
||||||
|
performed_by?: string;
|
||||||
|
cost?: number;
|
||||||
|
notes?: string;
|
||||||
|
next_action?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BackfillResult = {
|
||||||
|
model: string;
|
||||||
|
scanned: number;
|
||||||
|
embedded: number;
|
||||||
|
skipped: number;
|
||||||
|
failed: number;
|
||||||
|
dry_run: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MetadataRetryResult = {
|
||||||
|
scanned: number;
|
||||||
|
retried: number;
|
||||||
|
updated: number;
|
||||||
|
skipped: number;
|
||||||
|
failed: number;
|
||||||
|
dry_run: boolean;
|
||||||
|
};
|
||||||
Reference in new issue
Block a user