├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── ansiblePower.py ├── data ├── config.json └── history.json ├── logs └── app.log ├── static ├── css │ └── styles.css └── js │ └── main.js └── templates ├── base.html ├── history.html ├── index.html ├── partials ├── _header.html └── _sidebar.html └── settings.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore database and user data files 2 | *.db 3 | *.sqlite 4 | *.sqlite3 5 | *.log 6 | *.bak 7 | *.tmp 8 | *.swp 9 | *.swo 10 | *.pid 11 | *.lock 12 | 13 | # Ignore user data and history 14 | /data/*.json 15 | !data/config.json 16 | 17 | # Ignore logs 18 | /logs/ 19 | 20 | # Byte-compiled / optimized / DLL files 21 | __pycache__/ 22 | *.py[cod] 23 | *$py.class 24 | 25 | # VS Code settings 26 | .vscode/ 27 | 28 | # OS generated files 29 | .DS_Store 30 | Thumbs.db 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to AnsiblePower 2 | 3 | Thank you for your interest in improving **AnsiblePower**! 🎉 4 | 5 | ### How to Contribute: 6 | - Check out the list of open issues: 7 | 👉 [AnsiblePower Issues](https://github.com/pooyanazad/AnsiblePower/issues) 8 | - Resolve an issue or suggest improvements. 9 | 10 | Your contributions, whether big or small, help make **AnsiblePower** better for everyone! 💻✨ 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 pooyan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AnsiblePower 2 | 3 | **AnsiblePower** is a lightweight web interface inspired by Ansible Tower. It allows users to manage Ansible playbooks, view and edit host configurations, and monitor system performance via a modern and intuitive web interface. 4 | 5 | ## Features 6 | 7 | ### 1. Homepage 8 | - Lists available Ansible playbooks on "PLAYBOOKS_DIR" path 9 | - Buttons to: 10 | - **Run**: Executes a playbook and shows the output 11 | - **Show**: Displays the content of a playbook 12 | 13 | ### 2. History 14 | - Displays a table of previously run playbooks with timestamps and actions 15 | 16 | ### 3. Settings 17 | #### Hosts Management 18 | - View and edit `/etc/ansible/hosts` file 19 | - Displays error messages if the user lacks read/write permissions 20 | 21 | #### Master Node Status 22 | - Displays CPU and memory usage 23 | 24 | #### Clear History 25 | - Deletes all playbook execution history 26 | 27 | #### Dark Mode Toggle 28 | - Switch between light and dark modes 29 | 30 | ### 4. User-Friendly Interface 31 | - Modern design with rounded buttons, smooth animations, and a responsive layout 32 | - Sidebar with toggle functionality for better navigation 33 | 34 | ## Requirements 35 | - **Python 3.x** 36 | - **Flask** (`pip install flask`) 37 | - **psutil** (`pip install psutil`) 38 | - **Ansible** installed and accessible via `ansible-playbook` 39 | - **Replace below variables with yours in ansiblePower.py** 40 | 41 |  42 | 43 | ## Installation 44 | 45 | 1. Clone the repository: 46 | ```bash 47 | git clone https://github.com/pooyanazad/AnsiblePower.git 48 | cd AnsiblePower 49 | ``` 50 | 2. Install dependencies: 51 | ```bash 52 | pip install flask psutil 53 | ``` 54 | 3. Run the application: 55 | ```bash 56 | python ansiblePower.py 57 | ``` 58 | 59 | ## Contribution rules: 60 | https://github.com/pooyanazad/AnsiblePower/blob/main/CONTRIBUTING.md 61 | -------------------------------------------------------------------------------- /ansiblePower.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import json 4 | import subprocess 5 | import psutil 6 | import csv 7 | import logging 8 | from io import StringIO 9 | from flask import Flask, render_template, request, jsonify, session, redirect, url_for, Response 10 | 11 | # ============================================================================= 12 | # Custom Logging Handler: Keeps a maximum of 200 lines with newest messages at the top. 13 | # ============================================================================= 14 | class CustomRotatingLogHandler(logging.Handler): 15 | def __init__(self, filename, max_lines=200): 16 | super().__init__() 17 | self.filename = filename 18 | self.max_lines = max_lines 19 | log_dir = os.path.dirname(filename) 20 | if not os.path.exists(log_dir): 21 | os.makedirs(log_dir) 22 | if not os.path.exists(filename): 23 | with open(filename, "w") as f: 24 | f.write("") 25 | 26 | def emit(self, record): 27 | try: 28 | msg = self.format(record) 29 | if os.path.exists(self.filename): 30 | with open(self.filename, "r") as f: 31 | lines = f.readlines() 32 | else: 33 | lines = [] 34 | new_lines = [msg + "\n"] + lines 35 | new_lines = new_lines[:self.max_lines] 36 | with open(self.filename, "w") as f: 37 | f.writelines(new_lines) 38 | except Exception: 39 | self.handleError(record) 40 | 41 | logger = logging.getLogger("ansiblePower") 42 | logger.setLevel(logging.INFO) 43 | log_handler = CustomRotatingLogHandler("logs/app.log", max_lines=200) 44 | formatter = logging.Formatter("%(asctime)s %(levelname)s: %(message)s") 45 | log_handler.setFormatter(formatter) 46 | logger.addHandler(log_handler) 47 | 48 | # ============================================================================= 49 | # Flask App Setup 50 | # ============================================================================= 51 | app = Flask(__name__) 52 | app.secret_key = "some_random_secret_key" # Replace with your secret key 53 | 54 | # Configuration variables 55 | CONFIG_FILE = "data/config.json" 56 | DEFAULT_PLAYBOOKS_DIR = "/home/pooyan/ansible/playbooks" # Default path 57 | HOSTS_FILE = "/etc/ansible/hosts" # Adjust as needed. 58 | HISTORY_FILE = "data/history.json" 59 | 60 | def load_config(): 61 | if os.path.exists(CONFIG_FILE): 62 | with open(CONFIG_FILE, "r") as f: 63 | try: 64 | return json.load(f) 65 | except Exception as e: 66 | logger.error("Error loading config: %s", e) 67 | return {"playbooks_dir": DEFAULT_PLAYBOOKS_DIR} 68 | return {"playbooks_dir": DEFAULT_PLAYBOOKS_DIR} 69 | 70 | def save_config(config): 71 | try: 72 | with open(CONFIG_FILE, "w") as f: 73 | json.dump(config, f, indent=2) 74 | except Exception as e: 75 | logger.error("Error saving config: %s", e) 76 | 77 | def get_playbooks_dir(): 78 | config = load_config() 79 | return config.get("playbooks_dir", DEFAULT_PLAYBOOKS_DIR) 80 | 81 | def load_history(): 82 | if os.path.exists(HISTORY_FILE): 83 | with open(HISTORY_FILE, "r") as f: 84 | try: 85 | return json.load(f) 86 | except Exception as e: 87 | logger.error("Error loading history: %s", e) 88 | return [] 89 | return [] 90 | 91 | def save_history(history): 92 | try: 93 | with open(HISTORY_FILE, "w") as f: 94 | json.dump(history, f, indent=2) 95 | except Exception as e: 96 | logger.error("Error saving history: %s", e) 97 | 98 | # ============================================================================= 99 | # Routes 100 | # ============================================================================= 101 | @app.route("/") 102 | def homepage(): 103 | dark_mode = session.get("dark_mode", False) 104 | playbooks_dir = get_playbooks_dir() 105 | error = None 106 | prompt_for_dir = False 107 | 108 | if not os.path.exists(playbooks_dir): 109 | prompt_for_dir = True 110 | playbooks = [] 111 | elif not (os.access(playbooks_dir, os.R_OK) and os.access(playbooks_dir, os.W_OK)): 112 | error = (f"Insufficient permissions for the playbooks directory '{playbooks_dir}'. " 113 | "Ensure the directory is readable and writable by the current user.") 114 | playbooks = [] 115 | else: 116 | try: 117 | playbooks = [f for f in os.listdir(playbooks_dir) 118 | if f.endswith('.yml') or f.endswith('.yaml')] 119 | except Exception as e: 120 | logger.exception("Error listing playbooks: %s", e) 121 | playbooks = [] 122 | error = "An error occurred while accessing the playbooks directory." 123 | 124 | return render_template("index.html", playbooks=playbooks, dark_mode=dark_mode, 125 | error=error, prompt_for_dir=prompt_for_dir, playbooks_dir=playbooks_dir) 126 | 127 | @app.route("/run_playbook", methods=["POST"]) 128 | def run_playbook(): 129 | playbook_name = request.form.get("playbook") 130 | if not playbook_name: 131 | logger.error("No playbook specified in run_playbook") 132 | return jsonify({"error": "No playbook specified"}), 400 133 | 134 | playbooks_dir = get_playbooks_dir() 135 | playbook_path = os.path.join(playbooks_dir, playbook_name) 136 | if not os.path.exists(playbook_path): 137 | logger.error("Playbook does not exist: %s", playbook_path) 138 | return jsonify({"error": "Playbook does not exist"}), 404 139 | 140 | cmd = ["ansible-playbook", playbook_path] 141 | try: 142 | output = subprocess.check_output(cmd, stderr=subprocess.STDOUT) 143 | output = output.decode("utf-8") 144 | except subprocess.CalledProcessError as e: 145 | # Even if the play fails (e.g. unreachable host), record the output. 146 | output = e.output.decode("utf-8") 147 | except Exception as e: 148 | logger.exception("Unexpected error running playbook %s", playbook_name) 149 | output = "Unexpected error occurred: " + str(e) 150 | if not output.strip(): 151 | output = "No output produced." 152 | timestamp = subprocess.check_output(["date"]).decode("utf-8").strip() 153 | history = load_history() 154 | history.append({ 155 | "action": "run", 156 | "playbook": playbook_name, 157 | "output": output, 158 | "time": timestamp 159 | }) 160 | save_history(history) 161 | logger.info("Recorded playbook run: %s", playbook_name) 162 | return jsonify({"output": output}) 163 | 164 | @app.route("/show_playbook", methods=["POST"]) 165 | def show_playbook(): 166 | playbook_name = request.form.get("playbook") 167 | if not playbook_name: 168 | logger.error("No playbook specified in show_playbook") 169 | return jsonify({"error": "No playbook specified"}), 400 170 | 171 | playbooks_dir = get_playbooks_dir() 172 | playbook_path = os.path.join(playbooks_dir, playbook_name) 173 | if not os.path.exists(playbook_path): 174 | logger.error("Playbook does not exist: %s", playbook_path) 175 | return jsonify({"error": "Playbook does not exist"}), 404 176 | 177 | try: 178 | with open(playbook_path, "r") as f: 179 | content = f.read() 180 | logger.info("Displayed playbook: %s", playbook_name) 181 | return jsonify({"content": content}) 182 | except Exception as e: 183 | logger.exception("Error reading playbook %s", playbook_name) 184 | return jsonify({"error": "Error reading playbook"}), 500 185 | 186 | @app.route("/history") 187 | def history(): 188 | dark_mode = session.get("dark_mode", False) 189 | history_data = load_history() 190 | return render_template("history.html", history=history_data, dark_mode=dark_mode) 191 | 192 | @app.route("/settings") 193 | def settings(): 194 | dark_mode = session.get("dark_mode", False) 195 | config = load_config() 196 | playbooks_dir = config.get("playbooks_dir", DEFAULT_PLAYBOOKS_DIR) 197 | return render_template("settings.html", dark_mode=dark_mode, playbooks_dir=playbooks_dir) 198 | 199 | @app.route("/update_playbooks_dir", methods=["POST"]) 200 | def update_playbooks_dir(): 201 | new_dir = request.form.get("playbooks_dir", "").strip() 202 | if not new_dir: 203 | return jsonify({"error": "Directory path cannot be empty"}), 400 204 | 205 | config = load_config() 206 | config["playbooks_dir"] = new_dir 207 | save_config(config) 208 | logger.info("Updated playbooks directory to: %s", new_dir) 209 | return jsonify({"status": "ok", "message": "Playbooks directory updated successfully"}) 210 | 211 | @app.route("/get_hosts", methods=["GET"]) 212 | def get_hosts(): 213 | try: 214 | if not os.access(HOSTS_FILE, os.R_OK): 215 | logger.error("Read permission denied for hosts file: %s", HOSTS_FILE) 216 | return jsonify({"error": "Add read permission to user to file"}), 403 217 | if os.path.exists(HOSTS_FILE): 218 | with open(HOSTS_FILE, "r") as f: 219 | content = f.read() 220 | logger.info("Hosts file read successfully") 221 | return jsonify({"content": content}) 222 | else: 223 | logger.error("Hosts file not found: %s", HOSTS_FILE) 224 | return jsonify({"error": "Hosts file not found"}), 404 225 | except Exception as e: 226 | logger.exception("Error getting hosts file") 227 | return jsonify({"error": "Unexpected error occurred"}), 500 228 | 229 | @app.route("/save_hosts", methods=["POST"]) 230 | def save_hosts(): 231 | new_content = request.form.get("content", "") 232 | try: 233 | if not os.access(HOSTS_FILE, os.W_OK): 234 | logger.error("Write permission denied for hosts file: %s", HOSTS_FILE) 235 | return jsonify({"error": "Please add write permission to host file"}), 403 236 | with open(HOSTS_FILE, "w") as f: 237 | f.write(new_content) 238 | logger.info("Hosts file saved successfully") 239 | return jsonify({"status": "ok"}) 240 | except Exception as e: 241 | logger.exception("Error saving hosts file") 242 | return jsonify({"error": "Error saving hosts file"}), 500 243 | 244 | @app.route("/system_status", methods=["GET"]) 245 | def system_status(): 246 | try: 247 | cpu_percent = psutil.cpu_percent(interval=1) 248 | mem = psutil.virtual_memory() 249 | memory_percent = mem.percent 250 | logger.info("System status: CPU %s%%, Memory %s%%", cpu_percent, memory_percent) 251 | return jsonify({"cpu": cpu_percent, "memory": memory_percent}) 252 | except Exception as e: 253 | logger.exception("Error fetching system status") 254 | return jsonify({"error": "Error fetching system status"}), 500 255 | 256 | @app.route("/clear_history", methods=["POST"]) 257 | def clear_history(): 258 | try: 259 | save_history([]) 260 | logger.info("History cleared") 261 | return jsonify({"status": "ok"}) 262 | except Exception as e: 263 | logger.exception("Error clearing history") 264 | return jsonify({"error": "Error clearing history"}), 500 265 | 266 | @app.route("/toggle_dark_mode", methods=["POST"]) 267 | def toggle_dark_mode(): 268 | try: 269 | current = session.get("dark_mode", False) 270 | session["dark_mode"] = not current 271 | logger.info("Dark mode toggled to %s", session["dark_mode"]) 272 | return jsonify({"dark_mode": session["dark_mode"]}) 273 | except Exception as e: 274 | logger.exception("Error toggling dark mode") 275 | return jsonify({"error": "Error toggling dark mode"}), 500 276 | 277 | # --------------------------------------------------------------------------- 278 | # History Export and Import Endpoints (accessed from the History page) 279 | # --------------------------------------------------------------------------- 280 | @app.route("/export_history") 281 | def export_history(): 282 | export_format = request.args.get("format", "json") 283 | history_data = load_history() 284 | if export_format == "csv": 285 | si = StringIO() 286 | cw = csv.writer(si) 287 | cw.writerow(["time", "playbook", "action", "output"]) 288 | for record in history_data: 289 | cw.writerow([ 290 | record.get("time", ""), 291 | record.get("playbook", ""), 292 | record.get("action", ""), 293 | record.get("output", "") 294 | ]) 295 | output = si.getvalue() 296 | return Response(output, mimetype="text/csv", 297 | headers={"Content-Disposition": "attachment;filename=history.csv"}) 298 | else: 299 | return Response(json.dumps(history_data, indent=2), mimetype="application/json", 300 | headers={"Content-Disposition": "attachment;filename=history.json"}) 301 | 302 | @app.route("/import_history", methods=["POST"]) 303 | def import_history(): 304 | if "file" not in request.files: 305 | return jsonify({"error": "No file provided"}), 400 306 | file = request.files["file"] 307 | if file.filename == "": 308 | return jsonify({"error": "Empty file name"}), 400 309 | try: 310 | if file.filename.endswith(".json"): 311 | data = json.load(file) 312 | elif file.filename.endswith(".csv"): 313 | data = [] 314 | stream = StringIO(file.stream.read().decode("UTF8"), newline=None) 315 | csv_input = csv.DictReader(stream) 316 | for row in csv_input: 317 | data.append(row) 318 | else: 319 | return jsonify({"error": "Unsupported file type. Only .json and .csv allowed."}), 400 320 | save_history(data) 321 | logger.info("History imported from %s", file.filename) 322 | return jsonify({"status": "ok", "message": "History imported successfully."}) 323 | except Exception as e: 324 | logger.exception("Error importing history") 325 | return jsonify({"error": "Error processing file: " + str(e)}), 500 326 | 327 | # ============================================================================= 328 | # Main 329 | # ============================================================================= 330 | if __name__ == "__main__": 331 | try: 332 | if not os.path.exists("data"): 333 | os.makedirs("data") 334 | if not os.path.exists(HISTORY_FILE): 335 | save_history([]) 336 | if not os.path.exists(CONFIG_FILE): 337 | save_config({"playbooks_dir": DEFAULT_PLAYBOOKS_DIR}) 338 | app.run(host="0.0.0.0", port=5000) 339 | except Exception as e: 340 | logger.exception("Error starting application") 341 | raise 342 | -------------------------------------------------------------------------------- /data/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "playbooks_dir": "/home/pooyan" 3 | } -------------------------------------------------------------------------------- /data/history.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "action": "run", 4 | "playbook": "ping.yaml", 5 | "output": "Unexpected error occurred: [WinError 2] The system cannot find the file specified", 6 | "time": "Sun, May 18, 2025 12:55:44 PM" 7 | } 8 | ] -------------------------------------------------------------------------------- /logs/app.log: -------------------------------------------------------------------------------- 1 | 2025-05-18 12:55:44,559 INFO: Recorded playbook run: ping.yaml 2 | 2025-05-18 12:55:44,467 ERROR: Unexpected error running playbook ping.yaml 3 | Traceback (most recent call last): 4 | File "\\wsl.localhost\Ubuntu-24.04\home\pooyan\projects\AnsiblePower\ansiblePower.py", line 142, in run_playbook 5 | output = subprocess.check_output(cmd, stderr=subprocess.STDOUT) 6 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 7 | File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.11_3.11.2544.0_x64__qbz5n2kfra8p0\Lib\subprocess.py", line 466, in check_output 8 | return run(*popenargs, stdout=PIPE, timeout=timeout, check=True, 9 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 10 | File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.11_3.11.2544.0_x64__qbz5n2kfra8p0\Lib\subprocess.py", line 548, in run 11 | with Popen(*popenargs, **kwargs) as process: 12 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 13 | File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.11_3.11.2544.0_x64__qbz5n2kfra8p0\Lib\subprocess.py", line 1026, in __init__ 14 | self._execute_child(args, executable, preexec_fn, close_fds, 15 | File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.11_3.11.2544.0_x64__qbz5n2kfra8p0\Lib\subprocess.py", line 1538, in _execute_child 16 | hp, ht, pid, tid = _winapi.CreateProcess(executable, args, 17 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 18 | FileNotFoundError: [WinError 2] The system cannot find the file specified 19 | 2025-05-18 12:55:38,716 INFO: Dark mode toggled to True 20 | 2025-05-18 12:55:38,213 INFO: Dark mode toggled to False 21 | 2025-05-18 12:55:37,150 INFO: Dark mode toggled to True 22 | 2025-05-18 12:54:49,142 INFO: Dark mode toggled to False 23 | 2025-05-18 12:54:48,018 INFO: Dark mode toggled to True 24 | 2025-05-18 12:54:47,608 INFO: Dark mode toggled to False 25 | 2025-05-18 12:54:46,913 INFO: Dark mode toggled to True 26 | 2025-05-18 12:54:44,744 INFO: Dark mode toggled to False 27 | 2025-05-18 12:54:44,270 INFO: Dark mode toggled to True 28 | 2025-05-18 12:54:43,523 INFO: Dark mode toggled to False 29 | 2025-05-18 12:54:36,653 INFO: Dark mode toggled to True 30 | 2025-05-18 12:54:35,829 INFO: Dark mode toggled to False 31 | 2025-05-18 12:53:17,061 INFO: Dark mode toggled to True 32 | 2025-05-18 12:53:12,666 INFO: Dark mode toggled to False 33 | 2025-05-18 12:53:12,058 INFO: Dark mode toggled to True 34 | 2025-05-18 12:53:10,182 INFO: Dark mode toggled to False 35 | 2025-05-18 12:53:03,769 INFO: Displayed playbook: ping.yaml 36 | 2025-05-18 12:53:01,313 INFO: Updated playbooks directory to: /home/pooyan 37 | 2025-05-18 12:52:44,150 INFO: Updated playbooks directory to: /home/pooyan/ansible/playbooks 38 | 2025-05-18 12:52:36,383 INFO: History cleared 39 | 2025-05-18 12:52:30,683 INFO: Dark mode toggled to True 40 | -------------------------------------------------------------------------------- /static/css/styles.css: -------------------------------------------------------------------------------- 1 | /* Base styling */ 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | font-family: Arial, sans-serif; 6 | background: #f5f5f5; 7 | color: #333; 8 | } 9 | 10 | /* Dark mode */ 11 | .dark body { 12 | background: #333; 13 | color: #f5f5f5; 14 | } 15 | 16 | /* Sidebar styling */ 17 | .sidebar { 18 | background: #e9ecef; /* Light gray background in light mode */ 19 | padding: 15px; 20 | min-height: 100vh; 21 | } 22 | 23 | /* Dark mode sidebar */ 24 | .dark .sidebar { 25 | background: #444; /* Dark gray background in dark mode */ 26 | } 27 | 28 | /* Sidebar links */ 29 | .sidebar a { 30 | color: #333; 31 | text-decoration: none; 32 | padding: 8px 12px; 33 | display: block; 34 | border-radius: 4px; 35 | } 36 | 37 | .sidebar a:hover { 38 | background: #ddd; 39 | } 40 | 41 | .dark .sidebar a { 42 | color: #f5f5f5; 43 | } 44 | 45 | .dark .sidebar a:hover { 46 | background: #555; 47 | } 48 | 49 | /* Content styling */ 50 | .main { 51 | padding: 20px; 52 | } 53 | 54 | /* Playbook output styling */ 55 | .playbook-output { 56 | background: #fff; 57 | color: #000; 58 | padding: 10px; 59 | margin-top: 10px; 60 | border: 1px solid #ccc; 61 | border-radius: 5px; 62 | } 63 | 64 | .dark .playbook-output { 65 | background: #333; 66 | color: #fff; 67 | border: 1px solid #555; 68 | } 69 | 70 | /* List-group items (for playbooks) */ 71 | .list-group-item { 72 | background-color: #fff; 73 | color: #333; 74 | } 75 | 76 | .dark .list-group-item { 77 | background-color: #2c2f33; /* Dark background for items in dark mode */ 78 | color: #ffffff; 79 | border-color: #23272a; 80 | } 81 | 82 | /* Table adjustments in dark mode */ 83 | .dark table { 84 | color: #f5f5f5; 85 | background-color: #444; 86 | } 87 | 88 | .dark table th, 89 | .dark table td { 90 | border: 1px solid #555; 91 | } 92 | 93 | /* Alert margin */ 94 | .alert { 95 | margin-top: 20px; 96 | } 97 | 98 | /* Sidebar toggle button (if used) */ 99 | #toggle-sidebar-btn { 100 | background: #007acc; 101 | color: white; 102 | border: none; 103 | border-radius: 5px; 104 | padding: 5px 10px; 105 | margin-bottom: 10px; 106 | cursor: pointer; 107 | } -------------------------------------------------------------------------------- /static/js/main.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", function(){ 2 | // Optional: Sidebar toggle functionality if you add a toggle button. 3 | const toggleBtn = document.getElementById("toggle-sidebar-btn"); 4 | if(toggleBtn) { 5 | toggleBtn.addEventListener("click", function(){ 6 | const sidebar = document.querySelector(".sidebar"); 7 | sidebar.style.display = (sidebar.style.display === "none" || sidebar.style.display === "") ? "block" : "none"; 8 | }); 9 | } 10 | 11 | // Run playbook 12 | document.querySelectorAll(".run-btn").forEach(btn => { 13 | btn.addEventListener("click", function(){ 14 | const playbook = btn.getAttribute("data-playbook"); 15 | const outputEl = document.getElementById("output-" + playbook); 16 | outputEl.style.display = "block"; 17 | outputEl.textContent = "Running, Please wait..."; 18 | 19 | fetch("/run_playbook", { 20 | method: "POST", 21 | headers: {"Content-Type": "application/x-www-form-urlencoded"}, 22 | body: "playbook=" + encodeURIComponent(playbook) 23 | }) 24 | .then(res => res.json()) 25 | .then(data => { 26 | setTimeout(() => { 27 | outputEl.textContent = data.output || data.error; 28 | }, 1000); 29 | }); 30 | }); 31 | }); 32 | 33 | // Show playbook content 34 | document.querySelectorAll(".show-btn").forEach(btn => { 35 | btn.addEventListener("click", function(){ 36 | const playbook = btn.getAttribute("data-playbook"); 37 | const outputEl = document.getElementById("output-" + playbook); 38 | outputEl.style.display = "block"; 39 | outputEl.textContent = "Loading..."; 40 | 41 | fetch("/show_playbook", { 42 | method: "POST", 43 | headers: {"Content-Type": "application/x-www-form-urlencoded"}, 44 | body: "playbook=" + encodeURIComponent(playbook) 45 | }) 46 | .then(res => res.json()) 47 | .then(data => { 48 | outputEl.textContent = data.content || data.error || "Error fetching content."; 49 | }); 50 | }); 51 | }); 52 | 53 | // Hosts handling 54 | const showHostsBtn = document.getElementById("show-hosts-btn"); 55 | const editHostsBtn = document.getElementById("edit-hosts-btn"); 56 | const hostsBox = document.getElementById("hosts-box"); 57 | const hostsContent = document.getElementById("hosts-content"); 58 | const saveHostsBtn = document.getElementById("save-hosts-btn"); 59 | const hostsError = document.getElementById("hosts-error"); 60 | 61 | if(showHostsBtn && editHostsBtn && hostsBox && hostsContent && saveHostsBtn && hostsError) { 62 | showHostsBtn.addEventListener("click", function(){ 63 | fetch("/get_hosts") 64 | .then(r => r.json()) 65 | .then(data => { 66 | if(data.content) { 67 | hostsError.textContent = ""; 68 | hostsContent.value = data.content; 69 | hostsContent.setAttribute("readonly", "readonly"); 70 | hostsBox.style.display = "block"; 71 | saveHostsBtn.style.display = "none"; 72 | } else if(data.error) { 73 | hostsBox.style.display = "none"; 74 | hostsError.textContent = data.error; 75 | } 76 | }); 77 | }); 78 | 79 | editHostsBtn.addEventListener("click", function(){ 80 | fetch("/get_hosts") 81 | .then(r => r.json()) 82 | .then(data => { 83 | if(data.content) { 84 | hostsError.textContent = ""; 85 | hostsContent.value = data.content; 86 | hostsContent.removeAttribute("readonly"); 87 | hostsBox.style.display = "block"; 88 | saveHostsBtn.style.display = "inline-block"; 89 | } else if(data.error) { 90 | hostsBox.style.display = "none"; 91 | hostsError.textContent = data.error; 92 | } 93 | }); 94 | }); 95 | 96 | saveHostsBtn.addEventListener("click", function(){ 97 | fetch("/save_hosts", { 98 | method:"POST", 99 | headers: {"Content-Type": "application/x-www-form-urlencoded"}, 100 | body: "content=" + encodeURIComponent(hostsContent.value) 101 | }) 102 | .then(r => r.json()) 103 | .then(data => { 104 | if(data.status === "ok") { 105 | alert("Hosts saved."); 106 | } else if(data.error){ 107 | alert(data.error); 108 | } 109 | }); 110 | }); 111 | } 112 | 113 | // System status 114 | const statusBtn = document.getElementById("status-btn"); 115 | const statusBox = document.getElementById("status-box"); 116 | if(statusBtn && statusBox) { 117 | statusBtn.addEventListener("click", function(){ 118 | fetch("/system_status") 119 | .then(r => r.json()) 120 | .then(data => { 121 | statusBox.style.display = "block"; 122 | statusBox.textContent = "CPU: " + data.cpu + "% | Memory: " + data.memory + "%"; 123 | }); 124 | }); 125 | } 126 | 127 | // Clear history 128 | const clearHistoryBtn = document.getElementById("clear-history-btn"); 129 | if(clearHistoryBtn) { 130 | clearHistoryBtn.addEventListener("click", function(){ 131 | fetch("/clear_history", {method: "POST"}) 132 | .then(r => r.json()) 133 | .then(data => { 134 | if(data.status === "ok") { 135 | alert("History cleared."); 136 | location.reload(); 137 | } 138 | }); 139 | }); 140 | } 141 | 142 | // Toggle dark mode 143 | const toggleDarkModeBtn = document.getElementById("toggle-dark-mode"); 144 | if(toggleDarkModeBtn) { 145 | toggleDarkModeBtn.addEventListener("click", function(){ 146 | fetch("/toggle_dark_mode", {method: "POST"}) 147 | .then(r => r.json()) 148 | .then(data => { 149 | location.reload(); 150 | }); 151 | }); 152 | } 153 | }); -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |No history available.
8 | {% else %} 9 |Time | 13 |Playbook | 14 |Action | 15 |Output | 16 |
---|---|---|---|
{{ record.time }} | 22 |{{ record.playbook }} | 23 |{{ record.action }} | 24 |{{ record.output }} |
25 |
Playbooks directory '{{ playbooks_dir }}' does not exist.
15 | 23 |