# Copyright (C) 2025 James Brotosky, Brandon Wickline # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import logging import logging.config import logging.handlers import os from pathlib import Path import platform from dotenv import load_dotenv, set_key from utils.configmanager import ( get_system_value, load_system_config, load_user_config, ) def get_base_directory() -> Path: system = platform.system() home = Path.home() if system == "Windows": return Path(os.getenv("APPDATA", home / "AppData" / "Roaming")) / "Loxide" elif system == "Darwin": return home / "Library" / "Application Support" / "Loxide" else: return home / ".local" / "share" / "Loxide" def configure_logging(log_dir: Path, log_level: str = "INFO"): log_file = log_dir / "Loxide.log" config = { "version": 1, # Required key for dictConfig format version "disable_existing_loggers": False, # Keeps existing loggers active "formatters": { "detailed": { "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s" # Includes timestamp, logger name, level, and message }, "simple": { "format": "%(levelname)s - %(message)s" # Minimal format for console output }, }, "handlers": { "file": { "class": "logging.handlers.TimedRotatingFileHandler", "filename": str(log_file), "when": "midnight", # Rotate logs at midnight "interval": 1, # Every 1 day "backupCount": 7, # Keep 7 days of logs "encoding": "utf-8", # Ensure UTF-8 encoding "level": "DEBUG", # Always log DEBUG and above to file "formatter": "detailed", # Use detailed format }, "console": { "class": "logging.StreamHandler", "level": log_level.upper(), # System-configured level for console "formatter": "simple", # Use simple format }, }, "root": { "level": "DEBUG", # Root logger level "handlers": ["file", "console"], # Attach both handlers }, } # Add Windows Event Log handler if on Windows if platform.system() == "Windows": try: config["handlers"]["eventlog"] = { "class": "logging.handlers.NTEventLogHandler", "appname": "Loxide", # Event log source name "level": "CRITICAL", # Only log critical errors "formatter": "simple", # Use simple format } config["root"]["handlers"].append("eventlog") except Exception as e: logging.warning(f"Could not attach Windows Event Log handler: {e}") # Apply the logging configuration logging.config.dictConfig(config) logging.getLogger().debug("✅ Logging configured.") def setup(): """ Initialize the application environment: 1. Create directory structure 2. Load system config (immutable, from system_config.json) 3. Load user config (mutable, from user_config.json) 4. Configure logging 5. Set up .env with WORKING_DIR only """ base_dir = get_base_directory() dirs = { "config": base_dir / "config", "cache": base_dir / "cache", "logs": base_dir / "logs", } for name, path in dirs.items(): path.mkdir(parents=True, exist_ok=True) logging.debug(f"{name.capitalize()} directory ensured at: {path}") # Load system config (immutable) load_system_config() # Configure logging with system-defined log level log_level = get_system_value("LOG_LEVEL", str, "INFO") configure_logging(dirs["logs"], log_level) # Load user config (mutable) load_user_config(dirs["config"]) # Set up .env file - ONLY for WORKING_DIR (runtime-configurable value) env_path = base_dir / ".env" if not env_path.exists(): env_path.touch() load_dotenv(dotenv_path=env_path, override=True) # Set up working directory (only dynamic value in .env) working_dir = Path(os.getenv("WORKING_DIR") or (base_dir / "data")) working_dir.mkdir(parents=True, exist_ok=True) set_key(str(env_path), "WORKING_DIR", str(working_dir)) os.environ["WORKING_DIR"] = str(working_dir) logging.debug(f"Working directory set to: {working_dir}") # Create folder structure in working directory folders_structure = { "Approved": [], "Needs_Review": ["Review_First", "Review_Second", "HTML"], "Preflight": ["HTML"], "Archived": [], } for folder_name, subfolders in folders_structure.items(): folder_path = working_dir / folder_name folder_path.mkdir(parents=True, exist_ok=True) logging.debug(f"'{folder_name}' folder ensured at: {folder_path}") for subfolder in subfolders: subfolder_path = folder_path / subfolder subfolder_path.mkdir(parents=True, exist_ok=True) logging.debug(f"'{subfolder}' subfolder created at: {subfolder_path}") logging.info("✅ Setup complete")