pywebexec 0.1.1__py3-none-any.whl → 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pywebexec/pywebexec.py +306 -107
- pywebexec/static/css/style.css +112 -0
- pywebexec/static/images/favicon.svg +1 -0
- pywebexec/static/js/script.js +197 -0
- pywebexec/templates/index.html +11 -300
- pywebexec/version.py +2 -2
- {pywebexec-0.1.1.dist-info → pywebexec-1.1.0.dist-info}/METADATA +49 -14
- pywebexec-1.1.0.dist-info/RECORD +20 -0
- pywebexec-0.1.1.dist-info/RECORD +0 -17
- {pywebexec-0.1.1.dist-info → pywebexec-1.1.0.dist-info}/LICENSE +0 -0
- {pywebexec-0.1.1.dist-info → pywebexec-1.1.0.dist-info}/WHEEL +0 -0
- {pywebexec-0.1.1.dist-info → pywebexec-1.1.0.dist-info}/entry_points.txt +0 -0
- {pywebexec-0.1.1.dist-info → pywebexec-1.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
body { font-family: Arial, sans-serif; }
|
|
2
|
+
.table-container { height: 270px; overflow-y: auto; position: relative; }
|
|
3
|
+
table { width: 100%; border-collapse: collapse; }
|
|
4
|
+
th, td {
|
|
5
|
+
padding: 8px;
|
|
6
|
+
text-align: left;
|
|
7
|
+
border-bottom: 1px solid #ddd;
|
|
8
|
+
white-space: nowrap;
|
|
9
|
+
}
|
|
10
|
+
th { background-color: #f2f2f2; position: sticky; top: 0; z-index: 1; }
|
|
11
|
+
.outcol {
|
|
12
|
+
width: 100%;
|
|
13
|
+
}
|
|
14
|
+
.output {
|
|
15
|
+
white-space: pre-wrap;
|
|
16
|
+
background: #f0f0f0;
|
|
17
|
+
padding: 10px;
|
|
18
|
+
border: 1px solid #ccc;
|
|
19
|
+
font-family: monospace;
|
|
20
|
+
border-radius: 15px;
|
|
21
|
+
overflow-y: auto;
|
|
22
|
+
}
|
|
23
|
+
.copy-icon { cursor: pointer; }
|
|
24
|
+
.monospace { font-family: monospace; }
|
|
25
|
+
.copied { color: green; margin-left: 5px; }
|
|
26
|
+
button {
|
|
27
|
+
-webkit-appearance: none;
|
|
28
|
+
-webkit-border-radius: none;
|
|
29
|
+
appearance: none;
|
|
30
|
+
border-radius: 15px;
|
|
31
|
+
padding: 3px;
|
|
32
|
+
padding-right: 13px;
|
|
33
|
+
border: 1px #555 solid;
|
|
34
|
+
height: 22px;
|
|
35
|
+
font-size: 13px;
|
|
36
|
+
outline: none;
|
|
37
|
+
text-indent: 10px;
|
|
38
|
+
background-color: #eee;
|
|
39
|
+
}
|
|
40
|
+
form {
|
|
41
|
+
padding-bottom: 15px;
|
|
42
|
+
}
|
|
43
|
+
.status-icon {
|
|
44
|
+
display: inline-block;
|
|
45
|
+
width: 16px;
|
|
46
|
+
height: 16px;
|
|
47
|
+
margin-right: 5px;
|
|
48
|
+
background-size: contain;
|
|
49
|
+
background-repeat: no-repeat;
|
|
50
|
+
vertical-align: middle;
|
|
51
|
+
}
|
|
52
|
+
.status-running {
|
|
53
|
+
background-image: url("/static/images/running.svg")
|
|
54
|
+
}
|
|
55
|
+
.status-success {
|
|
56
|
+
background-image: url("/static/images/success.svg")
|
|
57
|
+
}
|
|
58
|
+
.status-failed {
|
|
59
|
+
background-image: url("/static/images/failed.svg")
|
|
60
|
+
}
|
|
61
|
+
.status-aborted {
|
|
62
|
+
background-image: url("/static/images/aborted.svg")
|
|
63
|
+
}
|
|
64
|
+
.copy_clip {
|
|
65
|
+
padding-right: 25px;
|
|
66
|
+
background-repeat: no-repeat;
|
|
67
|
+
background-position: right top;
|
|
68
|
+
background-size: 25px 16px;
|
|
69
|
+
white-space: nowrap;
|
|
70
|
+
}
|
|
71
|
+
.copy_clip:hover {
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
background-image: url("/static/images/copy.svg");
|
|
74
|
+
}
|
|
75
|
+
.copy_clip_ok, .copy_clip_ok:hover {
|
|
76
|
+
background-image: url("/static/images/copy_ok.svg");
|
|
77
|
+
}
|
|
78
|
+
input {
|
|
79
|
+
width: 50%;
|
|
80
|
+
-webkit-appearance: none;
|
|
81
|
+
-webkit-border-radius: none;
|
|
82
|
+
appearance: none;
|
|
83
|
+
border-radius: 15px;
|
|
84
|
+
padding: 3px;
|
|
85
|
+
padding-right: 13px;
|
|
86
|
+
border: 1px #aaa solid;
|
|
87
|
+
height: 15px;
|
|
88
|
+
font-size: 15px;
|
|
89
|
+
outline: none;
|
|
90
|
+
text-indent: 10px;
|
|
91
|
+
background-color: white;
|
|
92
|
+
}
|
|
93
|
+
.currentcommand {
|
|
94
|
+
background-color: #eef;
|
|
95
|
+
}
|
|
96
|
+
.resizer {
|
|
97
|
+
width: 100%;
|
|
98
|
+
height: 5px;
|
|
99
|
+
background: #aaa;
|
|
100
|
+
cursor: ns-resize;
|
|
101
|
+
position: absolute;
|
|
102
|
+
bottom: 0;
|
|
103
|
+
left: 0;
|
|
104
|
+
}
|
|
105
|
+
.resizer-container {
|
|
106
|
+
position: relative;
|
|
107
|
+
height: 5px;
|
|
108
|
+
margin-bottom: 10px;
|
|
109
|
+
}
|
|
110
|
+
tr.clickable-row {
|
|
111
|
+
cursor: pointer;
|
|
112
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="#000000" viewBox="-1 0 19 19" xmlns="http://www.w3.org/2000/svg" class="cf-icon-svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="M16.5 9.5a8 8 0 1 1-8-8 8 8 0 0 1 8 8zm-2.97.006a5.03 5.03 0 1 0-5.03 5.03 5.03 5.03 0 0 0 5.03-5.03zm-7.383-.4H4.289a4.237 4.237 0 0 1 2.565-3.498q.1-.042.2-.079a7.702 7.702 0 0 0-.907 3.577zm0 .8a7.7 7.7 0 0 0 .908 3.577q-.102-.037-.201-.079a4.225 4.225 0 0 1-2.565-3.498zm.8-.8a9.04 9.04 0 0 1 .163-1.402 6.164 6.164 0 0 1 .445-1.415c.289-.615.66-1.013.945-1.013.285 0 .656.398.945 1.013a6.18 6.18 0 0 1 .445 1.415 9.078 9.078 0 0 1 .163 1.402zm3.106.8a9.073 9.073 0 0 1-.163 1.402 6.187 6.187 0 0 1-.445 1.415c-.289.616-.66 1.013-.945 1.013-.285 0-.656-.397-.945-1.013a6.172 6.172 0 0 1-.445-1.415 9.036 9.036 0 0 1-.163-1.402zm1.438-3.391a4.211 4.211 0 0 1 1.22 2.591h-1.858a7.698 7.698 0 0 0-.908-3.577q.102.037.201.08a4.208 4.208 0 0 1 1.345.906zm-.638 3.391h1.858a4.238 4.238 0 0 1-2.565 3.498q-.1.043-.2.08a7.697 7.697 0 0 0 .907-3.578z"></path></g></svg>
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
let currentCommandId = null;
|
|
2
|
+
let outputInterval = null;
|
|
3
|
+
|
|
4
|
+
document.getElementById('launchForm').addEventListener('submit', async (event) => {
|
|
5
|
+
event.preventDefault();
|
|
6
|
+
const commandName = document.getElementById('commandName').value;
|
|
7
|
+
const params = document.getElementById('params').value.split(' ');
|
|
8
|
+
const response = await fetch('/run_command', {
|
|
9
|
+
method: 'POST',
|
|
10
|
+
headers: {
|
|
11
|
+
'Content-Type': 'application/json'
|
|
12
|
+
},
|
|
13
|
+
body: JSON.stringify({ command: commandName, params: params })
|
|
14
|
+
});
|
|
15
|
+
const data = await response.json();
|
|
16
|
+
fetchCommands();
|
|
17
|
+
viewOutput(data.command_id);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
async function fetchCommands() {
|
|
21
|
+
const response = await fetch('/commands');
|
|
22
|
+
const commands = await response.json();
|
|
23
|
+
commands.sort((a, b) => new Date(b.start_time) - new Date(a.start_time));
|
|
24
|
+
const commandsTbody = document.getElementById('commands');
|
|
25
|
+
commandsTbody.innerHTML = '';
|
|
26
|
+
if (!currentCommandId && commands.length) {
|
|
27
|
+
currentCommandId = commands[0].command_id;
|
|
28
|
+
viewOutput(currentCommandId);
|
|
29
|
+
}
|
|
30
|
+
commands.forEach(command => {
|
|
31
|
+
const commandRow = document.createElement('tr');
|
|
32
|
+
commandRow.className = `clickable-row ${command.command_id === currentCommandId ? 'currentcommand' : ''}`;
|
|
33
|
+
commandRow.onclick = () => viewOutput(command.command_id);
|
|
34
|
+
commandRow.innerHTML = `
|
|
35
|
+
<td class="monospace">
|
|
36
|
+
<span class="copy_clip" onclick="copyToClipboard('${command.command_id}', this)">${command.command_id.slice(0, 8)}</span>
|
|
37
|
+
</td>
|
|
38
|
+
<td><span class="status-icon status-${command.status}"></span>${command.status}</td>
|
|
39
|
+
<td>${formatTime(command.start_time)}</td>
|
|
40
|
+
<td>${command.status === 'running' ? formatDuration(command.start_time, new Date().toISOString()) : formatDuration(command.start_time, command.end_time)}</td>
|
|
41
|
+
<td>${command.exit_code}</td>
|
|
42
|
+
<td>${command.command.replace(/^\.\//, '')}</td>
|
|
43
|
+
<td class="monospace outcol">${command.last_output_line || ''}</td>
|
|
44
|
+
<td>
|
|
45
|
+
<button onclick="relaunchCommand('${command.command_id}')">Relaunch</button>
|
|
46
|
+
${command.status === 'running' ? `<button onclick="stopCommand('${command.command_id}')">Stop</button>` : ''}
|
|
47
|
+
</td>
|
|
48
|
+
`;
|
|
49
|
+
commandsTbody.appendChild(commandRow);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function fetchExecutables() {
|
|
54
|
+
const response = await fetch('/executables');
|
|
55
|
+
const executables = await response.json();
|
|
56
|
+
const commandNameSelect = document.getElementById('commandName');
|
|
57
|
+
commandNameSelect.innerHTML = '';
|
|
58
|
+
executables.forEach(executable => {
|
|
59
|
+
const option = document.createElement('option');
|
|
60
|
+
option.value = executable;
|
|
61
|
+
option.textContent = executable;
|
|
62
|
+
commandNameSelect.appendChild(option);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function fetchOutput(command_id) {
|
|
67
|
+
const outputDiv = document.getElementById('output');
|
|
68
|
+
const response = await fetch(`/command_output/${command_id}`);
|
|
69
|
+
const data = await response.json();
|
|
70
|
+
if (data.error) {
|
|
71
|
+
outputDiv.innerHTML = data.error;
|
|
72
|
+
clearInterval(outputInterval);
|
|
73
|
+
} else {
|
|
74
|
+
outputDiv.innerHTML = data.output;
|
|
75
|
+
outputDiv.scrollTop = outputDiv.scrollHeight;
|
|
76
|
+
if (data.status != 'running') {
|
|
77
|
+
clearInterval(outputInterval);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function viewOutput(command_id) {
|
|
83
|
+
adjustOutputHeight();
|
|
84
|
+
currentCommandId = command_id;
|
|
85
|
+
clearInterval(outputInterval);
|
|
86
|
+
const response = await fetch(`/command_status/${command_id}`);
|
|
87
|
+
const data = await response.json();
|
|
88
|
+
if (data.status === 'running') {
|
|
89
|
+
fetchOutput(command_id);
|
|
90
|
+
outputInterval = setInterval(() => fetchOutput(command_id), 1000);
|
|
91
|
+
} else {
|
|
92
|
+
fetchOutput(command_id);
|
|
93
|
+
}
|
|
94
|
+
fetchCommands(); // Refresh the command list to highlight the current command
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function relaunchCommand(command_id) {
|
|
98
|
+
const response = await fetch(`/command_status/${command_id}`);
|
|
99
|
+
const data = await response.json();
|
|
100
|
+
if (data.error) {
|
|
101
|
+
alert(data.error);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const relaunchResponse = await fetch('/run_command', {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: {
|
|
107
|
+
'Content-Type': 'application/json'
|
|
108
|
+
},
|
|
109
|
+
body: JSON.stringify({
|
|
110
|
+
command: data.command,
|
|
111
|
+
params: data.params
|
|
112
|
+
})
|
|
113
|
+
});
|
|
114
|
+
const relaunchData = await relaunchResponse.json();
|
|
115
|
+
fetchCommands();
|
|
116
|
+
viewOutput(relaunchData.command_id);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function stopCommand(command_id) {
|
|
120
|
+
const response = await fetch(`/stop_command/${command_id}`, {
|
|
121
|
+
method: 'POST'
|
|
122
|
+
});
|
|
123
|
+
const data = await response.json();
|
|
124
|
+
if (data.error) {
|
|
125
|
+
alert(data.error);
|
|
126
|
+
} else {
|
|
127
|
+
alert(data.message);
|
|
128
|
+
fetchCommands();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function formatTime(time) {
|
|
133
|
+
if (!time || time === 'N/A') return 'N/A';
|
|
134
|
+
const date = new Date(time);
|
|
135
|
+
return date.toISOString().slice(0, 16).replace('T', ' ');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function formatDuration(startTime, endTime) {
|
|
139
|
+
if (!startTime || !endTime) return 'N/A';
|
|
140
|
+
const start = new Date(startTime);
|
|
141
|
+
const end = new Date(endTime);
|
|
142
|
+
const duration = (end - start) / 1000;
|
|
143
|
+
const hours = Math.floor(duration / 3600);
|
|
144
|
+
const minutes = Math.floor((duration % 3600) / 60);
|
|
145
|
+
const seconds = Math.floor(duration % 60);
|
|
146
|
+
return `${hours}h ${minutes}m ${seconds}s`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function copyToClipboard(text, element) {
|
|
150
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
151
|
+
element.classList.add('copy_clip_ok');
|
|
152
|
+
setTimeout(() => {
|
|
153
|
+
element.classList.remove('copy_clip_ok');
|
|
154
|
+
}, 2000);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function adjustOutputHeight() {
|
|
159
|
+
const outputDiv = document.getElementById('output');
|
|
160
|
+
const windowHeight = window.innerHeight;
|
|
161
|
+
const outputTop = outputDiv.getBoundingClientRect().top;
|
|
162
|
+
const maxHeight = windowHeight - outputTop - 30; // 20px for padding/margin
|
|
163
|
+
outputDiv.style.maxHeight = `${maxHeight}px`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function initResizer() {
|
|
167
|
+
const resizer = document.getElementById('resizer');
|
|
168
|
+
const tableContainer = document.getElementById('tableContainer');
|
|
169
|
+
let startY, startHeight;
|
|
170
|
+
|
|
171
|
+
resizer.addEventListener('mousedown', (e) => {
|
|
172
|
+
startY = e.clientY;
|
|
173
|
+
startHeight = parseInt(document.defaultView.getComputedStyle(tableContainer).height, 10);
|
|
174
|
+
document.documentElement.addEventListener('mousemove', doDrag, false);
|
|
175
|
+
document.documentElement.addEventListener('mouseup', stopDrag, false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
function doDrag(e) {
|
|
179
|
+
tableContainer.style.height = `${startHeight + e.clientY - startY}px`;
|
|
180
|
+
adjustOutputHeight();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function stopDrag() {
|
|
184
|
+
document.documentElement.removeEventListener('mousemove', doDrag, false);
|
|
185
|
+
document.documentElement.removeEventListener('mouseup', stopDrag, false);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
window.addEventListener('resize', adjustOutputHeight);
|
|
190
|
+
window.addEventListener('load', () => {
|
|
191
|
+
adjustOutputHeight();
|
|
192
|
+
initResizer();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
fetchExecutables();
|
|
196
|
+
fetchCommands();
|
|
197
|
+
setInterval(fetchCommands, 5000);
|
pywebexec/templates/index.html
CHANGED
|
@@ -2,112 +2,16 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
|
-
<
|
|
6
|
-
<
|
|
7
|
-
|
|
8
|
-
.table-container { height: 380px; overflow-y: auto; position: relative; }
|
|
9
|
-
table { width: 100%; border-collapse: collapse; }
|
|
10
|
-
th, td { padding: 8px; text-align: left; border-bottom: 1px solid #ddd; }
|
|
11
|
-
th { background-color: #f2f2f2; position: sticky; top: 0; z-index: 1; }
|
|
12
|
-
.output {
|
|
13
|
-
white-space: pre-wrap;
|
|
14
|
-
background: #f0f0f0;
|
|
15
|
-
padding: 10px;
|
|
16
|
-
border: 1px solid #ccc;
|
|
17
|
-
font-family: monospace;
|
|
18
|
-
border-radius: 15px;
|
|
19
|
-
overflow-y: auto;
|
|
20
|
-
}
|
|
21
|
-
.copy-icon { cursor: pointer; }
|
|
22
|
-
.monospace { font-family: monospace; }
|
|
23
|
-
.copied { color: green; margin-left: 5px; }
|
|
24
|
-
button {
|
|
25
|
-
-webkit-appearance: none;
|
|
26
|
-
-webkit-border-radius: none;
|
|
27
|
-
appearance: none;
|
|
28
|
-
border-radius: 15px;
|
|
29
|
-
padding: 3px;
|
|
30
|
-
padding-right: 13px;
|
|
31
|
-
border: 1px #555 solid;
|
|
32
|
-
height: 22px;
|
|
33
|
-
font-size: 13px;
|
|
34
|
-
outline: none;
|
|
35
|
-
text-indent: 10px;
|
|
36
|
-
background-color: #eee;
|
|
37
|
-
display: inline-block;
|
|
38
|
-
vertical-align: middle;
|
|
39
|
-
}
|
|
40
|
-
form {
|
|
41
|
-
padding-bottom: 15px;
|
|
42
|
-
}
|
|
43
|
-
.status-icon {
|
|
44
|
-
display: inline-block;
|
|
45
|
-
width: 16px;
|
|
46
|
-
height: 16px;
|
|
47
|
-
margin-right: 5px;
|
|
48
|
-
background-size: contain;
|
|
49
|
-
background-repeat: no-repeat;
|
|
50
|
-
vertical-align: middle;
|
|
51
|
-
}
|
|
52
|
-
.status-running {
|
|
53
|
-
background-image: url("/static/images/running.svg")
|
|
54
|
-
}
|
|
55
|
-
.status-success {
|
|
56
|
-
background-image: url("/static/images/success.svg")
|
|
57
|
-
}
|
|
58
|
-
.status-failed {
|
|
59
|
-
background-image: url("/static/images/failed.svg")
|
|
60
|
-
}
|
|
61
|
-
.status-aborted {
|
|
62
|
-
background-image: url("/static/images/aborted.svg")
|
|
63
|
-
}
|
|
64
|
-
.copy_clip {
|
|
65
|
-
padding-right: 25px;
|
|
66
|
-
background-repeat: no-repeat;
|
|
67
|
-
background-position: right top;
|
|
68
|
-
background-size: 25px 16px;
|
|
69
|
-
white-space: nowrap;
|
|
70
|
-
}
|
|
71
|
-
.copy_clip_left {
|
|
72
|
-
padding-left: 25px;
|
|
73
|
-
padding-right: 0px;
|
|
74
|
-
background-position: left top;
|
|
75
|
-
}
|
|
76
|
-
.copy_clip:hover {
|
|
77
|
-
cursor: pointer;
|
|
78
|
-
background-image: url("/static/images/copy.svg");
|
|
79
|
-
}
|
|
80
|
-
.copy_clip_ok, .copy_clip_ok:hover {
|
|
81
|
-
background-image: url("/static/images/copy_ok.svg");
|
|
82
|
-
}
|
|
83
|
-
input {
|
|
84
|
-
width: 50%
|
|
85
|
-
}
|
|
86
|
-
.currentscript {
|
|
87
|
-
background-color: #eef;
|
|
88
|
-
}
|
|
89
|
-
.resizer {
|
|
90
|
-
width: 100%;
|
|
91
|
-
height: 5px;
|
|
92
|
-
background: #aaa;
|
|
93
|
-
cursor: ns-resize;
|
|
94
|
-
position: absolute;
|
|
95
|
-
bottom: 0;
|
|
96
|
-
left: 0;
|
|
97
|
-
}
|
|
98
|
-
.resizer-container {
|
|
99
|
-
position: relative;
|
|
100
|
-
height: 5px;
|
|
101
|
-
margin-bottom: 10px;
|
|
102
|
-
}
|
|
103
|
-
</style>
|
|
5
|
+
<link rel="icon" href="/static/images/favicon.svg" type="image/svg+xml">
|
|
6
|
+
<title>{{ title }}</title>
|
|
7
|
+
<link rel="stylesheet" href="/static/css/style.css">
|
|
104
8
|
</head>
|
|
105
9
|
<body>
|
|
106
|
-
<
|
|
10
|
+
<h2>{{ title }}</h2>
|
|
107
11
|
<form id="launchForm">
|
|
108
|
-
<label for="
|
|
109
|
-
<select id="
|
|
110
|
-
<label for="params">Params
|
|
12
|
+
<label for="commandName">Command</label>
|
|
13
|
+
<select id="commandName" name="commandName"></select>
|
|
14
|
+
<label for="params">Params</label>
|
|
111
15
|
<input type="text" id="params" name="params">
|
|
112
16
|
<button type="submit">Launch</button>
|
|
113
17
|
</form>
|
|
@@ -115,216 +19,23 @@
|
|
|
115
19
|
<table>
|
|
116
20
|
<thead>
|
|
117
21
|
<tr>
|
|
118
|
-
<th>
|
|
22
|
+
<th>ID</th>
|
|
119
23
|
<th>Status</th>
|
|
120
24
|
<th>Start Time</th>
|
|
121
25
|
<th>Duration</th>
|
|
122
26
|
<th>Exit</th>
|
|
123
27
|
<th>Command</th>
|
|
28
|
+
<th>Output</th>
|
|
124
29
|
<th>Actions</th>
|
|
125
30
|
</tr>
|
|
126
31
|
</thead>
|
|
127
|
-
<tbody id="
|
|
32
|
+
<tbody id="commands"></tbody>
|
|
128
33
|
</table>
|
|
129
34
|
</div>
|
|
130
35
|
<div class="resizer-container">
|
|
131
36
|
<div class="resizer" id="resizer"></div>
|
|
132
37
|
</div>
|
|
133
38
|
<div id="output" class="output"></div>
|
|
134
|
-
|
|
135
|
-
<script>
|
|
136
|
-
let currentScriptId = null;
|
|
137
|
-
let outputInterval = null;
|
|
138
|
-
|
|
139
|
-
document.getElementById('launchForm').addEventListener('submit', async (event) => {
|
|
140
|
-
event.preventDefault();
|
|
141
|
-
const scriptName = document.getElementById('scriptName').value;
|
|
142
|
-
const params = document.getElementById('params').value.split(' ');
|
|
143
|
-
const response = await fetch('/run_script', {
|
|
144
|
-
method: 'POST',
|
|
145
|
-
headers: {
|
|
146
|
-
'Content-Type': 'application/json'
|
|
147
|
-
},
|
|
148
|
-
body: JSON.stringify({ script_name: scriptName, params: params })
|
|
149
|
-
});
|
|
150
|
-
const data = await response.json();
|
|
151
|
-
fetchScripts();
|
|
152
|
-
viewOutput(data.script_id);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
async function fetchScripts() {
|
|
156
|
-
const response = await fetch('/scripts');
|
|
157
|
-
const scripts = await response.json();
|
|
158
|
-
scripts.sort((a, b) => new Date(b.start_time) - new Date(a.start_time));
|
|
159
|
-
const scriptsTbody = document.getElementById('scripts');
|
|
160
|
-
scriptsTbody.innerHTML = '';
|
|
161
|
-
scripts.forEach(script => {
|
|
162
|
-
const scriptRow = document.createElement('tr');
|
|
163
|
-
scriptRow.className = script.script_id === currentScriptId ? 'currentscript' : '';
|
|
164
|
-
scriptRow.innerHTML = `
|
|
165
|
-
<td class="monospace">
|
|
166
|
-
<span class="copy_clip" onclick="copyToClipboard('${script.script_id}', this)">${script.script_id.slice(0, 8)}</span>
|
|
167
|
-
</td>
|
|
168
|
-
<td><span class="status-icon status-${script.status}"></span>${script.status}</td>
|
|
169
|
-
<td>${formatTime(script.start_time)}</td>
|
|
170
|
-
<td>${script.status === 'running' ? formatDuration(script.start_time, new Date().toISOString()) : formatDuration(script.start_time, script.end_time)}</td>
|
|
171
|
-
<td>${script.exit_code}</td>
|
|
172
|
-
<td>${script.command.replace(/^\.\//, '')}</td>
|
|
173
|
-
<td>
|
|
174
|
-
<button onclick="viewOutput('${script.script_id}')">Log</button>
|
|
175
|
-
<button onclick="relaunchScript('${script.script_id}')">Relaunch</button>
|
|
176
|
-
${script.status === 'running' ? `<button onclick="stopScript('${script.script_id}')">Stop</button>` : ''}
|
|
177
|
-
</td>
|
|
178
|
-
`;
|
|
179
|
-
scriptsTbody.appendChild(scriptRow);
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async function fetchExecutables() {
|
|
184
|
-
const response = await fetch('/executables');
|
|
185
|
-
const executables = await response.json();
|
|
186
|
-
const scriptNameSelect = document.getElementById('scriptName');
|
|
187
|
-
scriptNameSelect.innerHTML = '';
|
|
188
|
-
executables.forEach(executable => {
|
|
189
|
-
const option = document.createElement('option');
|
|
190
|
-
option.value = executable;
|
|
191
|
-
option.textContent = executable;
|
|
192
|
-
scriptNameSelect.appendChild(option);
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async function fetchOutput(script_id) {
|
|
197
|
-
const outputDiv = document.getElementById('output');
|
|
198
|
-
const response = await fetch(`/script_output/${script_id}`);
|
|
199
|
-
const data = await response.json();
|
|
200
|
-
if (data.error) {
|
|
201
|
-
outputDiv.innerHTML = data.error;
|
|
202
|
-
clearInterval(outputInterval);
|
|
203
|
-
} else {
|
|
204
|
-
outputDiv.innerHTML = data.output;
|
|
205
|
-
outputDiv.scrollTop = outputDiv.scrollHeight;
|
|
206
|
-
if (data.status != 'running') {
|
|
207
|
-
clearInterval(outputInterval);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async function viewOutput(script_id) {
|
|
213
|
-
adjustOutputHeight();
|
|
214
|
-
currentScriptId = script_id;
|
|
215
|
-
clearInterval(outputInterval);
|
|
216
|
-
const response = await fetch(`/script_status/${script_id}`);
|
|
217
|
-
const data = await response.json();
|
|
218
|
-
if (data.status === 'running') {
|
|
219
|
-
fetchOutput(script_id);
|
|
220
|
-
outputInterval = setInterval(() => fetchOutput(script_id), 1000);
|
|
221
|
-
} else {
|
|
222
|
-
fetchOutput(script_id);
|
|
223
|
-
}
|
|
224
|
-
fetchScripts(); // Refresh the script list to highlight the current script
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async function relaunchScript(script_id) {
|
|
228
|
-
const response = await fetch(`/script_status/${script_id}`);
|
|
229
|
-
const data = await response.json();
|
|
230
|
-
if (data.error) {
|
|
231
|
-
alert(data.error);
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
const relaunchResponse = await fetch('/run_script', {
|
|
235
|
-
method: 'POST',
|
|
236
|
-
headers: {
|
|
237
|
-
'Content-Type': 'application/json'
|
|
238
|
-
},
|
|
239
|
-
body: JSON.stringify({
|
|
240
|
-
script_name: data.script_name,
|
|
241
|
-
params: data.params
|
|
242
|
-
})
|
|
243
|
-
});
|
|
244
|
-
const relaunchData = await relaunchResponse.json();
|
|
245
|
-
fetchScripts();
|
|
246
|
-
viewOutput(relaunchData.script_id);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
async function stopScript(script_id) {
|
|
250
|
-
const response = await fetch(`/stop_script/${script_id}`, {
|
|
251
|
-
method: 'POST'
|
|
252
|
-
});
|
|
253
|
-
const data = await response.json();
|
|
254
|
-
if (data.error) {
|
|
255
|
-
alert(data.error);
|
|
256
|
-
} else {
|
|
257
|
-
alert(data.message);
|
|
258
|
-
fetchScripts();
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function formatTime(time) {
|
|
263
|
-
if (!time || time === 'N/A') return 'N/A';
|
|
264
|
-
const date = new Date(time);
|
|
265
|
-
return date.toISOString().slice(0, 16).replace('T', ' ');
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function formatDuration(startTime, endTime) {
|
|
269
|
-
if (!startTime || !endTime) return 'N/A';
|
|
270
|
-
const start = new Date(startTime);
|
|
271
|
-
const end = new Date(endTime);
|
|
272
|
-
const duration = (end - start) / 1000;
|
|
273
|
-
const hours = Math.floor(duration / 3600);
|
|
274
|
-
const minutes = Math.floor((duration % 3600) / 60);
|
|
275
|
-
const seconds = Math.floor(duration % 60);
|
|
276
|
-
return `${hours}h ${minutes}m ${seconds}s`;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function copyToClipboard(text, element) {
|
|
280
|
-
navigator.clipboard.writeText(text).then(() => {
|
|
281
|
-
element.classList.add('copy_clip_ok');
|
|
282
|
-
setTimeout(() => {
|
|
283
|
-
element.classList.remove('copy_clip_ok');
|
|
284
|
-
}, 2000);
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function adjustOutputHeight() {
|
|
289
|
-
const outputDiv = document.getElementById('output');
|
|
290
|
-
const windowHeight = window.innerHeight;
|
|
291
|
-
const outputTop = outputDiv.getBoundingClientRect().top;
|
|
292
|
-
const maxHeight = windowHeight - outputTop - 30; // 20px for padding/margin
|
|
293
|
-
outputDiv.style.maxHeight = `${maxHeight}px`;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function initResizer() {
|
|
297
|
-
const resizer = document.getElementById('resizer');
|
|
298
|
-
const tableContainer = document.getElementById('tableContainer');
|
|
299
|
-
let startY, startHeight;
|
|
300
|
-
|
|
301
|
-
resizer.addEventListener('mousedown', (e) => {
|
|
302
|
-
startY = e.clientY;
|
|
303
|
-
startHeight = parseInt(document.defaultView.getComputedStyle(tableContainer).height, 10);
|
|
304
|
-
document.documentElement.addEventListener('mousemove', doDrag, false);
|
|
305
|
-
document.documentElement.addEventListener('mouseup', stopDrag, false);
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
function doDrag(e) {
|
|
309
|
-
tableContainer.style.height = `${startHeight + e.clientY - startY}px`;
|
|
310
|
-
adjustOutputHeight();
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function stopDrag() {
|
|
314
|
-
document.documentElement.removeEventListener('mousemove', doDrag, false);
|
|
315
|
-
document.documentElement.removeEventListener('mouseup', stopDrag, false);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
window.addEventListener('resize', adjustOutputHeight);
|
|
320
|
-
window.addEventListener('load', () => {
|
|
321
|
-
adjustOutputHeight();
|
|
322
|
-
initResizer();
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
fetchScripts();
|
|
326
|
-
fetchExecutables();
|
|
327
|
-
setInterval(fetchScripts, 5000);
|
|
328
|
-
</script>
|
|
39
|
+
<script type="text/javascript" src="/static/js/script.js"></script>
|
|
329
40
|
</body>
|
|
330
41
|
</html>
|
pywebexec/version.py
CHANGED