feat: add Continuity register to Domovoy
CI / test (push) Successful in 1m15s

This commit is contained in:
m
2026-08-16 15:59:19 +03:00
parent 2877571bb3
commit 543b93105b
7 changed files with 147 additions and 8 deletions
+2
View File
@@ -145,6 +145,8 @@ func creatorRoute(method, path string) (routeTarget, string, bool) {
if validID(id) {
return targetContinuity, "/v1/creator/prospective-items/" + id, true
}
case method == http.MethodGet && path == "/api/prospective-items":
return targetContinuity, "/v1/creator/prospective-items", true
}
return targetCore, "", false
}
+20
View File
@@ -78,6 +78,26 @@ func TestGatewayRoutesProspectiveItemToContinuity(t *testing.T) {
}
}
func TestGatewayRoutesProspectiveListToContinuity(t *testing.T) {
continuity := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/creator/prospective-items" || r.URL.RawQuery != "limit=25" {
t.Fatalf("unexpected Continuity list target %s?%s", r.URL.Path, r.URL.RawQuery)
}
_, _ = io.WriteString(w, `{"Items":[]}`)
}))
defer continuity.Close()
handler, err := newHandler("http://core-runtime.invalid", continuity.URL, continuity.Client())
if err != nil {
t.Fatal(err)
}
request := httptest.NewRequest(http.MethodGet, "/api/prospective-items?limit=25", nil)
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "Items") {
t.Fatalf("response = %d %s", response.Code, response.Body.String())
}
}
func TestConsoleAndSecurityHeaders(t *testing.T) {
handler, err := newHandler("http://core-runtime:8080", "http://continuity-runtime:8081", http.DefaultClient)
if err != nil {
+23
View File
@@ -93,6 +93,24 @@ button:focus-visible, input:focus-visible { outline: 2px solid var(--signal-brig
.signal-strip strong[data-state="error"] { color: var(--bad); }
.signal-strip strong[data-state="loading"] { color: var(--warn); animation: pulse 1.1s ease-in-out infinite; }
.register-panel { margin-bottom: 18px; border: 1px solid var(--line); background: rgba(16, 23, 20, .82); }
.register-panel > header { padding: 17px 18px; display: flex; align-items: end; justify-content: space-between; gap: 18px; border-bottom: 1px solid var(--line); }
.register-panel header p { margin: 0 0 6px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .13em; text-transform: uppercase; }
.register-panel h2 { margin: 0; font: 400 22px/1 var(--serif); }
.register-panel header > span { color: var(--muted); font: 9px/1 var(--mono); letter-spacing: .1em; }
.register-list { max-height: 260px; overflow: auto; scrollbar-color: var(--faint) transparent; }
.register-row { width: 100%; min-height: 58px; padding: 0 18px; display: grid; grid-template-columns: minmax(160px, 1.4fr) minmax(100px, .8fr) minmax(145px, 1fr) auto; gap: 18px; align-items: center; border: 0; border-bottom: 1px solid var(--line-soft); color: var(--muted); background: transparent; text-align: left; cursor: pointer; transition: background .18s, color .18s; }
.register-row:last-child { border-bottom: 0; }
.register-row:hover { color: var(--ink); background: var(--panel-high); }
.register-row[aria-current="true"] { color: var(--ink); background: rgba(201, 168, 95, .08); box-shadow: inset 3px 0 var(--signal); }
.register-row strong, .register-row span, .register-row time { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.register-row strong { font: 600 11px/1.3 var(--mono); }
.register-row span, .register-row time { font: 10px/1.3 var(--mono); }
.register-row em { padding: 5px 8px; border: 1px solid currentColor; border-radius: 100px; color: var(--good); font: normal 600 8px/1 var(--mono); letter-spacing: .07em; }
.register-row em.warn { color: var(--warn); }
.register-row em.bad { color: var(--bad); }
.register-empty { margin: 0; padding: 22px 18px; color: var(--faint); font-size: 11px; }
.data-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }
.data-card { min-width: 0; padding: 24px; border: 1px solid var(--line); background: linear-gradient(145deg, rgba(22, 31, 27, .94), rgba(13, 20, 17, .98)); box-shadow: 0 10px 40px rgba(0,0,0,.12); animation: reveal .55s both; }
.task-card { animation-delay: .08s; }
@@ -164,6 +182,11 @@ button:focus-visible, input:focus-visible { outline: 2px solid var(--signal-brig
.last-update { text-align: left; }
.signal-strip { grid-template-columns: 1fr; }
.signal-strip div { border-right: 0; border-bottom: 1px solid var(--line); }
.register-panel > header { align-items: start; }
.register-row { padding: 13px 16px; grid-template-columns: minmax(0, 1fr) auto; gap: 7px 12px; }
.register-row time { grid-column: 1; grid-row: 2; }
.register-row span { display: none; }
.register-row em { grid-column: 2; grid-row: 1 / span 2; }
.data-card { padding: 20px 17px; }
.data-card header { grid-template-columns: 24px 1fr; }
.status-badge { grid-column: 2; justify-self: start; }
+70 -6
View File
@@ -145,12 +145,7 @@ async function loadDashboard() {
setBadge("task", "НЕ ЗАГРУЖЕНА", "neutral");
setText("task-message", "Укажите задачу для проверки относительно активного фокуса.");
}
if (context["prospective-id"]) work.push(loadProspective(context["prospective-id"]));
else {
setSignal("prospective", "не выбрано", "idle");
setBadge("prospective", "НЕ ЗАГРУЖЕНА", "neutral");
setText("prospective-message", "Укажите идентификатор ProspectiveItem для проверки владельца и состояния.");
}
work.push(loadProspectiveRegister(context["prospective-id"]));
if (context["capability-id"]) work.push(loadCapability(context["capability-id"]));
else setSignal("capability", "не выбрана", "idle");
@@ -195,6 +190,75 @@ async function loadTask(taskID, query) {
}
}
async function loadProspectiveRegister(selectedID) {
const list = byId("prospective-list");
list.replaceChildren();
const loading = document.createElement("p");
loading.className = "register-empty";
loading.textContent = "Загружаю владельческий реестр…";
list.append(loading);
setSignal("prospective", "загрузка", "loading");
try {
const result = await request("/api/prospective-items?limit=25");
const items = Array.isArray(result.Items) ? result.Items : [];
setText("prospective-count", `${items.length} ${items.length === 1 ? "ЗАПИСЬ" : "ЗАПИСЕЙ"}`);
list.replaceChildren();
if (items.length === 0) {
const empty = document.createElement("p");
empty.className = "register-empty";
empty.textContent = "Для текущего Principal ожиданий нет.";
list.append(empty);
setSignal("prospective", "пусто", "idle");
setBadge("prospective", "НЕТ ЗАПИСЕЙ", "neutral");
return;
}
let effectiveID = selectedID;
if (!effectiveID) {
effectiveID = items[0].ID;
byId("prospective-id").value = effectiveID;
saveContext();
}
items.forEach((item) => list.append(createProspectiveRow(item, effectiveID)));
await loadProspective(effectiveID);
} catch (error) {
list.replaceChildren();
const failed = document.createElement("p");
failed.className = "register-empty";
failed.textContent = error.message;
list.append(failed);
setText("prospective-count", "ОШИБКА");
setSignal("prospective", "ошибка", "error");
setBadge("prospective", "НЕДОСТУПНА", "bad");
setText("prospective-message", error.message);
}
}
function createProspectiveRow(item, selectedID) {
const row = document.createElement("button");
row.type = "button";
row.className = "register-row";
row.setAttribute("aria-current", item.ID === selectedID ? "true" : "false");
row.setAttribute("aria-label", `Открыть ожидание ${item.ID}, состояние ${item.State}`);
const id = document.createElement("strong");
id.textContent = item.ID;
const goal = document.createElement("span");
goal.textContent = item.GoalID || "без цели";
const trigger = document.createElement("time");
trigger.textContent = item.TriggerAtUnixMillis ? new Date(item.TriggerAtUnixMillis).toLocaleString("ru-RU") : "без времени";
const state = document.createElement("em");
state.textContent = item.State || "UNKNOWN";
if (["TRIGGERED", "REVALIDATING", "DELIVERED"].includes(item.State)) state.className = "warn";
if (["SUPPRESSED", "EXPIRED"].includes(item.State)) state.className = "bad";
row.append(id, goal, trigger, state);
row.addEventListener("click", async () => {
byId("prospective-id").value = item.ID;
saveContext();
row.parentElement.querySelectorAll(".register-row").forEach((candidate) => candidate.setAttribute("aria-current", candidate === row ? "true" : "false"));
await loadProspective(item.ID);
});
return row;
}
async function loadProspective(prospectiveID) {
setSignal("prospective", "загрузка", "loading");
try {
+11 -1
View File
@@ -60,7 +60,7 @@
<input id="workspace-id" autocomplete="off" placeholder="workspace-id">
<label for="task-id">Задача</label>
<input id="task-id" autocomplete="off" placeholder="task-id">
<label for="prospective-id">Ожидание</label>
<label for="prospective-id">Выбранное ожидание</label>
<input id="prospective-id" autocomplete="off" placeholder="prospective-item-id">
<label for="capability-id">Возможность</label>
<input id="capability-id" autocomplete="off" value="cognitive-proposal">
@@ -90,6 +90,16 @@
<div><span>ВОЗМОЖНОСТЬ</span><strong id="capability-signal" data-state="idle">ожидает</strong></div>
</section>
<section class="register-panel" aria-labelledby="register-title">
<header>
<div><p>Continuity / ближайшие</p><h2 id="register-title">Реестр ожиданий</h2></div>
<span id="prospective-count">0 ЗАПИСЕЙ</span>
</header>
<div id="prospective-list" class="register-list">
<p class="register-empty">Реестр ещё не загружен.</p>
</div>
</section>
<section class="data-grid">
<article class="data-card focus-card">
<header><span class="card-number">01</span><div><p>Situation</p><h2>Активный фокус</h2></div><span id="focus-badge" class="status-badge neutral">НЕ ЗАГРУЖЕН</span></header>
@@ -0,0 +1,17 @@
{
"task_id": "DEV-000029",
"task_revision": 1,
"architecture_revision": "bootstrap-1",
"base_repo_revision": "2877571bb3ad8484cc3efaa0cf5b9e3a3311bc60",
"risk_tier": "C",
"intent": "Expose the bounded Creator-owned Continuity list through the narrow gateway and add a selectable upcoming-items register to the Domovoy console.",
"allowed_change_scope": {"paths": ["cmd/creator-web/**", "tests/creator_web/**", "dev/tasks/DEV-000029-continuity-register-ui.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "modules/**", "contracts/**", "adapters/**", "cmd/core-runtime/**", "cmd/continuity-runtime/**", "cmd/commit-runtime/**", "deploy/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0},
"contracts": {"consume": ["DEV-000028 Creator Continuity list"], "may_change": []},
"semantic_owner": "none-interface-adapter",
"assumptions": ["The first 25 trigger-ordered items are sufficient for the V1 operator register", "Explicit item selection remains browser-local non-secret context"],
"acceptance_scenarios": ["gateway exposes only GET for the Continuity list", "register renders only items returned for the authenticated Principal", "selecting a row loads the canonical item detail", "empty and error states are explicit", "desktop and mobile layouts remain usable"],
"required_tests": ["unit", "gateway allowlist", "runtime HTTP", "responsive browser", "architecture", "remote Docker smoke"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
+4 -1
View File
@@ -57,5 +57,8 @@ Post-ContinuityJson "http://localhost:8081/v1/prospective-items" @{
}
$prospective = Invoke-RestMethod -Uri "$BaseURL/api/prospective-items/$prospectiveID" -Method Get -Headers @{Authorization="Bearer $($issued.Token)"}
if ($prospective.ID -ne $prospectiveID -or $prospective.State -ne "ARMED") { throw "ProspectiveItem was not returned through the gateway" }
$prospectiveList = Invoke-RestMethod -Uri "$BaseURL/api/prospective-items?limit=25" -Method Get -Headers @{Authorization="Bearer $($issued.Token)"}
if (-not $prospectiveList.Items -or $prospectiveList.Items.Count -gt 25) { throw "ProspectiveItem register response is invalid" }
if ($prospectiveList.Items | Where-Object OwnerPrincipalID -ne "domovoy-creator-principal") { throw "ProspectiveItem register leaked another Principal" }
Write-Output "DEV-000027 remote Creator Continuity console: PASS"
Write-Output "DEV-000029 Creator Continuity register: PASS"