From 29cc7e840c334d308af6e68adf669ff76c63be90 Mon Sep 17 00:00:00 2001 From: wcollins Date: Fri, 5 Sep 2025 19:59:22 -0400 Subject: [PATCH 1/2] Add updated output styling --- .../torero_ui/static/css/dashboard.css | 1210 +++++++++++++++++ 1 file changed, 1210 insertions(+) diff --git a/opt/torero-ui/torero_ui/static/css/dashboard.css b/opt/torero-ui/torero_ui/static/css/dashboard.css index 61b7267..96d3d77 100644 --- a/opt/torero-ui/torero_ui/static/css/dashboard.css +++ b/opt/torero-ui/torero_ui/static/css/dashboard.css @@ -421,6 +421,1216 @@ body { } } +/* opentofu output styling with brand theme */ +.opentofu-tabs { + margin-top: 20px; + background-color: #000000; + border: 1px solid #5cfcfe; + border-radius: 4px; + padding: 15px; +} + +.opentofu-tabs .tab-buttons { + display: flex; + gap: 5px; + margin-bottom: 15px; + border-bottom: 2px solid #5cfcfe; + padding-bottom: 10px; +} + +.opentofu-tabs .tab-button { + background: #111111; + border: 1px solid #333333; + color: #ffffff; + font-family: 'Consolas', monospace; + font-size: 13px; + padding: 8px 16px; + cursor: pointer; + transition: all 0.2s ease; + text-transform: none; + letter-spacing: 0.5px; + border-radius: 4px 4px 0 0; +} + +.opentofu-tabs .tab-button:hover { + background: #222222; + color: #5cfcfe; + border-color: #5cfcfe; +} + +.opentofu-tabs .tab-button.active { + background: #222222; + color: #ccff00; + border-color: #ccff00; + border-bottom: 2px solid #222222; + margin-bottom: -2px; +} + +.opentofu-tab-content { + display: none; + padding: 20px; + background: #0a0a0a; + border-radius: 0 0 4px 4px; +} + +.opentofu-tab-content.active { + display: block; +} + +/* console output styling */ +.console-section { + margin-bottom: 20px; +} + +.console-header { + color: #5cfcfe; + font-size: 14px; + font-weight: bold; + margin-bottom: 10px; + padding: 8px; + background: #111111; + border-left: 3px solid #5cfcfe; +} + +.console-header-error { + color: #ccff00; + border-left-color: #ccff00; +} + +.tofu-console-output { + background: #1d2021; + color: #ebdbb2; + padding: 15px; + border-radius: 4px; + overflow-x: auto; + font-family: 'Consolas', 'Courier New', monospace; + font-size: 13px; + line-height: 1.6; + white-space: pre; + border: 1px solid #3c3836; + max-height: 600px; + overflow-y: auto; +} + +.tofu-console-error { + border-left: 3px solid #fb4934; +} + +/* state file display */ +.state-container { + padding: 10px; +} + +.state-summary { + background: #111111; + border: 1px solid #333333; + border-radius: 4px; + padding: 15px; + margin-bottom: 20px; +} + +.state-header { + color: #5cfcfe; + font-size: 15px; + font-weight: bold; + margin-bottom: 15px; + padding-bottom: 8px; + border-bottom: 1px solid #333333; +} + +.state-metadata { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 15px; +} + +.metadata-item { + display: flex; + justify-content: space-between; + padding: 8px; + background: #000000; + border-radius: 3px; + border: 1px solid #333333; +} + +.metadata-label { + color: #888888; + font-size: 12px; + text-transform: uppercase; +} + +.metadata-value { + color: #ccff00; + font-weight: bold; +} + +/* resources grid */ +.resources-section { + margin-top: 20px; +} + +.resource-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 15px; + margin-top: 15px; +} + +.resource-card { + background: #111111; + border: 1px solid #333333; + padding: 12px; + border-radius: 4px; + transition: all 0.2s ease; +} + +.resource-card:hover { + border-color: #5cfcfe; + background: #222222; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(92, 252, 254, 0.1); +} + +.resource-header { + display: flex; + justify-content: space-between; + margin-bottom: 8px; + padding-bottom: 8px; + border-bottom: 1px solid #333333; +} + +.resource-type { + color: #ccff00; + font-size: 12px; + font-weight: bold; +} + +.resource-mode { + color: #888888; + font-size: 11px; + text-transform: uppercase; +} + +.resource-name { + color: #5cfcfe; + font-size: 14px; + font-weight: bold; + margin-bottom: 8px; +} + +.resource-details { + display: flex; + justify-content: space-between; + font-size: 11px; +} + +.resource-provider { + color: #5cfcfe; +} + +.resource-instances { + color: #ccff00; +} + +/* outputs display */ +.outputs-container { + padding: 10px; +} + +.outputs-header { + color: #5cfcfe; + font-size: 15px; + font-weight: bold; + margin-bottom: 15px; + padding-bottom: 8px; + border-bottom: 1px solid #333333; +} + +.outputs-grid { + display: flex; + flex-direction: column; + gap: 12px; +} + +.output-item { + background: #111111; + border: 1px solid #333333; + padding: 12px; + border-radius: 4px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.output-key { + color: #5cfcfe; + font-weight: bold; + font-size: 14px; +} + +.output-value-container { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.output-value { + color: #ccff00; + word-break: break-word; + font-family: 'Consolas', monospace; + flex: 1; +} + +.output-type { + color: #888888; + font-size: 11px; + margin-left: 10px; + white-space: nowrap; +} + +/* timing information */ +.timing-container { + padding: 10px; +} + +.timing-header { + color: #5cfcfe; + font-size: 15px; + font-weight: bold; + margin-bottom: 15px; + padding-bottom: 8px; + border-bottom: 1px solid #333333; +} + +.timing-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 15px; +} + +.timing-item { + background: #111111; + border: 1px solid #333333; + padding: 12px; + border-radius: 4px; + display: flex; + justify-content: space-between; +} + +.timing-label { + color: #888888; + font-size: 12px; + text-transform: uppercase; +} + +.timing-value { + color: #5cfcfe; + font-weight: bold; +} + +.timing-success { + color: #ccff00 !important; +} + +.timing-error { + color: #ff4444 !important; +} + +/* no output message */ +.no-output { + text-align: center; + padding: 40px; + color: #888888; + font-style: italic; + background: #111111; + border: 1px dashed #333333; + border-radius: 4px; +} + +/* scrollbar styling for brand theme */ +.tofu-console-output::-webkit-scrollbar, +.opentofu-tab-content::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +.tofu-console-output::-webkit-scrollbar-track, +.opentofu-tab-content::-webkit-scrollbar-track { + background: #504945; + border-radius: 5px; +} + +.tofu-console-output::-webkit-scrollbar-thumb, +.opentofu-tab-content::-webkit-scrollbar-thumb { + background: #665c54; + border-radius: 5px; +} + +.tofu-console-output::-webkit-scrollbar-thumb:hover, +.opentofu-tab-content::-webkit-scrollbar-thumb:hover { + background: #7c6f64; +} + +/* python script output styling with brand theme */ +.python-tabs { + margin-top: 20px; + background-color: #000000; + border: 1px solid #5cfcfe; + border-radius: 4px; + padding: 15px; +} + +.python-tabs .tab-buttons { + display: flex; + gap: 5px; + margin-bottom: 15px; + border-bottom: 2px solid #5cfcfe; + padding-bottom: 10px; +} + +.python-tabs .tab-button { + background: #111111; + border: 1px solid #333333; + color: #ffffff; + font-family: 'Consolas', monospace; + font-size: 13px; + padding: 8px 16px; + cursor: pointer; + transition: all 0.2s ease; + text-transform: none; + letter-spacing: 0.5px; + border-radius: 4px 4px 0 0; +} + +.python-tabs .tab-button:hover { + background: #222222; + color: #5cfcfe; + border-color: #5cfcfe; +} + +.python-tabs .tab-button.active { + background: #222222; + color: #ccff00; + border-color: #ccff00; + border-bottom: 2px solid #222222; + margin-bottom: -2px; +} + +.python-tab-content { + display: none; + padding: 20px; + background: #0a0a0a; + border-radius: 0 0 4px 4px; +} + +.python-tab-content.active { + display: block; +} + +/* python console output styling with gruvbox */ +.python-console-output { + background: #1d2021; + color: #ebdbb2; + padding: 15px; + border-radius: 4px; + overflow-x: auto; + font-family: 'Consolas', 'Courier New', monospace; + font-size: 13px; + line-height: 1.6; + white-space: pre; + border: 1px solid #3c3836; + max-height: 600px; + overflow-y: auto; +} + +.python-console-error { + border-left: 3px solid #fb4934; +} + +/* python log level colors using gruvbox */ +.python-critical { color: #cc241d; font-weight: bold; } +.python-error { color: #fb4934; } +.python-warning { color: #d79921; } +.python-info { color: #458588; } +.python-debug { color: #928374; } +.python-success { color: #98971a; } +.python-json { color: #83a598; } +.python-file { color: #83a598; text-decoration: underline; } +.python-line { color: #fabd2f; font-weight: bold; } +.python-timing { color: #8ec07c; font-weight: bold; } + +/* python stack trace styling */ +.stacktrace-container { + padding: 10px; +} + +.stacktrace-header { + color: #5cfcfe; + font-size: 15px; + font-weight: bold; + margin-bottom: 15px; + padding-bottom: 8px; + border-bottom: 1px solid #5cfcfe; +} + +.stacktrace-item { + background: #111111; + border: 1px solid #333333; + margin-bottom: 15px; + border-radius: 4px; + overflow: hidden; +} + +.stacktrace-title { + background: #5cfcfe; + color: #000000; + padding: 8px 12px; + font-weight: bold; + font-size: 12px; +} + +.stacktrace-content { + padding: 15px; + background: #1d2021; + color: #ebdbb2; + font-family: 'Consolas', monospace; + font-size: 12px; + line-height: 1.4; + overflow-x: auto; +} + +.stacktrace-file { color: #83a598; } +.stacktrace-line { color: #fabd2f; font-weight: bold; } +.stacktrace-function { color: #b16286; } +.stacktrace-exception { color: #fb4934; font-weight: bold; } +.stacktrace-header-text { color: #fb4934; font-weight: bold; } + +/* python performance styling */ +.performance-container { + padding: 10px; +} + +.performance-header { + color: #5cfcfe; + font-size: 15px; + font-weight: bold; + margin-bottom: 15px; + padding-bottom: 8px; + border-bottom: 1px solid #5cfcfe; +} + +.performance-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 15px; + margin-bottom: 20px; +} + +.performance-item { + background: #111111; + border: 1px solid #333333; + padding: 12px; + border-radius: 4px; + display: flex; + justify-content: space-between; +} + +.performance-label { + color: #888888; + font-size: 12px; + text-transform: uppercase; +} + +.performance-value { + color: #5cfcfe; + font-weight: bold; +} + +.performance-success { + color: #ccff00 !important; +} + +.performance-error { + color: #ff4444 !important; +} + +.performance-subheader { + color: #5cfcfe; + font-size: 13px; + font-weight: bold; + margin: 15px 0 10px 0; +} + +.timing-context { + background: #111111; + border: 1px solid #333333; + margin-bottom: 10px; + padding: 10px; + border-radius: 4px; +} + +/* python environment styling */ +.environment-container { + padding: 10px; +} + +.environment-header { + color: #5cfcfe; + font-size: 15px; + font-weight: bold; + margin-bottom: 15px; + padding-bottom: 8px; + border-bottom: 1px solid #5cfcfe; +} + +.env-section { + margin-bottom: 20px; +} + +.env-subheader { + color: #ccff00; + font-size: 13px; + font-weight: bold; + margin-bottom: 10px; +} + +.env-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 10px; +} + +.env-item { + background: #111111; + border: 1px solid #333333; + padding: 8px; + border-radius: 3px; + display: flex; + justify-content: space-between; +} + +.env-label { + color: #888888; + font-size: 12px; + text-transform: uppercase; +} + +.env-value { + color: #ffffff; + font-weight: bold; +} + +.import-grid { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.import-item { + background: #111111; + border: 1px solid #5cfcfe; + color: #ccff00; + padding: 4px 8px; + border-radius: 3px; + font-size: 11px; + font-family: 'Consolas', monospace; +} + +.module-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.module-item { + background: #111111; + border: 1px solid #333333; + padding: 8px; + border-radius: 3px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.module-name { + color: #5cfcfe; + font-weight: bold; +} + +.module-version { + color: #ccff00; + font-size: 11px; + font-family: 'Consolas', monospace; +} + +/* ansible playbook output styling with brand theme */ +.ansible-tabs { + margin-top: 20px; + background-color: #000000; + border: 1px solid #5cfcfe; + border-radius: 4px; + padding: 15px; +} + +.ansible-tabs .tab-buttons { + display: flex; + gap: 5px; + margin-bottom: 15px; + border-bottom: 2px solid #5cfcfe; + padding-bottom: 10px; +} + +.ansible-tabs .tab-button { + background: #111111; + border: 1px solid #333333; + color: #ffffff; + font-family: 'Consolas', monospace; + font-size: 13px; + padding: 8px 16px; + cursor: pointer; + transition: all 0.2s ease; + text-transform: none; + letter-spacing: 0.5px; + border-radius: 4px 4px 0 0; +} + +.ansible-tabs .tab-button:hover { + background: #222222; + color: #5cfcfe; + border-color: #5cfcfe; +} + +.ansible-tabs .tab-button.active { + background: #222222; + color: #ccff00; + border-color: #ccff00; + border-bottom: 2px solid #222222; + margin-bottom: -2px; +} + +.ansible-tab-content { + display: none; + padding: 20px; + background: #0a0a0a; + border-radius: 0 0 4px 4px; +} + +.ansible-tab-content.active { + display: block; +} + +/* ansible console output styling with gruvbox */ +.ansible-console-output { + background: #1d2021; + color: #ebdbb2; + padding: 15px; + border-radius: 4px; + overflow-x: auto; + font-family: 'Consolas', 'Courier New', monospace; + font-size: 13px; + line-height: 1.6; + white-space: pre; + border: 1px solid #3c3836; + max-height: 600px; + overflow-y: auto; +} + +.ansible-console-error { + border-left: 3px solid #fb4934; +} + +/* ansible status colors using gruvbox */ +.ansible-ok { color: #98971a; } +.ansible-changed { color: #d79921; } +.ansible-failed { color: #fb4934; } +.ansible-skipped { color: #458588; } +.ansible-unreachable { color: #928374; } +.ansible-success { color: #98971a; } +.ansible-recap { color: #ebdbb2; font-weight: bold; } +.ansible-task { color: #458588; font-weight: bold; } +.ansible-task-name { color: #ebdbb2; } +.ansible-host { color: #d79921; font-weight: bold; } +.ansible-timing { color: #8ec07c; font-weight: bold; } +.ansible-json { color: #83a598; } +.ansible-yaml { color: #b16286; } + +/* ansible summary styling */ +.ansible-summary-container { + padding: 10px; +} + +.ansible-summary-header { + color: #5cfcfe; + font-size: 15px; + font-weight: bold; + margin-bottom: 15px; + padding-bottom: 8px; + border-bottom: 1px solid #5cfcfe; +} + +.ansible-overview { + margin-bottom: 20px; +} + +.summary-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 15px; + margin-bottom: 20px; +} + +.summary-item { + background: #111111; + border: 1px solid #333333; + padding: 12px; + border-radius: 4px; + display: flex; + justify-content: space-between; +} + +.summary-label { + color: #888888; + font-size: 12px; + text-transform: uppercase; +} + +.summary-value { + color: #ffffff; + font-weight: bold; +} + +.ansible-section-header { + color: #ccff00; + font-size: 14px; + font-weight: bold; + margin-bottom: 10px; + padding-bottom: 5px; + border-bottom: 1px solid #333333; +} + +.host-summary-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 15px; +} + +.host-summary-card { + background: #111111; + border: 1px solid #333333; + padding: 12px; + border-radius: 4px; +} + +.host-name { + color: #5cfcfe; + font-weight: bold; + font-size: 14px; + margin-bottom: 8px; +} + +.host-stats { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.stat-item { + font-size: 11px; + padding: 2px 6px; + border-radius: 2px; + background: rgba(255, 255, 255, 0.1); +} + +.plays-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.play-item { + background: #1a1a1a; + border: 1px solid #333333; + padding: 12px; + border-radius: 4px; +} + +.play-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 5px; +} + +.play-name { + color: #ccff00; + font-weight: bold; +} + +.play-status { + font-size: 11px; + padding: 2px 6px; + border-radius: 2px; +} + +.play-status.ok { + background: #ccff00; + color: #000; +} + +.play-status.failed { + background: #ff4444; + color: #fff; +} + +.play-details { + display: flex; + gap: 15px; + font-size: 12px; + color: #c9c9c9; +} + +/* ansible tasks styling */ +.ansible-tasks-container { + padding: 10px; +} + +.ansible-tasks-header { + color: #5cfcfe; + font-size: 15px; + font-weight: bold; + margin-bottom: 15px; + padding-bottom: 8px; + border-bottom: 1px solid #5cfcfe; +} + +.tasks-timeline { + display: flex; + flex-direction: column; + gap: 15px; +} + +.task-item { + display: flex; + align-items: flex-start; + gap: 15px; +} + +.task-indicator { + width: 12px; + height: 12px; + border-radius: 50%; + margin-top: 6px; + flex-shrink: 0; +} + +.task-indicator.ok { background: #00d924; } +.task-indicator.changed { background: #ff8c00; } +.task-indicator.failed { background: #ff073a; } +.task-indicator.skipped { background: #0088cc; } + +.task-content { + flex: 1; + background: #111111; + border: 1px solid #333333; + padding: 12px; + border-radius: 4px; +} + +.task-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.task-name { + color: #ffffff; + font-weight: bold; +} + +.task-status { + font-size: 11px; + padding: 2px 6px; + border-radius: 2px; +} + +.task-details { + display: flex; + gap: 15px; + margin-bottom: 8px; + font-size: 12px; +} + +.task-module { + color: #5cfcfe; + font-family: 'Consolas', monospace; +} + +.task-duration { + color: #ccff00; +} + +.task-hosts { + margin-bottom: 8px; +} + +.hosts-label { + color: #c9c9c9; + font-size: 11px; + margin-right: 8px; +} + +.host-tag { + background: #5cfcfe; + color: #000; + padding: 2px 6px; + border-radius: 2px; + font-size: 10px; + margin-right: 4px; +} + +.task-changes { + margin-top: 10px; +} + +.changes-header { + color: #ccff00; + font-size: 12px; + margin-bottom: 5px; +} + +.changes-content { + background: #000000; + border: 1px solid #333333; + padding: 8px; + border-radius: 3px; + font-size: 11px; + max-height: 200px; + overflow-y: auto; +} + +/* ansible hosts styling */ +.ansible-hosts-container { + padding: 10px; +} + +.ansible-hosts-header { + color: #5cfcfe; + font-size: 15px; + font-weight: bold; + margin-bottom: 15px; + padding-bottom: 8px; + border-bottom: 1px solid #5cfcfe; +} + +.hosts-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 15px; +} + +.host-result-card { + background: #111111; + border: 1px solid #333333; + padding: 15px; + border-radius: 4px; +} + +.host-result-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 1px solid #333333; +} + +.host-result-name { + color: #5cfcfe; + font-weight: bold; + font-size: 16px; +} + +.host-result-status { + font-size: 11px; + padding: 3px 8px; + border-radius: 3px; + font-weight: bold; +} + +.host-result-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); + gap: 8px; + margin-bottom: 12px; +} + +.result-stat { + display: flex; + flex-direction: column; + align-items: center; + padding: 6px; + background: #000000; + border-radius: 3px; +} + +.result-stat .stat-label { + color: #888888; + font-size: 10px; + text-transform: uppercase; +} + +.result-stat .stat-value { + font-weight: bold; + font-size: 14px; +} + +.host-tasks { + border-top: 1px solid #333333; + padding-top: 10px; +} + +.host-tasks-header { + color: #ccff00; + font-size: 12px; + font-weight: bold; + margin-bottom: 8px; +} + +.host-tasks-list { + display: flex; + flex-direction: column; + gap: 4px; +} + +.host-task-item { + display: flex; + align-items: center; + gap: 6px; + padding: 3px; + font-size: 11px; +} + +.host-task-item .task-indicator { + width: 6px; + height: 6px; + margin-top: 0; +} + +.task-text { + color: #d4d4d4; +} + +.more-tasks { + color: #c9c9c9; + font-size: 10px; + font-style: italic; + margin-top: 4px; +} + +/* ansible variables styling */ +.ansible-variables-container { + padding: 10px; +} + +.ansible-variables-header { + color: #5cfcfe; + font-size: 15px; + font-weight: bold; + margin-bottom: 15px; + padding-bottom: 8px; + border-bottom: 1px solid #5cfcfe; +} + +.variables-sections { + display: flex; + flex-direction: column; + gap: 20px; +} + +.variable-section { + background: #111111; + border: 1px solid #333333; + padding: 15px; + border-radius: 4px; +} + +.variable-category { + color: #ccff00; + font-size: 14px; + font-weight: bold; + margin-bottom: 12px; + padding-bottom: 6px; + border-bottom: 1px solid #333333; +} + +.variables-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.variable-item { + background: #000000; + border: 1px solid #333333; + padding: 10px; + border-radius: 3px; +} + +.variable-key { + color: #5cfcfe; + font-weight: bold; + font-size: 12px; + margin-bottom: 4px; +} + +.variable-value { + color: #ffffff; + font-family: 'Consolas', monospace; + font-size: 11px; + word-break: break-word; +} + +.more-vars { + color: #888888; + font-size: 11px; + font-style: italic; + text-align: center; + padding: 8px; +} + +/* scrollbar styling for code output */ +.python-console-output::-webkit-scrollbar, +.python-tab-content::-webkit-scrollbar, +.ansible-console-output::-webkit-scrollbar, +.ansible-tab-content::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +.python-console-output::-webkit-scrollbar-track, +.python-tab-content::-webkit-scrollbar-track, +.ansible-console-output::-webkit-scrollbar-track, +.ansible-tab-content::-webkit-scrollbar-track { + background: #504945; + border-radius: 5px; +} + +.python-console-output::-webkit-scrollbar-thumb, +.python-tab-content::-webkit-scrollbar-thumb, +.ansible-console-output::-webkit-scrollbar-thumb, +.ansible-tab-content::-webkit-scrollbar-thumb { + background: #665c54; + border-radius: 5px; +} + +.python-console-output::-webkit-scrollbar-thumb:hover, +.python-tab-content::-webkit-scrollbar-thumb:hover, +.ansible-console-output::-webkit-scrollbar-thumb:hover, +.ansible-tab-content::-webkit-scrollbar-thumb:hover { + background: #7c6f64; +} + /* utility classes */ .text-center { text-align: center; } .text-success { color: #ccff00; } From d2e3819de1fa087d21a3c4fc9066b48a16e2e549 Mon Sep 17 00:00:00 2001 From: wcollins Date: Fri, 5 Sep 2025 20:00:15 -0400 Subject: [PATCH 2/2] Add modified dashboard components --- .../torero_ui/static/js/dashboard.js | 1243 ++++++++++++++++- 1 file changed, 1221 insertions(+), 22 deletions(-) diff --git a/opt/torero-ui/torero_ui/static/js/dashboard.js b/opt/torero-ui/torero_ui/static/js/dashboard.js index 5e99a65..f966cfa 100644 --- a/opt/torero-ui/torero_ui/static/js/dashboard.js +++ b/opt/torero-ui/torero_ui/static/js/dashboard.js @@ -369,28 +369,45 @@ function generateExecutionDetailHTML(execution) { html += ''; - // add stdout if available - if (execution.stdout) { - html += ` -

Standard Output

-
${escapeHtml(execution.stdout)}
- `; - } - - // add stderr if available - if (execution.stderr) { - html += ` -

Standard Error

-
${escapeHtml(execution.stderr)}
- `; - } - - // add execution data if available - if (execution.execution_data && Object.keys(execution.execution_data).length > 0) { - html += ` -

Execution Data

-
${escapeHtml(JSON.stringify(execution.execution_data, null, 2))}
- `; + // check service type and use appropriate formatter + if (execution.service_type === 'opentofu-plan' && execution.execution_data && + typeof execution.execution_data === 'object' && + (execution.execution_data.stdout || execution.execution_data.state_file)) { + // use specialized opentofu formatter + html += formatOpenTofuOutput(execution.execution_data); + } else if (execution.service_type === 'python-script' && execution.execution_data && + typeof execution.execution_data === 'object') { + // use specialized python formatter + html += formatPythonOutput(execution.execution_data); + } else if (execution.service_type === 'ansible-playbook' && execution.execution_data && + typeof execution.execution_data === 'object') { + // use specialized ansible formatter + html += formatAnsibleOutput(execution.execution_data); + } else { + // use standard output display for other service types + // add stdout if available + if (execution.stdout) { + html += ` +

Standard Output

+
${escapeHtml(execution.stdout)}
+ `; + } + + // add stderr if available + if (execution.stderr) { + html += ` +

Standard Error

+
${escapeHtml(execution.stderr)}
+ `; + } + + // add execution data if available + if (execution.execution_data && Object.keys(execution.execution_data).length > 0) { + html += ` +

Execution Data

+
${escapeHtml(JSON.stringify(execution.execution_data, null, 2))}
+ `; + } } return html; @@ -450,6 +467,1188 @@ function escapeHtml(text) { return div.innerHTML; } +// opentofu output formatting functions +function formatOpenTofuOutput(executionData) { + // parse the data if it's a string + const data = typeof executionData === 'string' ? JSON.parse(executionData) : executionData; + + // generate unique ids for tabs within this execution + const tabPrefix = 'tofu-' + Date.now(); + + return ` +
+
+ + + + + +
+ +
+ ${formatConsoleOutput(data)} +
+ +
+ ${formatStateFile(data.state_file)} +
+ +
+ ${formatTofuOutputs(data.state_file?.outputs)} +
+ +
+ ${formatTimingInfo(data)} +
+ +
+
${escapeHtml(JSON.stringify(data, null, 2))}
+
+
+ `; +} + +// format console output with ansi to html conversion using dracula theme +function formatConsoleOutput(data) { + if (!data.stdout && !data.stderr) { + return '
No console output available
'; + } + + let html = ''; + + if (data.stdout) { + const formattedStdout = convertAnsiToHtml(data.stdout); + html += ` +
+

Standard Output

+
${formattedStdout}
+
+ `; + } + + if (data.stderr) { + const formattedStderr = convertAnsiToHtml(data.stderr); + html += ` +
+

Standard Error

+
${formattedStderr}
+
+ `; + } + + return html; +} + +// convert ansi escape codes to html with gruvbox theme colors +function convertAnsiToHtml(text) { + if (!text) return ''; + + // gruvbox dark theme colors + const gruvbox = { + black: '#282828', + red: '#cc241d', + green: '#98971a', + yellow: '#d79921', + blue: '#458588', + magenta: '#b16286', + cyan: '#689d6a', + white: '#a89984', + brightBlack: '#928374', + brightRed: '#fb4934', + brightGreen: '#b8bb26', + brightYellow: '#fabd2f', + brightBlue: '#83a598', + brightMagenta: '#d3869b', + brightCyan: '#8ec07c', + brightWhite: '#ebdbb2' + }; + + // first escape html to prevent xss + let result = escapeHtml(text); + + // replace ansi color codes with html spans + // handle 8-color and 16-color ansi codes + result = result + // reset + .replace(/\x1b\[0m/g, '') + .replace(/\x1b\[m/g, '') + + // bold/bright + .replace(/\x1b\[1m/g, '') + .replace(/\x1b\[22m/g, '') + + // dim + .replace(/\x1b\[2m/g, '') + + // italic + .replace(/\x1b\[3m/g, '') + .replace(/\x1b\[23m/g, '') + + // underline + .replace(/\x1b\[4m/g, '') + .replace(/\x1b\[24m/g, '') + + // foreground colors (30-37, 90-97) + .replace(/\x1b\[30m/g, ``) + .replace(/\x1b\[31m/g, ``) + .replace(/\x1b\[32m/g, ``) + .replace(/\x1b\[33m/g, ``) + .replace(/\x1b\[34m/g, ``) + .replace(/\x1b\[35m/g, ``) + .replace(/\x1b\[36m/g, ``) + .replace(/\x1b\[37m/g, ``) + + // bright foreground colors + .replace(/\x1b\[90m/g, ``) + .replace(/\x1b\[91m/g, ``) + .replace(/\x1b\[92m/g, ``) + .replace(/\x1b\[93m/g, ``) + .replace(/\x1b\[94m/g, ``) + .replace(/\x1b\[95m/g, ``) + .replace(/\x1b\[96m/g, ``) + .replace(/\x1b\[97m/g, ``) + + // background colors (40-47, 100-107) + .replace(/\x1b\[40m/g, ``) + .replace(/\x1b\[41m/g, ``) + .replace(/\x1b\[42m/g, ``) + .replace(/\x1b\[43m/g, ``) + .replace(/\x1b\[44m/g, ``) + .replace(/\x1b\[45m/g, ``) + .replace(/\x1b\[46m/g, ``) + .replace(/\x1b\[47m/g, ``) + + // handle combined codes like \x1b[1;32m (bold green) + .replace(/\x1b\[1;30m/g, ``) + .replace(/\x1b\[1;31m/g, ``) + .replace(/\x1b\[1;32m/g, ``) + .replace(/\x1b\[1;33m/g, ``) + .replace(/\x1b\[1;34m/g, ``) + .replace(/\x1b\[1;35m/g, ``) + .replace(/\x1b\[1;36m/g, ``) + .replace(/\x1b\[1;37m/g, ``) + + // handle any remaining escape sequences + .replace(/\x1b\[[0-9;]*m/g, ''); + + // clean up any unclosed spans at the end + const openSpans = (result.match(/]*>/g) || []).length; + const closeSpans = (result.match(/<\/span>/g) || []).length; + if (openSpans > closeSpans) { + result += ''.repeat(openSpans - closeSpans); + } + + return result; +} + +// format state file information +function formatStateFile(stateFile) { + if (!stateFile) { + return '
No state information available
'; + } + + const resources = stateFile.resources || []; + const outputs = stateFile.outputs || {}; + + return ` +
+
+

State Summary

+ +
+ + ${resources.length > 0 ? formatResources(resources) : ''} +
+ `; +} + +// format resource list from state file +function formatResources(resources) { + if (!resources || resources.length === 0) { + return ''; + } + + return ` +
+

Resources (${resources.length})

+
+ ${resources.map(resource => ` +
+
+ ${resource.type || 'unknown'} + ${resource.mode || ''} +
+
${resource.name || 'unnamed'}
+
+ ${extractProviderName(resource.provider)} + Instances: ${resource.instances ? resource.instances.length : 0} +
+
+ `).join('')} +
+
+ `; +} + +// extract provider name from full provider string +function extractProviderName(provider) { + if (!provider) return 'unknown'; + // extract from format like: provider["registry.opentofu.org/hashicorp/null"] + const match = provider.match(/provider\["[^/]*\/([^/]*)\/([^"]*)/); + if (match) { + return `${match[1]}/${match[2]}`; + } + return provider; +} + +// format tofu outputs +function formatTofuOutputs(outputs) { + if (!outputs || Object.keys(outputs).length === 0) { + return '
No outputs defined
'; + } + + return ` +
+

Terraform Outputs

+
+ ${Object.entries(outputs).map(([key, value]) => ` +
+
${key}
+
+ ${formatOutputValue(value.value)} + ${value.type || 'unknown'} +
+
+ `).join('')} +
+
+ `; +} + +// format output value based on type +function formatOutputValue(value) { + if (value === null || value === undefined) { + return 'null'; + } + if (typeof value === 'object') { + return JSON.stringify(value, null, 2); + } + return String(value); +} + +// format timing information +function formatTimingInfo(data) { + if (!data.start_time && !data.end_time && !data.elapsed_time) { + return '
No timing information available
'; + } + + const startTime = data.start_time ? new Date(data.start_time) : null; + const endTime = data.end_time ? new Date(data.end_time) : null; + + return ` +
+

Execution Timing

+
+ ${startTime ? ` +
+ Started: + ${startTime.toLocaleString()} +
+ ` : ''} + ${endTime ? ` +
+ Completed: + ${endTime.toLocaleString()} +
+ ` : ''} + ${data.elapsed_time ? ` +
+ Duration: + ${data.elapsed_time.toFixed(3)} seconds +
+ ` : ''} + ${data.return_code !== undefined ? ` +
+ Return Code: + ${data.return_code} +
+ ` : ''} +
+
+ `; +} + +// switch between opentofu output tabs +function switchOpenTofuTab(tabId, button) { + // find the parent container + const container = button.closest('.opentofu-tabs'); + + // hide all tab contents in this container + const contents = container.querySelectorAll('.opentofu-tab-content'); + contents.forEach(content => { + content.classList.remove('active'); + }); + + // remove active class from all buttons in this container + const buttons = container.querySelectorAll('.tab-button'); + buttons.forEach(btn => { + btn.classList.remove('active'); + }); + + // show selected tab + const targetTab = document.getElementById(tabId); + if (targetTab) { + targetTab.classList.add('active'); + } + + // mark button as active + button.classList.add('active'); +} + +// python script output formatting functions +function formatPythonOutput(executionData) { + const data = typeof executionData === 'string' ? JSON.parse(executionData) : executionData; + const tabPrefix = 'python-' + Date.now(); + + return ` +
+
+ + + + + +
+ +
+ ${formatPythonConsoleOutput(data)} +
+ +
+ ${formatPythonStackTrace(data)} +
+ +
+ ${formatPythonPerformance(data)} +
+ +
+ ${formatPythonEnvironment(data)} +
+ +
+
${escapeHtml(JSON.stringify(data, null, 2))}
+
+
+ `; +} + +// format python console output with log level detection +function formatPythonConsoleOutput(data) { + if (!data.stdout && !data.stderr) { + return '
No console output available
'; + } + + let html = ''; + + if (data.stdout) { + const formattedStdout = formatPythonLogOutput(data.stdout); + html += ` +
+

Standard Output

+
${formattedStdout}
+
+ `; + } + + if (data.stderr) { + const formattedStderr = formatPythonLogOutput(data.stderr); + html += ` +
+

Standard Error

+
${formattedStderr}
+
+ `; + } + + return html; +} + +// format python output with log level detection and syntax highlighting +function formatPythonLogOutput(text) { + if (!text) return ''; + + let result = escapeHtml(text); + + // python log level patterns with colors + result = result + .replace(/\b(CRITICAL|FATAL)(\s*[:\-]|\b)/gi, '$1$2') + .replace(/\b(ERROR)(\s*[:\-]|\b)/gi, '$1$2') + .replace(/\b(WARNING|WARN)(\s*[:\-]|\b)/gi, '$1$2') + .replace(/\b(INFO)(\s*[:\-]|\b)/gi, '$1$2') + .replace(/\b(DEBUG)(\s*[:\-]|\b)/gi, '$1$2') + + // highlight json objects + .replace(/(\{[^{}]*\})/g, '$1') + + // highlight file paths and line numbers + .replace(/(\w+\.py):(\d+)/g, '$1:$2') + + // highlight execution times + .replace(/(\d+\.?\d*)\s*(s|ms|seconds?|milliseconds?)\b/gi, '$1$2') + + // highlight success indicators + .replace(/\b(SUCCESS|PASSED|OK|COMPLETE)\b/gi, '$1') + + // highlight failure indicators + .replace(/\b(FAILED?|FAILURE|ERROR|EXCEPTION|TRACEBACK)\b/gi, '$1'); + + return result; +} + +// format python stack trace with file links +function formatPythonStackTrace(data) { + const text = data.stderr || data.stdout || ''; + + if (!text.includes('Traceback') && !text.includes('Exception')) { + return '
No stack trace information available
'; + } + + // extract traceback sections + const tracebackPattern = /Traceback \(most recent call last\):[\s\S]*?(?=\n\n|\n[A-Z]|\n$|$)/g; + const tracebacks = text.match(tracebackPattern) || []; + + if (tracebacks.length === 0) { + return '
No stack trace information available
'; + } + + return ` +
+

Stack Traces (${tracebacks.length})

+ ${tracebacks.map((trace, index) => ` +
+
Traceback ${index + 1}
+
${formatStackTraceText(trace)}
+
+ `).join('')} +
+ `; +} + +// format individual stack trace with enhanced readability +function formatStackTraceText(text) { + let result = escapeHtml(text); + + result = result + // highlight file paths and line numbers + .replace(/(File\s+)"([^"]+)",\s+line\s+(\d+)/g, + '$1"$2", line $3') + + // highlight function names + .replace(/in\s+([a-zA-Z_][a-zA-Z0-9_]*)/g, 'in $1') + + // highlight exception types + .replace(/^([A-Z][a-zA-Z]*Error|[A-Z][a-zA-Z]*Exception):/gm, + '$1:') + + // highlight traceback header + .replace(/(Traceback \(most recent call last\):)/, '$1'); + + return result; +} + +// format python performance metrics +function formatPythonPerformance(data) { + const text = (data.stdout || '') + (data.stderr || ''); + + // extract timing information + const timingPatterns = [ + /executed in (\d+\.?\d*)\s*(s|ms|seconds?|milliseconds?)/gi, + /took (\d+\.?\d*)\s*(s|ms|seconds?|milliseconds?)/gi, + /duration[:\s]+(\d+\.?\d*)\s*(s|ms|seconds?|milliseconds?)/gi, + /time[:\s]+(\d+\.?\d*)\s*(s|ms|seconds?|milliseconds?)/gi + ]; + + let timings = []; + timingPatterns.forEach(pattern => { + let match; + while ((match = pattern.exec(text)) !== null) { + timings.push({ + value: parseFloat(match[1]), + unit: match[2], + context: text.substring(Math.max(0, match.index - 50), match.index + 100) + }); + } + }); + + if (timings.length === 0 && !data.elapsed_time) { + return '
No performance information available
'; + } + + return ` +
+

Performance Metrics

+
+ ${data.elapsed_time ? ` +
+ Total Execution Time: + ${data.elapsed_time.toFixed(3)}s +
+ ` : ''} + ${data.return_code !== undefined ? ` +
+ Exit Code: + ${data.return_code} +
+ ` : ''} + ${timings.map((timing, index) => ` +
+ Timing ${index + 1}: + ${timing.value}${timing.unit} +
+ `).join('')} +
+ ${timings.length > 0 ? ` +
+
Timing Context
+ ${timings.map((timing, index) => ` +
+ Timing ${index + 1}: +
${escapeHtml(timing.context)}
+
+ `).join('')} +
+ ` : ''} +
+ `; +} + +// format python environment information +function formatPythonEnvironment(data) { + const text = (data.stdout || '') + (data.stderr || ''); + + // extract import statements and module information + const imports = extractPythonImports(text); + const modules = extractPythonModules(text); + + return ` +
+

Environment Information

+ + ${data.start_time ? ` +
+
Execution Context
+
+
+ Started: + ${new Date(data.start_time).toLocaleString()} +
+ ${data.end_time ? ` +
+ Completed: + ${new Date(data.end_time).toLocaleString()} +
+ ` : ''} +
+
+ ` : ''} + + ${imports.length > 0 ? ` +
+
Detected Imports (${imports.length})
+
+ ${imports.map(imp => ` + ${imp} + `).join('')} +
+
+ ` : ''} + + ${modules.length > 0 ? ` +
+
Module Information
+
+ ${modules.map(module => ` +
+ ${module.name} + ${module.version ? `${module.version}` : ''} +
+ `).join('')} +
+
+ ` : ''} +
+ `; +} + +// extract python imports from output +function extractPythonImports(text) { + const importPatterns = [ + /^import\s+([a-zA-Z_][a-zA-Z0-9_.]*)/gm, + /^from\s+([a-zA-Z_][a-zA-Z0-9_.]*)\s+import/gm + ]; + + let imports = new Set(); + + importPatterns.forEach(pattern => { + let match; + while ((match = pattern.exec(text)) !== null) { + imports.add(match[1]); + } + }); + + return Array.from(imports).slice(0, 20); // limit to 20 imports +} + +// extract python module versions from output +function extractPythonModules(text) { + const modulePattern = /([a-zA-Z_][a-zA-Z0-9_-]+)[:\s]+(\d+\.\d+(?:\.\d+)?)/g; + let modules = []; + let match; + + while ((match = modulePattern.exec(text)) !== null) { + modules.push({ + name: match[1], + version: match[2] + }); + } + + return modules.slice(0, 10); // limit to 10 modules +} + +// switch between python output tabs +function switchPythonTab(tabId, button) { + const container = button.closest('.python-tabs'); + + const contents = container.querySelectorAll('.python-tab-content'); + contents.forEach(content => { + content.classList.remove('active'); + }); + + const buttons = container.querySelectorAll('.tab-button'); + buttons.forEach(btn => { + btn.classList.remove('active'); + }); + + const targetTab = document.getElementById(tabId); + if (targetTab) { + targetTab.classList.add('active'); + } + + button.classList.add('active'); +} + +// ansible playbook output formatting functions +function formatAnsibleOutput(executionData) { + const data = typeof executionData === 'string' ? JSON.parse(executionData) : executionData; + const tabPrefix = 'ansible-' + Date.now(); + + return ` +
+
+ + + + + + +
+ +
+ ${formatAnsibleSummary(data)} +
+ +
+ ${formatAnsibleTasks(data)} +
+ +
+ ${formatAnsibleHosts(data)} +
+ +
+ ${formatAnsibleVariables(data)} +
+ +
+ ${formatAnsibleConsole(data)} +
+ +
+
${escapeHtml(JSON.stringify(data, null, 2))}
+
+
+ `; +} + +// format ansible play summary +function formatAnsibleSummary(data) { + const text = (data.stdout || '') + (data.stderr || ''); + + // extract play and task summary information + const playResults = extractAnsiblePlayResults(text); + const hostSummary = extractAnsibleHostSummary(text); + + return ` +
+

Playbook Summary

+ +
+
+
+ Total Plays: + ${playResults.length} +
+
+ Hosts: + ${Object.keys(hostSummary).length} +
+
+ Duration: + ${data.elapsed_time ? data.elapsed_time.toFixed(2) + 's' : 'N/A'} +
+
+ Result: + ${data.return_code === 0 ? 'SUCCESS' : 'FAILED'} +
+
+
+ + ${Object.keys(hostSummary).length > 0 ? ` +
+
Host Summary
+
+ ${Object.entries(hostSummary).map(([host, stats]) => ` +
+
${host}
+
+ ${stats.ok || 0} ok + ${stats.changed || 0} changed + ${stats.failed || 0} failed + ${stats.skipped || 0} skipped +
+
+ `).join('')} +
+
+ ` : ''} + + ${playResults.length > 0 ? ` +
+
Play Results
+
+ ${playResults.map((play, index) => ` +
+
+ ${play.name || `Play ${index + 1}`} + ${play.status.toUpperCase()} +
+
+ Tasks: ${play.tasks || 0} + Hosts: ${play.hosts || 0} +
+
+ `).join('')} +
+
+ ` : ''} +
+ `; +} + +// format ansible task details +function formatAnsibleTasks(data) { + const text = (data.stdout || '') + (data.stderr || ''); + const tasks = extractAnsibleTasks(text); + + if (tasks.length === 0) { + return '
No task information available
'; + } + + return ` +
+

Task Details (${tasks.length})

+ +
+ ${tasks.map((task, index) => ` +
+
+
+
+ ${task.name || `Task ${index + 1}`} + ${task.status.toUpperCase()} +
+
+
${task.module || 'unknown'}
+ ${task.duration ? `
${task.duration}
` : ''} +
+ ${task.hosts && task.hosts.length > 0 ? ` +
+ Affected hosts: + ${task.hosts.map(host => `${host}`).join('')} +
+ ` : ''} + ${task.changes && task.changes.length > 0 ? ` +
+
Changes:
+
${escapeHtml(task.changes.join('\n'))}
+
+ ` : ''} +
+
+ `).join('')} +
+
+ `; +} + +// format ansible host results +function formatAnsibleHosts(data) { + const text = (data.stdout || '') + (data.stderr || ''); + const hostResults = extractAnsibleHostResults(text); + + if (Object.keys(hostResults).length === 0) { + return '
No host results available
'; + } + + return ` +
+

Host Results

+ +
+ ${Object.entries(hostResults).map(([host, result]) => ` +
+
+ ${host} + ${result.overall_status.toUpperCase()} +
+ +
+
+ OK: + ${result.ok || 0} +
+
+ Changed: + ${result.changed || 0} +
+
+ Failed: + ${result.failed || 0} +
+
+ Skipped: + ${result.skipped || 0} +
+
+ Unreachable: + ${result.unreachable || 0} +
+
+ + ${result.tasks && result.tasks.length > 0 ? ` +
+
Tasks:
+
+ ${result.tasks.slice(0, 5).map(task => ` +
+ + ${task.name} +
+ `).join('')} + ${result.tasks.length > 5 ? `
+${result.tasks.length - 5} more tasks
` : ''} +
+
+ ` : ''} +
+ `).join('')} +
+
+ `; +} + +// format ansible variables +function formatAnsibleVariables(data) { + const text = (data.stdout || '') + (data.stderr || ''); + const variables = extractAnsibleVariables(text); + + if (Object.keys(variables).length === 0) { + return '
No variable information available
'; + } + + return ` +
+

Variables & Facts

+ +
+ ${Object.entries(variables).map(([category, vars]) => ` +
+
${category}
+
+ ${Object.entries(vars).slice(0, 10).map(([key, value]) => ` +
+
${key}
+
+ ${typeof value === 'object' ? + `
${escapeHtml(JSON.stringify(value, null, 2))}
` : + escapeHtml(String(value)) + } +
+
+ `).join('')} + ${Object.keys(vars).length > 10 ? `
+${Object.keys(vars).length - 10} more variables
` : ''} +
+
+ `).join('')} +
+
+ `; +} + +// format ansible console output +function formatAnsibleConsole(data) { + if (!data.stdout && !data.stderr) { + return '
No console output available
'; + } + + let html = ''; + + if (data.stdout) { + const formattedStdout = formatAnsibleLogOutput(data.stdout); + html += ` +
+

Standard Output

+
${formattedStdout}
+
+ `; + } + + if (data.stderr) { + const formattedStderr = formatAnsibleLogOutput(data.stderr); + html += ` +
+

Standard Error

+
${formattedStderr}
+
+ `; + } + + return html; +} + +// format ansible log output with color coding +function formatAnsibleLogOutput(text) { + if (!text) return ''; + + let result = escapeHtml(text); + + // ansible status patterns with colors + result = result + .replace(/\b(PLAY RECAP)\b/g, '$1') + .replace(/\b(TASK|PLAY)\s*\[(.*?)\]/g, '$1 [$2]') + .replace(/\b(ok|OK)\b/g, '$1') + .replace(/\b(changed|CHANGED)\b/g, '$1') + .replace(/\b(failed|FAILED|fatal)\b/g, '$1') + .replace(/\b(skipping|skipped|SKIPPED)\b/g, '$1') + .replace(/\b(unreachable|UNREACHABLE)\b/g, '$1') + + // highlight ansible host references + .replace(/(\w+\.[\w.-]+|\d+\.\d+\.\d+\.\d+)\s*:/g, '$1:') + + // highlight timing + .replace(/(\d+\.?\d*)\s*(s|sec|seconds?)\b/gi, '$1$2') + + // highlight json/yaml content + .replace(/(\{[^{}]*\})/g, '$1') + .replace(/(---|\.\.\.|^[\s]*[-\w]+:)/gm, '$1'); + + return result; +} + +// extraction functions for ansible data +function extractAnsiblePlayResults(text) { + const playPattern = /PLAY \[(.*?)\]/g; + let plays = []; + let match; + + while ((match = playPattern.exec(text)) !== null) { + plays.push({ + name: match[1], + status: text.includes('fatal:') ? 'failed' : 'ok', + tasks: 0, + hosts: 0 + }); + } + + return plays; +} + +function extractAnsibleHostSummary(text) { + const recapPattern = /PLAY RECAP[\s\S]*?(?=\n\n|$)/; + const match = text.match(recapPattern); + + if (!match) return {}; + + const recapText = match[0]; + const hostPattern = /(\S+)\s+:\s+ok=(\d+)\s+changed=(\d+)(?:\s+unreachable=(\d+))?\s+failed=(\d+)(?:\s+skipped=(\d+))?/g; + let hosts = {}; + let hostMatch; + + while ((hostMatch = hostPattern.exec(recapText)) !== null) { + hosts[hostMatch[1]] = { + ok: parseInt(hostMatch[2]), + changed: parseInt(hostMatch[3]), + unreachable: parseInt(hostMatch[4] || 0), + failed: parseInt(hostMatch[5]), + skipped: parseInt(hostMatch[6] || 0) + }; + } + + return hosts; +} + +function extractAnsibleTasks(text) { + const taskPattern = /TASK \[(.*?)\][\s\S]*?(?=TASK \[|PLAY \[|PLAY RECAP|$)/g; + let tasks = []; + let match; + + while ((match = taskPattern.exec(text)) !== null) { + const taskText = match[0]; + const name = match[1]; + + tasks.push({ + name: name, + status: taskText.includes('failed:') ? 'failed' : + taskText.includes('changed:') ? 'changed' : + taskText.includes('skipping:') ? 'skipped' : 'ok', + module: extractModuleName(taskText), + hosts: extractTaskHosts(taskText), + changes: extractTaskChanges(taskText) + }); + } + + return tasks; +} + +function extractAnsibleHostResults(text) { + const hostSummary = extractAnsibleHostSummary(text); + const tasks = extractAnsibleTasks(text); + + let hostResults = {}; + + Object.entries(hostSummary).forEach(([host, stats]) => { + hostResults[host] = { + ...stats, + overall_status: stats.failed > 0 ? 'failed' : + stats.changed > 0 ? 'changed' : 'ok', + tasks: tasks.filter(task => task.hosts.includes(host)) + }; + }); + + return hostResults; +} + +function extractAnsibleVariables(text) { + // basic variable extraction - could be enhanced based on actual ansible output format + return { + 'Facts': extractAnsibleFacts(text), + 'Variables': extractAnsibleVars(text) + }; +} + +function extractModuleName(taskText) { + const moduleMatch = taskText.match(/(\w+):\s*\{/); + return moduleMatch ? moduleMatch[1] : 'unknown'; +} + +function extractTaskHosts(taskText) { + const hostMatches = taskText.match(/(\w+[\w.-]*)\s*:/g); + return hostMatches ? hostMatches.map(h => h.replace(':', '')) : []; +} + +function extractTaskChanges(taskText) { + const changePattern = /changed:\s*\[(.*?)\]/g; + let changes = []; + let match; + + while ((match = changePattern.exec(taskText)) !== null) { + changes.push(match[1]); + } + + return changes; +} + +function extractAnsibleFacts(text) { + // extract common ansible facts from output + const facts = {}; + const factPatterns = { + 'ansible_os_family': /ansible_os_family['"]\s*:\s*['"]([^'"]+)['"]/, + 'ansible_distribution': /ansible_distribution['"]\s*:\s*['"]([^'"]+)['"]/, + 'ansible_python_version': /ansible_python_version['"]\s*:\s*['"]([^'"]+)['"]/ + }; + + Object.entries(factPatterns).forEach(([key, pattern]) => { + const match = text.match(pattern); + if (match) facts[key] = match[1]; + }); + + return facts; +} + +function extractAnsibleVars(text) { + // extract ansible variables from output + const vars = {}; + const varPattern = /(\w+)\s*:\s*([^,}\n]+)/g; + let match; + + while ((match = varPattern.exec(text)) !== null && Object.keys(vars).length < 10) { + if (!match[1].startsWith('ansible_')) { + vars[match[1]] = match[2].trim(); + } + } + + return vars; +} + +// switch between ansible output tabs +function switchAnsibleTab(tabId, button) { + const container = button.closest('.ansible-tabs'); + + const contents = container.querySelectorAll('.ansible-tab-content'); + contents.forEach(content => { + content.classList.remove('active'); + }); + + const buttons = container.querySelectorAll('.tab-button'); + buttons.forEach(btn => { + btn.classList.remove('active'); + }); + + const targetTab = document.getElementById(tabId); + if (targetTab) { + targetTab.classList.add('active'); + } + + button.classList.add('active'); +} + // close modal when clicking outside window.onclick = function(event) { const modal = document.getElementById('execution-modal');