huskies: merge 776
This commit is contained in:
@@ -146,22 +146,6 @@ export const gatewayApi = {
|
|||||||
return gatewayRequest<GatewayInfo>("/api/gateway");
|
return gatewayRequest<GatewayInfo>("/api/gateway");
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Add a new project to the gateway config.
|
|
||||||
addProject(name: string, url: string): Promise<GatewayProject> {
|
|
||||||
return gatewayRequest<GatewayProject>("/api/gateway/projects", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ name, url }),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Remove a project from the gateway config.
|
|
||||||
removeProject(name: string): Promise<void> {
|
|
||||||
return gatewayRequest<void>(
|
|
||||||
`/api/gateway/projects/${encodeURIComponent(name)}`,
|
|
||||||
{ method: "DELETE" },
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Send a heartbeat for an agent to update its last-seen timestamp.
|
/// Send a heartbeat for an agent to update its last-seen timestamp.
|
||||||
heartbeat(id: string): Promise<void> {
|
heartbeat(id: string): Promise<void> {
|
||||||
return gatewayRequest<void>(`/gateway/agents/${id}/heartbeat`, {
|
return gatewayRequest<void>(`/gateway/agents/${id}/heartbeat`, {
|
||||||
|
|||||||
@@ -368,11 +368,6 @@ export function GatewayPanel() {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [pipeline, setPipeline] = useState<AllProjectsPipeline | null>(null);
|
const [pipeline, setPipeline] = useState<AllProjectsPipeline | null>(null);
|
||||||
|
|
||||||
// Add-project form state
|
|
||||||
const [newProjectName, setNewProjectName] = useState("");
|
|
||||||
const [newProjectUrl, setNewProjectUrl] = useState("");
|
|
||||||
const [addingProject, setAddingProject] = useState(false);
|
|
||||||
|
|
||||||
// Keep stable refs so polling intervals don't recreate on state changes.
|
// Keep stable refs so polling intervals don't recreate on state changes.
|
||||||
const setAgentsRef = useRef(setAgents);
|
const setAgentsRef = useRef(setAgents);
|
||||||
setAgentsRef.current = setAgents;
|
setAgentsRef.current = setAgents;
|
||||||
@@ -447,24 +442,6 @@ export function GatewayPanel() {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleAddProject = useCallback(async () => {
|
|
||||||
const name = newProjectName.trim();
|
|
||||||
const url = newProjectUrl.trim();
|
|
||||||
if (!name || !url) return;
|
|
||||||
setAddingProject(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const created = await gatewayApi.addProject(name, url);
|
|
||||||
setProjects((prev) => [...prev, created]);
|
|
||||||
setNewProjectName("");
|
|
||||||
setNewProjectUrl("");
|
|
||||||
} catch (e) {
|
|
||||||
setError(e instanceof Error ? e.message : String(e));
|
|
||||||
} finally {
|
|
||||||
setAddingProject(false);
|
|
||||||
}
|
|
||||||
}, [newProjectName, newProjectUrl]);
|
|
||||||
|
|
||||||
const handleSwitchProject = useCallback(async (name: string) => {
|
const handleSwitchProject = useCallback(async (name: string) => {
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
@@ -481,18 +458,6 @@ export function GatewayPanel() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleRemoveProject = useCallback(async (name: string) => {
|
|
||||||
if (!window.confirm(`Remove project "${name}"? This cannot be undone.`)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
await gatewayApi.removeProject(name);
|
|
||||||
setProjects((prev) => prev.filter((p) => p.name !== name));
|
|
||||||
} catch (e) {
|
|
||||||
setError(e instanceof Error ? e.message : String(e));
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -657,97 +622,8 @@ export function GatewayPanel() {
|
|||||||
<div style={{ fontWeight: 600, color: "#e6edf3" }}>{p.name}</div>
|
<div style={{ fontWeight: 600, color: "#e6edf3" }}>{p.name}</div>
|
||||||
<div style={{ fontSize: "0.8em", color: "#8b949e" }}>{p.url}</div>
|
<div style={{ fontSize: "0.8em", color: "#8b949e" }}>{p.url}</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
</div>
|
||||||
type="button"
|
|
||||||
data-testid={`remove-project-${p.name}`}
|
|
||||||
onClick={() => handleRemoveProject(p.name)}
|
|
||||||
style={{
|
|
||||||
fontSize: "0.8em",
|
|
||||||
padding: "4px 10px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
border: "1px solid #f85149",
|
|
||||||
background: "none",
|
|
||||||
color: "#f85149",
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Add project form */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
marginTop: "12px",
|
|
||||||
display: "flex",
|
|
||||||
gap: "8px",
|
|
||||||
alignItems: "flex-end",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ flex: "1 1 140px" }}>
|
|
||||||
<div style={{ fontSize: "0.75em", color: "#8b949e", marginBottom: "4px" }}>
|
|
||||||
Name
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
data-testid="new-project-name"
|
|
||||||
type="text"
|
|
||||||
placeholder="my-project"
|
|
||||||
value={newProjectName}
|
|
||||||
onChange={(e) => setNewProjectName(e.target.value)}
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
padding: "6px 10px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
border: "1px solid #30363d",
|
|
||||||
background: "#0d1117",
|
|
||||||
color: "#e6edf3",
|
|
||||||
fontSize: "0.85em",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ flex: "2 1 200px" }}>
|
|
||||||
<div style={{ fontSize: "0.75em", color: "#8b949e", marginBottom: "4px" }}>
|
|
||||||
Container URL
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
data-testid="new-project-url"
|
|
||||||
type="text"
|
|
||||||
placeholder="http://localhost:3001"
|
|
||||||
value={newProjectUrl}
|
|
||||||
onChange={(e) => setNewProjectUrl(e.target.value)}
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
padding: "6px 10px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
border: "1px solid #30363d",
|
|
||||||
background: "#0d1117",
|
|
||||||
color: "#e6edf3",
|
|
||||||
fontSize: "0.85em",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
data-testid="add-project-button"
|
|
||||||
onClick={handleAddProject}
|
|
||||||
disabled={addingProject || !newProjectName.trim() || !newProjectUrl.trim()}
|
|
||||||
style={{
|
|
||||||
padding: "6px 14px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
border: "1px solid #238636",
|
|
||||||
background: addingProject ? "#1a2f1a" : "#238636",
|
|
||||||
color: "#fff",
|
|
||||||
cursor: addingProject ? "not-allowed" : "pointer",
|
|
||||||
fontWeight: 600,
|
|
||||||
fontSize: "0.85em",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{addingProject ? "Adding…" : "Add Project"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|||||||
@@ -35,11 +35,13 @@ fn list_id_at<T: CrdtNode>(list: &ListCrdt<T>, idx: usize) -> Option<OpId> {
|
|||||||
|
|
||||||
use super::state::{
|
use super::state::{
|
||||||
apply_and_persist, get_crdt, rebuild_active_agent_index, rebuild_agent_throttle_index,
|
apply_and_persist, get_crdt, rebuild_active_agent_index, rebuild_agent_throttle_index,
|
||||||
rebuild_merge_job_index, rebuild_test_job_index, rebuild_token_index,
|
rebuild_gateway_project_index, rebuild_merge_job_index, rebuild_test_job_index,
|
||||||
|
rebuild_token_index,
|
||||||
};
|
};
|
||||||
use super::types::{
|
use super::types::{
|
||||||
ActiveAgentCrdt, ActiveAgentView, AgentThrottleCrdt, AgentThrottleView, MergeJobCrdt,
|
ActiveAgentCrdt, ActiveAgentView, AgentThrottleCrdt, AgentThrottleView, GatewayProjectCrdt,
|
||||||
MergeJobView, TestJobCrdt, TestJobView, TokenUsageCrdt, TokenUsageView,
|
GatewayProjectView, MergeJobCrdt, MergeJobView, TestJobCrdt, TestJobView, TokenUsageCrdt,
|
||||||
|
TokenUsageView,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── tokens ───────────────────────────────────────────────────────────
|
// ── tokens ───────────────────────────────────────────────────────────
|
||||||
@@ -605,6 +607,91 @@ fn extract_agent_throttle_view(entry: &AgentThrottleCrdt) -> Option<AgentThrottl
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── gateway_projects ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Write or update a gateway-project entry keyed by `name`.
|
||||||
|
///
|
||||||
|
/// If an entry for `name` already exists it is updated in place;
|
||||||
|
/// otherwise a new entry is inserted.
|
||||||
|
pub fn write_gateway_project(name: &str, url: &str) {
|
||||||
|
let Some(state_mutex) = get_crdt() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(mut state) = state_mutex.lock() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(&idx) = state.gateway_project_index.get(name) {
|
||||||
|
apply_and_persist(&mut state, |s| {
|
||||||
|
s.crdt.doc.gateway_projects[idx].url.set(url.to_string())
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let entry: JsonValue = json!({
|
||||||
|
"name": name,
|
||||||
|
"url": url,
|
||||||
|
})
|
||||||
|
.into();
|
||||||
|
apply_and_persist(&mut state, |s| {
|
||||||
|
s.crdt.doc.gateway_projects.insert(ROOT_ID, entry)
|
||||||
|
});
|
||||||
|
state.gateway_project_index = rebuild_gateway_project_index(&state.crdt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read all gateway-project entries.
|
||||||
|
pub fn read_all_gateway_projects() -> Option<Vec<GatewayProjectView>> {
|
||||||
|
let state_mutex = get_crdt()?;
|
||||||
|
let state = state_mutex.lock().ok()?;
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for entry in state.crdt.doc.gateway_projects.iter() {
|
||||||
|
if let Some(v) = extract_gateway_project_view(entry) {
|
||||||
|
out.push(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a single gateway-project entry by `name`.
|
||||||
|
pub fn read_gateway_project(name: &str) -> Option<GatewayProjectView> {
|
||||||
|
let state_mutex = get_crdt()?;
|
||||||
|
let state = state_mutex.lock().ok()?;
|
||||||
|
let &idx = state.gateway_project_index.get(name)?;
|
||||||
|
extract_gateway_project_view(&state.crdt.doc.gateway_projects[idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tombstone a gateway-project entry by `name`.
|
||||||
|
///
|
||||||
|
/// Returns `true` if the entry existed and a delete op was issued.
|
||||||
|
pub fn delete_gateway_project(name: &str) -> bool {
|
||||||
|
let Some(state_mutex) = get_crdt() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Ok(mut state) = state_mutex.lock() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(&idx) = state.gateway_project_index.get(name) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(op_id) = list_id_at(&state.crdt.doc.gateway_projects, idx) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
apply_and_persist(&mut state, |s| s.crdt.doc.gateway_projects.delete(op_id));
|
||||||
|
state.gateway_project_index = rebuild_gateway_project_index(&state.crdt);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_gateway_project_view(entry: &GatewayProjectCrdt) -> Option<GatewayProjectView> {
|
||||||
|
let name = match entry.name.view() {
|
||||||
|
JsonValue::String(s) if !s.is_empty() => s,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
let url = match entry.url.view() {
|
||||||
|
JsonValue::String(s) => s,
|
||||||
|
_ => String::new(),
|
||||||
|
};
|
||||||
|
Some(GatewayProjectView { name, url })
|
||||||
|
}
|
||||||
|
|
||||||
// ── Tests ─────────────────────────────────────────────────────────────
|
// ── Tests ─────────────────────────────────────────────────────────────
|
||||||
//
|
//
|
||||||
// The `tokens` collection is used as the representative collection for the
|
// The `tokens` collection is used as the representative collection for the
|
||||||
|
|||||||
@@ -28,11 +28,12 @@ mod write;
|
|||||||
|
|
||||||
pub use gateway_config::{read_gateway_active_project, write_gateway_active_project};
|
pub use gateway_config::{read_gateway_active_project, write_gateway_active_project};
|
||||||
pub use lww_maps::{
|
pub use lww_maps::{
|
||||||
delete_active_agent, delete_agent_throttle, delete_merge_job, delete_test_job,
|
delete_active_agent, delete_agent_throttle, delete_gateway_project, delete_merge_job,
|
||||||
delete_token_usage, read_active_agent, read_agent_throttle, read_all_active_agents,
|
delete_test_job, delete_token_usage, read_active_agent, read_agent_throttle,
|
||||||
read_all_agent_throttles, read_all_merge_jobs, read_all_test_jobs, read_all_token_usage,
|
read_all_active_agents, read_all_agent_throttles, read_all_gateway_projects,
|
||||||
|
read_all_merge_jobs, read_all_test_jobs, read_all_token_usage, read_gateway_project,
|
||||||
read_merge_job, read_test_job, read_token_usage, write_active_agent, write_agent_throttle,
|
read_merge_job, read_test_job, read_token_usage, write_active_agent, write_agent_throttle,
|
||||||
write_merge_job, write_test_job, write_token_usage,
|
write_gateway_project, write_merge_job, write_test_job, write_token_usage,
|
||||||
};
|
};
|
||||||
pub use ops::{all_ops_json, apply_remote_op, ops_since, our_vector_clock, subscribe_ops};
|
pub use ops::{all_ops_json, apply_remote_op, ops_since, our_vector_clock, subscribe_ops};
|
||||||
pub use presence::{
|
pub use presence::{
|
||||||
@@ -46,9 +47,9 @@ pub use read::{
|
|||||||
pub use state::init;
|
pub use state::init;
|
||||||
pub use types::{
|
pub use types::{
|
||||||
ActiveAgentCrdt, ActiveAgentView, AgentThrottleCrdt, AgentThrottleView, CrdtEvent,
|
ActiveAgentCrdt, ActiveAgentView, AgentThrottleCrdt, AgentThrottleView, CrdtEvent,
|
||||||
GatewayConfigCrdt, MergeJobCrdt, MergeJobView, NodePresenceCrdt, NodePresenceView, PipelineDoc,
|
GatewayConfigCrdt, GatewayProjectCrdt, GatewayProjectView, MergeJobCrdt, MergeJobView,
|
||||||
PipelineItemCrdt, PipelineItemView, TestJobCrdt, TestJobView, TokenUsageCrdt, TokenUsageView,
|
NodePresenceCrdt, NodePresenceView, PipelineDoc, PipelineItemCrdt, PipelineItemView,
|
||||||
subscribe,
|
TestJobCrdt, TestJobView, TokenUsageCrdt, TokenUsageView, subscribe,
|
||||||
};
|
};
|
||||||
pub use write::{
|
pub use write::{
|
||||||
migrate_names_from_slugs, migrate_story_ids_to_numeric, name_from_story_id, write_item,
|
migrate_names_from_slugs, migrate_story_ids_to_numeric, name_from_story_id, write_item,
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ pub(super) struct CrdtState {
|
|||||||
pub(super) test_job_index: HashMap<String, usize>,
|
pub(super) test_job_index: HashMap<String, usize>,
|
||||||
/// Maps node_id → index in the agent_throttle ListCrdt for O(1) lookup.
|
/// Maps node_id → index in the agent_throttle ListCrdt for O(1) lookup.
|
||||||
pub(super) agent_throttle_index: HashMap<String, usize>,
|
pub(super) agent_throttle_index: HashMap<String, usize>,
|
||||||
|
/// Maps project name → index in the gateway_projects ListCrdt for O(1) lookup.
|
||||||
|
pub(super) gateway_project_index: HashMap<String, usize>,
|
||||||
/// Channel sender for fire-and-forget op persistence.
|
/// Channel sender for fire-and-forget op persistence.
|
||||||
pub(super) persist_tx: mpsc::UnboundedSender<SignedOp>,
|
pub(super) persist_tx: mpsc::UnboundedSender<SignedOp>,
|
||||||
/// Max sequence number seen across all ops during init() replay.
|
/// Max sequence number seen across all ops during init() replay.
|
||||||
@@ -161,6 +163,7 @@ pub async fn init(db_path: &Path) -> Result<(), sqlx::Error> {
|
|||||||
let active_agent_index = rebuild_active_agent_index(&crdt);
|
let active_agent_index = rebuild_active_agent_index(&crdt);
|
||||||
let test_job_index = rebuild_test_job_index(&crdt);
|
let test_job_index = rebuild_test_job_index(&crdt);
|
||||||
let agent_throttle_index = rebuild_agent_throttle_index(&crdt);
|
let agent_throttle_index = rebuild_agent_throttle_index(&crdt);
|
||||||
|
let gateway_project_index = rebuild_gateway_project_index(&crdt);
|
||||||
|
|
||||||
// Advance the top-level list clocks to the Lamport floor so that
|
// Advance the top-level list clocks to the Lamport floor so that
|
||||||
// list-level inserts don't re-emit low seq numbers.
|
// list-level inserts don't re-emit low seq numbers.
|
||||||
@@ -171,6 +174,7 @@ pub async fn init(db_path: &Path) -> Result<(), sqlx::Error> {
|
|||||||
crdt.doc.active_agents.advance_seq(lamport_floor);
|
crdt.doc.active_agents.advance_seq(lamport_floor);
|
||||||
crdt.doc.test_jobs.advance_seq(lamport_floor);
|
crdt.doc.test_jobs.advance_seq(lamport_floor);
|
||||||
crdt.doc.agent_throttle.advance_seq(lamport_floor);
|
crdt.doc.agent_throttle.advance_seq(lamport_floor);
|
||||||
|
crdt.doc.gateway_projects.advance_seq(lamport_floor);
|
||||||
crdt.doc
|
crdt.doc
|
||||||
.gateway_config
|
.gateway_config
|
||||||
.active_project
|
.active_project
|
||||||
@@ -228,6 +232,7 @@ pub async fn init(db_path: &Path) -> Result<(), sqlx::Error> {
|
|||||||
active_agent_index,
|
active_agent_index,
|
||||||
test_job_index,
|
test_job_index,
|
||||||
agent_throttle_index,
|
agent_throttle_index,
|
||||||
|
gateway_project_index,
|
||||||
persist_tx,
|
persist_tx,
|
||||||
lamport_floor,
|
lamport_floor,
|
||||||
};
|
};
|
||||||
@@ -271,6 +276,7 @@ pub fn init_for_test() {
|
|||||||
active_agent_index: HashMap::new(),
|
active_agent_index: HashMap::new(),
|
||||||
test_job_index: HashMap::new(),
|
test_job_index: HashMap::new(),
|
||||||
agent_throttle_index: HashMap::new(),
|
agent_throttle_index: HashMap::new(),
|
||||||
|
gateway_project_index: HashMap::new(),
|
||||||
persist_tx,
|
persist_tx,
|
||||||
lamport_floor: 0,
|
lamport_floor: 0,
|
||||||
};
|
};
|
||||||
@@ -385,6 +391,19 @@ pub(super) fn rebuild_agent_throttle_index(crdt: &BaseCrdt<PipelineDoc>) -> Hash
|
|||||||
map
|
map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rebuild the project name → gateway_projects list index.
|
||||||
|
pub(super) fn rebuild_gateway_project_index(
|
||||||
|
crdt: &BaseCrdt<PipelineDoc>,
|
||||||
|
) -> HashMap<String, usize> {
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
for (i, entry) in crdt.doc.gateway_projects.iter().enumerate() {
|
||||||
|
if let JsonValue::String(ref k) = entry.name.view() {
|
||||||
|
map.insert(k.clone(), i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
// ── Write path ───────────────────────────────────────────────────────
|
// ── Write path ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Create a CRDT op via `op_fn`, sign it, apply it, and send it to the
|
/// Create a CRDT op via `op_fn`, sign it, apply it, and send it to the
|
||||||
@@ -597,6 +616,7 @@ mod tests {
|
|||||||
active_agent_index: HashMap::new(),
|
active_agent_index: HashMap::new(),
|
||||||
test_job_index: HashMap::new(),
|
test_job_index: HashMap::new(),
|
||||||
agent_throttle_index: HashMap::new(),
|
agent_throttle_index: HashMap::new(),
|
||||||
|
gateway_project_index: HashMap::new(),
|
||||||
persist_tx,
|
persist_tx,
|
||||||
lamport_floor: 0,
|
lamport_floor: 0,
|
||||||
};
|
};
|
||||||
@@ -669,6 +689,7 @@ mod tests {
|
|||||||
active_agent_index: HashMap::new(),
|
active_agent_index: HashMap::new(),
|
||||||
test_job_index: HashMap::new(),
|
test_job_index: HashMap::new(),
|
||||||
agent_throttle_index: HashMap::new(),
|
agent_throttle_index: HashMap::new(),
|
||||||
|
gateway_project_index: HashMap::new(),
|
||||||
persist_tx,
|
persist_tx,
|
||||||
lamport_floor: 0,
|
lamport_floor: 0,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ pub struct PipelineDoc {
|
|||||||
pub active_agents: ListCrdt<ActiveAgentCrdt>,
|
pub active_agents: ListCrdt<ActiveAgentCrdt>,
|
||||||
pub test_jobs: ListCrdt<TestJobCrdt>,
|
pub test_jobs: ListCrdt<TestJobCrdt>,
|
||||||
pub agent_throttle: ListCrdt<AgentThrottleCrdt>,
|
pub agent_throttle: ListCrdt<AgentThrottleCrdt>,
|
||||||
|
pub gateway_projects: ListCrdt<GatewayProjectCrdt>,
|
||||||
pub gateway_config: GatewayConfigCrdt,
|
pub gateway_config: GatewayConfigCrdt,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,6 +206,16 @@ pub struct AgentThrottleCrdt {
|
|||||||
pub limit: LwwRegisterCrdt<f64>,
|
pub limit: LwwRegisterCrdt<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// CRDT entry for a gateway project registered in `gateway_config.projects`.
|
||||||
|
#[add_crdt_fields]
|
||||||
|
#[derive(Clone, CrdtNode, Debug)]
|
||||||
|
pub struct GatewayProjectCrdt {
|
||||||
|
/// Unique key: project name (e.g. `"huskies"`).
|
||||||
|
pub name: LwwRegisterCrdt<String>,
|
||||||
|
/// Container base URL (e.g. `"http://huskies:3001"`).
|
||||||
|
pub url: LwwRegisterCrdt<String>,
|
||||||
|
}
|
||||||
|
|
||||||
// ── LWW-map view types ───────────────────────────────────────────────
|
// ── LWW-map view types ───────────────────────────────────────────────
|
||||||
|
|
||||||
/// Snapshot of a single token-usage entry.
|
/// Snapshot of a single token-usage entry.
|
||||||
@@ -256,6 +267,13 @@ pub struct AgentThrottleView {
|
|||||||
pub limit: f64,
|
pub limit: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Snapshot of a single gateway-project entry.
|
||||||
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct GatewayProjectView {
|
||||||
|
pub name: String,
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::super::state::emit_event;
|
use super::super::state::emit_event;
|
||||||
|
|||||||
Reference in New Issue
Block a user