- Consolidated all system/user config logic into configmanager.py - Removed duplicate loaders from setup.py and TUI.py - Eliminated .env redundancy; now only stores WORKING_DIR - Clarified boundaries: system config immutable, user config mutable - Updated TUI to use save_user_config() - Removed all deprecated/legacy config functions and aliases
This commit is contained in:
+15
-42
@@ -4,7 +4,6 @@ import sys
|
||||
from typing import Optional
|
||||
|
||||
import dotenv
|
||||
from dotenv import set_key
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.containers import Vertical
|
||||
from textual.message import Message
|
||||
@@ -38,8 +37,8 @@ from TUI.resultsdisplay import ResultsDisplay
|
||||
from TUI.theme_amber_terminal import get_amber_terminal_theme
|
||||
from TUI.theme_retro_terminal import get_retro_terminal_theme
|
||||
from TUI.themeselector import ThemeSelector
|
||||
from utils.configmanager import load_env
|
||||
from utils.setup import get_base_directory, load_user_config
|
||||
from utils.configmanager import get_user_value, load_env, save_user_config
|
||||
from utils.setup import get_base_directory
|
||||
from utils.utils import open_directory
|
||||
|
||||
dotenv.load_dotenv()
|
||||
@@ -58,47 +57,17 @@ logger = logging.getLogger(__name__)
|
||||
# ---------------------------------------------------------------------------
|
||||
def _persist_user_theme(theme_name: str) -> None:
|
||||
"""
|
||||
Store the chosen Textual theme in the user's config:
|
||||
<base>/config/user_config.json
|
||||
and also mirror to <base>/.env so load_env(...) sees it.
|
||||
Store the chosen Textual theme in the user's config using the config manager.
|
||||
No need to touch .env - config manager handles everything.
|
||||
"""
|
||||
base_dir = get_base_directory()
|
||||
config_dir = base_dir / "config"
|
||||
user_config_path = config_dir / "user_config.json"
|
||||
env_path = base_dir / ".env"
|
||||
|
||||
# ensure dirs / files exist similarly to setup()
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
if not user_config_path.exists():
|
||||
# minimal default like your load_user_config does
|
||||
user_config_path.write_text(
|
||||
'{"URL": "", "LOG_LEVEL": "INFO"}\n', encoding="utf-8"
|
||||
)
|
||||
|
||||
# load existing user config
|
||||
user_conf = load_user_config(config_dir)
|
||||
user_conf["TEXTUAL_THEME"] = theme_name
|
||||
|
||||
# write it back
|
||||
user_config_path.write_text(
|
||||
# pretty print so it stays human-readable
|
||||
__import__("json").dumps(user_conf, indent=4),
|
||||
encoding="utf-8",
|
||||
)
|
||||
logger.debug("Updated user_config.json with TEXTUAL_THEME=%s", theme_name)
|
||||
|
||||
# mirror to .env (like setup.write_config_to_env does)
|
||||
env_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not env_path.exists():
|
||||
env_path.touch()
|
||||
try:
|
||||
set_key(str(env_path), "TEXTUAL_THEME", theme_name)
|
||||
except Exception as exc: # keep going even if .env write fails
|
||||
logger.warning("Failed to mirror TEXTUAL_THEME to .env: %s", exc)
|
||||
|
||||
# reload so load_env(...) sees the new value right now
|
||||
dotenv.load_dotenv(dotenv_path=env_path, override=True)
|
||||
logger.debug("Reloaded .env from %s", env_path)
|
||||
save_user_config(config_dir, {"TEXTUAL_THEME": theme_name})
|
||||
logger.debug("Updated user config with TEXTUAL_THEME=%s", theme_name)
|
||||
except Exception as exc:
|
||||
logger.error("Failed to save TEXTUAL_THEME: %s", exc)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -115,7 +84,7 @@ class MainMenuScreen(Screen):
|
||||
"move_agent_workflow_button",
|
||||
),
|
||||
("📊 - Review and appove OTP Activities", "otp_activities_button"),
|
||||
("🔇 - Find and Move Quiet Hosts to Enforcement", "find_quiet_button"),
|
||||
("📇 - Find and Move Quiet Hosts to Enforcement", "find_quiet_button"),
|
||||
],
|
||||
"policy": [
|
||||
("🔒 - Prepare Policy For Enforcement", "policy_prep_button"),
|
||||
@@ -126,7 +95,7 @@ class MainMenuScreen(Screen):
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.extras = load_env("EXTRAS")
|
||||
self.extras = get_user_value("EXTRAS", str, "NOTTODAY")
|
||||
wd = load_env("WORKING_DIR") or os.getcwd()
|
||||
if not os.path.isdir(wd):
|
||||
wd = os.getcwd()
|
||||
@@ -380,10 +349,11 @@ class Loxide(App[Message]):
|
||||
BINDINGS = [
|
||||
("q", "quit", "Quit"),
|
||||
("f", "open_fe", "Launch Explorer"),
|
||||
("r", "refresh", "Refresh"),
|
||||
]
|
||||
|
||||
def __init__(self, api: AirlockAPIWrapper):
|
||||
self._textual_theme = load_env("TEXTUAL_THEME") or "nord"
|
||||
self._textual_theme = get_user_value("TEXTUAL_THEME", str, "nord")
|
||||
super().__init__()
|
||||
self.api = api
|
||||
wd = load_env("WORKING_DIR") or os.getcwd()
|
||||
@@ -421,6 +391,9 @@ class Loxide(App[Message]):
|
||||
self.theme = self._textual_theme
|
||||
self.push_screen(MainMenuScreen())
|
||||
|
||||
def action_refresh(self) -> None:
|
||||
self.refresh_data()
|
||||
|
||||
def action_quit(self) -> None:
|
||||
global _PENDING_JOB
|
||||
_PENDING_JOB = None
|
||||
|
||||
+26
-26
@@ -183,21 +183,21 @@ class AgentMoveOperations(Widget):
|
||||
f"Operation: {operation_name}",
|
||||
f"{'=' * 50}",
|
||||
"",
|
||||
f"✅ Successful ({len(successful)}):",
|
||||
f"✅ Successful ({len(successful)}):",
|
||||
]
|
||||
|
||||
if successful:
|
||||
for agent, result in successful:
|
||||
results_lines.append(f" ✅ {agent.hostname}")
|
||||
results_lines.append(f" ✅ {agent.hostname}")
|
||||
else:
|
||||
results_lines.append(" (none)")
|
||||
|
||||
results_lines.append("")
|
||||
results_lines.append(f"❌ Failed ({len(unsuccessful)}):")
|
||||
results_lines.append(f"⌠Failed ({len(unsuccessful)}):")
|
||||
|
||||
if unsuccessful:
|
||||
for agent, error in unsuccessful:
|
||||
results_lines.append(f" ❌ {agent.hostname}: {error}")
|
||||
results_lines.append(f" ⌠{agent.hostname}: {error}")
|
||||
else:
|
||||
results_lines.append(" (none)")
|
||||
|
||||
@@ -232,9 +232,9 @@ class AgentMoveOperations(Widget):
|
||||
- Operations panel: 1/3 width
|
||||
- Results area: Initially hidden, shown after operation completion
|
||||
"""
|
||||
yield Header(show_clock=True, icon="⚙")
|
||||
yield Header(show_clock=True, icon="âš™")
|
||||
title_text = Static(
|
||||
f"🖥️ Agent Operations - {len(self.agents)} device(s) selected",
|
||||
f"ðŸ–¥ï¸ Agent Operations - {len(self.agents)} device(s) selected",
|
||||
id="move_ops_title",
|
||||
)
|
||||
title_text.styles.margin = (0, 0, 1, 0)
|
||||
@@ -269,32 +269,32 @@ class AgentMoveOperations(Widget):
|
||||
yield operations_label
|
||||
|
||||
# Operation buttons
|
||||
export_csv_btn = Button("📈 Export CSV", id="export_csv_btn")
|
||||
export_csv_btn = Button("📈 Export CSV", id="export_csv_btn")
|
||||
export_csv_btn.styles.width = "100%"
|
||||
export_csv_btn.styles.margin = (0, 0, 1, 0)
|
||||
yield export_csv_btn
|
||||
|
||||
local_approval_btn = Button(
|
||||
"✔️ Local Approval Mode", id="local_approval_btn"
|
||||
"âœ”ï¸ Local Approval Mode", id="local_approval_btn"
|
||||
)
|
||||
local_approval_btn.styles.width = "100%"
|
||||
local_approval_btn.styles.margin = (0, 0, 1, 0)
|
||||
yield local_approval_btn
|
||||
|
||||
otp_gen_btn = Button("🎫 Generate One Time Passes", id="otp_gen_btn")
|
||||
otp_gen_btn = Button("🎫 Generate One Time Passes", id="otp_gen_btn")
|
||||
otp_gen_btn.styles.width = "100%"
|
||||
otp_gen_btn.styles.margin = (0, 0, 1, 0)
|
||||
yield otp_gen_btn
|
||||
|
||||
toggle_enforcement_btn = Button(
|
||||
"🔄 Toggle Audit/Enforcement", id="toggle_enforcement_btn"
|
||||
"🔄 Toggle Audit/Enforcement", id="toggle_enforcement_btn"
|
||||
)
|
||||
toggle_enforcement_btn.styles.width = "100%"
|
||||
toggle_enforcement_btn.styles.margin = (0, 0, 1, 0)
|
||||
yield toggle_enforcement_btn
|
||||
|
||||
other_policy_btn = Button(
|
||||
"🔀 Move to Other Policy", id="other_policy_btn"
|
||||
"🔀 Move to Other Policy", id="other_policy_btn"
|
||||
)
|
||||
other_policy_btn.styles.width = "100%"
|
||||
other_policy_btn.styles.margin = (0, 0, 1, 0)
|
||||
@@ -305,7 +305,7 @@ class AgentMoveOperations(Widget):
|
||||
status_label.styles.margin = (2, 0, 0, 0)
|
||||
yield status_label
|
||||
|
||||
back_button = Button("← Back", id="back_button")
|
||||
back_button = Button("↠Back", id="back_button")
|
||||
back_button.styles.width = "50%"
|
||||
back_button.styles.margin = (0, 1, 1, 0)
|
||||
yield back_button
|
||||
@@ -369,17 +369,17 @@ class AgentMoveOperations(Widget):
|
||||
|
||||
pyperclip.copy(results_text.text)
|
||||
self.app.notify(
|
||||
"📋✅ Results copied to clipboard!",
|
||||
"📋✅ Results copied to clipboard!",
|
||||
severity="information",
|
||||
timeout=2,
|
||||
)
|
||||
except ImportError:
|
||||
self.app.notify(
|
||||
"❌ pyperclip not installed. Run: pip install pyperclip",
|
||||
"⌠pyperclip not installed. Run: pip install pyperclip",
|
||||
severity="warning",
|
||||
)
|
||||
except Exception as e:
|
||||
self.app.notify(f"⌠Failed to copy: {str(e)}", severity="error")
|
||||
self.app.notify(f"âÂÅ’ Failed to copy: {str(e)}", severity="error")
|
||||
event.stop()
|
||||
elif btn_id == "export_csv_btn":
|
||||
self._start_export_csv_operation()
|
||||
@@ -427,7 +427,7 @@ class AgentMoveOperations(Widget):
|
||||
self.operation_in_progress = True
|
||||
|
||||
status_label = self.query_one("#status_label", Static)
|
||||
status_label.update("✔️ Moving agents to local approval...")
|
||||
status_label.update("âœ”ï¸ Moving agents to local approval...")
|
||||
|
||||
# Get API from app
|
||||
api = self.app.api
|
||||
@@ -462,12 +462,12 @@ class AgentMoveOperations(Widget):
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during local approval operation: {e}")
|
||||
status_label.update(f"❌ Error: {str(e)}")
|
||||
status_label.update(f"⌠Error: {str(e)}")
|
||||
self.operation_in_progress = False
|
||||
return
|
||||
|
||||
self.operation_in_progress = False
|
||||
status_label.update("✅ Operation complete!")
|
||||
status_label.update("✅ Operation complete!")
|
||||
|
||||
# Display results in the widget
|
||||
self._display_results("Local Approval Mode", successful, unsuccessful)
|
||||
@@ -511,9 +511,9 @@ class AgentMoveOperations(Widget):
|
||||
file_path = os.path.join(str(path), filename)
|
||||
df.to_csv(file_path, index=False)
|
||||
successful.append(file_path)
|
||||
status_label.update(f"✅ Exported to {file_path}")
|
||||
status_label.update(f"✅ Exported to {file_path}")
|
||||
except Exception:
|
||||
status_label.update("❌ Failed")
|
||||
status_label.update("⌠Failed")
|
||||
|
||||
self.operation_in_progress = False
|
||||
|
||||
@@ -557,7 +557,7 @@ class AgentMoveOperations(Widget):
|
||||
self.operation_in_progress = True
|
||||
|
||||
status_label = self.query_one("#status_label", Static)
|
||||
status_label.update("â³ Toggling enforcement mode...")
|
||||
status_label.update("â³ Toggling enforcement mode...")
|
||||
|
||||
# Get API from app
|
||||
api = self.app.api
|
||||
@@ -567,9 +567,9 @@ class AgentMoveOperations(Widget):
|
||||
|
||||
try:
|
||||
from services.agenthandler import moveAgentToRelatedPolicy
|
||||
from utils.configmanager import get_protected_json
|
||||
from utils.configmanager import get_system_json
|
||||
|
||||
policy_relationship_map = get_protected_json("POLICY_MAP_ENF_AUD", "{}")
|
||||
policy_relationship_map = get_system_json("POLICY_MAP_ENF_AUD", "{}")
|
||||
|
||||
for agent in self.agents:
|
||||
try:
|
||||
@@ -593,12 +593,12 @@ class AgentMoveOperations(Widget):
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during toggle enforcement operation: {e}")
|
||||
status_label.update(f"❌ Error: {str(e)}")
|
||||
status_label.update(f"⌠Error: {str(e)}")
|
||||
self.operation_in_progress = False
|
||||
return
|
||||
|
||||
self.operation_in_progress = False
|
||||
status_label.update("✅ Operation complete!")
|
||||
status_label.update("✅ Operation complete!")
|
||||
|
||||
# Display results in the widget
|
||||
self._display_results("Toggle Audit/Enforcement", successful, unsuccessful)
|
||||
@@ -663,7 +663,7 @@ class AgentMoveOperations(Widget):
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading policies: {e}")
|
||||
status_label.update(f"❌ Error: {str(e)}")
|
||||
status_label.update(f"⌠Error: {str(e)}")
|
||||
self.operation_in_progress = False
|
||||
self.selected_operation = ""
|
||||
self.app.notify(f"Failed to load policies: {str(e)}", severity="error")
|
||||
|
||||
Reference in New Issue
Block a user