Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55f2f9affa | ||
|
|
e47e033dda | ||
|
|
2dc5587fd1 | ||
|
|
992c84240b | ||
|
|
cfeb6aeba7 | ||
|
|
f0d45ca9f1 | ||
|
|
e20c9fdfc0 | ||
|
|
cb50a120e5 | ||
|
|
b30eb38eca | ||
|
|
2083096aed | ||
|
|
671ecb4968 | ||
|
|
af9d5809e9 | ||
|
|
990928618d | ||
|
|
9fac8284f5 |
@@ -67,14 +67,26 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px;
|
||||
.side-note small, .ai-info-card p, .ai-data-form small { display: block; margin-top: 9px; color: var(--muted); line-height: 1.45; }
|
||||
.chat-main { min-width: 0; min-height: 0; overflow: hidden; display: grid; }
|
||||
.surface-panel { min-height: 0; position: relative; overflow-y: auto; }
|
||||
.active-panel { display: grid; grid-template-rows: minmax(0, 1fr) auto auto; min-height: 0; }
|
||||
.messages { width: min(100%, 920px); min-height: 0; margin: 0 auto; padding: 28px 18px; display: flex; flex-direction: column; gap: 14px; overflow-y: auto; }
|
||||
.message { max-width: min(720px, 92%); padding: 14px 16px; border: 1px solid var(--line); border-radius: var(--radius); line-height: 1.45; white-space: pre-wrap; }
|
||||
.message.user { align-self: flex-end; background: #151e1a; }
|
||||
.message.assistant { align-self: flex-start; background: #0f1613; }
|
||||
.active-panel { display: grid; grid-template-rows: minmax(0, 1fr) auto auto; min-height: 0; background: radial-gradient(circle at 50% 0%, rgba(201, 168, 95, .045), transparent 42%); }
|
||||
.messages { width: min(100%, 920px); min-height: 0; margin: 0 auto; padding: clamp(28px, 5vh, 56px) 18px 24px; display: flex; flex-direction: column; gap: 14px; overflow-y: auto; scrollbar-color: #39483f transparent; }
|
||||
.message { max-width: min(720px, 92%); padding: 13px 16px 14px; border: 1px solid var(--line); border-radius: var(--radius); line-height: 1.45; }
|
||||
.message.user { align-self: flex-end; background: #18221c; border-color: #3a493d; }
|
||||
.message.assistant { align-self: flex-start; background: #111a15; box-shadow: 0 8px 20px rgba(0, 0, 0, .12); }
|
||||
.message-head { display: flex; align-items: baseline; gap: 9px; margin-bottom: 8px; }
|
||||
.message-role { color: var(--signal); font: 600 10px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
|
||||
.message.user .message-role { color: var(--good); }
|
||||
.message-time { color: #68776d; font: 9px/1 var(--mono); }
|
||||
.message-content { white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.message small { display: block; margin-top: 8px; color: var(--muted); font: 10px/1 var(--mono); text-transform: uppercase; }
|
||||
.composer { width: min(100%, 920px); margin: 0 auto; padding: 16px 18px 10px; display: grid; grid-template-columns: minmax(0, 1fr) 130px; gap: 12px; background: linear-gradient(transparent, var(--base) 20%); }
|
||||
.composer button { height: 48px; align-self: end; }
|
||||
.composer { width: min(100%, 920px); margin: 0 auto; padding: 10px 18px 8px; display: grid; grid-template-columns: minmax(0, 1fr) 130px; gap: 12px; border-top: 1px solid rgba(42, 53, 47, .85); background: linear-gradient(var(--base), rgba(11, 16, 14, .98)); }
|
||||
.composer-input-wrap { min-width: 0; border: 1px solid #3a493f; border-radius: 5px; background: #101813; transition: border-color .15s ease, box-shadow .15s ease; }
|
||||
.composer-input-wrap:focus-within { border-color: var(--signal); box-shadow: 0 0 0 3px rgba(201, 168, 95, .12); }
|
||||
.composer textarea { min-height: 48px; max-height: 220px; resize: none; border: 0; background: transparent; }
|
||||
.composer-hint { display: flex; justify-content: space-between; gap: 12px; padding: 0 12px 8px; color: #68776d; font: 9px/1 var(--mono); }
|
||||
.composer-hint output { color: var(--muted); white-space: nowrap; }
|
||||
.composer button { height: 48px; align-self: end; display: inline-flex; align-items: center; justify-content: space-between; gap: 8px; padding-inline: 15px; }
|
||||
.composer button[aria-busy="true"] { color: #4b432d; background: #ad9353; }
|
||||
.composer button span:last-child { opacity: .65; font: 16px/1 var(--mono); }
|
||||
.composer button:disabled { cursor: wait; filter: saturate(.3); }
|
||||
.media-tools { width: min(100%, 920px); margin: 0 auto; padding: 0 18px 10px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; color: var(--muted); font: 10px/1 var(--mono); text-transform: uppercase; letter-spacing: .06em; }
|
||||
.media-tools label { display: inline-flex; align-items: center; gap: 6px; min-height: 30px; padding: 0 8px; border: 1px solid var(--line); color: var(--ink); cursor: pointer; }
|
||||
@@ -107,8 +119,9 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px;
|
||||
.empty-state.error { color: var(--bad); }
|
||||
@media (max-width: 640px) {
|
||||
.chat-view { height: auto; min-height: 100vh; overflow: visible; display: block; }
|
||||
.composer { grid-template-columns: 1fr; }
|
||||
.composer { grid-template-columns: 1fr; padding-inline: 12px; }
|
||||
.composer button { width: 100%; }
|
||||
.composer-hint { font-size: 8px; }
|
||||
.conversation-starters { align-items: stretch; }
|
||||
.conversation-starters span { width: 100%; }
|
||||
.chat-header { gap: 10px; padding: 0 12px; }
|
||||
@@ -117,8 +130,12 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px;
|
||||
.chat-header > .operator-link, .build-meta { display: none; }
|
||||
.chat-header > #logout { flex: 0 0 38px; }
|
||||
.chat-layout { height: auto; display: block; }
|
||||
.chat-layout { display: flex; flex-direction: column; }
|
||||
.chat-main { order: 1; }
|
||||
.chat-side, .chat-main { min-height: auto; max-height: none; overflow: visible; }
|
||||
.chat-left, .chat-right { border: 0; border-bottom: 1px solid var(--line); }
|
||||
.chat-left { order: 2; }
|
||||
.chat-right { order: 3; }
|
||||
.chat-main { height: 70vh; min-height: 520px; }
|
||||
.chat-left { padding-bottom: 20px; }
|
||||
.chat-right { padding-top: 20px; }
|
||||
|
||||
@@ -6,6 +6,8 @@ const messages = document.querySelector("#messages");
|
||||
const chatForm = document.querySelector("#chat-form");
|
||||
const messageInput = document.querySelector("#message");
|
||||
const sendButton = document.querySelector("#send");
|
||||
const sendLabel = document.querySelector("#send-label");
|
||||
const messageCount = document.querySelector("#message-count");
|
||||
const logoutButton = document.querySelector("#logout");
|
||||
const activeFocus = document.querySelector("#active-focus");
|
||||
const cognitiveStatus = document.querySelector("#cognitive-status");
|
||||
@@ -24,6 +26,13 @@ let conversationID = sessionStorage.getItem("domovoy.chat.conversation") || "";
|
||||
let localHistory = [];
|
||||
let cognitiveStatusTimer = null;
|
||||
|
||||
function resizeComposerInput() {
|
||||
if (!messageInput) return;
|
||||
messageInput.style.height = "auto";
|
||||
messageInput.style.height = `${Math.min(messageInput.scrollHeight, 220)}px`;
|
||||
if (messageCount) messageCount.textContent = `${messageInput.value.length.toLocaleString("ru-RU")} / 16 384`;
|
||||
}
|
||||
|
||||
class ChatApiError extends Error {
|
||||
constructor(status, detail = "") {
|
||||
super(detail || `chat api returned ${status}`);
|
||||
@@ -95,6 +104,7 @@ function showChat() {
|
||||
}
|
||||
startCognitiveStatusPolling();
|
||||
messageInput.focus();
|
||||
resizeComposerInput();
|
||||
}
|
||||
|
||||
function showLogin() {
|
||||
@@ -106,7 +116,22 @@ function showLogin() {
|
||||
function appendMessage(kind, text, meta = "", media = null) {
|
||||
const node = document.createElement("div");
|
||||
node.className = `message ${kind}`;
|
||||
node.textContent = text;
|
||||
const head = document.createElement("div");
|
||||
head.className = "message-head";
|
||||
const role = document.createElement("strong");
|
||||
role.className = "message-role";
|
||||
role.textContent = kind === "user" ? "Вы" : "Домовой";
|
||||
const time = document.createElement("time");
|
||||
time.className = "message-time";
|
||||
const now = new Date();
|
||||
time.dateTime = now.toISOString();
|
||||
time.textContent = now.toLocaleTimeString("ru-RU", {hour: "2-digit", minute: "2-digit"});
|
||||
head.append(role, time);
|
||||
node.appendChild(head);
|
||||
const content = document.createElement("div");
|
||||
content.className = "message-content";
|
||||
content.textContent = text;
|
||||
node.appendChild(content);
|
||||
if (meta) {
|
||||
const small = document.createElement("small");
|
||||
small.textContent = meta;
|
||||
@@ -279,6 +304,9 @@ async function sendChatMessage(text, options = {}) {
|
||||
if (!clean) return;
|
||||
if (!options.silentUser) appendMessage("user", clean);
|
||||
sendButton.disabled = true;
|
||||
sendButton.setAttribute("aria-busy", "true");
|
||||
if (sendLabel) sendLabel.textContent = "Домовой обрабатывает";
|
||||
messages.setAttribute("aria-busy", "true");
|
||||
try {
|
||||
const result = await api("/api/chat/messages", {method: "POST", body: JSON.stringify({conversation_id: conversationID, message: clean})});
|
||||
conversationID = result.conversation_id;
|
||||
@@ -299,6 +327,9 @@ async function sendChatMessage(text, options = {}) {
|
||||
}
|
||||
} finally {
|
||||
sendButton.disabled = false;
|
||||
sendButton.removeAttribute("aria-busy");
|
||||
if (sendLabel) sendLabel.textContent = "Отправить";
|
||||
messages.removeAttribute("aria-busy");
|
||||
messageInput.focus();
|
||||
}
|
||||
}
|
||||
@@ -334,9 +365,12 @@ chatForm.addEventListener("submit", async (event) => {
|
||||
const text = messageInput.value.trim();
|
||||
if (!text) return;
|
||||
messageInput.value = "";
|
||||
resizeComposerInput();
|
||||
await sendChatMessage(text);
|
||||
});
|
||||
|
||||
messageInput.addEventListener("input", resizeComposerInput);
|
||||
|
||||
async function processMedia(input, path, label) {
|
||||
const file = input?.files?.[0];
|
||||
if (!file || !mediaStatus) return;
|
||||
@@ -405,3 +439,4 @@ if (token) {
|
||||
} else {
|
||||
showLogin();
|
||||
}
|
||||
resizeComposerInput();
|
||||
|
||||
@@ -57,8 +57,11 @@
|
||||
<section id="chat-panel" class="surface-panel active-panel">
|
||||
<div id="messages" class="messages" aria-live="polite"></div>
|
||||
<form id="chat-form" class="composer">
|
||||
<textarea id="message" rows="3" maxlength="16384" placeholder="Напишите Домовому обычное сообщение..." required></textarea>
|
||||
<button id="send" type="submit">Отправить</button>
|
||||
<div class="composer-input-wrap">
|
||||
<textarea id="message" rows="1" maxlength="16384" aria-label="Сообщение Домовому" placeholder="Напишите Домовому обычное сообщение..." required></textarea>
|
||||
<div class="composer-hint"><span>Enter — отправить · Shift+Enter — новая строка</span><output id="message-count" aria-live="polite">0 / 16384</output></div>
|
||||
</div>
|
||||
<button id="send" type="submit"><span id="send-label">Отправить</span><span aria-hidden="true">↵</span></button>
|
||||
</form>
|
||||
<div class="media-tools" aria-label="Мультимодальные сообщения">
|
||||
<label>Аудио <input id="audio-input" type="file" accept="audio/*"></label>
|
||||
|
||||
+10
-1
@@ -32,7 +32,7 @@ func main() {
|
||||
lifeboat := supervisor.NewPersistentLifeboat(store)
|
||||
if _, err := lifeboat.Snapshot(); errors.Is(err, os.ErrNotExist) {
|
||||
baseline := supervisor.Release{ID: envOr("SUPERVISOR_BASELINE_RELEASE", "bootstrap"), Version: envOr("SUPERVISOR_BASELINE_RELEASE", "bootstrap")}
|
||||
if ok, startErr := lifeboat.Start(baseline); startErr != nil || !ok {
|
||||
if ok, startErr := lifeboat.EstablishBaseline(baseline); startErr != nil || !ok {
|
||||
fatal(fmt.Errorf("initialize baseline: %v", startErr))
|
||||
}
|
||||
if ok, observeErr := lifeboat.Record(supervisor.Observation{ReleaseID: baseline.ID, Ready: true, Reason: "supervisor baseline"}); observeErr != nil || !ok {
|
||||
@@ -53,6 +53,7 @@ func main() {
|
||||
control := http.NewServeMux()
|
||||
control.HandleFunc("GET /v1/state", s.state)
|
||||
control.HandleFunc("POST /v1/releases/start", s.start)
|
||||
control.HandleFunc("POST /v1/releases/baseline", s.baseline)
|
||||
control.HandleFunc("POST /v1/releases/observe", s.observe)
|
||||
control.HandleFunc("POST /v1/releases/promote", s.promote)
|
||||
control.HandleFunc("POST /v1/releases/rollback", s.rollback)
|
||||
@@ -141,6 +142,14 @@ func (s *server) start(w http.ResponseWriter, r *http.Request) {
|
||||
ok, err := s.lifeboat.Start(release)
|
||||
respond(w, ok, err)
|
||||
}
|
||||
func (s *server) baseline(w http.ResponseWriter, r *http.Request) {
|
||||
var release supervisor.Release
|
||||
if !decode(w, r, &release) {
|
||||
return
|
||||
}
|
||||
ok, err := s.lifeboat.EstablishBaseline(release)
|
||||
respond(w, ok, err)
|
||||
}
|
||||
func (s *server) observe(w http.ResponseWriter, r *http.Request) {
|
||||
var observation supervisor.Observation
|
||||
if !decode(w, r, &observation) {
|
||||
|
||||
@@ -29,6 +29,7 @@ for attempt in $(seq 1 30); do
|
||||
sleep 2
|
||||
done
|
||||
|
||||
control /v1/releases/baseline -H 'Content-Type: application/json' --data "{\"id\":\"stable-$REVISION\",\"version\":\"$REVISION\"}" || fail_rollback
|
||||
control /v1/releases/start -H 'Content-Type: application/json' --data "{\"id\":\"candidate-$REVISION\",\"version\":\"$REVISION\"}" || fail_rollback
|
||||
control /v1/releases/observe -H 'Content-Type: application/json' --data "{\"release_id\":\"candidate-$REVISION\",\"ready\":true,\"reason\":\"candidate healthcheck passed\",\"observed_at_unix_millis\":$(date +%s%3N)}" || fail_rollback
|
||||
control /v1/traffic/canary || fail_rollback
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"task_file":"dev/tasks/GEN2-DEPLOY-001-supervisor-and-production-rollback.json","task_id":"GEN2-DEPLOY-001","task_revision":1}
|
||||
{"task_file":"dev/tasks/GEN2-027-production-canary-and-controlled-gen2-traffic-transition.json","task_id":"GEN2-027","task_revision":1}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"evidence_id": "gen2-deploy-001-production-recovery-2026-08-29",
|
||||
"task_id": "GEN2-DEPLOY-001",
|
||||
"production_revision_before": "36fc8ec",
|
||||
"production_revision_after": "2083096aedaec73191e0bc9e2566f596e1810d82",
|
||||
"topology": {
|
||||
"supervisor": "assistant-supervisor-1",
|
||||
"public_traffic": "Supervisor reverse proxy on creator web host port",
|
||||
"control_api": "Supervisor loopback-only host control port",
|
||||
"durable_state": "assistant_assistant_supervisor_state:/var/lib/supervisor/release-state.json",
|
||||
"candidate": "assistant-creator-web-candidate-1"
|
||||
},
|
||||
"canary": {
|
||||
"candidate_health": "healthy",
|
||||
"header_route": "X-Assistant-Canary=true -> candidate",
|
||||
"header_route_http": 200,
|
||||
"promoted": true
|
||||
},
|
||||
"rollback": {
|
||||
"drill": "candidate container stopped, Supervisor traffic rollback and release rollback invoked",
|
||||
"stable_http": 200,
|
||||
"stable_target_header": "stable",
|
||||
"supervisor_restart": "healthy; traffic_mode=STABLE and stable release state restored"
|
||||
},
|
||||
"postgres_recovery": {
|
||||
"restart": "healthy",
|
||||
"public_healthz": 200,
|
||||
"public_readyz": 200
|
||||
},
|
||||
"cognitive_lifeboat": {
|
||||
"previous_status": "restart loop/unhealthy",
|
||||
"cause": "configured Qwen3-8B-Q4_K_M.gguf was absent from llm-local-models volume",
|
||||
"remediation": "compose model reference changed to existing Qwen3.8-27B-UD-Q2_K_XL.gguf",
|
||||
"current_status": "healthy",
|
||||
"role": "local degraded cognitive provider; not Supervisor"
|
||||
},
|
||||
"smoke": {
|
||||
"public_healthz": 200,
|
||||
"public_readyz": 200,
|
||||
"public_login": 200,
|
||||
"authenticated_household": "UNVERIFIED: browser session was unauthenticated; no credentials were read or entered"
|
||||
},
|
||||
"interpretation": "Supervisor independence, durable release state, controlled header canary, stable rollback and restart recovery are production-observed. Authenticated household acceptance remains an explicit gap."
|
||||
}
|
||||
@@ -27,3 +27,12 @@ func TestGen2ContextDoesNotProjectAnotherPrincipal(t *testing.T) {
|
||||
t.Fatalf("scoped context=%#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGen2ContextProjectsExplicitRememberedAssertion(t *testing.T) {
|
||||
got := domain.BuildScopedGen2Context([]interaction.HumanUtterance{
|
||||
{ID: "assertion", PrincipalID: "principal-1", Content: "Для этого разговора запомни: мой тестовый цвет — синий.", ReceivedAtUnixMillis: 1},
|
||||
}, "principal-1")
|
||||
if got.Source != "retrieval:interaction-utterances" || got.UsedPersonalMemoryFallback || !strings.Contains(got.Text, "тестовый цвет — синий") {
|
||||
t.Fatalf("remembered assertion was not projected: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,5 +123,31 @@ func personalPreference(message string) string {
|
||||
return value
|
||||
}
|
||||
}
|
||||
// Explicit remembered assertions are still human-provided context, not
|
||||
// verified facts. Project the compact key/value statement without storing a
|
||||
// second copy outside the canonical utterance history.
|
||||
if index := strings.Index(lower, "запомни"); index >= 0 {
|
||||
remembered := strings.TrimSpace(lower[index+len("запомни"):])
|
||||
remembered = strings.TrimLeft(remembered, " :—-")
|
||||
for _, prefix := range []string{"мой ", "моя ", "моё ", "мое "} {
|
||||
if !strings.HasPrefix(remembered, prefix) {
|
||||
continue
|
||||
}
|
||||
statement := strings.TrimSpace(remembered[len(prefix):])
|
||||
separator := strings.IndexAny(statement, "—-:")
|
||||
if separator <= 0 || separator >= len(statement)-1 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(statement[:separator])
|
||||
separatorWidth := 1
|
||||
if strings.HasPrefix(statement[separator:], "—") {
|
||||
separatorWidth = len("—")
|
||||
}
|
||||
value := strings.Trim(strings.TrimSpace(statement[separator+separatorWidth:]), " .,!?:;—-")
|
||||
if key != "" && len([]rune(value)) >= 2 {
|
||||
return key + " — " + value
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -91,6 +91,10 @@ func (p *PersistentLifeboat) transition(fn func(*Lifeboat) bool) (bool, error) {
|
||||
func (p *PersistentLifeboat) Start(candidate Release) (bool, error) {
|
||||
return p.transition(func(value *Lifeboat) bool { return value.Start(candidate) })
|
||||
}
|
||||
|
||||
func (p *PersistentLifeboat) EstablishBaseline(release Release) (bool, error) {
|
||||
return p.transition(func(value *Lifeboat) bool { return value.EstablishBaseline(release) })
|
||||
}
|
||||
func (p *PersistentLifeboat) Record(observation Observation) (bool, error) {
|
||||
return p.transition(func(value *Lifeboat) bool { return value.Record(observation) })
|
||||
}
|
||||
|
||||
@@ -18,24 +18,25 @@ const (
|
||||
)
|
||||
|
||||
type Release struct {
|
||||
ID string
|
||||
Version string
|
||||
State ReleaseState
|
||||
LastKnownGood bool
|
||||
ID string `json:"id"`
|
||||
Version string `json:"version"`
|
||||
State ReleaseState `json:"state"`
|
||||
LastKnownGood bool `json:"last_known_good"`
|
||||
}
|
||||
type Observation struct {
|
||||
ReleaseID string
|
||||
Ready bool
|
||||
Reason string
|
||||
ObservedAtUnixMillis int64
|
||||
ReleaseID string `json:"release_id"`
|
||||
Ready bool `json:"ready"`
|
||||
Reason string `json:"reason"`
|
||||
ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"`
|
||||
}
|
||||
type Lifeboat struct {
|
||||
Current Release
|
||||
LastKnownGood Release
|
||||
Observations []Observation
|
||||
TrafficMode TrafficMode
|
||||
StableUpstream string
|
||||
CandidateUpstream string
|
||||
Current Release `json:"current"`
|
||||
LastKnownGood Release `json:"last_known_good"`
|
||||
RollbackTarget Release `json:"rollback_target"`
|
||||
Observations []Observation `json:"observations"`
|
||||
TrafficMode TrafficMode `json:"traffic_mode"`
|
||||
StableUpstream string `json:"stable_upstream"`
|
||||
CandidateUpstream string `json:"candidate_upstream"`
|
||||
}
|
||||
|
||||
func (l *Lifeboat) Start(candidate Release) bool {
|
||||
@@ -43,9 +44,25 @@ func (l *Lifeboat) Start(candidate Release) bool {
|
||||
return false
|
||||
}
|
||||
candidate.State = ReleaseCandidate
|
||||
if l.LastKnownGood.ID != "" {
|
||||
l.RollbackTarget = l.LastKnownGood
|
||||
}
|
||||
l.Current = candidate
|
||||
return true
|
||||
}
|
||||
|
||||
func (l *Lifeboat) EstablishBaseline(release Release) bool {
|
||||
if release.ID == "" || release.Version == "" {
|
||||
return false
|
||||
}
|
||||
release.State = ReleaseHealthy
|
||||
release.LastKnownGood = true
|
||||
l.Current = release
|
||||
l.LastKnownGood = release
|
||||
l.RollbackTarget = Release{}
|
||||
l.TrafficMode = TrafficStable
|
||||
return true
|
||||
}
|
||||
func (l *Lifeboat) Record(o Observation) bool {
|
||||
if o.ReleaseID == "" {
|
||||
return false
|
||||
@@ -64,11 +81,18 @@ func (l *Lifeboat) Record(o Observation) bool {
|
||||
return true
|
||||
}
|
||||
func (l *Lifeboat) Rollback() bool {
|
||||
if l.LastKnownGood.ID == "" {
|
||||
target := l.RollbackTarget
|
||||
if target.ID == "" {
|
||||
target = l.LastKnownGood
|
||||
}
|
||||
if target.ID == "" {
|
||||
return false
|
||||
}
|
||||
l.Current = l.LastKnownGood
|
||||
l.Current = target
|
||||
l.Current.State = ReleaseRolledBack
|
||||
l.Current.LastKnownGood = true
|
||||
l.LastKnownGood = l.Current
|
||||
l.RollbackTarget = Release{}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user