contacts system prim

This commit is contained in:
dr-frmr 2024-10-10 15:21:38 -04:00
parent dba98d1884
commit 5eb9492e6c
No known key found for this signature in database
11 changed files with 4097 additions and 0 deletions

10
Cargo.lock generated
View File

@ -1849,6 +1849,16 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2"
[[package]]
name = "contacts"
version = "0.1.0"
dependencies = [
"kinode_process_lib 0.9.1",
"serde",
"serde_json",
"wit-bindgen",
]
[[package]]
name = "convert_case"
version = "0.4.0"

View File

@ -17,6 +17,7 @@ members = [
"kinode/packages/app_store/app_store", "kinode/packages/app_store/ft_worker",
"kinode/packages/app_store/download", "kinode/packages/app_store/install", "kinode/packages/app_store/uninstall", "kinode/packages/app_store/downloads", "kinode/packages/app_store/chain",
"kinode/packages/chess/chess",
"kinode/packages/contacts/contacts",
"kinode/packages/homepage/homepage",
"kinode/packages/kino_updates/blog", "kinode/packages/kino_updates/globe",
"kinode/packages/kns_indexer/kns_indexer", "kinode/packages/kns_indexer/get_block", "kinode/packages/kns_indexer/state",

3174
kinode/packages/contacts/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,10 @@
[workspace]
resolver = "2"
members = [
"contacts",
]
[profile.release]
panic = "abort"
opt-level = "s"
lto = true

View File

@ -0,0 +1,19 @@
[package]
name = "contacts"
version = "0.1.0"
edition = "2021"
[features]
simulation-mode = []
[dependencies]
kinode_process_lib = "0.9.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
wit-bindgen = "0.24.0"
[lib]
crate-type = ["cdylib"]
[package.metadata.component]
package = "kinode:process"

View File

@ -0,0 +1 @@
data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTM2IiBoZWlnaHQ9IjEzNiIgdmlld0JveD0iMCAwIDEzNiAxMzYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxMzYiIGhlaWdodD0iMTM2IiByeD0iNjgiIGZpbGw9IiNGRkY1RDkiIGZpbGwtb3BhY2l0eT0iMC40Ii8+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF8xODIxXzkwOTMpIj4KPHBhdGggZD0iTTY3LjQ4NTUgOTQuOTg4MUM2NS44MTYxIDk0Ljk4ODEgNjQuMTQ2NyA5NC45NjQzIDYyLjQ3NzIgOTQuOTg4MUM2MS41NjAyIDk1LjAxMTkgNjEuMDQyOSA5NC40MTc0IDYwLjkzNzEgOTMuNjQ0N0M2MC42NjY3IDkxLjU1MjMgNjAuMzQ5MyA4OS40NTk4IDYwLjA2NzEgODcuMzY3NEM2MC4wMjAxIDg3LjAxMDggNTkuODc5IDg2Ljc5NjggNTkuNTM4MSA4Ni42NTQxQzU4LjIzMzEgODYuMDgzNCA1Ny4wMjIyIDg1LjM1ODIgNTUuODgxOCA4NC40OTA0QzU1LjYyMzEgODQuMjg4MiA1NS4zOTk4IDg0LjI2NDUgNTUuMDk0MSA4NC4zOTUyQzUzLjE1NDMgODUuMjAzNyA1MS4yMTQ0IDg1Ljk4ODMgNDkuMjc0NiA4Ni43NzNDNDguMjc1MyA4Ny4xNzcyIDQ3Ljc5MzMgODcuMDIyNyA0Ny4yNTI1IDg2LjA5NTNDNDUuNTcxMyA4My4xNTg4IDQzLjg5MDEgODAuMjEwNCA0Mi4yMDg5IDc3LjI2MkM0MS44MDkyIDc2LjU3MjUgNDEuOTI2NyA3NS45NDIzIDQyLjU0OTggNzUuNDQzQzQ0LjIzMSA3NC4wOTk2IDQ1LjkxMjIgNzIuNzU2MiA0Ny42MDUyIDcxLjQzNjVDNDcuODk5MSA3MS4yMTA2IDQ4LjAwNDkgNzAuOTYxIDQ3Ljk1NzkgNzAuNTkyNEM0Ny43ODE1IDY5LjIwMTQgNDcuNzkzMyA2Ny43OTg2IDQ3Ljk1NzkgNjYuNDA3NkM0OC4wMDQ5IDY2LjA1MDkgNDcuOTEwOCA2NS44MTMxIDQ3LjYyODcgNjUuNTg3M0M0NS45OTQ1IDY0LjMwMzMgNDQuMzYwNCA2My4wMDc0IDQyLjczNzkgNjEuNzExNUM0MS44Nzk3IDYxLjAzMzkgNDEuNzczOSA2MC41MTA4IDQyLjMxNDcgNTkuNTcxNkM0NC4wMDc3IDU2LjYxMTMgNDUuNjg4OCA1My42MzkxIDQ3LjM5MzYgNTAuNjc4OEM0Ny43NDYyIDUwLjA3MjUgNDguMzgxMSA0OS44NTg1IDQ5LjA3NDcgNTAuMTQzOEM1MC40MjY3IDUwLjY5MDcgNTEuNzc4OCA1MS4yMzc1IDUzLjEzMDggNTEuNzk2M0M1My44MjQ0IDUyLjA4MTcgNTQuNTE4IDUyLjM1NTEgNTUuMjExNyA1Mi42NTIzQzU1LjQyMzMgNTIuNzQ3NCA1NS41ODc5IDUyLjczNTUgNTUuNzc2IDUyLjU5MjlDNTYuOTg2OSA1MS42NjU1IDU4LjMwMzYgNTAuODkyOCA1OS43MDI3IDUwLjI4NjVDNTkuOTQ5NiA1MC4xNzk1IDYwLjAyMDEgNDkuOTg5MiA2MC4wNDM2IDQ5Ljc1MTVDNjAuMzE0IDQ3LjgxMzYgNjAuNTg0NCA0NS44NzU3IDYwLjg1NDggNDMuOTM3OUM2MC44OTAxIDQzLjcxMiA2MC45MTM2IDQzLjQ4NjEgNjAuOTQ4OSA0My4yNjAyQzYxLjA5IDQyLjQxNjEgNjEuNTYwMiA0MiA2Mi4zNzE0IDQyQzY0LjQ0MDYgNDIgNjYuNTA5NyA0MiA2OC41Nzg5IDQyQzY5LjkwNzQgNDIgNzEuMjQ3NiA0MiA3Mi41NzYxIDQyQzczLjQ0NjEgNDIgNzMuOTI4MSA0Mi40NTE4IDc0LjA1NzUgNDMuMzMxNUM3NC4zNTE0IDQ1LjQzNTggNzQuNjQ1MyA0Ny41NDAyIDc0LjkyNzQgNDkuNjQ0NUM3NC45NzQ1IDQ5Ljk4OTIgNzUuMTE1NSA1MC4xNzk1IDc1LjQzMyA1MC4zMzRDNzYuNzM4IDUwLjkxNjYgNzcuOTcyNCA1MS42Mjk5IDc5LjExMjggNTIuNTA5NkM3OS4zNTk3IDUyLjY5OTkgNzkuNTU5NSA1Mi43MjM2IDc5Ljg0MTcgNTIuNjA0OEM4MS44MDUgNTEuNzk2MyA4My43Njg0IDUwLjk4NzkgODUuNzQzNSA1MC4xOTEzQzg2LjY3MjMgNDkuODEwOSA4Ny4yMTMxIDUwLjAxMyA4Ny43MTg2IDUwLjg5MjhDODkuMzc2MyA1My44MDU1IDkxLjAzMzkgNTYuNzE4MyA5Mi42OTE2IDU5LjYxOTFDOTMuMTczNiA2MC40NTEzIDkzLjA2NzggNjEuMDIyIDkyLjMxNTQgNjEuNjI4M0M5MC42NDYgNjIuOTU5OCA4OC45NzY1IDY0LjI3OTUgODcuMjk1NCA2NS41ODczQzg3LjAyNDkgNjUuODAxMyA4Ni45NDI3IDY2LjAyNzEgODYuOTc3OSA2Ni4zNkM4Ny4xNTQzIDY3Ljc4NjcgODcuMTU0MyA2OS4yMTMzIDg2Ljk3NzkgNzAuNjRDODYuOTQyNyA3MC45NjEgODcuMDI1IDcxLjE4NjkgODcuMjgzNiA3MS4zODlDODguOTI5NSA3Mi42ODQ4IDkwLjU3NTQgNzMuOTkyNiA5Mi4yMjE0IDc1LjMwMDRDOTMuMDU2MSA3NS45NjYxIDkzLjE2MTkgNzYuNDg5MiA5Mi42MzI4IDc3LjQxNjZDOTAuOTc1MiA4MC4zNDEyIDg5LjMwNTcgODMuMjUzOSA4Ny42MzYzIDg2LjE2NjdDODcuMTg5NSA4Ni45NTEzIDg2LjYwMTcgODcuMTUzNCA4NS43NjcgODYuODA4N0M4My44MDM3IDg2LjAxMjEgODEuODUyMSA4NS4yMTU2IDc5LjkwMDUgODQuNDA3MUM3OS41NzEzIDg0LjI2NDUgNzkuMzI0NCA4NC4yODgyIDc5LjAzMDUgODQuNTE0MUM3Ny45MDE5IDg1LjM5MzkgNzYuNjc5MiA4Ni4wOTUzIDc1LjM3NDIgODYuNjY2Qzc1LjA2ODUgODYuNzk2OCA3NC45MzkyIDg2Ljk4NyA3NC44OTIyIDg3LjMwOEM3NC42MSA4OS40MDA0IDc0LjMxNjEgOTEuNDkyOCA3NC4wMTA0IDkzLjU4NTJDNzMuOTYzNCA5My45MDYyIDczLjgzNDEgOTQuMjI3MiA3My42Njk1IDk0LjUwMDdDNzMuNDM0NCA5NC44NTczIDczLjA1ODEgOTQuOTg4MSA3Mi42MjMxIDk0Ljk4ODFDNzAuOTA2NyA5NC45ODgxIDY5LjE3ODUgOTQuOTg4MSA2Ny40NjIgOTQuOTg4MUg2Ny40ODU1Wk02Ny40NzM4IDU5LjE1NTVDNjIuNDMwMiA1OS4xNTU1IDU4LjI2ODQgNjMuMzUyMiA1OC4yNTY2IDY4LjQ2NDNDNTguMjQ0OSA3My42MDAzIDYyLjQwNjcgNzcuODIwOCA2Ny40ODU1IDc3LjgyMDhDNzIuNTUyNiA3Ny44MjA4IDc2LjcxNDQgNzMuNjEyMiA3Ni43MTQ0IDY4LjQ4ODFDNzYuNzE0NCA2My4zNDAzIDcyLjU2NDQgNTkuMTU1NSA2Ny40NjIgNTkuMTQzNkw2Ny40NzM4IDU5LjE1NTVaIiBmaWxsPSIjRkZGNUQ5Ii8+CjwvZz4KPGRlZnM+CjxjbGlwUGF0aCBpZD0iY2xpcDBfMTgyMV85MDkzIj4KPHJlY3Qgd2lkdGg9IjUxIiBoZWlnaHQ9IjUzIiBmaWxsPSJ3aGl0ZSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNDIgNDIpIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg==

View File

@ -0,0 +1,291 @@
use kinode_process_lib::{
await_message, call_init, eth, get_blob, homepage, http, kernel_types, net, println, Address,
LazyLoadBlob, Message, NodeId, ProcessId, Request, Response, SendError, SendErrorKind,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
const ICON: &str = include_str!("icon");
#[derive(Debug, Serialize, Deserialize)]
struct ContactsState {
our: Address,
}
impl ContactsState {
fn new(our: Address) -> Self {
Self { our }
}
}
wit_bindgen::generate!({
path: "target/wit",
world: "process-v0",
});
call_init!(initialize);
fn initialize(our: Address) {
// add ourselves to the homepage
homepage::add_to_homepage("Contacts", Some(ICON), Some("/"), None);
// Grab our state, then enter the main event loop.
let mut state: ContactsState = ContactsState::new(our);
let mut http_server = http::server::HttpServer::new(5);
// Serve the index.html and other UI files found in pkg/ui at the root path.
// Serving securely at `settings-sys` subdomain
http_server
.serve_ui(
&state.our,
"ui",
vec!["/"],
http::server::HttpBindingConfig::default().secure_subdomain(true),
)
.unwrap();
http_server.secure_bind_http_path("/ask").unwrap();
http_server.secure_bind_ws_path("/").unwrap();
main_loop(&mut state, &mut http_server);
}
fn main_loop(state: &mut ContactsState, http_server: &mut http::server::HttpServer) {
loop {
match await_message() {
Err(send_error) => {
println!("got send error: {send_error:?}");
continue;
}
Ok(Message::Request {
source,
body,
expects_response,
..
}) => {
if source.node() != state.our.node {
continue; // ignore messages from other nodes
}
let response = handle_request(&source, &body, state, http_server);
// state.ws_update(http_server);
if expects_response.is_some() {
Response::new()
.body(serde_json::to_vec(&response).unwrap())
.send()
.unwrap();
}
}
_ => continue, // ignore responses
}
}
}
fn handle_request(
source: &Address,
body: &[u8],
state: &mut ContactsState,
http_server: &mut http::server::HttpServer,
) -> SettingsResponse {
// source node is ALWAYS ourselves since networking is disabled
if source.process == "http_server:distro:sys" {
// receive HTTP requests and websocket connection messages from our server
let server_request = http_server
.parse_request(body)
.map_err(|_| SettingsError::MalformedRequest)?;
http_server.handle_request(
server_request,
|req| {
let result = handle_http_request(state, &req);
match result {
Ok((resp, blob)) => (resp, blob),
Err(e) => {
println!("error handling HTTP request: {e}");
(
http::server::HttpResponse {
status: 500,
headers: HashMap::new(),
},
Some(LazyLoadBlob {
mime: Some("application/text".to_string()),
bytes: e.to_string().as_bytes().to_vec(),
}),
)
}
}
},
|_channel_id, _message_type, _blob| {
// we don't expect websocket messages
},
);
Ok(None)
} else {
let settings_request = serde_json::from_slice::<SettingsRequest>(body)
.map_err(|_| SettingsError::MalformedRequest)?;
handle_settings_request(state, settings_request)
}
}
/// Handle HTTP requests from our own frontend.
fn handle_http_request(
state: &mut ContactsState,
http_request: &http::server::IncomingHttpRequest,
) -> anyhow::Result<(http::server::HttpResponse, Option<LazyLoadBlob>)> {
match http_request.method()?.as_str() {
"GET" => {
state.fetch()?;
Ok((
http::server::HttpResponse::new(http::StatusCode::OK)
.header("Content-Type", "application/json"),
Some(LazyLoadBlob::new(
Some("application/json"),
serde_json::to_vec(&state)?,
)),
))
}
"POST" => {
let Some(blob) = get_blob() else {
return Err(anyhow::anyhow!("malformed request"));
};
let request = serde_json::from_slice::<SettingsRequest>(&blob.bytes)?;
let response = handle_settings_request(state, request);
Ok((
http::server::HttpResponse::new(http::StatusCode::OK)
.header("Content-Type", "application/json"),
match response {
Ok(Some(data)) => Some(LazyLoadBlob::new(
Some("application/json"),
serde_json::to_vec(&data)?,
)),
Ok(None) => None,
Err(e) => Some(LazyLoadBlob::new(
Some("application/json"),
serde_json::to_vec(&e)?,
)),
},
))
}
// Any other method will be rejected.
_ => Ok((
http::server::HttpResponse::new(http::StatusCode::METHOD_NOT_ALLOWED),
None,
)),
}
}
fn handle_settings_request(
state: &mut SettingsState,
request: SettingsRequest,
) -> SettingsResponse {
match request {
SettingsRequest::Hi {
node,
content,
timeout,
} => {
if let Err(SendError { kind, .. }) = Request::to((&node, "net", "distro", "sys"))
.body(content.into_bytes())
.send_and_await_response(timeout)
.unwrap()
{
match kind {
SendErrorKind::Timeout => {
println!("message to {node} timed out");
return Err(SettingsError::HiTimeout);
}
SendErrorKind::Offline => {
println!("{node} is offline or does not exist");
return Err(SettingsError::HiOffline);
}
}
} else {
return Ok(None);
}
}
SettingsRequest::PeerId(node) => {
// get peer info
match Request::to(("our", "net", "distro", "sys"))
.body(rmp_serde::to_vec(&net::NetAction::GetPeer(node)).unwrap())
.send_and_await_response(30)
.unwrap()
{
Ok(msg) => match rmp_serde::from_slice::<net::NetResponse>(msg.body()) {
Ok(net::NetResponse::Peer(Some(peer))) => {
println!("got peer info: {peer:?}");
return Ok(Some(SettingsData::PeerId(peer)));
}
Ok(net::NetResponse::Peer(None)) => {
println!("peer not found");
return Ok(None);
}
_ => {
return Err(SettingsError::KernelNonresponsive);
}
},
Err(_) => {
return Err(SettingsError::KernelNonresponsive);
}
}
}
SettingsRequest::EthConfig(action) => {
match Request::to(("our", "eth", "distro", "sys"))
.body(serde_json::to_vec(&action).unwrap())
.send_and_await_response(30)
.unwrap()
{
Ok(msg) => match serde_json::from_slice::<eth::EthConfigResponse>(msg.body()) {
Ok(eth::EthConfigResponse::PermissionDenied) => {
return Err(SettingsError::KernelNonresponsive);
}
Ok(other) => {
println!("eth config action succeeded: {other:?}");
}
Err(_) => {
return Err(SettingsError::KernelNonresponsive);
}
},
Err(_) => {
return Err(SettingsError::KernelNonresponsive);
}
}
}
SettingsRequest::Shutdown => {
// shutdown the node IMMEDIATELY!
Request::to(("our", "kernel", "distro", "sys"))
.body(serde_json::to_vec(&kernel_types::KernelCommand::Shutdown).unwrap())
.send()
.unwrap();
}
SettingsRequest::KillProcess(pid) => {
// kill a process
if let Err(_) = Request::to(("our", "kernel", "distro", "sys"))
.body(serde_json::to_vec(&kernel_types::KernelCommand::KillProcess(pid)).unwrap())
.send_and_await_response(30)
.unwrap()
{
return SettingsResponse::Err(SettingsError::KernelNonresponsive);
}
}
SettingsRequest::SetStylesheet(stylesheet) => {
let Ok(()) = kinode_process_lib::vfs::File {
path: "/homepage:sys/pkg/kinode.css".to_string(),
timeout: 5,
}
.write(stylesheet.as_bytes()) else {
return SettingsResponse::Err(SettingsError::KernelNonresponsive);
};
Request::to(("our", "homepage", "homepage", "sys"))
.body(
serde_json::json!({ "SetStylesheet": stylesheet })
.to_string()
.as_bytes(),
)
.send()
.unwrap();
state.stylesheet = Some(stylesheet);
return SettingsResponse::Ok(None);
}
}
state.fetch().map_err(|_| SettingsError::StateFetchFailed)?;
SettingsResponse::Ok(None)
}

View File

@ -0,0 +1,18 @@
{
"name": "Contacts",
"description": "Store and manage your contacts.",
"image": "",
"properties": {
"package_name": "contacts",
"current_version": "0.1.0",
"publisher": "sys",
"mirrors": [],
"code_hashes": {
"0.1.0": ""
},
"wit_version": 0,
"dependencies": []
},
"external_url": "https://kinode.org",
"animation_url": ""
}

View File

@ -0,0 +1,18 @@
[
{
"process_name": "contacts",
"process_wasm_path": "/contacts.wasm",
"on_exit": "Restart",
"request_networking": false,
"request_capabilities": [
"homepage:homepage:sys",
"http_server:distro:sys",
"vfs:distro:sys"
],
"grant_capabilities": [
"http_server:distro:sys",
"vfs:distro:sys"
],
"public": false
}
]

View File

@ -0,0 +1,235 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="/kinode.css">
<link
href="https://api.fontshare.com/v2/css?f[]=clash-display@400,700,500,600,300&display=swap"
rel="stylesheet"
/>
<script src="/our.js"></script>
<script>
document.title = window.our.node + " - settings";
</script>
<style>
h1,
h2,
h3,
h4,
h5,
h6,
p,
a,
li {
font-family: 'Kode Mono', monospace;
}
h1 {
padding: 20px;
max-width: 960px;
min-width: 300px;
margin: 0 auto;
}
main {
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20px 20px;
grid-auto-flow: row;
grid-template-areas:
"diagnostics diagnostics diagnostics"
"node-info pings pings"
"eth-rpc-providers eth-rpc-providers eth-rpc-settings"
"kernel kernel kernel"
"kinode-css kinode-css kinode-css";
padding: 20px;
max-width: 960px;
min-width: 300px;
}
article#net-diagnostics {
grid-area: diagnostics;
}
p#diagnostics,
p#peer-pki-response,
p#peer-ping-response {
white-space: pre-wrap;
}
article#node-info {
grid-area: node-info;
word-wrap: break-word;
display: flex;
flex-direction: column;
justify-content: space-around;
}
#shutdown {
background-color: var(--ansi-red)
}
#shutdown:hover {
background-color: var(--maroon);
}
article#pings {
grid-area: pings;
}
article#eth-rpc-providers {
grid-area: eth-rpc-providers;
}
article#eth-rpc-settings {
grid-area: eth-rpc-settings;
}
article#kernel {
grid-area: kernel;
}
article#kinode-css {
grid-area: kinode-css;
}
textarea#stylesheet-editor {
width: 100%;
min-width: 300px;
min-height: 400px;
}
div#provider-edits {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px 20px;
grid-auto-flow: row;
}
article {
border: 1px solid #444;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
max-height: 600px;
overflow-y: auto;
}
ul {
list-style-type: none;
padding: 0;
}
li {
padding: 8px;
margin-bottom: 6px;
border-radius: 4px;
word-wrap: break-word;
white-space: pre-wrap;
}
#process-map li p:first-child {
font-weight: bold;
}
#process-map li ul {
padding-left: 20px;
}
#process-map li ul li {
margin-bottom: 1px;
padding: 0;
}
button.kill-process {
padding: 3px 6px;
margin: 10px;
}
</style>
</head>
<body>
<h1>system diagnostics & settings</h1>
<main>
<article id="net-diagnostics">
<h2>networking diagnostics</h2>
<p id="diagnostics"></p>
</article>
<article id="node-info">
<h2>node info</h2>
<p id="node-name"></p>
<p id="net-key"></p>
<p id="ip-ports"></p>
<p id="routers"></p>
<button id="shutdown">shut down node(!)</button>
</article>
<article id="pings">
<h2>fetch PKI data</h2>
<form id="get-peer-pki">
<input type="text" name="peer" placeholder="peer-name.os">
<button type="submit">get peer info</button>
</form>
<p id="peer-pki-response"></p>
<h2>ping a node</h2>
<form id="ping-peer">
<input type="text" name="peer" placeholder="peer-name.os">
<input type="text" name="content" placeholder="message">
<input type="number" name="timeout" placeholder="timeout (seconds)">
<button type="submit">ping</button>
</form>
<p id="peer-ping-response"></p>
</article>
<article id="eth-rpc-providers">
<h2>ETH RPC providers</h2>
<div id="provider-edits">
<form id="add-eth-provider">
<input type="number" name="chain-id" placeholder="1">
<input type="text" name="rpc-url" placeholder="wss://rpc-url.com">
<button type="submit">add provider</button>
</form>
<form id="remove-eth-provider">
<input type="number" name="chain-id" placeholder="1">
<input type="text" name="rpc-url" placeholder="wss://rpc-url.com">
<button type="submit">remove provider</button>
</form>
</div>
<ul id="providers"></ul>
</article>
<article id="eth-rpc-settings">
<h2>ETH RPC settings</h2>
<p id="public"></p>
<div>
<p>nodes allowed to connect:</p>
<ul id="allowed-nodes"></ul>
</div>
<div>
<p>nodes banned from connecting:</p>
<ul id="denied-nodes"></ul>
</div>
</article>
<article id="kernel">
<h2>running processes</h2>
<ul id="process-map"></ul>
</article>
<article id="kinode-css">
<h2>stylesheet editor</h2>
<textarea id="stylesheet-editor"></textarea>
<button id="save-stylesheet">update kinode.css</button>
</article>
<script src="/settings:settings:sys/script.js"></script>
</main>
</body>
</html>

View File

@ -0,0 +1,320 @@
const APP_PATH = '/settings:settings:sys/ask';
// Fetch initial data and populate the UI
function init() {
fetch(APP_PATH)
.then(response => response.json())
.then(data => {
populate(data);
});
}
function api_call(body) {
fetch(APP_PATH, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
}
function shutdown() {
api_call("Shutdown");
setTimeout(() => {
window.location.reload();
}, 1000);
}
function populate(data) {
populate_node_info(data.identity);
populate_net_diagnostics(data.diagnostics);
populate_eth_rpc_providers(data.eth_rpc_providers);
populate_eth_rpc_settings(data.eth_rpc_access_settings);
populate_process_map(data.process_map);
populate_stylesheet_editor(data.stylesheet);
}
function populate_node_info(identity) {
document.getElementById('node-name').innerText = identity.name;
document.getElementById('net-key').innerText = identity.networking_key;
if (identity.ws_routing) {
document.getElementById('ip-ports').innerText = identity.ws_routing;
} else {
document.getElementById('ip-ports').style.display = 'none';
}
if (identity.routers) {
document.getElementById('routers').innerText = identity.routers;
} else {
document.getElementById('routers').style.display = 'none';
}
}
function populate_net_diagnostics(diagnostics) {
document.getElementById('diagnostics').innerText = diagnostics;
}
function populate_eth_rpc_providers(providers) {
const ul = document.getElementById('providers');
ul.innerHTML = '';
providers.forEach(provider => {
const li = document.createElement('li');
li.innerHTML = `${JSON.stringify(provider, undefined, 2)}`;
ul.appendChild(li);
});
}
function populate_eth_rpc_settings(settings) {
if (settings.public) {
document.getElementById('public').innerText = 'status: public';
document.getElementById('allowed-nodes').style.display = 'none';
} else {
document.getElementById('public').innerText = 'status: private';
const ul = document.getElementById('allowed-nodes');
ul.innerHTML = '';
if (settings.allow.length === 0) {
const li = document.createElement('li');
li.innerHTML = `<li>(none)</li>`;
ul.appendChild(li);
} else {
settings.allow.forEach(allowed_node => {
const li = document.createElement('li');
li.innerHTML = `<li>${allowed_node}</li>`;
ul.appendChild(li);
});
}
}
const ul = document.getElementById('denied-nodes');
ul.innerHTML = '';
if (settings.deny.length === 0) {
const li = document.createElement('li');
li.innerHTML = `<li>(none)</li>`;
ul.appendChild(li);
} else {
settings.deny.forEach(denied_node => {
const li = document.createElement('li');
li.innerHTML = `<li>${denied_node}</li>`;
ul.appendChild(li);
});
}
}
function populate_process_map(process_map) {
// apps we don't want user to kill, also runtime modules that cannot be killed
const do_not_kill = [
'settings:setting:sys',
'main:app_store:sys',
'net:distro:sys',
'kernel:distro:sys',
'kv:distro:sys',
'sqlite:distro:sys',
'eth:distro:sys',
'vfs:distro:sys',
'state:distro:sys',
'kns_indexer:kns_indexer:sys',
'http_client:distro:sys',
'http_server:distro:sys',
'terminal:terminal:sys',
'timer:distro:sys',
];
const ul = document.getElementById('process-map');
ul.innerHTML = '';
Object.entries(process_map).forEach(([id, process]) => {
const li = document.createElement('li');
const toggleButton = document.createElement('button');
toggleButton.textContent = `${id}`;
toggleButton.onclick = function () {
const details = this.nextElementSibling;
details.style.display = details.style.display === 'none' ? 'block' : 'none';
};
li.appendChild(toggleButton);
const detailsDiv = document.createElement('div');
detailsDiv.style.display = 'none';
if (!do_not_kill.includes(id)) {
const killButton = document.createElement('button');
killButton.className = 'kill-process';
killButton.setAttribute('data-id', id);
killButton.textContent = 'kill';
detailsDiv.appendChild(killButton);
}
const publicInfo = document.createElement('p');
publicInfo.textContent = `public: ${process.public}`;
detailsDiv.appendChild(publicInfo);
const onExit = document.createElement('p');
onExit.textContent = `on_exit: ${process.on_exit}`;
detailsDiv.appendChild(onExit);
if (process.wit_version) {
const witVersion = document.createElement('p');
witVersion.textContent = `wit_version: ${process.wit_version}`;
detailsDiv.appendChild(witVersion);
}
if (process.wasm_bytes_handle) {
const wasmBytesHandle = document.createElement('p');
wasmBytesHandle.textContent = `wasm_bytes_handle: ${process.wasm_bytes_handle}`;
detailsDiv.appendChild(wasmBytesHandle);
}
const capsList = document.createElement('ul');
process.capabilities.forEach(cap => {
const capLi = document.createElement('li');
capLi.textContent = `${cap.issuer}(${JSON.stringify(JSON.parse(cap.params), null, 2)})`;
capsList.appendChild(capLi);
});
detailsDiv.appendChild(capsList);
li.appendChild(detailsDiv);
ul.appendChild(li);
});
document.querySelectorAll('.kill-process').forEach(button => {
button.addEventListener('click', () => {
api_call({ "KillProcess": button.getAttribute('data-id') });
});
});
}
function populate_stylesheet_editor(stylesheet) {
document.getElementById('stylesheet-editor').value = stylesheet;
}
function save_stylesheet() {
const stylesheet = document.getElementById('stylesheet-editor').value;
api_call({ "SetStylesheet": stylesheet });
}
// Call init to start the application
init();
// Setup event listeners
document.getElementById('shutdown').addEventListener('click', shutdown);
document.getElementById('save-stylesheet').addEventListener('click', save_stylesheet);
document.getElementById('get-peer-pki').addEventListener('submit', (e) => {
e.preventDefault();
const data = new FormData(e.target);
const body = {
"PeerId": data.get('peer'),
};
fetch(APP_PATH, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
}).then(response => response.json())
.then(data => {
if (data === null) {
document.getElementById('peer-pki-response').innerText = "no pki data for peer";
} else {
e.target.reset();
document.getElementById('peer-pki-response').innerText = JSON.stringify(data, undefined, 2);
}
});
})
document.getElementById('ping-peer').addEventListener('submit', (e) => {
e.preventDefault();
const data = new FormData(e.target);
const body = {
"Hi": {
node: data.get('peer'),
content: data.get('content'),
timeout: Number(data.get('timeout')),
}
};
fetch(APP_PATH, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
}).then(response => response.json())
.then(data => {
if (data === null) {
e.target.reset();
document.getElementById('peer-ping-response').innerText = "ping successful!";
} else if (data === "HiTimeout") {
document.getElementById('peer-ping-response').innerText = "node timed out";
} else if (data === "HiOffline") {
document.getElementById('peer-ping-response').innerText = "node is offline";
}
});
})
document.getElementById('add-eth-provider').addEventListener('submit', (e) => {
e.preventDefault();
const data = new FormData(e.target);
const rpc_url = data.get('rpc-url');
// validate rpc url
if (!rpc_url.startsWith('wss://') && !rpc_url.startsWith('ws://')) {
alert('Invalid RPC URL');
return;
}
const body = {
"EthConfig": {
"AddProvider": {
chain_id: Number(data.get('chain-id')),
trusted: false,
provider: { "RpcUrl": rpc_url },
}
}
};
fetch(APP_PATH, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
}).then(response => response.json())
.then(data => {
if (data === null) {
e.target.reset();
return;
} else {
alert(data);
}
});
})
document.getElementById('remove-eth-provider').addEventListener('submit', (e) => {
e.preventDefault();
const data = new FormData(e.target);
const body = {
"EthConfig": {
"RemoveProvider": [Number(data.get('chain-id')), data.get('rpc-url')]
}
};
fetch(APP_PATH, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
}).then(response => response.json())
.then(data => {
if (data === null) {
e.target.reset();
return;
} else {
alert(data);
}
});
})
// Setup WebSocket connection
const wsProtocol = location.protocol === 'https:' ? 'wss://' : 'ws://';
const ws = new WebSocket(wsProtocol + location.host + "/settings:settings:sys/");
ws.onmessage = event => {
const data = JSON.parse(event.data);
console.log(data);
populate(data);
};