huskies: merge 567_story_gateway_ui_project_management_add_and_remove_projects
This commit is contained in:
@@ -86,4 +86,20 @@ export const gatewayApi = {
|
|||||||
getGatewayInfo(): Promise<GatewayInfo> {
|
getGatewayInfo(): Promise<GatewayInfo> {
|
||||||
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" },
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -186,6 +186,11 @@ export function GatewayPanel() {
|
|||||||
const [generating, setGenerating] = useState(false);
|
const [generating, setGenerating] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Add-project form state
|
||||||
|
const [newProjectName, setNewProjectName] = useState("");
|
||||||
|
const [newProjectUrl, setNewProjectUrl] = useState("");
|
||||||
|
const [addingProject, setAddingProject] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
gatewayApi
|
gatewayApi
|
||||||
.listAgents()
|
.listAgents()
|
||||||
@@ -234,6 +239,37 @@ 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 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
|
||||||
style={{
|
style={{
|
||||||
@@ -330,6 +366,138 @@ export function GatewayPanel() {
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Project management */}
|
||||||
|
<section style={{ marginTop: "32px" }}>
|
||||||
|
<h2
|
||||||
|
style={{
|
||||||
|
fontSize: "1.1em",
|
||||||
|
fontWeight: 600,
|
||||||
|
marginBottom: "12px",
|
||||||
|
borderBottom: "1px solid #21262d",
|
||||||
|
paddingBottom: "8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Projects{" "}
|
||||||
|
{projects.length > 0 && (
|
||||||
|
<span style={{ fontSize: "0.8em", color: "#8b949e", fontWeight: 400 }}>
|
||||||
|
({projects.length})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* Existing projects list */}
|
||||||
|
{projects.map((p) => (
|
||||||
|
<div
|
||||||
|
key={p.name}
|
||||||
|
data-testid={`project-row-${p.name}`}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "12px",
|
||||||
|
padding: "10px 14px",
|
||||||
|
background: "#161b22",
|
||||||
|
border: "1px solid #30363d",
|
||||||
|
borderRadius: "8px",
|
||||||
|
marginBottom: "8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{ fontWeight: 600, color: "#e6edf3" }}>{p.name}</div>
|
||||||
|
<div style={{ fontSize: "0.8em", color: "#8b949e" }}>{p.url}</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
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>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
+159
-18
@@ -97,8 +97,8 @@ struct AssignAgentRequest {
|
|||||||
/// Shared gateway state threaded through HTTP handlers.
|
/// Shared gateway state threaded through HTTP handlers.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct GatewayState {
|
pub struct GatewayState {
|
||||||
/// The parsed gateway config with all registered projects.
|
/// The live set of registered projects (initially loaded from `projects.toml`).
|
||||||
pub config: GatewayConfig,
|
pub projects: Arc<RwLock<BTreeMap<String, ProjectEntry>>>,
|
||||||
/// The currently active project name.
|
/// The currently active project name.
|
||||||
pub active_project: Arc<RwLock<String>>,
|
pub active_project: Arc<RwLock<String>>,
|
||||||
/// HTTP client for proxying requests to project containers.
|
/// HTTP client for proxying requests to project containers.
|
||||||
@@ -126,6 +126,21 @@ fn load_agents(config_dir: &Path) -> Vec<JoinedAgent> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Persist the current projects map to `<config_dir>/projects.toml`.
|
||||||
|
/// Silently ignores write errors or skips when `config_dir` is empty.
|
||||||
|
async fn save_config(projects: &BTreeMap<String, ProjectEntry>, config_dir: &Path) {
|
||||||
|
if config_dir.as_os_str().is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let path = config_dir.join("projects.toml");
|
||||||
|
let config = GatewayConfig {
|
||||||
|
projects: projects.clone(),
|
||||||
|
};
|
||||||
|
if let Ok(data) = toml::to_string_pretty(&config) {
|
||||||
|
let _ = tokio::fs::write(&path, data).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Persist the current agent list to `<config_dir>/gateway_agents.json`.
|
/// Persist the current agent list to `<config_dir>/gateway_agents.json`.
|
||||||
/// Silently ignores write errors (e.g. read-only filesystem or empty path).
|
/// Silently ignores write errors (e.g. read-only filesystem or empty path).
|
||||||
async fn save_agents(agents: &[JoinedAgent], config_dir: &Path) {
|
async fn save_agents(agents: &[JoinedAgent], config_dir: &Path) {
|
||||||
@@ -151,7 +166,7 @@ impl GatewayState {
|
|||||||
let first = config.projects.keys().next().unwrap().clone();
|
let first = config.projects.keys().next().unwrap().clone();
|
||||||
let agents = load_agents(&config_dir);
|
let agents = load_agents(&config_dir);
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
config,
|
projects: Arc::new(RwLock::new(config.projects)),
|
||||||
active_project: Arc::new(RwLock::new(first)),
|
active_project: Arc::new(RwLock::new(first)),
|
||||||
client: Client::new(),
|
client: Client::new(),
|
||||||
joined_agents: Arc::new(RwLock::new(agents)),
|
joined_agents: Arc::new(RwLock::new(agents)),
|
||||||
@@ -165,8 +180,9 @@ impl GatewayState {
|
|||||||
/// Get the URL of the currently active project.
|
/// Get the URL of the currently active project.
|
||||||
async fn active_url(&self) -> Result<String, String> {
|
async fn active_url(&self) -> Result<String, String> {
|
||||||
let name = self.active_project.read().await.clone();
|
let name = self.active_project.read().await.clone();
|
||||||
self.config
|
self.projects
|
||||||
.projects
|
.read()
|
||||||
|
.await
|
||||||
.get(&name)
|
.get(&name)
|
||||||
.map(|p| p.url.clone())
|
.map(|p| p.url.clone())
|
||||||
.ok_or_else(|| format!("active project '{name}' not found in config"))
|
.ok_or_else(|| format!("active project '{name}' not found in config"))
|
||||||
@@ -485,8 +501,10 @@ async fn handle_switch_project(params: &Value, state: &GatewayState) -> JsonRpcR
|
|||||||
return JsonRpcResponse::error(None, -32602, "missing required parameter: project".into());
|
return JsonRpcResponse::error(None, -32602, "missing required parameter: project".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
if !state.config.projects.contains_key(project) {
|
let url = {
|
||||||
let available: Vec<&str> = state.config.projects.keys().map(|s| s.as_str()).collect();
|
let projects = state.projects.read().await;
|
||||||
|
if !projects.contains_key(project) {
|
||||||
|
let available: Vec<&str> = projects.keys().map(|s| s.as_str()).collect();
|
||||||
return JsonRpcResponse::error(
|
return JsonRpcResponse::error(
|
||||||
None,
|
None,
|
||||||
-32602,
|
-32602,
|
||||||
@@ -496,16 +514,17 @@ async fn handle_switch_project(params: &Value, state: &GatewayState) -> JsonRpcR
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
projects[project].url.clone()
|
||||||
|
};
|
||||||
|
|
||||||
*state.active_project.write().await = project.to_string();
|
*state.active_project.write().await = project.to_string();
|
||||||
|
|
||||||
let url = &state.config.projects[project].url;
|
|
||||||
JsonRpcResponse::success(
|
JsonRpcResponse::success(
|
||||||
None,
|
None,
|
||||||
json!({
|
json!({
|
||||||
"content": [{
|
"content": [{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"text": format!("Switched to project '{project}' ({})", url)
|
"text": format!("Switched to project '{project}' ({url})")
|
||||||
}]
|
}]
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -562,8 +581,15 @@ async fn handle_gateway_status(state: &GatewayState) -> JsonRpcResponse {
|
|||||||
async fn handle_gateway_health(state: &GatewayState) -> JsonRpcResponse {
|
async fn handle_gateway_health(state: &GatewayState) -> JsonRpcResponse {
|
||||||
let mut results = BTreeMap::new();
|
let mut results = BTreeMap::new();
|
||||||
|
|
||||||
for (name, entry) in &state.config.projects {
|
let project_entries: Vec<(String, String)> = state
|
||||||
let health_url = format!("{}/health", entry.url.trim_end_matches('/'));
|
.projects
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.iter()
|
||||||
|
.map(|(n, e)| (n.clone(), e.url.clone()))
|
||||||
|
.collect();
|
||||||
|
for (name, url) in &project_entries {
|
||||||
|
let health_url = format!("{}/health", url.trim_end_matches('/'));
|
||||||
let status = match state.client.get(&health_url).send().await {
|
let status = match state.client.get(&health_url).send().await {
|
||||||
Ok(resp) => {
|
Ok(resp) => {
|
||||||
if resp.status().is_success() {
|
if resp.status().is_success() {
|
||||||
@@ -749,7 +775,7 @@ pub async fn gateway_assign_agent_handler(
|
|||||||
.and_then(|p| if p.is_empty() { None } else { Some(p) });
|
.and_then(|p| if p.is_empty() { None } else { Some(p) });
|
||||||
|
|
||||||
if let Some(ref p) = project
|
if let Some(ref p) = project
|
||||||
&& !state.config.projects.contains_key(p.as_str())
|
&& !state.projects.read().await.contains_key(p.as_str())
|
||||||
{
|
{
|
||||||
return Response::builder()
|
return Response::builder()
|
||||||
.status(StatusCode::BAD_REQUEST)
|
.status(StatusCode::BAD_REQUEST)
|
||||||
@@ -797,8 +823,15 @@ pub async fn gateway_health_handler(state: Data<&Arc<GatewayState>>) -> Response
|
|||||||
let mut all_healthy = true;
|
let mut all_healthy = true;
|
||||||
let mut statuses = BTreeMap::new();
|
let mut statuses = BTreeMap::new();
|
||||||
|
|
||||||
for (name, entry) in &state.config.projects {
|
let project_entries: Vec<(String, String)> = state
|
||||||
let health_url = format!("{}/health", entry.url.trim_end_matches('/'));
|
.projects
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.iter()
|
||||||
|
.map(|(n, e)| (n.clone(), e.url.clone()))
|
||||||
|
.collect();
|
||||||
|
for (name, url) in &project_entries {
|
||||||
|
let health_url = format!("{}/health", url.trim_end_matches('/'));
|
||||||
let healthy = match state.client.get(&health_url).send().await {
|
let healthy = match state.client.get(&health_url).send().await {
|
||||||
Ok(resp) => resp.status().is_success(),
|
Ok(resp) => resp.status().is_success(),
|
||||||
Err(_) => false,
|
Err(_) => false,
|
||||||
@@ -1000,8 +1033,9 @@ pub async fn gateway_index_handler() -> Response {
|
|||||||
pub async fn gateway_api_handler(state: Data<&Arc<GatewayState>>) -> Response {
|
pub async fn gateway_api_handler(state: Data<&Arc<GatewayState>>) -> Response {
|
||||||
let active = state.active_project.read().await.clone();
|
let active = state.active_project.read().await.clone();
|
||||||
let projects: Vec<Value> = state
|
let projects: Vec<Value> = state
|
||||||
.config
|
|
||||||
.projects
|
.projects
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(name, entry)| {
|
.map(|(name, entry)| {
|
||||||
json!({
|
json!({
|
||||||
@@ -1065,6 +1099,104 @@ pub async fn gateway_switch_handler(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Project management API ───────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Request body for adding a new project.
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct AddProjectRequest {
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `POST /api/gateway/projects` — add a new project to the gateway config.
|
||||||
|
///
|
||||||
|
/// Expects JSON `{ "name": "...", "url": "..." }`. Returns the created project
|
||||||
|
/// or 409 Conflict if a project with the same name already exists.
|
||||||
|
#[handler]
|
||||||
|
pub async fn gateway_add_project_handler(
|
||||||
|
state: Data<&Arc<GatewayState>>,
|
||||||
|
body: Json<AddProjectRequest>,
|
||||||
|
) -> Response {
|
||||||
|
let name = body.0.name.trim().to_string();
|
||||||
|
let url = body.0.url.trim().to_string();
|
||||||
|
|
||||||
|
if name.is_empty() {
|
||||||
|
return Response::builder()
|
||||||
|
.status(StatusCode::BAD_REQUEST)
|
||||||
|
.body(Body::from("project name must not be empty"));
|
||||||
|
}
|
||||||
|
if url.is_empty() {
|
||||||
|
return Response::builder()
|
||||||
|
.status(StatusCode::BAD_REQUEST)
|
||||||
|
.body(Body::from("project url must not be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut projects = state.projects.write().await;
|
||||||
|
if projects.contains_key(&name) {
|
||||||
|
return Response::builder()
|
||||||
|
.status(StatusCode::CONFLICT)
|
||||||
|
.body(Body::from(format!("project '{name}' already exists")));
|
||||||
|
}
|
||||||
|
projects.insert(name.clone(), ProjectEntry { url: url.clone() });
|
||||||
|
}
|
||||||
|
|
||||||
|
let snapshot = state.projects.read().await.clone();
|
||||||
|
save_config(&snapshot, &state.config_dir).await;
|
||||||
|
|
||||||
|
crate::slog!("[gateway] Added project '{name}' ({url})");
|
||||||
|
let body_val = json!({ "name": name, "url": url });
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(Body::from(
|
||||||
|
serde_json::to_vec(&body_val).unwrap_or_default(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `DELETE /api/gateway/projects/:name` — remove a project from the gateway config.
|
||||||
|
///
|
||||||
|
/// Returns 204 No Content on success. Returns 400 if this is the last project
|
||||||
|
/// (the gateway requires at least one project to remain configured).
|
||||||
|
#[handler]
|
||||||
|
pub async fn gateway_remove_project_handler(
|
||||||
|
PoemPath(name): PoemPath<String>,
|
||||||
|
state: Data<&Arc<GatewayState>>,
|
||||||
|
) -> Response {
|
||||||
|
let active = state.active_project.read().await.clone();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut projects = state.projects.write().await;
|
||||||
|
if !projects.contains_key(&name) {
|
||||||
|
return Response::builder()
|
||||||
|
.status(StatusCode::NOT_FOUND)
|
||||||
|
.body(Body::from(format!("project '{name}' not found")));
|
||||||
|
}
|
||||||
|
if projects.len() == 1 {
|
||||||
|
return Response::builder()
|
||||||
|
.status(StatusCode::BAD_REQUEST)
|
||||||
|
.body(Body::from("cannot remove the last project"));
|
||||||
|
}
|
||||||
|
projects.remove(&name);
|
||||||
|
}
|
||||||
|
|
||||||
|
let snapshot = state.projects.read().await.clone();
|
||||||
|
save_config(&snapshot, &state.config_dir).await;
|
||||||
|
|
||||||
|
// If the removed project was active, switch to the first remaining.
|
||||||
|
if active == name {
|
||||||
|
let first = state.projects.read().await.keys().next().cloned();
|
||||||
|
if let Some(new_active) = first {
|
||||||
|
*state.active_project.write().await = new_active;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
crate::slog!("[gateway] Removed project '{name}'");
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::NO_CONTENT)
|
||||||
|
.body(Body::empty())
|
||||||
|
}
|
||||||
|
|
||||||
// ── Bot configuration API ────────────────────────────────────────────
|
// ── Bot configuration API ────────────────────────────────────────────
|
||||||
|
|
||||||
/// Request/response body for the bot configuration API.
|
/// Request/response body for the bot configuration API.
|
||||||
@@ -1173,7 +1305,7 @@ pub async fn gateway_bot_config_save_handler(
|
|||||||
if let Some(h) = handle.take() {
|
if let Some(h) = handle.take() {
|
||||||
h.abort();
|
h.abort();
|
||||||
}
|
}
|
||||||
let gateway_projects: Vec<String> = state.config.projects.keys().cloned().collect();
|
let gateway_projects: Vec<String> = state.projects.read().await.keys().cloned().collect();
|
||||||
let new_handle = spawn_gateway_bot(
|
let new_handle = spawn_gateway_bot(
|
||||||
&state.config_dir,
|
&state.config_dir,
|
||||||
Arc::clone(&state.active_project),
|
Arc::clone(&state.active_project),
|
||||||
@@ -1422,8 +1554,9 @@ pub async fn run(config_path: &Path, port: u16) -> Result<(), std::io::Error> {
|
|||||||
crate::slog!(
|
crate::slog!(
|
||||||
"[gateway] Registered projects: {}",
|
"[gateway] Registered projects: {}",
|
||||||
state_arc
|
state_arc
|
||||||
.config
|
|
||||||
.projects
|
.projects
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
.keys()
|
.keys()
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
@@ -1437,7 +1570,7 @@ pub async fn run(config_path: &Path, port: u16) -> Result<(), std::io::Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Spawn the Matrix bot if `.huskies/bot.toml` exists in the config directory.
|
// Spawn the Matrix bot if `.huskies/bot.toml` exists in the config directory.
|
||||||
let gateway_projects: Vec<String> = state_arc.config.projects.keys().cloned().collect();
|
let gateway_projects: Vec<String> = state_arc.projects.read().await.keys().cloned().collect();
|
||||||
let bot_abort = spawn_gateway_bot(
|
let bot_abort = spawn_gateway_bot(
|
||||||
&config_dir,
|
&config_dir,
|
||||||
Arc::clone(&state_arc.active_project),
|
Arc::clone(&state_arc.active_project),
|
||||||
@@ -1451,6 +1584,14 @@ pub async fn run(config_path: &Path, port: u16) -> Result<(), std::io::Error> {
|
|||||||
.at("/bot-config", poem::get(gateway_bot_config_page_handler))
|
.at("/bot-config", poem::get(gateway_bot_config_page_handler))
|
||||||
.at("/api/gateway", poem::get(gateway_api_handler))
|
.at("/api/gateway", poem::get(gateway_api_handler))
|
||||||
.at("/api/gateway/switch", poem::post(gateway_switch_handler))
|
.at("/api/gateway/switch", poem::post(gateway_switch_handler))
|
||||||
|
.at(
|
||||||
|
"/api/gateway/projects",
|
||||||
|
poem::post(gateway_add_project_handler),
|
||||||
|
)
|
||||||
|
.at(
|
||||||
|
"/api/gateway/projects/:name",
|
||||||
|
poem::delete(gateway_remove_project_handler),
|
||||||
|
)
|
||||||
.at(
|
.at(
|
||||||
"/api/gateway/bot-config",
|
"/api/gateway/bot-config",
|
||||||
poem::get(gateway_bot_config_get_handler).post(gateway_bot_config_save_handler),
|
poem::get(gateway_bot_config_get_handler).post(gateway_bot_config_save_handler),
|
||||||
|
|||||||
Reference in New Issue
Block a user