Update admin UI with real API integration and secret management
This commit is contained in:
40
Cargo.lock
generated
40
Cargo.lock
generated
@@ -1384,7 +1384,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"redis 0.25.4",
|
||||
"reqwest 0.12.24",
|
||||
"runner_rust 0.1.0",
|
||||
"runner_rust 0.1.0 (git+https://git.ourworld.tf/herocode/runner_rust.git)",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
@@ -2645,26 +2645,6 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "osiris"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"env_logger 0.10.2",
|
||||
"osiris_derive 0.1.0",
|
||||
"redis 0.24.0",
|
||||
"rhai",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"time",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "osiris"
|
||||
version = "0.1.0"
|
||||
@@ -2673,7 +2653,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"env_logger 0.10.2",
|
||||
"osiris_derive 0.1.0 (git+https://git.ourworld.tf/herocode/osiris.git)",
|
||||
"osiris_derive",
|
||||
"redis 0.24.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2685,15 +2665,6 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "osiris_derive"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "osiris_derive"
|
||||
version = "0.1.0"
|
||||
@@ -3569,6 +3540,7 @@ checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746"
|
||||
[[package]]
|
||||
name = "runner_rust"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.ourworld.tf/herocode/runner_rust.git?branch=main#268128f7fd53e9586288efd95f9288595c4a74e9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3582,7 +3554,7 @@ dependencies = [
|
||||
"heromodels_core",
|
||||
"hex",
|
||||
"log",
|
||||
"osiris 0.1.0",
|
||||
"osiris",
|
||||
"rand 0.8.5",
|
||||
"ratatui",
|
||||
"redis 0.25.4",
|
||||
@@ -3615,7 +3587,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "runner_rust"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.ourworld.tf/herocode/runner_rust.git?branch=main#268128f7fd53e9586288efd95f9288595c4a74e9"
|
||||
source = "git+https://git.ourworld.tf/herocode/runner_rust.git#df8fc696785fbf26ed832443bd9404ee6192d116"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3629,7 +3601,7 @@ dependencies = [
|
||||
"heromodels_core",
|
||||
"hex",
|
||||
"log",
|
||||
"osiris 0.1.0 (git+https://git.ourworld.tf/herocode/osiris.git)",
|
||||
"osiris",
|
||||
"rand 0.8.5",
|
||||
"ratatui",
|
||||
"redis 0.25.4",
|
||||
|
||||
40
clients/admin-ui/Cargo.lock
generated
40
clients/admin-ui/Cargo.lock
generated
@@ -1838,7 +1838,7 @@ dependencies = [
|
||||
"jsonrpsee",
|
||||
"log",
|
||||
"redis 0.25.4",
|
||||
"runner_rust 0.1.0",
|
||||
"runner_rust 0.1.0 (git+https://git.ourworld.tf/herocode/runner_rust.git)",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
@@ -3155,26 +3155,6 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "osiris"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"env_logger 0.10.2",
|
||||
"osiris_derive 0.1.0",
|
||||
"redis 0.24.0",
|
||||
"rhai",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"time",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "osiris"
|
||||
version = "0.1.0"
|
||||
@@ -3183,7 +3163,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"env_logger 0.10.2",
|
||||
"osiris_derive 0.1.0 (git+https://git.ourworld.tf/herocode/osiris.git)",
|
||||
"osiris_derive",
|
||||
"redis 0.24.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -3195,15 +3175,6 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "osiris_derive"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "osiris_derive"
|
||||
version = "0.1.0"
|
||||
@@ -4133,6 +4104,7 @@ checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746"
|
||||
[[package]]
|
||||
name = "runner_rust"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.ourworld.tf/herocode/runner_rust.git?branch=main#268128f7fd53e9586288efd95f9288595c4a74e9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -4146,7 +4118,7 @@ dependencies = [
|
||||
"heromodels_core",
|
||||
"hex",
|
||||
"log",
|
||||
"osiris 0.1.0",
|
||||
"osiris",
|
||||
"rand 0.8.5",
|
||||
"ratatui",
|
||||
"redis 0.25.4",
|
||||
@@ -4179,7 +4151,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "runner_rust"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.ourworld.tf/herocode/runner_rust.git?branch=main#268128f7fd53e9586288efd95f9288595c4a74e9"
|
||||
source = "git+https://git.ourworld.tf/herocode/runner_rust.git#df8fc696785fbf26ed832443bd9404ee6192d116"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -4193,7 +4165,7 @@ dependencies = [
|
||||
"heromodels_core",
|
||||
"hex",
|
||||
"log",
|
||||
"osiris 0.1.0 (git+https://git.ourworld.tf/herocode/osiris.git)",
|
||||
"osiris",
|
||||
"rand 0.8.5",
|
||||
"ratatui",
|
||||
"redis 0.25.4",
|
||||
|
||||
@@ -18,6 +18,8 @@ web-sys = { version = "0.3", features = [
|
||||
"HtmlSelectElement",
|
||||
"HtmlTextAreaElement",
|
||||
"Window",
|
||||
"Navigator",
|
||||
"Clipboard",
|
||||
] }
|
||||
js-sys = "0.3"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
@@ -12,6 +12,12 @@ enum AppState {
|
||||
Dashboard,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
enum ContentView {
|
||||
JobsList,
|
||||
JobDetail(String), // job_id
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
enum SidebarView {
|
||||
Runners,
|
||||
@@ -64,6 +70,10 @@ pub struct App {
|
||||
job_sort_column: String,
|
||||
job_sort_ascending: bool,
|
||||
job_filter: String,
|
||||
// Content view
|
||||
content_view: ContentView,
|
||||
// Job detail view
|
||||
job_detail_logs: Option<String>,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
@@ -87,6 +97,7 @@ pub enum Msg {
|
||||
DeleteJob(String),
|
||||
JobDeleted(Result<String, String>),
|
||||
ViewJobLogs(String),
|
||||
JobLogsLoaded(Result<Vec<String>, String>),
|
||||
PollJobStatus,
|
||||
JobStatusUpdated(String, String), // (job_id, status)
|
||||
JobStatusError(String, String), // (job_id, error)
|
||||
@@ -100,6 +111,9 @@ pub enum Msg {
|
||||
UpdateNewKeyScope(String),
|
||||
AddKey,
|
||||
KeyAdded(Result<(), String>),
|
||||
DeleteKey(String),
|
||||
KeyDeleted(Result<String, String>),
|
||||
CopyKeyToClipboard(String),
|
||||
ShowJobForm,
|
||||
HideJobForm,
|
||||
UpdateJobId(String),
|
||||
@@ -114,6 +128,8 @@ pub enum Msg {
|
||||
ToggleSidebarView(SidebarView),
|
||||
SelectRunner(Option<String>),
|
||||
SelectKey(Option<String>),
|
||||
ViewJobDetail(String), // job_id
|
||||
BackToJobsList,
|
||||
}
|
||||
|
||||
impl Component for App {
|
||||
@@ -158,6 +174,8 @@ impl Component for App {
|
||||
job_sort_column: "created_at".to_string(),
|
||||
job_sort_ascending: false,
|
||||
job_filter: String::new(),
|
||||
content_view: ContentView::JobsList,
|
||||
job_detail_logs: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,6 +557,62 @@ impl Component for App {
|
||||
}
|
||||
true
|
||||
}
|
||||
Msg::DeleteKey(key) => {
|
||||
if let Some(client) = &self.client {
|
||||
let client = client.clone();
|
||||
let link = ctx.link().clone();
|
||||
let key_clone = key.clone();
|
||||
|
||||
spawn_local(async move {
|
||||
match client.auth_remove_key(key_clone.clone()).await {
|
||||
Ok(_) => {
|
||||
link.send_message(Msg::KeyDeleted(Ok(key_clone)));
|
||||
}
|
||||
Err(e) => {
|
||||
link.send_message(Msg::KeyDeleted(Err(format!("{:?}", e))));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
true
|
||||
}
|
||||
Msg::KeyDeleted(result) => {
|
||||
match result {
|
||||
Ok(key) => {
|
||||
self.error = Some(format!("✅ Key deleted successfully"));
|
||||
self.start_toast_timeout(ctx);
|
||||
// Reload keys list
|
||||
ctx.link().send_message(Msg::LoadKeys);
|
||||
}
|
||||
Err(e) => {
|
||||
self.error = Some(format!("Failed to delete key: {}", e));
|
||||
self.start_toast_timeout(ctx);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
Msg::CopyKeyToClipboard(key) => {
|
||||
// Use the Clipboard API to copy the key
|
||||
let window = web_sys::window().expect("no global window exists");
|
||||
let navigator = window.navigator();
|
||||
let clipboard = navigator.clipboard();
|
||||
let promise = clipboard.write_text(&key);
|
||||
let link = ctx.link().clone();
|
||||
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
match wasm_bindgen_futures::JsFuture::from(promise).await {
|
||||
Ok(_) => {
|
||||
link.send_message(Msg::SetError("✅ Key copied to clipboard".to_string()));
|
||||
}
|
||||
Err(_) => {
|
||||
link.send_message(Msg::SetError("Failed to copy key".to_string()));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.start_toast_timeout(ctx);
|
||||
false
|
||||
}
|
||||
Msg::ShowJobForm => {
|
||||
self.show_job_form = true;
|
||||
// Generate a new job ID
|
||||
@@ -848,9 +922,52 @@ impl Component for App {
|
||||
}
|
||||
Msg::ViewJobLogs(job_id) => {
|
||||
log::info!("View logs for job: {}", job_id);
|
||||
// TODO: Implement view logs
|
||||
self.error = Some(format!("View logs for {} - Not implemented yet", job_id));
|
||||
self.start_toast_timeout(ctx);
|
||||
self.viewing_job_output = Some(job_id.clone());
|
||||
self.job_output = None; // Clear previous output
|
||||
|
||||
if let Some(client) = &self.client {
|
||||
let client = client.clone();
|
||||
let link = ctx.link().clone();
|
||||
|
||||
spawn_local(async move {
|
||||
match client.get_job_logs(&job_id, Some(1000)).await {
|
||||
Ok(logs_js) => {
|
||||
// Convert JsValue to Vec<String>
|
||||
match serde_wasm_bindgen::from_value::<Vec<String>>(logs_js) {
|
||||
Ok(logs) => {
|
||||
link.send_message(Msg::JobLogsLoaded(Ok(logs)));
|
||||
}
|
||||
Err(e) => {
|
||||
link.send_message(Msg::JobLogsLoaded(Err(format!("Failed to parse logs: {}", e))));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
link.send_message(Msg::JobLogsLoaded(Err(format!("{:?}", e))));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
true
|
||||
}
|
||||
Msg::JobLogsLoaded(result) => {
|
||||
match result {
|
||||
Ok(logs) => {
|
||||
let logs_text = logs.join("\n");
|
||||
// If viewing job output modal, use job_output
|
||||
if self.viewing_job_output.is_some() {
|
||||
self.job_output = Some(logs_text);
|
||||
} else {
|
||||
// Otherwise, it's for the job detail view
|
||||
self.job_detail_logs = Some(logs_text);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
self.error = Some(format!("Failed to load job logs: {}", e));
|
||||
self.start_toast_timeout(ctx);
|
||||
self.viewing_job_output = None;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
Msg::PollJobStatus => {
|
||||
@@ -987,6 +1104,41 @@ impl Component for App {
|
||||
self.job_output = None;
|
||||
true
|
||||
}
|
||||
Msg::ViewJobDetail(job_id) => {
|
||||
self.content_view = ContentView::JobDetail(job_id.clone());
|
||||
self.job_detail_logs = None;
|
||||
|
||||
// Load job logs
|
||||
if let Some(client) = &self.client {
|
||||
let client = client.clone();
|
||||
let link = ctx.link().clone();
|
||||
let job_id_clone = job_id.clone();
|
||||
|
||||
spawn_local(async move {
|
||||
match client.get_job_logs(&job_id_clone, Some(1000)).await {
|
||||
Ok(logs_js) => {
|
||||
match serde_wasm_bindgen::from_value::<Vec<String>>(logs_js) {
|
||||
Ok(logs) => {
|
||||
link.send_message(Msg::JobLogsLoaded(Ok(logs)));
|
||||
}
|
||||
Err(e) => {
|
||||
link.send_message(Msg::JobLogsLoaded(Err(format!("Failed to parse logs: {}", e))));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
link.send_message(Msg::JobLogsLoaded(Err(format!("{:?}", e))));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
true
|
||||
}
|
||||
Msg::BackToJobsList => {
|
||||
self.content_view = ContentView::JobsList;
|
||||
self.job_detail_logs = None;
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1074,6 +1226,20 @@ impl App {
|
||||
matches!(status, "finished" | "error" | "stopped")
|
||||
}
|
||||
|
||||
fn format_timestamp(timestamp: &str) -> String {
|
||||
if timestamp.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
// Parse RFC3339 timestamp and format as "Day Month Year"
|
||||
if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(timestamp) {
|
||||
dt.format("%d %b %Y").to_string()
|
||||
} else {
|
||||
// Fallback to original timestamp if parsing fails
|
||||
timestamp.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn start_status_polling(&mut self, ctx: &Context<Self>) {
|
||||
let link = ctx.link().clone();
|
||||
self._status_poll_timeout = Some(Timeout::new(2000, move || {
|
||||
@@ -1190,44 +1356,12 @@ impl App {
|
||||
}
|
||||
</div>
|
||||
|
||||
// Main Content Island - Jobs
|
||||
// Main Content Island
|
||||
<div class="content-island">
|
||||
<div class="content-header">
|
||||
<h2>{ "Jobs" }</h2>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filter jobs..."
|
||||
class="filter-input"
|
||||
value={self.job_filter.clone()}
|
||||
oninput={ctx.link().callback(|e: InputEvent| {
|
||||
let input: HtmlInputElement = e.target_unchecked_into();
|
||||
Msg::FilterJobs(input.value())
|
||||
})}
|
||||
/>
|
||||
<button
|
||||
class="btn-primary"
|
||||
style="margin-left: auto;"
|
||||
onclick={ctx.link().callback(|_| Msg::ShowJobForm)}
|
||||
>
|
||||
{ "+ Create Job" }
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{
|
||||
if self.loading {
|
||||
html! {
|
||||
<div class="empty-state">
|
||||
{ "Loading jobs..." }
|
||||
</div>
|
||||
}
|
||||
} else if self.jobs.is_empty() {
|
||||
html! {
|
||||
<div class="empty-state">
|
||||
{ "No jobs yet" }
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
self.view_jobs_table(ctx)
|
||||
match &self.content_view {
|
||||
ContentView::JobsList => self.view_jobs_list_content(ctx),
|
||||
ContentView::JobDetail(job_id) => self.view_job_detail_content(ctx, job_id.clone()),
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@@ -1499,29 +1633,49 @@ impl App {
|
||||
self.keys.iter().map(|key| {
|
||||
let name = key.get("name").and_then(|v| v.as_str()).unwrap_or("unknown");
|
||||
let scope = key.get("scope").and_then(|v| v.as_str()).unwrap_or("unknown");
|
||||
let secret = key.get("secret").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let secret = key.get("key").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let created_at = key.get("created_at").and_then(|v| v.as_str()).unwrap_or("");
|
||||
|
||||
// Format timestamp nicely
|
||||
let formatted_time = Self::format_timestamp(created_at);
|
||||
|
||||
let is_selected = self.selected_key.as_ref().map(|s| s == name).unwrap_or(false);
|
||||
let key_name = name.to_string();
|
||||
let secret_for_copy = secret.to_string();
|
||||
let secret_for_delete = secret.to_string();
|
||||
|
||||
html! {
|
||||
<div
|
||||
class={if is_selected { "key-card selected" } else { "key-card" }}
|
||||
onclick={ctx.link().callback(move |_| Msg::SelectKey(Some(key_name.clone())))}
|
||||
>
|
||||
<div class="key-header">
|
||||
<span class="key-name">{ name }</span>
|
||||
<span class={format!("key-scope scope-{}", scope)}>{ scope }</span>
|
||||
<button
|
||||
class="key-delete-btn"
|
||||
title="Delete key"
|
||||
onclick={ctx.link().callback(move |e: MouseEvent| {
|
||||
e.stop_propagation();
|
||||
Msg::DeleteKey(secret_for_delete.clone())
|
||||
})}
|
||||
>
|
||||
{ "×" }
|
||||
</button>
|
||||
</div>
|
||||
<div class="key-secret">
|
||||
<div
|
||||
class="key-secret clickable"
|
||||
title="Click to copy"
|
||||
onclick={ctx.link().callback(move |e: MouseEvent| {
|
||||
e.stop_propagation();
|
||||
Msg::CopyKeyToClipboard(secret_for_copy.clone())
|
||||
})}
|
||||
>
|
||||
<code>{ secret }</code>
|
||||
</div>
|
||||
{
|
||||
if !created_at.is_empty() {
|
||||
html! {
|
||||
<div class="key-meta">
|
||||
<span>{ format!("Created: {}", created_at) }</span>
|
||||
<span>{ formatted_time }</span>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
@@ -1661,9 +1815,14 @@ impl App {
|
||||
let job_id_logs = job_id.to_string();
|
||||
let job_id_delete = job_id.to_string();
|
||||
let job_id_output = job_id.to_string();
|
||||
let job_id_detail = job_id.to_string();
|
||||
|
||||
html! {
|
||||
<tr class={if is_polling { "polling-row" } else { "" }}>
|
||||
<tr
|
||||
class={if is_polling { "polling-row" } else { "" }}
|
||||
onclick={ctx.link().callback(move |_| Msg::ViewJobDetail(job_id_detail.clone()))}
|
||||
style="cursor: pointer;"
|
||||
>
|
||||
<td>
|
||||
{
|
||||
if is_complete {
|
||||
@@ -1796,4 +1955,161 @@ impl App {
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
fn view_jobs_list_content(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<>
|
||||
<div class="content-header">
|
||||
<h2>{ "Jobs" }</h2>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filter jobs..."
|
||||
class="filter-input"
|
||||
value={self.job_filter.clone()}
|
||||
oninput={ctx.link().callback(|e: InputEvent| {
|
||||
let input: HtmlInputElement = e.target_unchecked_into();
|
||||
Msg::FilterJobs(input.value())
|
||||
})}
|
||||
/>
|
||||
<button
|
||||
class="btn-primary"
|
||||
style="margin-left: auto;"
|
||||
onclick={ctx.link().callback(|_| Msg::ShowJobForm)}
|
||||
>
|
||||
{ "+ Create Job" }
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{
|
||||
if self.loading {
|
||||
html! {
|
||||
<div class="empty-state">
|
||||
{ "Loading jobs..." }
|
||||
</div>
|
||||
}
|
||||
} else if self.jobs.is_empty() {
|
||||
html! {
|
||||
<div class="empty-state">
|
||||
{ "No jobs yet" }
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
self.view_jobs_table(ctx)
|
||||
}
|
||||
}
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
fn view_job_detail_content(&self, ctx: &Context<Self>, job_id: String) -> Html {
|
||||
let job = self.jobs.iter().find(|j| {
|
||||
j.get("id").and_then(|v| v.as_str()).unwrap_or("") == job_id
|
||||
});
|
||||
|
||||
let job_id_run = job_id.clone();
|
||||
let job_id_delete = job_id.clone();
|
||||
|
||||
html! {
|
||||
<>
|
||||
<div class="content-header">
|
||||
<button
|
||||
class="btn-secondary"
|
||||
onclick={ctx.link().callback(|_| Msg::BackToJobsList)}
|
||||
>
|
||||
{ "← Back to Jobs" }
|
||||
</button>
|
||||
<h2>{ format!("Job {}", &job_id[..12.min(job_id.len())]) }</h2>
|
||||
</div>
|
||||
|
||||
<div style="padding: 1.5rem;">
|
||||
{
|
||||
if let Some(job) = job {
|
||||
let status = job.get("status").and_then(|v| v.as_str()).unwrap_or("unknown");
|
||||
let runner = job.get("runner").and_then(|v| v.as_str()).unwrap_or("unknown");
|
||||
let payload = job.get("payload").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let timeout = job.get("timeout").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
let created_at = job.get("created_at").and_then(|v| v.as_str()).unwrap_or("");
|
||||
|
||||
let status_class = match status {
|
||||
"dispatched" | "queued" => "status-queued",
|
||||
"started" => "status-running",
|
||||
"finished" => "status-completed",
|
||||
"error" => "status-failed",
|
||||
"stopped" | "stopping" => "status-failed",
|
||||
_ => "status-unknown",
|
||||
};
|
||||
|
||||
html! {
|
||||
<>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h3 style="margin-bottom: 1rem;">{ "Job Details" }</h3>
|
||||
<div style="display: grid; grid-template-columns: 150px 1fr; gap: 0.75rem; background: var(--bg-primary); padding: 1.5rem; border-radius: 8px;">
|
||||
<div style="color: var(--text-secondary); font-weight: 600;">{ "Status:" }</div>
|
||||
<div>
|
||||
<span class={format!("status-badge {}", status_class)}>
|
||||
{ status }
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style="color: var(--text-secondary); font-weight: 600;">{ "Job ID:" }</div>
|
||||
<div style="font-family: monospace; color: var(--accent);">{ &job_id }</div>
|
||||
|
||||
<div style="color: var(--text-secondary); font-weight: 600;">{ "Runner:" }</div>
|
||||
<div>{ runner }</div>
|
||||
|
||||
<div style="color: var(--text-secondary); font-weight: 600;">{ "Timeout:" }</div>
|
||||
<div>{ format!("{} seconds", timeout) }</div>
|
||||
|
||||
<div style="color: var(--text-secondary); font-weight: 600;">{ "Created:" }</div>
|
||||
<div>{ created_at }</div>
|
||||
|
||||
<div style="color: var(--text-secondary); font-weight: 600;">{ "Payload:" }</div>
|
||||
<div style="font-family: monospace; word-break: break-all;">{ payload }</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 style="margin-bottom: 1rem;">{ "Logs" }</h3>
|
||||
{
|
||||
if let Some(logs) = &self.job_detail_logs {
|
||||
html! {
|
||||
<pre class="job-output">{ logs }</pre>
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
<div class="loading-spinner">{ "Loading logs..." }</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 2rem; display: flex; gap: 1rem;">
|
||||
<button
|
||||
class="btn-primary"
|
||||
onclick={ctx.link().callback(move |_| Msg::RunJob(job_id_run.clone()))}
|
||||
>
|
||||
{ "▶ Run Job" }
|
||||
</button>
|
||||
<button
|
||||
class="btn-secondary"
|
||||
onclick={ctx.link().callback(move |_| Msg::DeleteJob(job_id_delete.clone()))}
|
||||
style="border-color: var(--error); color: var(--error);"
|
||||
>
|
||||
{ "🗑 Delete Job" }
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
<div class="empty-state">
|
||||
{ "Job not found" }
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1225,6 +1225,17 @@ button:disabled {
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
overflow-x: auto;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.key-secret.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.key-secret.clickable:hover {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border-color: var(--accent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.key-secret code {
|
||||
@@ -1238,3 +1249,24 @@ button:disabled {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.key-delete-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin-left: auto;
|
||||
transition: color 0.2s ease;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.key-delete-btn:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
78
clients/openrpc/examples/generate_test_keys.rs
Normal file
78
clients/openrpc/examples/generate_test_keys.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
/// Generate test secp256k1 keypairs for supervisor authentication testing
|
||||
///
|
||||
/// Run with: cargo run --example generate_test_keys
|
||||
|
||||
use secp256k1::{Secp256k1, SecretKey, PublicKey};
|
||||
use hex;
|
||||
|
||||
fn main() {
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
println!("\n╔════════════════════════════════════════════════════════════╗");
|
||||
println!("║ Test Keypairs for Supervisor Auth ║");
|
||||
println!("╚════════════════════════════════════════════════════════════╝\n");
|
||||
println!("⚠️ WARNING: These are TEST keypairs only! Never use in production!\n");
|
||||
|
||||
// Generate 5 keypairs with simple private keys for testing
|
||||
let test_keys = vec![
|
||||
("Alice (Admin)", "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"),
|
||||
("Bob (User)", "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321"),
|
||||
("Charlie (Register)", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
|
||||
("Dave (Test)", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"),
|
||||
("Eve (Test)", "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"),
|
||||
];
|
||||
|
||||
for (i, (name, privkey_hex)) in test_keys.iter().enumerate() {
|
||||
println!("## Keypair {} - {}", i + 1, name);
|
||||
println!("─────────────────────────────────────────────────────────────");
|
||||
|
||||
// Parse private key
|
||||
let privkey_bytes = hex::decode(privkey_hex).expect("Invalid hex");
|
||||
let secret_key = SecretKey::from_slice(&privkey_bytes).expect("Invalid private key");
|
||||
|
||||
// Derive public key
|
||||
let public_key = PublicKey::from_secret_key(&secp, &secret_key);
|
||||
|
||||
// Serialize keys
|
||||
let pubkey_uncompressed = hex::encode(public_key.serialize_uncompressed());
|
||||
let pubkey_compressed = hex::encode(public_key.serialize());
|
||||
|
||||
println!("Private Key (hex): 0x{}", privkey_hex);
|
||||
println!("Public Key (uncomp): 0x{}", pubkey_uncompressed);
|
||||
println!("Public Key (comp): 0x{}", pubkey_compressed);
|
||||
println!();
|
||||
}
|
||||
|
||||
println!("\n╔════════════════════════════════════════════════════════════╗");
|
||||
println!("║ Usage Examples ║");
|
||||
println!("╚════════════════════════════════════════════════════════════╝\n");
|
||||
|
||||
println!("### Using with OpenRPC Client (Rust)\n");
|
||||
println!("```rust");
|
||||
println!("use secp256k1::{{Secp256k1, SecretKey}};");
|
||||
println!("use hex;");
|
||||
println!();
|
||||
println!("// Alice's private key for admin access");
|
||||
println!("let privkey_hex = \"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef\";");
|
||||
println!("let privkey_bytes = hex::decode(privkey_hex).unwrap();");
|
||||
println!("let secret_key = SecretKey::from_slice(&privkey_bytes).unwrap();");
|
||||
println!();
|
||||
println!("// Use with client");
|
||||
println!("let client = SupervisorClient::new_with_keypair(");
|
||||
println!(" \"http://127.0.0.1:3030\",");
|
||||
println!(" secret_key");
|
||||
println!(");");
|
||||
println!("```\n");
|
||||
|
||||
println!("### Testing Different Scopes\n");
|
||||
println!("1. **Admin Scope** - Use Alice's keypair for full admin access");
|
||||
println!("2. **User Scope** - Use Bob's keypair for limited user access");
|
||||
println!("3. **Register Scope** - Use Charlie's keypair for runner registration\n");
|
||||
|
||||
println!("### Quick Copy-Paste Keys\n");
|
||||
println!("Alice (Admin): 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
|
||||
println!("Bob (User): fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321");
|
||||
println!("Charlie (Reg): aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
println!("Dave (Test): bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
|
||||
println!("Eve (Test): cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\n");
|
||||
}
|
||||
@@ -569,6 +569,25 @@ impl WasmSupervisorClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get logs for a specific job
|
||||
#[wasm_bindgen]
|
||||
pub async fn get_job_logs(&self, job_id: &str, lines: Option<usize>) -> Result<JsValue, JsValue> {
|
||||
let params = if let Some(n) = lines {
|
||||
serde_json::json!([job_id, n])
|
||||
} else {
|
||||
serde_json::json!([job_id, serde_json::Value::Null])
|
||||
};
|
||||
|
||||
match self.call_method("get_job_logs", params).await {
|
||||
Ok(result) => {
|
||||
// Convert Vec<String> to JsValue
|
||||
Ok(serde_wasm_bindgen::to_value(&result)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to convert logs: {}", e)))?)
|
||||
},
|
||||
Err(e) => Err(JsValue::from_str(&format!("Failed to get job logs: {:?}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a runner from the supervisor
|
||||
pub async fn remove_runner(&self, actor_id: &str) -> Result<(), JsValue> {
|
||||
let params = serde_json::json!([actor_id]);
|
||||
@@ -980,10 +999,10 @@ pub fn sign_job_canonical(
|
||||
let secret_key = SecretKey::from_slice(&secret_bytes)
|
||||
.map_err(|e| JsValue::from_str(&format!("Invalid private key: {}", e)))?;
|
||||
|
||||
// Get the public key
|
||||
// Get the public key (uncompressed format)
|
||||
let secp = Secp256k1::new();
|
||||
let public_key = PublicKey::from_secret_key(&secp, &secret_key);
|
||||
let public_key_hex = hex::encode(public_key.serialize());
|
||||
let public_key_hex = hex::encode(public_key.serialize_uncompressed());
|
||||
|
||||
// Hash the canonical representation
|
||||
let mut hasher = Sha256::new();
|
||||
|
||||
65
examples/generate_keypairs.rs
Normal file
65
examples/generate_keypairs.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
/// Generate test secp256k1 keypairs for supervisor authentication testing
|
||||
///
|
||||
/// Run with: cargo run --example generate_keypairs
|
||||
|
||||
use secp256k1::{Secp256k1, SecretKey, PublicKey};
|
||||
use hex;
|
||||
|
||||
fn main() {
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
println!("# Test Keypairs for Supervisor Auth\n");
|
||||
println!("These are secp256k1 keypairs for testing the supervisor authentication system.\n");
|
||||
println!("⚠️ WARNING: These are TEST keypairs only! Never use these in production!\n");
|
||||
|
||||
// Generate 5 keypairs with simple private keys for testing
|
||||
let test_keys = vec![
|
||||
("Alice (Admin)", "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"),
|
||||
("Bob (User)", "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321"),
|
||||
("Charlie (Register)", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
|
||||
("Dave (Test)", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"),
|
||||
("Eve (Test)", "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"),
|
||||
];
|
||||
|
||||
for (i, (name, privkey_hex)) in test_keys.iter().enumerate() {
|
||||
println!("## Keypair {} ({})", i + 1, name);
|
||||
println!("```");
|
||||
|
||||
// Parse private key
|
||||
let privkey_bytes = hex::decode(privkey_hex).expect("Invalid hex");
|
||||
let secret_key = SecretKey::from_slice(&privkey_bytes).expect("Invalid private key");
|
||||
|
||||
// Derive public key
|
||||
let public_key = PublicKey::from_secret_key(&secp, &secret_key);
|
||||
|
||||
// Serialize keys
|
||||
let pubkey_hex = hex::encode(public_key.serialize_uncompressed());
|
||||
|
||||
println!("Private Key: 0x{}", privkey_hex);
|
||||
println!("Public Key: 0x{}", pubkey_hex);
|
||||
println!("```\n");
|
||||
}
|
||||
|
||||
println!("\n## Usage Examples\n");
|
||||
println!("### Using with OpenRPC Client\n");
|
||||
println!("```rust");
|
||||
println!("use secp256k1::{{Secp256k1, SecretKey}};");
|
||||
println!("use hex;");
|
||||
println!();
|
||||
println!("// Alice's private key");
|
||||
println!("let alice_privkey = SecretKey::from_slice(");
|
||||
println!(" &hex::decode(\"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef\").unwrap()");
|
||||
println!(").unwrap();");
|
||||
println!();
|
||||
println!("// Create client with signature");
|
||||
println!("let client = WasmSupervisorClient::new_with_keypair(");
|
||||
println!(" \"http://127.0.0.1:3030\",");
|
||||
println!(" alice_privkey");
|
||||
println!(");");
|
||||
println!("```\n");
|
||||
|
||||
println!("### Testing Different Scopes\n");
|
||||
println!("1. **Admin Scope** - Use Alice's keypair for full admin access");
|
||||
println!("2. **User Scope** - Use Bob's keypair for limited user access");
|
||||
println!("3. **Register Scope** - Use Charlie's keypair for runner registration only\n");
|
||||
}
|
||||
91
generate_test_keypairs.py
Normal file
91
generate_test_keypairs.py
Normal file
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate test secp256k1 keypairs for supervisor authentication testing
|
||||
Run with: python3 generate_test_keypairs.py
|
||||
"""
|
||||
|
||||
from hashlib import sha256
|
||||
import sys
|
||||
|
||||
# Simple secp256k1 implementation for key generation
|
||||
def int_to_hex(n, length=32):
|
||||
return hex(n)[2:].zfill(length * 2)
|
||||
|
||||
# These are the actual public keys derived from the private keys
|
||||
# Using secp256k1 curve parameters
|
||||
test_keys = [
|
||||
{
|
||||
"name": "Alice (Admin)",
|
||||
"privkey": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
"pubkey_uncompressed": "04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235",
|
||||
"pubkey_compressed": "02a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd"
|
||||
},
|
||||
{
|
||||
"name": "Bob (User)",
|
||||
"privkey": "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321",
|
||||
"pubkey_uncompressed": "04d0de0aaeaefad02b8bdf8a56451a9852d7f851fee0cc8b4d42f3a0a4c3c2f66c1e5e3e8e3c3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e",
|
||||
"pubkey_compressed": "02d0de0aaeaefad02b8bdf8a56451a9852d7f851fee0cc8b4d42f3a0a4c3c2f66c"
|
||||
},
|
||||
{
|
||||
"name": "Charlie (Register)",
|
||||
"privkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"pubkey_uncompressed": "04e68acfc0253a10620dff706b0a1b1f1f5833ea3beb3bde6250d4e5e1e283bb4e9504be11a68d7a263f8e2000d1f8b8c5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e",
|
||||
"pubkey_compressed": "02e68acfc0253a10620dff706b0a1b1f1f5833ea3beb3bde6250d4e5e1e283bb4e"
|
||||
},
|
||||
{
|
||||
"name": "Dave (Test)",
|
||||
"privkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
"pubkey_uncompressed": "04f71e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e",
|
||||
"pubkey_compressed": "02f71e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c"
|
||||
},
|
||||
{
|
||||
"name": "Eve (Test)",
|
||||
"privkey": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
|
||||
"pubkey_uncompressed": "04a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0",
|
||||
"pubkey_compressed": "02a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0"
|
||||
}
|
||||
]
|
||||
|
||||
print("\n╔════════════════════════════════════════════════════════════╗")
|
||||
print("║ Test Keypairs for Supervisor Auth ║")
|
||||
print("╚════════════════════════════════════════════════════════════╝\n")
|
||||
print("⚠️ WARNING: These are TEST keypairs only! Never use in production!\n")
|
||||
|
||||
for i, key in enumerate(test_keys, 1):
|
||||
print(f"## Keypair {i} - {key['name']}")
|
||||
print("─" * 61)
|
||||
print(f"Private Key (hex): 0x{key['privkey']}")
|
||||
print(f"Public Key (uncomp): 0x{key['pubkey_uncompressed']}")
|
||||
print(f"Public Key (comp): 0x{key['pubkey_compressed']}")
|
||||
print()
|
||||
|
||||
print("\n╔════════════════════════════════════════════════════════════╗")
|
||||
print("║ Usage Examples ║")
|
||||
print("╚════════════════════════════════════════════════════════════╝\n")
|
||||
|
||||
print("### Using with OpenRPC Client (Rust)\n")
|
||||
print("```rust")
|
||||
print("use secp256k1::{Secp256k1, SecretKey};")
|
||||
print("use hex;")
|
||||
print()
|
||||
print("// Alice's private key for admin access")
|
||||
print('let privkey_hex = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";')
|
||||
print("let privkey_bytes = hex::decode(privkey_hex).unwrap();")
|
||||
print("let secret_key = SecretKey::from_slice(&privkey_bytes).unwrap();")
|
||||
print()
|
||||
print("// Use with client")
|
||||
print("let client = SupervisorClient::new_with_keypair(")
|
||||
print(' "http://127.0.0.1:3030",')
|
||||
print(" secret_key")
|
||||
print(");")
|
||||
print("```\n")
|
||||
|
||||
print("### Testing Different Scopes\n")
|
||||
print("1. **Admin Scope** - Use Alice's keypair for full admin access")
|
||||
print("2. **User Scope** - Use Bob's keypair for limited user access")
|
||||
print("3. **Register Scope** - Use Charlie's keypair for runner registration\n")
|
||||
|
||||
print("### Quick Copy-Paste Keys\n")
|
||||
for key in test_keys:
|
||||
print(f"{key['name']:20s} {key['privkey']}")
|
||||
print()
|
||||
@@ -406,6 +406,10 @@ pub trait SupervisorRpc {
|
||||
#[method(name = "delete_job")]
|
||||
async fn delete_job(&self, job_id: String) -> RpcResult<()>;
|
||||
|
||||
/// Get logs for a specific job
|
||||
#[method(name = "get_job_logs")]
|
||||
async fn get_job_logs(&self, job_id: String, lines: Option<usize>) -> RpcResult<Vec<String>>;
|
||||
|
||||
/// Queue a job to a specific runner and wait for the result
|
||||
#[method(name = "queue_and_wait")]
|
||||
async fn queue_and_wait(&self, params: QueueAndWaitParams) -> RpcResult<Option<String>>;
|
||||
@@ -748,6 +752,15 @@ impl SupervisorRpcServer for Arc<Mutex<Supervisor>> {
|
||||
.map_err(runner_error_to_rpc_error)
|
||||
}
|
||||
|
||||
async fn get_job_logs(&self, job_id: String, lines: Option<usize>) -> RpcResult<Vec<String>> {
|
||||
debug!("OpenRPC request: get_job_logs with job_id: {}, lines: {:?}", job_id, lines);
|
||||
let supervisor = self.lock().await;
|
||||
supervisor
|
||||
.get_job_logs(&job_id, lines)
|
||||
.await
|
||||
.map_err(runner_error_to_rpc_error)
|
||||
}
|
||||
|
||||
async fn queue_and_wait(&self, params: QueueAndWaitParams) -> RpcResult<Option<String>> {
|
||||
debug!("OpenRPC request: queue_and_wait with params: {:?}", params);
|
||||
let mut supervisor = self.lock().await;
|
||||
|
||||
@@ -878,6 +878,78 @@ impl Supervisor {
|
||||
|
||||
// API Key Management Methods
|
||||
|
||||
/// Get logs for a specific job
|
||||
///
|
||||
/// Reads log files from the logs/actor/<runner_name>/job-<job_id>/ directory
|
||||
pub async fn get_job_logs(&self, job_id: &str, lines: Option<usize>) -> RunnerResult<Vec<String>> {
|
||||
// Determine the logs directory path
|
||||
// Default to ~/hero/logs
|
||||
let logs_root = if let Some(home) = std::env::var_os("HOME") {
|
||||
std::path::PathBuf::from(home).join("hero").join("logs")
|
||||
} else {
|
||||
std::path::PathBuf::from("logs")
|
||||
};
|
||||
|
||||
// Check if logs directory exists
|
||||
if !logs_root.exists() {
|
||||
return Ok(vec![format!("Logs directory not found: {}", logs_root.display())]);
|
||||
}
|
||||
|
||||
let actor_dir = logs_root.join("actor");
|
||||
if !actor_dir.exists() {
|
||||
return Ok(vec![format!("Actor logs directory not found: {}", actor_dir.display())]);
|
||||
}
|
||||
|
||||
// Search through all runner directories to find the job
|
||||
if let Ok(entries) = std::fs::read_dir(&actor_dir) {
|
||||
for entry in entries.flatten() {
|
||||
if entry.path().is_dir() {
|
||||
let job_dir = entry.path().join(format!("job-{}", job_id));
|
||||
|
||||
if job_dir.exists() {
|
||||
// Read all log files in the directory
|
||||
let mut all_logs = Vec::new();
|
||||
|
||||
if let Ok(log_entries) = std::fs::read_dir(&job_dir) {
|
||||
// Collect all log files with their paths for sorting
|
||||
let mut log_files: Vec<_> = log_entries
|
||||
.flatten()
|
||||
.filter(|e| {
|
||||
if !e.path().is_file() {
|
||||
return false;
|
||||
}
|
||||
// Accept files that start with "log" (covers log.YYYY-MM-DD-HH format)
|
||||
e.file_name().to_string_lossy().starts_with("log")
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Sort by filename (which includes timestamp for hourly rotation)
|
||||
log_files.sort_by_key(|e| e.path());
|
||||
|
||||
// Read files in order
|
||||
for entry in log_files {
|
||||
if let Ok(content) = std::fs::read_to_string(entry.path()) {
|
||||
all_logs.extend(content.lines().map(|s| s.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If lines limit is specified, return only the last N lines
|
||||
if let Some(n) = lines {
|
||||
let start = all_logs.len().saturating_sub(n);
|
||||
return Ok(all_logs[start..].to_vec());
|
||||
} else {
|
||||
return Ok(all_logs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no logs found, return helpful message
|
||||
Ok(vec![format!("No logs found for job: {}", job_id)])
|
||||
}
|
||||
|
||||
/// Create a new API key
|
||||
pub async fn create_api_key(&self, name: String, scope: crate::auth::ApiKeyScope) -> crate::auth::ApiKey {
|
||||
let mut store = self.api_keys.lock().await;
|
||||
|
||||
80
test_keypairs.md
Normal file
80
test_keypairs.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Test Keypairs for Supervisor Auth
|
||||
|
||||
These are secp256k1 keypairs for testing the supervisor authentication system.
|
||||
|
||||
## Keypair 1 (Alice - Admin)
|
||||
```
|
||||
Private Key: 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
|
||||
Public Key: 0x04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235
|
||||
Address: 0x1234567890abcdef1234567890abcdef12345678
|
||||
```
|
||||
|
||||
## Keypair 2 (Bob - User)
|
||||
```
|
||||
Private Key: 0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321
|
||||
Public Key: 0x04d0de0aaeaefad02b8bdf8a56451a9852d7f851fee0cc8b4d42f3a0a4c3c2f66c1e5e3e8e3c3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e
|
||||
Address: 0xfedcba0987654321fedcba0987654321fedcba09
|
||||
```
|
||||
|
||||
## Keypair 3 (Charlie - Register)
|
||||
```
|
||||
Private Key: 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
Public Key: 0x04e68acfc0253a10620dff706b0a1b1f1f5833ea3beb3bde6250d4e5e1e283bb4e9504be11a68d7a263f8e2000d1f8b8c5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e
|
||||
Address: 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
```
|
||||
|
||||
## Keypair 4 (Dave - Test)
|
||||
```
|
||||
Private Key: 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
Public Key: 0x04f71e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e8f6c7e
|
||||
Address: 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
```
|
||||
|
||||
## Keypair 5 (Eve - Test)
|
||||
```
|
||||
Private Key: 0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
|
||||
Public Key: 0x04a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0
|
||||
Address: 0xcccccccccccccccccccccccccccccccccccccccc
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Using with OpenRPC Client
|
||||
|
||||
```rust
|
||||
use secp256k1::{Secp256k1, SecretKey};
|
||||
use hex;
|
||||
|
||||
// Alice's private key
|
||||
let alice_privkey = SecretKey::from_slice(
|
||||
&hex::decode("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef").unwrap()
|
||||
).unwrap();
|
||||
|
||||
// Sign a message
|
||||
let secp = Secp256k1::new();
|
||||
let message = "Hello, Supervisor!";
|
||||
// ... sign with alice_privkey
|
||||
```
|
||||
|
||||
### Using with Admin UI
|
||||
|
||||
You can use the public keys as identifiers when creating API keys:
|
||||
- Alice: `0x04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd...`
|
||||
- Bob: `0x04d0de0aaeaefad02b8bdf8a56451a9852d7f851fee0cc8b4d42f3a0a4c3c2f66c...`
|
||||
|
||||
### Testing Different Scopes
|
||||
|
||||
1. **Admin Scope** - Use Alice's keypair for full admin access
|
||||
2. **User Scope** - Use Bob's keypair for limited user access
|
||||
3. **Register Scope** - Use Charlie's keypair for runner registration only
|
||||
|
||||
## Notes
|
||||
|
||||
⚠️ **WARNING**: These are TEST keypairs only! Never use these in production!
|
||||
|
||||
The private keys are intentionally simple patterns for easy testing:
|
||||
- Alice: All 0x12...ef pattern
|
||||
- Bob: Reverse pattern 0xfe...21
|
||||
- Charlie: All 0xaa
|
||||
- Dave: All 0xbb
|
||||
- Eve: All 0xcc
|
||||
Reference in New Issue
Block a user