Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8fe3a05d8b |
@@ -2,8 +2,8 @@ name: Build Library
|
||||
run-name: ${{ gitea.actor }}
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- master
|
||||
branches:
|
||||
- RustImplementation
|
||||
paths:
|
||||
- airlock_libs/**
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ class MultiAgentSelector(Widget):
|
||||
|
||||
def compose(self):
|
||||
yield Header(show_clock=True, icon="⚙")
|
||||
title_text = Static("🖥️ Agent Selector", id="selector_title")
|
||||
title_text = Static("🖥️ Agent Selector", id="selector_title")
|
||||
title_text.styles.margin = (0, 0, 0, 1)
|
||||
yield title_text
|
||||
|
||||
@@ -175,7 +175,7 @@ class MultiAgentSelector(Widget):
|
||||
match_list.add_option((name, name))
|
||||
unmatched_label = self.query_one("#unmatched_label", Static)
|
||||
if unmatched:
|
||||
unmatched_label.update(f"âš ï¸ No matches for: {', '.join(unmatched)}")
|
||||
unmatched_label.update(f"No matches for: {', '.join(unmatched)}")
|
||||
else:
|
||||
unmatched_label.update("")
|
||||
|
||||
|
||||
@@ -23,8 +23,10 @@ import webbrowser
|
||||
from textual.containers import Horizontal, Vertical, VerticalScroll
|
||||
from textual.message import Message
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Button, Rule, Static
|
||||
from textual.widgets import Button, Input, Rule, Static, Switch
|
||||
|
||||
from utils.configmanager import get_user_value, save_user_config
|
||||
from utils.setup import get_base_directory
|
||||
from utils.versionchecker import (
|
||||
RELEASES_PAGE_URL,
|
||||
UpdateCheckResult,
|
||||
@@ -81,6 +83,36 @@ class SettingsWidget(Widget):
|
||||
#light_themes_col {
|
||||
margin-left: 1;
|
||||
}
|
||||
|
||||
/* Telemetry section */
|
||||
#telemetry_row {
|
||||
height: auto;
|
||||
margin: 1 0;
|
||||
}
|
||||
|
||||
#telemetry_label {
|
||||
width: auto;
|
||||
margin-right: 1;
|
||||
}
|
||||
|
||||
#telemetry_url_container {
|
||||
height: auto;
|
||||
margin: 1 0;
|
||||
}
|
||||
|
||||
#telemetry_url_label {
|
||||
width: auto;
|
||||
margin-right: 1;
|
||||
}
|
||||
|
||||
#telemetry_url_input {
|
||||
width: 1fr;
|
||||
}
|
||||
|
||||
.settings_description {
|
||||
color: $text-muted;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
"""
|
||||
|
||||
class ThemeSelected(Message):
|
||||
@@ -146,6 +178,40 @@ class SettingsWidget(Widget):
|
||||
|
||||
yield Rule()
|
||||
|
||||
# Telemetry Section
|
||||
with Vertical(id="telemetry_section") as telemetry:
|
||||
telemetry.styles.height = "auto"
|
||||
yield Static(
|
||||
"📊 Telemetry Settings",
|
||||
id="telemetry_title",
|
||||
classes="settings_section_title",
|
||||
)
|
||||
|
||||
telemetry_enabled = get_user_value("TELEMETRY", bool, False)
|
||||
|
||||
with Horizontal(id="telemetry_row"):
|
||||
yield Static("Enable Telemetry: ", id="telemetry_label")
|
||||
yield Switch(value=telemetry_enabled, id="telemetry_switch")
|
||||
|
||||
# URL input container - shown only when telemetry is enabled
|
||||
if telemetry_enabled:
|
||||
telemetry_url = get_user_value("TELEM_URL", str, "")
|
||||
with Horizontal(id="telemetry_url_container"):
|
||||
yield Static("Telemetry URL: ", id="telemetry_url_label")
|
||||
yield Input(
|
||||
value=telemetry_url,
|
||||
placeholder="https://your-telemetry-endpoint.com",
|
||||
id="telemetry_url_input",
|
||||
)
|
||||
|
||||
yield Static(
|
||||
"Help improve Loxide by sending anonymous usage data",
|
||||
id="telemetry_description",
|
||||
classes="settings_description",
|
||||
)
|
||||
|
||||
yield Rule()
|
||||
|
||||
# Theme Section - Three columns: Dark 1, Dark 2, Light
|
||||
with Vertical(id="themes_section") as themes:
|
||||
themes.styles.height = "auto"
|
||||
@@ -231,6 +297,85 @@ class SettingsWidget(Widget):
|
||||
self.post_message(self.ThemeSelected(theme_name))
|
||||
event.stop()
|
||||
|
||||
def on_switch_changed(self, event: Switch.Changed) -> None:
|
||||
"""Handle telemetry switch toggle."""
|
||||
if event.switch.id == "telemetry_switch":
|
||||
enabled = event.value
|
||||
self._persist_telemetry_setting(enabled)
|
||||
|
||||
if enabled:
|
||||
self._mount_telemetry_url_input()
|
||||
else:
|
||||
self._unmount_telemetry_url_input()
|
||||
|
||||
def on_input_changed(self, event: Input.Changed) -> None:
|
||||
"""Handle telemetry URL input changes."""
|
||||
if event.input.id == "telemetry_url_input":
|
||||
self._persist_telemetry_url(event.value)
|
||||
|
||||
def _persist_telemetry_setting(self, enabled: bool) -> None:
|
||||
"""Store the telemetry opt-in/out setting in the user's config."""
|
||||
base_dir = get_base_directory()
|
||||
config_dir = base_dir / "config"
|
||||
|
||||
try:
|
||||
save_user_config(config_dir, {"TELEMETRY": enabled})
|
||||
logger.debug("Updated user config with TELEMETRY=%s", enabled)
|
||||
status = "enabled" if enabled else "disabled"
|
||||
self.app.notify(f"📊 Telemetry {status}", timeout=2)
|
||||
except Exception as exc:
|
||||
logger.error("Failed to save TELEMETRY setting: %s", exc)
|
||||
self.app.notify(f"⚠️ Failed to save setting: {exc}", severity="warning")
|
||||
|
||||
def _persist_telemetry_url(self, url: str) -> None:
|
||||
"""Store the telemetry URL in the user's config."""
|
||||
base_dir = get_base_directory()
|
||||
config_dir = base_dir / "config"
|
||||
|
||||
try:
|
||||
save_user_config(config_dir, {"TELEM_URL": url})
|
||||
logger.debug("Updated user config with TELEM_URL=%s", url)
|
||||
except Exception as exc:
|
||||
logger.error("Failed to save TELEM_URL setting: %s", exc)
|
||||
|
||||
def _mount_telemetry_url_input(self) -> None:
|
||||
"""Mount the telemetry URL input container."""
|
||||
try:
|
||||
# Check if already mounted
|
||||
self.query_one("#telemetry_url_container")
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
telemetry_url = get_user_value("TELEM_URL", str, "")
|
||||
|
||||
# Create container with children
|
||||
container = Horizontal(
|
||||
Static("Telemetry URL: ", id="telemetry_url_label"),
|
||||
Input(
|
||||
value=telemetry_url,
|
||||
placeholder="https://your-telemetry-endpoint.com",
|
||||
id="telemetry_url_input",
|
||||
),
|
||||
id="telemetry_url_container",
|
||||
)
|
||||
|
||||
# Mount after the telemetry_row within telemetry_section
|
||||
try:
|
||||
telemetry_section = self.query_one("#telemetry_section")
|
||||
telemetry_row = self.query_one("#telemetry_row")
|
||||
telemetry_section.mount(container, after=telemetry_row)
|
||||
except Exception as exc:
|
||||
logger.error("Failed to mount telemetry URL input: %s", exc)
|
||||
|
||||
def _unmount_telemetry_url_input(self) -> None:
|
||||
"""Unmount the telemetry URL input container."""
|
||||
try:
|
||||
container = self.query_one("#telemetry_url_container")
|
||||
container.remove()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _check_for_updates(self) -> None:
|
||||
"""Check for updates and update UI."""
|
||||
if self._checking:
|
||||
@@ -497,6 +642,36 @@ class ThemeSelector(Widget):
|
||||
#light_themes_col {
|
||||
margin-left: 1;
|
||||
}
|
||||
|
||||
/* Telemetry section */
|
||||
#telemetry_row {
|
||||
height: auto;
|
||||
margin: 1 0;
|
||||
}
|
||||
|
||||
#telemetry_label {
|
||||
width: auto;
|
||||
margin-right: 1;
|
||||
}
|
||||
|
||||
#telemetry_url_container {
|
||||
height: auto;
|
||||
margin: 1 0;
|
||||
}
|
||||
|
||||
#telemetry_url_label {
|
||||
width: auto;
|
||||
margin-right: 1;
|
||||
}
|
||||
|
||||
#telemetry_url_input {
|
||||
width: 1fr;
|
||||
}
|
||||
|
||||
.settings_description {
|
||||
color: $text-muted;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
"""
|
||||
|
||||
class ThemeSelected(Message):
|
||||
|
||||
Generated
+1
-1
@@ -26,7 +26,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "airlock_libs"
|
||||
version = "7.4.1"
|
||||
version = "6.1.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"crossbeam",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "airlock_libs"
|
||||
version = "7.4.1"
|
||||
version = "6.1.1"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "airlock_libs"
|
||||
version = "7.4.1"
|
||||
version = "6.1.1"
|
||||
description = "Airlock Digital API Wrapper"
|
||||
readme = "README.md"
|
||||
license = { text = "AGPL-3.0-only" }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use serde_json::json;
|
||||
|
||||
use crate::modules::datatypes::*;
|
||||
use crate::prelude::*;
|
||||
use opentelemetry::trace::SpanContext;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn pull_policy_exec_histories(
|
||||
@@ -74,8 +75,8 @@ pub fn pull_policy_exec_histories(
|
||||
.set_draw_target(ProgressDrawTarget::stderr());
|
||||
progress_bar.lock().unwrap().set_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template("Total - Policy Name: {msg}: {spinner:.green} [{elapsed_precise}] [{bar:40.green/blue}] {pos}/{len}")
|
||||
.unwrap().progress_chars("⣿⣦⣀")
|
||||
.template("Total Completion: {spinner:.green} [{elapsed_precise}] [{bar:40.green/blue}] {pos}/{len} {message}")
|
||||
.unwrap(),
|
||||
);
|
||||
let client: Client = tracer.in_span("Building HTTP Client", |cx| {
|
||||
let client_result: Result<Client, reqwest::Error> = build_client(headers);
|
||||
@@ -107,10 +108,9 @@ pub fn pull_policy_exec_histories(
|
||||
});
|
||||
let cutoff: chrono::NaiveDateTime =
|
||||
Local::now().naive_local() - chrono::Duration::days(days);
|
||||
let (tx, rx) = unbounded::<(SpanContext, Vec<Group>)>();
|
||||
let (tx, rx) = unbounded::<Vec<Group>>();
|
||||
let pb_clone = progress_bar.clone();
|
||||
thread::spawn(move || {
|
||||
let tracer = global::tracer("loxide");
|
||||
let mut seen: HashMap<(String, String, String), Group> = if writeable_filepath.exists()
|
||||
{
|
||||
let contents: String = fs::read_to_string(&writeable_filepath).unwrap_or_default();
|
||||
@@ -139,22 +139,7 @@ pub fn pull_policy_exec_histories(
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
while let Ok((parent_spancontext, parsed_responses)) = rx.recv() {
|
||||
let parent_ctx = Context::new().with_remote_span_context(parent_spancontext);
|
||||
let span = tracer.build_with_context(
|
||||
tracer
|
||||
.span_builder("Deduplicate and Write")
|
||||
.with_kind(trace::SpanKind::Consumer),
|
||||
&parent_ctx,
|
||||
);
|
||||
let cx = Context::current_with_span(span);
|
||||
cx.span().add_event(
|
||||
"Received Data from Producer",
|
||||
vec![KeyValue::new(
|
||||
"Items to Process",
|
||||
parsed_responses.len().to_string(),
|
||||
)],
|
||||
);
|
||||
while let Ok(parsed_responses) = rx.recv() {
|
||||
for executions in parsed_responses {
|
||||
if executions.checkpoint.is_empty() || executions.datetime.is_empty() {
|
||||
continue;
|
||||
@@ -183,28 +168,11 @@ pub fn pull_policy_exec_histories(
|
||||
};
|
||||
let data_write: String = serde_json::to_string_pretty(&final_response).unwrap();
|
||||
match fs::write(&writeable_filepath, data_write) {
|
||||
Ok(_) => {
|
||||
cx.span().add_event(
|
||||
"Writing Data to File",
|
||||
vec![KeyValue::new("Success", "Ok".to_string())],
|
||||
);
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
cx.span().add_event(
|
||||
"Writing Data to File",
|
||||
vec![KeyValue::new("Failed", e.to_string())],
|
||||
);
|
||||
cx.span()
|
||||
.set_status(Status::error("Failed to Write to File"));
|
||||
println!("Failed to write to: {:?}: {}", &writeable_filepath, e);
|
||||
}
|
||||
}
|
||||
cx.span().add_event(
|
||||
"Finished Deduplicating Data",
|
||||
vec![KeyValue::new(
|
||||
"Items Successfully Processed",
|
||||
seen.len().to_string(),
|
||||
)],
|
||||
);
|
||||
}
|
||||
});
|
||||
let mut first_date: Option<NaiveDate> = None;
|
||||
@@ -213,42 +181,21 @@ pub fn pull_policy_exec_histories(
|
||||
.lock()
|
||||
.unwrap()
|
||||
.enable_steady_tick(std::time::Duration::from_millis(100));
|
||||
pb_clone
|
||||
.lock()
|
||||
.unwrap()
|
||||
.set_message(policy_names.clone().unwrap_or("Statistics".to_string()));
|
||||
let span: opentelemetry::trace::SpanRef<'_> = cx.span();
|
||||
span.set_attribute(KeyValue::new("Days", days.to_string()));
|
||||
span.set_attribute(KeyValue::new(
|
||||
"Policy Name",
|
||||
policy_names
|
||||
.clone()
|
||||
.unwrap_or("Statistics Monitoring".to_string()),
|
||||
policy_names.clone().unwrap_or_default(),
|
||||
));
|
||||
loop {
|
||||
let execution_histories = tracer.in_span(checkpoint_number.to_string(), |cx| {
|
||||
cx.span().add_event(
|
||||
"Retrieving Responses from API",
|
||||
vec![KeyValue::new(
|
||||
"Checkpoint Number",
|
||||
checkpoint_number.to_string(),
|
||||
)],
|
||||
);
|
||||
let results: ApiResponse = rt.block_on(history_logging(
|
||||
let results: ApiResponse = history_logging(
|
||||
&base_url,
|
||||
&exec_types,
|
||||
&checkpoint_number,
|
||||
&policy_names,
|
||||
&client,
|
||||
));
|
||||
cx.span().add_event(
|
||||
"Got Responses from API",
|
||||
vec![KeyValue::new(
|
||||
"Items in Response",
|
||||
results.response.exechistories.len().to_string(),
|
||||
)],
|
||||
);
|
||||
cx.span().set_status(Status::Ok);
|
||||
cx.span().set_attribute(KeyValue::new(
|
||||
"items_in_response",
|
||||
results.response.exechistories.len().to_string(),
|
||||
@@ -259,16 +206,7 @@ pub fn pull_policy_exec_histories(
|
||||
if parsed_responses.is_empty() {
|
||||
break;
|
||||
}
|
||||
match tx.send((cx.span().span_context().clone(), parsed_responses.clone())) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
cx.span().add_event(
|
||||
"Failed to Send Items to Processor",
|
||||
vec![KeyValue::new("Response from Processor", e.to_string())],
|
||||
);
|
||||
cx.span().set_status(Status::error("Processor Failed"))
|
||||
}
|
||||
}
|
||||
tx.send(parsed_responses.clone()).unwrap();
|
||||
checkpoint_number = parsed_responses.last().unwrap().checkpoint.clone();
|
||||
if let Some(last_item) = parsed_responses.last()
|
||||
&& let Ok(last_date) = NaiveDate::parse_from_str(
|
||||
@@ -317,10 +255,11 @@ fn build_client(headers: HeaderMap) -> Result<reqwest::Client, reqwest::Error> {
|
||||
Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.default_headers(headers)
|
||||
.timeout(std::time::Duration::from_secs(300))
|
||||
.build()
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "history_logging")]
|
||||
#[tokio::main]
|
||||
async fn history_logging(
|
||||
base_url: &String,
|
||||
exec_types: &String,
|
||||
@@ -328,28 +267,21 @@ async fn history_logging(
|
||||
policy_names: &Option<String>,
|
||||
client: &Client,
|
||||
) -> ApiResponse {
|
||||
let policy_json = match policy_names {
|
||||
Some(name) => format!(r#"[ "{}" ]"#, name), // JSON array with one element
|
||||
None => "[]".to_string(), // Empty JSON array
|
||||
};
|
||||
let payload = format!(
|
||||
r#"{{
|
||||
"type": {},
|
||||
"checkpoint": "{}",
|
||||
"policy": {}
|
||||
}}"#,
|
||||
exec_types, checkpoint_number, policy_json
|
||||
);
|
||||
let payload = json!({
|
||||
"type": exec_types,
|
||||
"checkpoint": checkpoint_number,
|
||||
"policy": policy_names.as_ref().map(|p| vec![p]),
|
||||
});
|
||||
let res: Result<reqwest::Response, reqwest::Error> = client
|
||||
.post(format!("{}/v1/logging/exechistories", base_url))
|
||||
.body(payload)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await;
|
||||
match res {
|
||||
Ok(res) => {
|
||||
let first_response: ApiResponse = serde_json::from_str(&res.text().await.unwrap())
|
||||
.expect("Failed to retrieve response from API");
|
||||
first_response
|
||||
return first_response;
|
||||
}
|
||||
Err(_res) => {
|
||||
let failed_response: ApiResponse = ApiResponse {
|
||||
@@ -358,7 +290,7 @@ async fn history_logging(
|
||||
exechistories: vec![],
|
||||
},
|
||||
};
|
||||
failed_response
|
||||
return failed_response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,7 +357,7 @@ Loxide is designed for three primary user classes with varying levels of experti
|
||||
|
||||
#### 2.4.3 Network Requirements
|
||||
|
||||
- Outbound HTTPS (port 443) to Airlock server
|
||||
- Outbound to Airlock server
|
||||
- Outbound HTTPS to Gitea instance (development only)
|
||||
- No inbound connections required
|
||||
- Proxy support via standard environment variables
|
||||
|
||||
+1
-1
@@ -23,4 +23,4 @@ pyperclip==1.11.0
|
||||
|
||||
# Custom/Private packages
|
||||
--extra-index-url https://git.racooncity.org/api/packages/brotoskyj/pypi/simple/
|
||||
airlock_libs==7.4.1
|
||||
airlock_libs==6.1.1
|
||||
@@ -34,7 +34,7 @@ import requests
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Current application version - UPDATE THIS ON EACH RELEASE
|
||||
__version__ = "1.0.0"
|
||||
__version__ = "1.1.0"
|
||||
|
||||
# Gitea release API configuration
|
||||
GITEA_API_BASE = "https://git.racooncity.org/api/v1"
|
||||
|
||||
Reference in New Issue
Block a user