Akoda35 commited on
Commit
16a0ca6
·
verified ·
1 Parent(s): b7c25e6

Upload 8 files

Browse files
templates/cpanel/scripts/_form.html ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- Script Form Partial -->
2
+ <div class="space-y-6">
3
+ <!-- Name -->
4
+ <div>
5
+ <label for="id_name" class="block text-sm font-medium text-code-text mb-2">
6
+ {{ form.name.label }}
7
+ </label>
8
+ {{ form.name }}
9
+ {% if form.name.errors %}
10
+ <p class="mt-1 text-sm text-code-red">{{ form.name.errors.0 }}</p>
11
+ {% endif %}
12
+ </div>
13
+
14
+ <!-- Description -->
15
+ <div>
16
+ <label for="id_description" class="block text-sm font-medium text-code-text mb-2">
17
+ {{ form.description.label }}
18
+ </label>
19
+ {{ form.description }}
20
+ {% if form.description.errors %}
21
+ <p class="mt-1 text-sm text-code-red">{{ form.description.errors.0 }}</p>
22
+ {% endif %}
23
+ </div>
24
+
25
+ <!-- Code -->
26
+ <div>
27
+ <label for="id_code" class="block text-sm font-medium text-code-text mb-2">
28
+ {{ form.code.label }}
29
+ </label>
30
+ <!-- Hidden textarea for Django form submission -->
31
+ {{ form.code }}
32
+ <!-- Ace Editor container -->
33
+ <div id="code-editor-container" style="height: 400px;" class="rounded-lg border border-code-border overflow-hidden"></div>
34
+ {% if form.code.errors %}
35
+ <p class="mt-1 text-sm text-code-red">{{ form.code.errors.0 }}</p>
36
+ {% endif %}
37
+ </div>
38
+
39
+ <!-- Environment and Timeout Row -->
40
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
41
+ <!-- Environment -->
42
+ <div>
43
+ <label for="id_environment" class="block text-sm font-medium text-code-text mb-2">
44
+ {{ form.environment.label }}
45
+ </label>
46
+ {{ form.environment }}
47
+ {% if form.environment.errors %}
48
+ <p class="mt-1 text-sm text-code-red">{{ form.environment.errors.0 }}</p>
49
+ {% endif %}
50
+ </div>
51
+
52
+ <!-- Timeout -->
53
+ <div>
54
+ <label for="id_timeout_seconds" class="block text-sm font-medium text-code-text mb-2">
55
+ {{ form.timeout_seconds.label }}
56
+ </label>
57
+ {{ form.timeout_seconds }}
58
+ {% if form.timeout_seconds.help_text %}
59
+ <p class="mt-1 text-xs text-code-muted">{{ form.timeout_seconds.help_text }}</p>
60
+ {% endif %}
61
+ {% if form.timeout_seconds.errors %}
62
+ <p class="mt-1 text-sm text-code-red">{{ form.timeout_seconds.errors.0 }}</p>
63
+ {% endif %}
64
+ </div>
65
+ </div>
66
+
67
+ <!-- Tags -->
68
+ <div>
69
+ <label class="block text-sm font-medium text-code-text mb-2">
70
+ {{ form.tags.label }}
71
+ </label>
72
+ <div class="flex flex-wrap gap-2 p-4 bg-code-bg border border-code-border rounded-lg min-h-[3rem]">
73
+ {% for checkbox in form.tags %}
74
+ <label class="inline-flex items-center px-3 py-1.5 rounded-full cursor-pointer border transition-colors
75
+ {% if checkbox.data.selected %}border-code-accent bg-code-accent/20{% else %}border-code-border bg-code-surface hover:border-code-accent/50{% endif %}">
76
+ <input type="checkbox" name="tags" value="{{ checkbox.data.value }}" {% if checkbox.data.selected %}checked{% endif %} class="sr-only peer">
77
+ <span class="w-2 h-2 rounded-full mr-2 tag-dot-{{ checkbox.data.attrs.data_color|default:'gray' }}"></span>
78
+ <span class="text-sm text-code-text">{{ checkbox.choice_label }}</span>
79
+ </label>
80
+ {% empty %}
81
+ <p class="text-sm text-code-muted">No tags available. <a href="{% url 'cpanel:tag_list' %}" class="text-code-accent hover:underline">Create tags</a></p>
82
+ {% endfor %}
83
+ </div>
84
+ {% if form.tags.errors %}
85
+ <p class="mt-1 text-sm text-code-red">{{ form.tags.errors.0 }}</p>
86
+ {% endif %}
87
+ </div>
88
+
89
+ <!-- Enabled -->
90
+ <div class="flex items-center space-x-3">
91
+ {{ form.is_enabled }}
92
+ <label for="id_is_enabled" class="text-sm font-medium text-code-text">
93
+ {{ form.is_enabled.label }}
94
+ </label>
95
+ {% if form.is_enabled.errors %}
96
+ <p class="text-sm text-code-red">{{ form.is_enabled.errors.0 }}</p>
97
+ {% endif %}
98
+ </div>
99
+
100
+ {% if schedule_form %}
101
+ <!-- Schedule Configuration Section -->
102
+ <div class="mt-8 pt-6 border-t border-code-border">
103
+ <h3 class="text-lg font-semibold text-code-text mb-4">Schedule Configuration</h3>
104
+
105
+ <!-- Run Mode Selection -->
106
+ <div class="mb-6">
107
+ <label class="block text-sm font-medium text-code-text mb-3">Run Mode</label>
108
+ <div class="grid grid-cols-3 gap-4">
109
+ {% for radio in schedule_form.run_mode %}
110
+ <label class="relative flex items-center justify-center p-4 border border-code-border rounded-lg cursor-pointer hover:border-code-accent transition-colors {% if radio.data.selected %}border-code-accent bg-code-accent/10{% endif %}">
111
+ <input type="radio" name="run_mode" value="{{ radio.data.value }}" {% if radio.data.selected %}checked{% endif %} class="sr-only peer" onchange="toggleScheduleOptions(this.value)">
112
+ <span class="text-code-text font-medium">{{ radio.choice_label }}</span>
113
+ </label>
114
+ {% endfor %}
115
+ </div>
116
+ {% if schedule_form.run_mode.errors %}
117
+ <p class="mt-1 text-sm text-code-red">{{ schedule_form.run_mode.errors.0 }}</p>
118
+ {% endif %}
119
+ </div>
120
+
121
+ <!-- Interval Options (shown when interval mode selected) -->
122
+ <div id="interval-options" class="mb-6 {% if schedule_form.instance.run_mode != 'interval' %}hidden{% endif %}">
123
+ <label for="id_interval_minutes" class="block text-sm font-medium text-code-text mb-2">
124
+ {{ schedule_form.interval_minutes.label }}
125
+ </label>
126
+ {{ schedule_form.interval_minutes }}
127
+ {% if schedule_form.interval_minutes.errors %}
128
+ <p class="mt-1 text-sm text-code-red">{{ schedule_form.interval_minutes.errors.0 }}</p>
129
+ {% endif %}
130
+ </div>
131
+
132
+ <!-- Daily Options (shown when daily mode selected) -->
133
+ <div id="daily-options" class="mb-6 {% if schedule_form.instance.run_mode != 'daily' %}hidden{% endif %}">
134
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
135
+ <div>
136
+ <label for="id_daily_times_input" class="block text-sm font-medium text-code-text mb-2">
137
+ {{ schedule_form.daily_times_input.label }}
138
+ </label>
139
+ {{ schedule_form.daily_times_input }}
140
+ <p class="mt-1 text-xs text-code-muted">{{ schedule_form.daily_times_input.help_text }}</p>
141
+ {% if schedule_form.daily_times_input.errors %}
142
+ <p class="mt-1 text-sm text-code-red">{{ schedule_form.daily_times_input.errors.0 }}</p>
143
+ {% endif %}
144
+ </div>
145
+ <div>
146
+ <label for="id_timezone" class="block text-sm font-medium text-code-text mb-2">
147
+ Timezone
148
+ </label>
149
+ {{ schedule_form.timezone }}
150
+ {% if schedule_form.timezone.errors %}
151
+ <p class="mt-1 text-sm text-code-red">{{ schedule_form.timezone.errors.0 }}</p>
152
+ {% endif %}
153
+ </div>
154
+ </div>
155
+ </div>
156
+
157
+ <!-- Schedule Active Toggle -->
158
+ <div class="flex items-center space-x-3">
159
+ {{ schedule_form.is_active }}
160
+ <label for="id_is_active" class="text-sm font-medium text-code-text">
161
+ {{ schedule_form.is_active.label }}
162
+ </label>
163
+ {% if schedule_form.is_active.errors %}
164
+ <p class="text-sm text-code-red">{{ schedule_form.is_active.errors.0 }}</p>
165
+ {% endif %}
166
+ </div>
167
+ </div>
168
+
169
+ <script>
170
+ function toggleScheduleOptions(mode) {
171
+ document.getElementById('interval-options').classList.toggle('hidden', mode !== 'interval');
172
+ document.getElementById('daily-options').classList.toggle('hidden', mode !== 'daily');
173
+ }
174
+ </script>
175
+ {% endif %}
176
+
177
+ <!-- Notification Configuration Section -->
178
+ <div class="mt-8 pt-6 border-t border-code-border">
179
+ <h3 class="text-lg font-semibold text-code-text mb-4">Notification Settings</h3>
180
+
181
+ <div class="space-y-6">
182
+ <!-- Notify On -->
183
+ <div>
184
+ <label for="id_notify_on" class="block text-sm font-medium text-code-text mb-2">
185
+ {{ form.notify_on.label }}
186
+ </label>
187
+ {{ form.notify_on }}
188
+ {% if form.notify_on.errors %}
189
+ <p class="mt-1 text-sm text-code-red">{{ form.notify_on.errors.0 }}</p>
190
+ {% endif %}
191
+ </div>
192
+
193
+ <!-- Override Email -->
194
+ <div>
195
+ <label for="id_notify_email" class="block text-sm font-medium text-code-text mb-2">
196
+ {{ form.notify_email.label }}
197
+ </label>
198
+ {{ form.notify_email }}
199
+ {% if form.notify_email.help_text %}
200
+ <p class="mt-1 text-xs text-code-muted">{{ form.notify_email.help_text }}</p>
201
+ {% endif %}
202
+ {% if form.notify_email.errors %}
203
+ <p class="mt-1 text-sm text-code-red">{{ form.notify_email.errors.0 }}</p>
204
+ {% endif %}
205
+ </div>
206
+
207
+ <!-- Webhook Configuration -->
208
+ <div class="p-4 bg-code-bg rounded-lg space-y-4">
209
+ <div class="flex items-center space-x-3">
210
+ {{ form.notify_webhook_enabled }}
211
+ <label for="id_notify_webhook_enabled" class="text-sm font-medium text-code-text">
212
+ {{ form.notify_webhook_enabled.label }}
213
+ </label>
214
+ </div>
215
+
216
+ <div>
217
+ <label for="id_notify_webhook_url" class="block text-sm font-medium text-code-text mb-2">
218
+ {{ form.notify_webhook_url.label }}
219
+ </label>
220
+ {{ form.notify_webhook_url }}
221
+ {% if form.notify_webhook_url.help_text %}
222
+ <p class="mt-1 text-xs text-code-muted">{{ form.notify_webhook_url.help_text }}</p>
223
+ {% endif %}
224
+ {% if form.notify_webhook_url.errors %}
225
+ <p class="mt-1 text-sm text-code-red">{{ form.notify_webhook_url.errors.0 }}</p>
226
+ {% endif %}
227
+ </div>
228
+ </div>
229
+ </div>
230
+ </div>
231
+ </div>
templates/cpanel/scripts/_form_main.html ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- Main Form Content: Name, Description, Code Editor -->
2
+ <div class="bg-code-surface border border-code-border rounded-xl p-6">
3
+ <!-- Name -->
4
+ <div class="mb-6">
5
+ <label for="id_name" class="block text-sm font-medium text-code-text mb-2">
6
+ {{ form.name.label }}
7
+ </label>
8
+ {{ form.name }}
9
+ {% if form.name.errors %}
10
+ <p class="mt-1 text-sm text-code-red">{{ form.name.errors.0 }}</p>
11
+ {% endif %}
12
+ </div>
13
+
14
+ <!-- Description -->
15
+ <div class="mb-6">
16
+ <label for="id_description" class="block text-sm font-medium text-code-text mb-2">
17
+ {{ form.description.label }}
18
+ </label>
19
+ {{ form.description }}
20
+ {% if form.description.errors %}
21
+ <p class="mt-1 text-sm text-code-red">{{ form.description.errors.0 }}</p>
22
+ {% endif %}
23
+ </div>
24
+
25
+ <!-- Code Editor -->
26
+ <div>
27
+ <label for="id_code" class="block text-sm font-medium text-code-text mb-2">
28
+ {{ form.code.label }}
29
+ </label>
30
+ <!-- Hidden textarea for Django form submission -->
31
+ {{ form.code }}
32
+ <!-- Monaco Editor container -->
33
+ <div id="code-editor-container"
34
+ style="height: calc(100vh - 26rem); min-height: 400px; max-height: 700px;"
35
+ class="rounded-lg border border-code-border overflow-hidden"></div>
36
+ {% if form.code.errors %}
37
+ <p class="mt-1 text-sm text-code-red">{{ form.code.errors.0 }}</p>
38
+ {% endif %}
39
+ </div>
40
+ </div>
templates/cpanel/scripts/_form_sidebar.html ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- Sidebar: Properties and Settings -->
2
+ <div class="lg:sticky lg:top-24 space-y-4 lg:max-h-[calc(100vh-8rem)] lg:overflow-y-auto scrollbar-thin lg:pr-1">
3
+
4
+ <!-- Execution Settings (Always Visible) -->
5
+ <div class="bg-code-surface border border-code-border rounded-xl p-5">
6
+ <h3 class="text-sm font-semibold text-code-text mb-4 flex items-center">
7
+ <svg class="w-4 h-4 mr-2 text-code-muted" fill="none" stroke="currentColor" viewBox="0 0 24 24">
8
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
9
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
10
+ </svg>
11
+ Execution Settings
12
+ </h3>
13
+
14
+ <!-- Environment -->
15
+ <div class="mb-4">
16
+ <label for="id_environment" class="block text-sm font-medium text-code-text mb-2">
17
+ {{ form.environment.label }}
18
+ </label>
19
+ {{ form.environment }}
20
+ {% if form.environment.errors %}
21
+ <p class="mt-1 text-sm text-code-red">{{ form.environment.errors.0 }}</p>
22
+ {% endif %}
23
+ </div>
24
+
25
+ <!-- Timeout -->
26
+ <div class="mb-4">
27
+ <label for="id_timeout_seconds" class="block text-sm font-medium text-code-text mb-2">
28
+ {{ form.timeout_seconds.label }}
29
+ </label>
30
+ {{ form.timeout_seconds }}
31
+ {% if form.timeout_seconds.help_text %}
32
+ <p class="mt-1 text-xs text-code-muted">{{ form.timeout_seconds.help_text }}</p>
33
+ {% endif %}
34
+ {% if form.timeout_seconds.errors %}
35
+ <p class="mt-1 text-sm text-code-red">{{ form.timeout_seconds.errors.0 }}</p>
36
+ {% endif %}
37
+ </div>
38
+
39
+ <!-- Enabled Toggle -->
40
+ <div class="flex items-center space-x-3 pt-3 border-t border-code-border">
41
+ {{ form.is_enabled }}
42
+ <label for="id_is_enabled" class="text-sm font-medium text-code-text">
43
+ {{ form.is_enabled.label }}
44
+ </label>
45
+ {% if form.is_enabled.errors %}
46
+ <p class="text-sm text-code-red">{{ form.is_enabled.errors.0 }}</p>
47
+ {% endif %}
48
+ </div>
49
+ </div>
50
+
51
+ <!-- Tags (Always Visible) -->
52
+ <div class="bg-code-surface border border-code-border rounded-xl p-5">
53
+ <h3 class="text-sm font-semibold text-code-text mb-4 flex items-center">
54
+ <svg class="w-4 h-4 mr-2 text-code-muted" fill="none" stroke="currentColor" viewBox="0 0 24 24">
55
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>
56
+ </svg>
57
+ {{ form.tags.label }}
58
+ </h3>
59
+ <div class="flex flex-wrap gap-2 min-h-[2.5rem]">
60
+ {% for tag in available_tags %}
61
+ <label class="tag-label inline-flex items-center px-3 py-1.5 rounded-full cursor-pointer border transition-colors
62
+ {% if tag.pk in selected_tag_ids %}border-code-accent bg-code-accent/20{% else %}border-code-border bg-code-bg hover:border-code-accent/50{% endif %}">
63
+ <input type="checkbox" name="tags" value="{{ tag.pk }}" {% if tag.pk in selected_tag_ids %}checked{% endif %} class="sr-only">
64
+ <span class="w-2 h-2 rounded-full mr-2 tag-dot-{{ tag.color }}"></span>
65
+ <span class="text-sm text-code-text">{{ tag.name }}</span>
66
+ </label>
67
+ {% empty %}
68
+ <p class="text-sm text-code-muted">No tags available. <a href="{% url 'cpanel:tag_list' %}" class="text-code-accent hover:underline">Create tags</a></p>
69
+ {% endfor %}
70
+ </div>
71
+ {% if form.tags.errors %}
72
+ <p class="mt-1 text-sm text-code-red">{{ form.tags.errors.0 }}</p>
73
+ {% endif %}
74
+ </div>
75
+
76
+ {% if schedule_form %}
77
+ <!-- Schedule Configuration (Collapsible) -->
78
+ <div class="bg-code-surface border border-code-border rounded-xl overflow-hidden">
79
+ <button type="button" onclick="toggleSection('schedule')"
80
+ class="w-full px-5 py-4 flex items-center justify-between text-left hover:bg-code-bg/50 transition-colors">
81
+ <h3 class="text-sm font-semibold text-code-text flex items-center">
82
+ <svg class="w-4 h-4 mr-2 text-code-muted" fill="none" stroke="currentColor" viewBox="0 0 24 24">
83
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
84
+ </svg>
85
+ Schedule
86
+ </h3>
87
+ <svg id="schedule-chevron" class="w-4 h-4 text-code-muted transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
88
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
89
+ </svg>
90
+ </button>
91
+ <div id="schedule-content" class="px-5 pb-5 space-y-4">
92
+ <!-- Run Mode Selection -->
93
+ <div>
94
+ <label class="block text-sm font-medium text-code-text mb-3">Run Mode</label>
95
+ <div class="space-y-2">
96
+ {% for radio in schedule_form.run_mode %}
97
+ <label class="flex items-center p-3 border border-code-border rounded-lg cursor-pointer hover:border-code-accent/50 transition-colors {% if radio.data.selected %}border-code-accent bg-code-accent/10{% endif %}">
98
+ <input type="radio" name="run_mode" value="{{ radio.data.value }}" {% if radio.data.selected %}checked{% endif %} class="sr-only peer" onchange="toggleScheduleOptions(this.value)">
99
+ <span class="w-4 h-4 rounded-full border-2 mr-3 flex items-center justify-center {% if radio.data.selected %}border-code-accent{% else %}border-code-border{% endif %}">
100
+ {% if radio.data.selected %}
101
+ <span class="w-2 h-2 rounded-full bg-code-accent"></span>
102
+ {% endif %}
103
+ </span>
104
+ <span class="text-sm text-code-text">{{ radio.choice_label }}</span>
105
+ </label>
106
+ {% endfor %}
107
+ </div>
108
+ {% if schedule_form.run_mode.errors %}
109
+ <p class="mt-1 text-sm text-code-red">{{ schedule_form.run_mode.errors.0 }}</p>
110
+ {% endif %}
111
+ </div>
112
+
113
+ <!-- Interval Options -->
114
+ <div id="interval-options" class="{% if schedule_form.instance.run_mode != 'interval' %}hidden{% endif %}">
115
+ <label for="id_interval_minutes" class="block text-sm font-medium text-code-text mb-2">
116
+ {{ schedule_form.interval_minutes.label }}
117
+ </label>
118
+ {{ schedule_form.interval_minutes }}
119
+ {% if schedule_form.interval_minutes.errors %}
120
+ <p class="mt-1 text-sm text-code-red">{{ schedule_form.interval_minutes.errors.0 }}</p>
121
+ {% endif %}
122
+ </div>
123
+
124
+ <!-- Daily Options -->
125
+ <div id="daily-options" class="space-y-4 {% if schedule_form.instance.run_mode != 'daily' %}hidden{% endif %}">
126
+ <div>
127
+ <label for="id_daily_times_input" class="block text-sm font-medium text-code-text mb-2">
128
+ {{ schedule_form.daily_times_input.label }}
129
+ </label>
130
+ {{ schedule_form.daily_times_input }}
131
+ <p class="mt-1 text-xs text-code-muted">{{ schedule_form.daily_times_input.help_text }}</p>
132
+ {% if schedule_form.daily_times_input.errors %}
133
+ <p class="mt-1 text-sm text-code-red">{{ schedule_form.daily_times_input.errors.0 }}</p>
134
+ {% endif %}
135
+ </div>
136
+ <div>
137
+ <label for="id_timezone" class="block text-sm font-medium text-code-text mb-2">
138
+ Timezone
139
+ </label>
140
+ {{ schedule_form.timezone }}
141
+ {% if schedule_form.timezone.errors %}
142
+ <p class="mt-1 text-sm text-code-red">{{ schedule_form.timezone.errors.0 }}</p>
143
+ {% endif %}
144
+ </div>
145
+ </div>
146
+
147
+ <!-- Weekly Options -->
148
+ <div id="weekly-options" class="space-y-4 {% if schedule_form.instance.run_mode != 'weekly' %}hidden{% endif %}">
149
+ <div>
150
+ <label class="block text-sm font-medium text-code-text mb-2">Days of Week</label>
151
+ <div class="grid grid-cols-2 gap-2">
152
+ {% for checkbox in schedule_form.weekly_days_input %}
153
+ <label class="flex items-center p-2 border border-code-border rounded-lg cursor-pointer hover:border-code-accent/50 transition-colors {% if checkbox.data.selected %}border-code-accent bg-code-accent/10{% endif %}">
154
+ <input type="checkbox" name="weekly_days_input" value="{{ checkbox.data.value }}" {% if checkbox.data.selected %}checked{% endif %} class="w-4 h-4 text-code-accent bg-code-bg border-code-border rounded focus:ring-code-accent focus:ring-2 mr-2">
155
+ <span class="text-sm text-code-text">{{ checkbox.choice_label }}</span>
156
+ </label>
157
+ {% endfor %}
158
+ </div>
159
+ {% if schedule_form.weekly_days_input.errors %}
160
+ <p class="mt-1 text-sm text-code-red">{{ schedule_form.weekly_days_input.errors.0 }}</p>
161
+ {% endif %}
162
+ </div>
163
+ <div>
164
+ <label for="id_weekly_times_input" class="block text-sm font-medium text-code-text mb-2">
165
+ {{ schedule_form.weekly_times_input.label }}
166
+ </label>
167
+ {{ schedule_form.weekly_times_input }}
168
+ <p class="mt-1 text-xs text-code-muted">{{ schedule_form.weekly_times_input.help_text }}</p>
169
+ {% if schedule_form.weekly_times_input.errors %}
170
+ <p class="mt-1 text-sm text-code-red">{{ schedule_form.weekly_times_input.errors.0 }}</p>
171
+ {% endif %}
172
+ </div>
173
+ <div>
174
+ <label class="block text-sm font-medium text-code-text mb-2">Timezone</label>
175
+ {{ schedule_form.timezone }}
176
+ </div>
177
+ </div>
178
+
179
+ <!-- Monthly Options -->
180
+ <div id="monthly-options" class="space-y-4 {% if schedule_form.instance.run_mode != 'monthly' %}hidden{% endif %}">
181
+ <div>
182
+ <label class="block text-sm font-medium text-code-text mb-2">Days of Month</label>
183
+ <div class="grid grid-cols-7 gap-1">
184
+ {% for checkbox in schedule_form.monthly_days_input %}
185
+ <label class="flex items-center justify-center p-2 border border-code-border rounded cursor-pointer hover:border-code-accent/50 transition-colors text-center {% if checkbox.data.selected %}border-code-accent bg-code-accent/10{% endif %}">
186
+ <input type="checkbox" name="monthly_days_input" value="{{ checkbox.data.value }}" {% if checkbox.data.selected %}checked{% endif %} class="sr-only">
187
+ <span class="text-xs text-code-text">{{ checkbox.choice_label }}</span>
188
+ </label>
189
+ {% endfor %}
190
+ </div>
191
+ {% if schedule_form.monthly_days_input.errors %}
192
+ <p class="mt-1 text-sm text-code-red">{{ schedule_form.monthly_days_input.errors.0 }}</p>
193
+ {% endif %}
194
+ </div>
195
+ <div>
196
+ <label for="id_monthly_times_input" class="block text-sm font-medium text-code-text mb-2">
197
+ {{ schedule_form.monthly_times_input.label }}
198
+ </label>
199
+ {{ schedule_form.monthly_times_input }}
200
+ <p class="mt-1 text-xs text-code-muted">{{ schedule_form.monthly_times_input.help_text }}</p>
201
+ {% if schedule_form.monthly_times_input.errors %}
202
+ <p class="mt-1 text-sm text-code-red">{{ schedule_form.monthly_times_input.errors.0 }}</p>
203
+ {% endif %}
204
+ </div>
205
+ <div>
206
+ <label class="block text-sm font-medium text-code-text mb-2">Timezone</label>
207
+ {{ schedule_form.timezone }}
208
+ </div>
209
+ </div>
210
+
211
+ <!-- Schedule Active Toggle -->
212
+ <div class="flex items-center space-x-3 pt-3 border-t border-code-border">
213
+ {{ schedule_form.is_active }}
214
+ <label for="id_is_active" class="text-sm font-medium text-code-text">
215
+ {{ schedule_form.is_active.label }}
216
+ </label>
217
+ {% if schedule_form.is_active.errors %}
218
+ <p class="text-sm text-code-red">{{ schedule_form.is_active.errors.0 }}</p>
219
+ {% endif %}
220
+ </div>
221
+ </div>
222
+ </div>
223
+ {% endif %}
224
+
225
+ <!-- Notifications (Collapsible) -->
226
+ <div class="bg-code-surface border border-code-border rounded-xl overflow-hidden">
227
+ <button type="button" onclick="toggleSection('notifications')"
228
+ class="w-full px-5 py-4 flex items-center justify-between text-left hover:bg-code-bg/50 transition-colors">
229
+ <h3 class="text-sm font-semibold text-code-text flex items-center">
230
+ <svg class="w-4 h-4 mr-2 text-code-muted" fill="none" stroke="currentColor" viewBox="0 0 24 24">
231
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
232
+ </svg>
233
+ Notifications
234
+ </h3>
235
+ <svg id="notifications-chevron" class="w-4 h-4 text-code-muted transition-transform rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
236
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
237
+ </svg>
238
+ </button>
239
+ <div id="notifications-content" class="hidden px-5 pb-5 space-y-4">
240
+ <!-- Notify On -->
241
+ <div>
242
+ <label for="id_notify_on" class="block text-sm font-medium text-code-text mb-2">
243
+ {{ form.notify_on.label }}
244
+ </label>
245
+ {{ form.notify_on }}
246
+ {% if form.notify_on.errors %}
247
+ <p class="mt-1 text-sm text-code-red">{{ form.notify_on.errors.0 }}</p>
248
+ {% endif %}
249
+ </div>
250
+
251
+ <!-- Override Email -->
252
+ <div>
253
+ <label for="id_notify_email" class="block text-sm font-medium text-code-text mb-2">
254
+ {{ form.notify_email.label }}
255
+ </label>
256
+ {{ form.notify_email }}
257
+ {% if form.notify_email.help_text %}
258
+ <p class="mt-1 text-xs text-code-muted">{{ form.notify_email.help_text }}</p>
259
+ {% endif %}
260
+ {% if form.notify_email.errors %}
261
+ <p class="mt-1 text-sm text-code-red">{{ form.notify_email.errors.0 }}</p>
262
+ {% endif %}
263
+ </div>
264
+
265
+ <!-- Webhook Configuration -->
266
+ <div class="p-4 bg-code-bg rounded-lg space-y-4">
267
+ <div class="flex items-center space-x-3">
268
+ {{ form.notify_webhook_enabled }}
269
+ <label for="id_notify_webhook_enabled" class="text-sm font-medium text-code-text">
270
+ {{ form.notify_webhook_enabled.label }}
271
+ </label>
272
+ </div>
273
+
274
+ <div>
275
+ <label for="id_notify_webhook_url" class="block text-sm font-medium text-code-text mb-2">
276
+ {{ form.notify_webhook_url.label }}
277
+ </label>
278
+ {{ form.notify_webhook_url }}
279
+ {% if form.notify_webhook_url.help_text %}
280
+ <p class="mt-1 text-xs text-code-muted">{{ form.notify_webhook_url.help_text }}</p>
281
+ {% endif %}
282
+ {% if form.notify_webhook_url.errors %}
283
+ <p class="mt-1 text-sm text-code-red">{{ form.notify_webhook_url.errors.0 }}</p>
284
+ {% endif %}
285
+ </div>
286
+ </div>
287
+ </div>
288
+ </div>
289
+
290
+ <!-- Action Buttons -->
291
+ <div class="bg-code-surface border border-code-border rounded-xl p-5">
292
+ <div class="flex flex-col space-y-3">
293
+ <button type="submit"
294
+ class="w-full px-4 py-3 bg-code-accent hover:bg-code-accent/90 text-white font-semibold rounded-lg transition-colors">
295
+ {% if script %}Update Script{% else %}Create Script{% endif %}
296
+ </button>
297
+ <a href="{% if script %}{% url 'cpanel:script_detail' pk=script.pk %}{% else %}{% url 'cpanel:script_list' %}{% endif %}"
298
+ class="w-full px-4 py-3 bg-code-surface border border-code-border hover:border-code-accent text-code-text text-center rounded-lg transition-colors">
299
+ Cancel
300
+ </a>
301
+ </div>
302
+ </div>
303
+ </div>
templates/cpanel/scripts/create.html ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Create Script - PyRunner{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="flex">
7
+ {% include "cpanel/_sidebar.html" %}
8
+
9
+ <div class="flex-1 p-8">
10
+ <!-- Header -->
11
+ <div class="mb-8">
12
+ <nav class="flex items-center space-x-2 text-sm text-code-muted mb-4">
13
+ <a href="{% url 'cpanel:script_list' %}" class="hover:text-code-accent">Scripts</a>
14
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
15
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
16
+ </svg>
17
+ <span class="text-code-text">Create</span>
18
+ </nav>
19
+ <h1 class="text-2xl font-bold text-code-text">Create New Script</h1>
20
+ <p class="text-code-muted">Write and configure a new Python script</p>
21
+ </div>
22
+
23
+ <!-- Form with Two-Column Layout -->
24
+ <form method="post">
25
+ {% csrf_token %}
26
+
27
+ <div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
28
+ <!-- Main Column: Name, Description, Code Editor -->
29
+ <div class="lg:col-span-7">
30
+ {% include "cpanel/scripts/_form_main.html" %}
31
+ </div>
32
+
33
+ <!-- Sidebar: Properties and Settings -->
34
+ <div class="lg:col-span-5">
35
+ {% include "cpanel/scripts/_form_sidebar.html" %}
36
+ </div>
37
+ </div>
38
+ </form>
39
+ </div>
40
+ </div>
41
+ {% endblock %}
42
+
43
+ {% block extra_scripts %}
44
+ <script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script>
45
+ <script>
46
+ // Collapsible sections toggle
47
+ function toggleSection(sectionId) {
48
+ const content = document.getElementById(sectionId + '-content');
49
+ const chevron = document.getElementById(sectionId + '-chevron');
50
+
51
+ if (content && chevron) {
52
+ content.classList.toggle('hidden');
53
+ chevron.classList.toggle('rotate-180');
54
+ }
55
+ }
56
+
57
+ // Schedule options toggle
58
+ function toggleScheduleOptions(mode) {
59
+ const intervalOptions = document.getElementById('interval-options');
60
+ const dailyOptions = document.getElementById('daily-options');
61
+
62
+ if (intervalOptions) {
63
+ intervalOptions.classList.toggle('hidden', mode !== 'interval');
64
+ }
65
+ if (dailyOptions) {
66
+ dailyOptions.classList.toggle('hidden', mode !== 'daily');
67
+ }
68
+
69
+ // Update radio button styles
70
+ document.querySelectorAll('input[name="run_mode"]').forEach(radio => {
71
+ const label = radio.closest('label');
72
+ const dot = label.querySelector('span > span');
73
+
74
+ if (radio.checked) {
75
+ label.classList.add('border-code-accent', 'bg-code-accent/10');
76
+ label.classList.remove('border-code-border');
77
+ label.querySelector('span').classList.add('border-code-accent');
78
+ label.querySelector('span').classList.remove('border-code-border');
79
+ if (!dot) {
80
+ const indicator = label.querySelector('span.w-4');
81
+ indicator.innerHTML = '<span class="w-2 h-2 rounded-full bg-code-accent"></span>';
82
+ }
83
+ } else {
84
+ label.classList.remove('border-code-accent', 'bg-code-accent/10');
85
+ label.classList.add('border-code-border');
86
+ label.querySelector('span').classList.remove('border-code-accent');
87
+ label.querySelector('span').classList.add('border-code-border');
88
+ const indicator = label.querySelector('span.w-4');
89
+ indicator.innerHTML = '';
90
+ }
91
+ });
92
+ }
93
+
94
+ // Monaco Editor initialization
95
+ require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' } });
96
+
97
+ require(['vs/editor/editor.main'], function () {
98
+ const textarea = document.getElementById('id_code');
99
+ const container = document.getElementById('code-editor-container');
100
+
101
+ if (!textarea || !container) {
102
+ console.error('Editor elements not found');
103
+ return;
104
+ }
105
+
106
+ textarea.style.display = 'none';
107
+
108
+ const editor = monaco.editor.create(container, {
109
+ value: textarea.value,
110
+ language: 'python',
111
+ theme: 'vs-dark',
112
+ fontSize: 14,
113
+ fontFamily: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace",
114
+ minimap: { enabled: false },
115
+ scrollBeyondLastLine: false,
116
+ lineNumbers: 'on',
117
+ tabSize: 4,
118
+ insertSpaces: true,
119
+ automaticLayout: true
120
+ });
121
+
122
+ editor.onDidChangeModelContent(function () {
123
+ textarea.value = editor.getValue();
124
+ });
125
+ });
126
+
127
+ // Tag selection handling
128
+ document.querySelectorAll('.tag-label').forEach(label => {
129
+ label.addEventListener('click', function(e) {
130
+ if (e.target.tagName === 'INPUT') return;
131
+ e.preventDefault(); // Prevent browser's default toggle (input is inside label)
132
+ const checkbox = this.querySelector('input[type="checkbox"]');
133
+ checkbox.checked = !checkbox.checked;
134
+ updateTagStyle(this, checkbox.checked);
135
+ });
136
+ });
137
+
138
+ function updateTagStyle(label, isChecked) {
139
+ if (isChecked) {
140
+ label.classList.add('border-code-accent', 'bg-code-accent/20');
141
+ label.classList.remove('border-code-border', 'bg-code-bg');
142
+ } else {
143
+ label.classList.remove('border-code-accent', 'bg-code-accent/20');
144
+ label.classList.add('border-code-border', 'bg-code-bg');
145
+ }
146
+ }
147
+ </script>
148
+ {% endblock %}
templates/cpanel/scripts/detail.html ADDED
@@ -0,0 +1,510 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}{{ script.name }} - PyRunner{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="flex">
7
+ {% include "cpanel/_sidebar.html" %}
8
+
9
+ <div class="flex-1 p-8">
10
+ <!-- Header -->
11
+ <div class="mb-8">
12
+ <nav class="flex items-center space-x-2 text-sm text-code-muted mb-4">
13
+ <a href="{% url 'cpanel:script_list' %}" class="hover:text-code-accent">Scripts</a>
14
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
15
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
16
+ </svg>
17
+ <span class="text-code-text">{{ script.name }}</span>
18
+ </nav>
19
+
20
+ <div class="flex items-start justify-between">
21
+ <div>
22
+ <h1 class="text-2xl font-bold text-code-text flex items-center">
23
+ {{ script.name }}
24
+ {% if script.is_archived %}
25
+ <span class="ml-3 px-2 py-1 text-xs rounded bg-code-red/20 text-code-red">Archived</span>
26
+ {% elif script.is_enabled %}
27
+ <span class="ml-3 px-2 py-1 text-xs rounded bg-code-green/20 text-code-green">Enabled</span>
28
+ {% else %}
29
+ <span class="ml-3 px-2 py-1 text-xs rounded bg-code-yellow/20 text-code-yellow">Disabled</span>
30
+ {% endif %}
31
+ </h1>
32
+ {% if script.description %}
33
+ <p class="text-code-muted mt-1">{{ script.description }}</p>
34
+ {% endif %}
35
+ {% if script.tags.exists %}
36
+ <div class="flex flex-wrap gap-2 mt-3">
37
+ {% for tag in script.tags.all %}
38
+ <a href="{% url 'cpanel:script_list' %}?tag={{ tag.pk }}"
39
+ class="inline-flex items-center px-2.5 py-1 text-xs rounded-full tag-{{ tag.color }} hover:opacity-80 transition-opacity">
40
+ {{ tag.name }}
41
+ </a>
42
+ {% endfor %}
43
+ </div>
44
+ {% endif %}
45
+ </div>
46
+
47
+ <!-- Actions -->
48
+ <div class="flex items-center space-x-3">
49
+ {% if script.is_archived %}
50
+ <!-- Archived script actions -->
51
+ <form method="post" action="{% url 'cpanel:script_restore' pk=script.pk %}">
52
+ {% csrf_token %}
53
+ <button type="submit"
54
+ class="px-4 py-2 bg-code-green hover:bg-code-green/90 text-white font-semibold rounded-lg transition-colors flex items-center space-x-2">
55
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
56
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"/>
57
+ </svg>
58
+ <span>Restore</span>
59
+ </button>
60
+ </form>
61
+
62
+ <form method="post" action="{% url 'cpanel:script_delete' pk=script.pk %}"
63
+ onsubmit="return confirm('PERMANENTLY delete {{ script.name }}? This cannot be undone and will delete all run history.');">
64
+ {% csrf_token %}
65
+ <button type="submit"
66
+ class="px-4 py-2 bg-code-red hover:bg-code-red/90 text-white font-semibold rounded-lg transition-colors flex items-center space-x-2">
67
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
68
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
69
+ </svg>
70
+ <span>Delete</span>
71
+ </button>
72
+ </form>
73
+ {% else %}
74
+ <!-- Normal script actions -->
75
+ <form method="post" action="{% url 'cpanel:script_run' pk=script.pk %}">
76
+ {% csrf_token %}
77
+ <button type="submit"
78
+ class="px-4 py-2 bg-code-green hover:bg-code-green/90 text-white font-semibold rounded-lg transition-colors flex items-center space-x-2 {% if not script.can_run %}opacity-50 cursor-not-allowed{% endif %}"
79
+ {% if not script.can_run %}disabled{% endif %}>
80
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
81
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/>
82
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
83
+ </svg>
84
+ <span>Run</span>
85
+ </button>
86
+ </form>
87
+
88
+ <a href="{% url 'cpanel:script_edit' pk=script.pk %}"
89
+ class="px-4 py-2 bg-code-surface border border-code-border hover:border-code-accent text-code-text rounded-lg transition-colors flex items-center space-x-2">
90
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
91
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
92
+ </svg>
93
+ <span>Edit</span>
94
+ </a>
95
+
96
+ <form method="post" action="{% url 'cpanel:script_toggle' pk=script.pk %}">
97
+ {% csrf_token %}
98
+ <button type="submit"
99
+ class="px-4 py-2 bg-code-surface border border-code-border hover:border-code-accent text-code-text rounded-lg transition-colors">
100
+ {% if script.is_enabled %}Disable{% else %}Enable{% endif %}
101
+ </button>
102
+ </form>
103
+
104
+ <form method="post" action="{% url 'cpanel:script_archive' pk=script.pk %}"
105
+ onsubmit="return confirm('Archive {{ script.name }}? The script will be hidden and its schedule paused.');">
106
+ {% csrf_token %}
107
+ <button type="submit"
108
+ class="px-4 py-2 bg-code-surface border border-code-border hover:border-code-red text-code-muted hover:text-code-red rounded-lg transition-colors flex items-center space-x-2">
109
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
110
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"/>
111
+ </svg>
112
+ <span>Archive</span>
113
+ </button>
114
+ </form>
115
+ {% endif %}
116
+ </div>
117
+ </div>
118
+ </div>
119
+
120
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
121
+ <!-- Left Column: Code and Info -->
122
+ <div class="lg:col-span-2 space-y-6">
123
+ <!-- Code Block -->
124
+ <div class="bg-code-surface border border-code-border rounded-xl overflow-hidden">
125
+ <div class="px-4 py-3 border-b border-code-border bg-code-bg flex items-center justify-between">
126
+ <span class="text-sm font-medium text-code-text">Python Code</span>
127
+ <span class="text-xs text-code-muted">{{ script.code|length }} chars</span>
128
+ </div>
129
+ <div id="code-viewer-container" style="height: 500px;" class="overflow-hidden"></div>
130
+ <textarea id="code-content" class="hidden">{{ script.code }}</textarea>
131
+ </div>
132
+ </div>
133
+
134
+ <!-- Right Column: Metadata and Stats -->
135
+ <div class="space-y-6">
136
+ <!-- Script Info -->
137
+ <div class="bg-code-surface border border-code-border rounded-xl p-6">
138
+ <h3 class="text-sm font-semibold text-code-text mb-4">Script Info</h3>
139
+ <dl class="space-y-4">
140
+ <div>
141
+ <dt class="text-xs text-code-muted uppercase tracking-wider">Environment</dt>
142
+ <dd class="text-code-text mt-1">
143
+ <a href="{% url 'cpanel:environment_detail' pk=script.environment.pk %}" class="text-code-accent hover:underline">
144
+ {{ script.environment.name }}
145
+ </a>
146
+ </dd>
147
+ </div>
148
+ <div>
149
+ <dt class="text-xs text-code-muted uppercase tracking-wider">Timeout</dt>
150
+ <dd class="text-code-text mt-1">{{ script.timeout_seconds }} seconds</dd>
151
+ </div>
152
+ <div>
153
+ <dt class="text-xs text-code-muted uppercase tracking-wider">Created</dt>
154
+ <dd class="text-code-text mt-1">{{ script.created_at|date:"M d, Y H:i" }}</dd>
155
+ </div>
156
+ <div>
157
+ <dt class="text-xs text-code-muted uppercase tracking-wider">Updated</dt>
158
+ <dd class="text-code-text mt-1">{{ script.updated_at|date:"M d, Y H:i" }}</dd>
159
+ </div>
160
+ {% if script.created_by %}
161
+ <div>
162
+ <dt class="text-xs text-code-muted uppercase tracking-wider">Created By</dt>
163
+ <dd class="text-code-text mt-1">{{ script.created_by.email }}</dd>
164
+ </div>
165
+ {% endif %}
166
+ </dl>
167
+
168
+ {% if script.is_archived %}
169
+ <div class="mt-4 pt-4 border-t border-code-border">
170
+ <div>
171
+ <dt class="text-xs text-code-muted uppercase tracking-wider">Archived</dt>
172
+ <dd class="text-code-red mt-1">{{ script.archived_at|date:"M d, Y H:i" }}</dd>
173
+ </div>
174
+ {% if script.archived_by %}
175
+ <div class="mt-2">
176
+ <dt class="text-xs text-code-muted uppercase tracking-wider">Archived By</dt>
177
+ <dd class="text-code-text mt-1">{{ script.archived_by.email }}</dd>
178
+ </div>
179
+ {% endif %}
180
+ </div>
181
+ {% endif %}
182
+ </div>
183
+
184
+ <!-- Stats -->
185
+ <div class="bg-code-surface border border-code-border rounded-xl p-6">
186
+ <h3 class="text-sm font-semibold text-code-text mb-4">Statistics</h3>
187
+ <div class="grid grid-cols-2 gap-4">
188
+ <div class="text-center p-3 bg-code-bg rounded-lg">
189
+ <div class="text-2xl font-bold text-code-accent">{{ script.run_count }}</div>
190
+ <div class="text-xs text-code-muted">Total Runs</div>
191
+ </div>
192
+ <div class="text-center p-3 bg-code-bg rounded-lg">
193
+ <div class="text-2xl font-bold {% if script.success_rate >= 80 %}text-code-green{% elif script.success_rate >= 50 %}text-code-yellow{% else %}text-code-red{% endif %}">
194
+ {% if script.run_count > 0 %}{{ script.success_rate|floatformat:0 }}%{% else %}-{% endif %}
195
+ </div>
196
+ <div class="text-xs text-code-muted">Success Rate</div>
197
+ </div>
198
+ </div>
199
+ </div>
200
+ </div>
201
+ </div>
202
+
203
+ <!-- Schedule, Webhook, Notifications Row -->
204
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mt-6">
205
+ <!-- Schedule Info Card -->
206
+ <div class="bg-code-surface border border-code-border rounded-xl p-6">
207
+ <div class="flex items-center justify-between mb-4">
208
+ <h3 class="text-sm font-semibold text-code-text">Schedule</h3>
209
+ {% if schedule.run_mode != 'manual' %}
210
+ <form method="post" action="{% url 'cpanel:schedule_toggle' pk=script.pk %}">
211
+ {% csrf_token %}
212
+ <button type="submit" class="text-xs px-2 py-1 rounded {% if schedule.is_active %}bg-code-green/20 text-code-green{% else %}bg-code-yellow/20 text-code-yellow{% endif %}">
213
+ {% if schedule.is_active %}Active{% else %}Paused{% endif %}
214
+ </button>
215
+ </form>
216
+ {% endif %}
217
+ </div>
218
+
219
+ <dl class="space-y-4">
220
+ <div>
221
+ <dt class="text-xs text-code-muted uppercase tracking-wider">Mode</dt>
222
+ <dd class="text-code-text mt-1">{{ schedule.schedule_display }}</dd>
223
+ </div>
224
+
225
+ {% if schedule.next_run %}
226
+ <div>
227
+ <dt class="text-xs text-code-muted uppercase tracking-wider">Next Run</dt>
228
+ <dd class="text-code-text mt-1">{{ schedule.next_run|date:"M d, Y H:i" }} UTC</dd>
229
+ </div>
230
+ {% endif %}
231
+
232
+ {% if schedule.last_scheduled_run %}
233
+ <div>
234
+ <dt class="text-xs text-code-muted uppercase tracking-wider">Last Scheduled Run</dt>
235
+ <dd class="text-code-text mt-1">{{ schedule.last_scheduled_run|date:"M d, Y H:i" }} UTC</dd>
236
+ </div>
237
+ {% endif %}
238
+ </dl>
239
+
240
+ <div class="mt-4 pt-4 border-t border-code-border">
241
+ <a href="{% url 'cpanel:schedule_history' pk=script.pk %}" class="text-xs text-code-accent hover:underline">
242
+ View schedule history
243
+ </a>
244
+ </div>
245
+ </div>
246
+
247
+ <!-- Webhook Card -->
248
+ <div class="bg-code-surface border border-code-border rounded-xl p-6">
249
+ <div class="flex items-center justify-between mb-4">
250
+ <h3 class="text-sm font-semibold text-code-text">Webhook</h3>
251
+ {% if script.has_webhook %}
252
+ <span class="text-xs px-2 py-1 rounded bg-code-green/20 text-code-green">Enabled</span>
253
+ {% else %}
254
+ <span class="text-xs px-2 py-1 rounded bg-code-muted/20 text-code-muted">Disabled</span>
255
+ {% endif %}
256
+ </div>
257
+
258
+ {% if script.has_webhook %}
259
+ <div class="space-y-4">
260
+ <div>
261
+ <dt class="text-xs text-code-muted uppercase tracking-wider mb-2">Webhook URL <span class="text-code-yellow">(sensitive)</span></dt>
262
+ <div class="flex items-center space-x-2">
263
+ <input type="password"
264
+ id="webhook-url"
265
+ value="{{ request.scheme }}://{{ request.get_host }}{% url 'webhook_trigger' token=script.webhook_token %}"
266
+ readonly
267
+ class="flex-1 px-3 py-2 bg-code-bg border border-code-border rounded text-code-text text-xs font-mono">
268
+ <button type="button"
269
+ onclick="toggleWebhookVisibility()"
270
+ id="webhook-toggle-btn"
271
+ class="px-3 py-2 bg-code-bg hover:bg-code-border text-code-muted text-xs rounded transition-colors border border-code-border"
272
+ title="Show/Hide URL">
273
+ <svg id="webhook-eye-icon" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
274
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
275
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
276
+ </svg>
277
+ </button>
278
+ <button type="button"
279
+ onclick="copyWebhookUrl()"
280
+ class="px-3 py-2 bg-code-accent hover:bg-code-accent/90 text-white text-xs rounded transition-colors"
281
+ title="Copy URL">
282
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
283
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
284
+ </svg>
285
+ </button>
286
+ </div>
287
+ </div>
288
+
289
+ <div>
290
+ <dt class="text-xs text-code-muted uppercase tracking-wider mb-2">Example curl</dt>
291
+ <button type="button"
292
+ id="curl-toggle-btn"
293
+ onclick="toggleCurlExample()"
294
+ class="text-xs text-code-accent hover:underline mb-2">
295
+ Show example
296
+ </button>
297
+ <pre id="curl-example" class="hidden p-3 bg-code-bg rounded text-xs font-mono text-code-muted overflow-x-auto">curl -X POST \
298
+ -H "Content-Type: application/json" \
299
+ -d '{"key": "value"}' \
300
+ "{{ request.scheme }}://{{ request.get_host }}{% url 'webhook_trigger' token=script.webhook_token %}"</pre>
301
+ </div>
302
+
303
+ <div class="flex items-center space-x-2 pt-2">
304
+ <form method="post" action="{% url 'cpanel:webhook_regenerate' pk=script.pk %}"
305
+ onsubmit="return confirm('Regenerate webhook URL? The old URL will stop working.');">
306
+ {% csrf_token %}
307
+ <button type="submit" class="text-xs text-code-yellow hover:underline">
308
+ Regenerate URL
309
+ </button>
310
+ </form>
311
+ <span class="text-code-muted">|</span>
312
+ <form method="post" action="{% url 'cpanel:webhook_disable' pk=script.pk %}"
313
+ onsubmit="return confirm('Disable webhook? The URL will stop working.');">
314
+ {% csrf_token %}
315
+ <button type="submit" class="text-xs text-code-red hover:underline">
316
+ Disable
317
+ </button>
318
+ </form>
319
+ </div>
320
+ </div>
321
+ {% else %}
322
+ <p class="text-code-muted text-sm mb-4">Enable webhooks to trigger this script via HTTP requests from external services.</p>
323
+ <form method="post" action="{% url 'cpanel:webhook_enable' pk=script.pk %}">
324
+ {% csrf_token %}
325
+ <button type="submit" class="px-4 py-2 bg-code-accent hover:bg-code-accent/90 text-white text-sm rounded transition-colors">
326
+ Enable Webhook
327
+ </button>
328
+ </form>
329
+ {% endif %}
330
+ </div>
331
+
332
+ <!-- Notifications Card -->
333
+ <div class="bg-code-surface border border-code-border rounded-xl p-6">
334
+ <div class="flex items-center justify-between mb-4">
335
+ <h3 class="text-sm font-semibold text-code-text">Notifications</h3>
336
+ {% if script.notify_on != 'never' %}
337
+ <span class="text-xs px-2 py-1 rounded bg-code-accent/20 text-code-accent">
338
+ {{ script.get_notify_on_display }}
339
+ </span>
340
+ {% else %}
341
+ <span class="text-xs px-2 py-1 rounded bg-code-muted/20 text-code-muted">Disabled</span>
342
+ {% endif %}
343
+ </div>
344
+
345
+ {% if script.notify_on != 'never' %}
346
+ <dl class="space-y-4">
347
+ <div>
348
+ <dt class="text-xs text-code-muted uppercase tracking-wider">Notify On</dt>
349
+ <dd class="text-code-text mt-1">{{ script.get_notify_on_display }}</dd>
350
+ </div>
351
+
352
+ {% if script.notify_email %}
353
+ <div>
354
+ <dt class="text-xs text-code-muted uppercase tracking-wider">Email Override</dt>
355
+ <dd class="text-code-text mt-1">{{ script.notify_email }}</dd>
356
+ </div>
357
+ {% else %}
358
+ <div>
359
+ <dt class="text-xs text-code-muted uppercase tracking-wider">Email</dt>
360
+ <dd class="text-code-muted mt-1 text-sm">Using global default</dd>
361
+ </div>
362
+ {% endif %}
363
+
364
+ {% if script.notify_webhook_enabled and script.notify_webhook_url %}
365
+ <div>
366
+ <dt class="text-xs text-code-muted uppercase tracking-wider">Webhook</dt>
367
+ <dd class="text-code-text mt-1 text-sm break-all">{{ script.notify_webhook_url }}</dd>
368
+ </div>
369
+ {% endif %}
370
+ </dl>
371
+ {% else %}
372
+ <p class="text-code-muted text-sm">
373
+ Notifications are disabled for this script.
374
+ <a href="{% url 'cpanel:script_edit' pk=script.pk %}" class="text-code-accent hover:underline">
375
+ Edit settings
376
+ </a>
377
+ </p>
378
+ {% endif %}
379
+ </div>
380
+ </div>
381
+
382
+ <!-- Recent Runs -->
383
+ <div class="mt-8">
384
+ <h2 class="text-lg font-semibold text-code-text mb-4">Recent Runs</h2>
385
+
386
+ {% if recent_runs %}
387
+ <div class="bg-code-surface border border-code-border rounded-xl overflow-hidden">
388
+ <table class="w-full">
389
+ <thead class="bg-code-bg border-b border-code-border">
390
+ <tr>
391
+ <th class="px-6 py-3 text-left text-xs font-semibold text-code-muted uppercase tracking-wider">Status</th>
392
+ <th class="px-6 py-3 text-left text-xs font-semibold text-code-muted uppercase tracking-wider">Duration</th>
393
+ <th class="px-6 py-3 text-left text-xs font-semibold text-code-muted uppercase tracking-wider">Triggered By</th>
394
+ <th class="px-6 py-3 text-left text-xs font-semibold text-code-muted uppercase tracking-wider">Started</th>
395
+ <th class="px-6 py-3 text-left text-xs font-semibold text-code-muted uppercase tracking-wider"></th>
396
+ </tr>
397
+ </thead>
398
+ <tbody class="divide-y divide-code-border">
399
+ {% for run in recent_runs %}
400
+ <tr class="hover:bg-code-bg/50 transition-colors">
401
+ <td class="px-6 py-4">
402
+ {% if run.status == 'success' %}
403
+ <span class="px-2 py-1 text-xs rounded bg-code-green/20 text-code-green">Success</span>
404
+ {% elif run.status == 'failed' %}
405
+ <span class="px-2 py-1 text-xs rounded bg-code-red/20 text-code-red">Failed</span>
406
+ {% elif run.status == 'timeout' %}
407
+ <span class="px-2 py-1 text-xs rounded bg-code-red/20 text-code-red">Timeout</span>
408
+ {% elif run.status == 'running' %}
409
+ <span class="px-2 py-1 text-xs rounded bg-code-yellow/20 text-code-yellow">Running</span>
410
+ {% elif run.status == 'pending' %}
411
+ <span class="px-2 py-1 text-xs rounded bg-code-muted/20 text-code-muted">Pending</span>
412
+ {% elif run.status == 'cancelled' %}
413
+ <span class="px-2 py-1 text-xs rounded bg-code-muted/20 text-code-muted">Cancelled</span>
414
+ {% endif %}
415
+ </td>
416
+ <td class="px-6 py-4 text-code-text">
417
+ {% if run.duration_display %}{{ run.duration_display }}{% else %}-{% endif %}
418
+ </td>
419
+ <td class="px-6 py-4 text-code-muted">
420
+ {% if run.triggered_by %}{{ run.triggered_by.email }}{% else %}System{% endif %}
421
+ </td>
422
+ <td class="px-6 py-4 text-code-muted text-sm">
423
+ {% if run.started_at %}{{ run.started_at|date:"M d, H:i" }}{% else %}{{ run.created_at|date:"M d, H:i" }}{% endif %}
424
+ </td>
425
+ <td class="px-6 py-4 text-right">
426
+ <a href="{% url 'cpanel:run_detail' pk=run.pk %}" class="text-code-accent hover:underline text-sm">
427
+ View
428
+ </a>
429
+ </td>
430
+ </tr>
431
+ {% endfor %}
432
+ </tbody>
433
+ </table>
434
+ </div>
435
+ {% else %}
436
+ <div class="bg-code-surface border border-code-border rounded-xl p-8 text-center">
437
+ <p class="text-code-muted">No runs yet. Click "Run" to execute this script.</p>
438
+ </div>
439
+ {% endif %}
440
+ </div>
441
+ </div>
442
+ </div>
443
+
444
+ <script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script>
445
+ <script>
446
+ require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' } });
447
+ require(['vs/editor/editor.main'], function () {
448
+ const container = document.getElementById('code-viewer-container');
449
+ const codeContent = document.getElementById('code-content');
450
+
451
+ monaco.editor.create(container, {
452
+ value: codeContent.value,
453
+ language: 'python',
454
+ theme: 'vs-dark',
455
+ fontSize: 14,
456
+ fontFamily: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace",
457
+ minimap: { enabled: false },
458
+ scrollBeyondLastLine: false,
459
+ lineNumbers: 'on',
460
+ readOnly: true,
461
+ automaticLayout: true
462
+ });
463
+ });
464
+ </script>
465
+
466
+ <script>
467
+ function copyWebhookUrl() {
468
+ const urlInput = document.getElementById('webhook-url');
469
+ if (urlInput) {
470
+ navigator.clipboard.writeText(urlInput.value).then(() => {
471
+ // Show brief feedback
472
+ const btn = event.currentTarget;
473
+ const originalHTML = btn.innerHTML;
474
+ btn.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>';
475
+ setTimeout(() => {
476
+ btn.innerHTML = originalHTML;
477
+ }, 1500);
478
+ }).catch(err => {
479
+ // Fallback for older browsers
480
+ urlInput.select();
481
+ document.execCommand('copy');
482
+ });
483
+ }
484
+ }
485
+
486
+ function toggleWebhookVisibility() {
487
+ const urlInput = document.getElementById('webhook-url');
488
+ const eyeIcon = document.getElementById('webhook-eye-icon');
489
+ if (urlInput.type === 'password') {
490
+ urlInput.type = 'text';
491
+ eyeIcon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/>';
492
+ } else {
493
+ urlInput.type = 'password';
494
+ eyeIcon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>';
495
+ }
496
+ }
497
+
498
+ function toggleCurlExample() {
499
+ const curlExample = document.getElementById('curl-example');
500
+ const toggleBtn = document.getElementById('curl-toggle-btn');
501
+ if (curlExample.classList.contains('hidden')) {
502
+ curlExample.classList.remove('hidden');
503
+ toggleBtn.textContent = 'Hide example';
504
+ } else {
505
+ curlExample.classList.add('hidden');
506
+ toggleBtn.textContent = 'Show example';
507
+ }
508
+ }
509
+ </script>
510
+ {% endblock %}
templates/cpanel/scripts/edit.html ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Edit {{ script.name }} - PyRunner{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="flex">
7
+ {% include "cpanel/_sidebar.html" %}
8
+
9
+ <div class="flex-1 p-8">
10
+ <!-- Header -->
11
+ <div class="mb-8">
12
+ <nav class="flex items-center space-x-2 text-sm text-code-muted mb-4">
13
+ <a href="{% url 'cpanel:script_list' %}" class="hover:text-code-accent">Scripts</a>
14
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
15
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
16
+ </svg>
17
+ <a href="{% url 'cpanel:script_detail' pk=script.pk %}" class="hover:text-code-accent">{{ script.name }}</a>
18
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
20
+ </svg>
21
+ <span class="text-code-text">Edit</span>
22
+ </nav>
23
+ <h1 class="text-2xl font-bold text-code-text">Edit Script</h1>
24
+ <p class="text-code-muted">Update {{ script.name }}</p>
25
+ </div>
26
+
27
+ <!-- Form with Two-Column Layout -->
28
+ <form method="post">
29
+ {% csrf_token %}
30
+
31
+ <div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
32
+ <!-- Main Column: Name, Description, Code Editor -->
33
+ <div class="lg:col-span-7">
34
+ {% include "cpanel/scripts/_form_main.html" %}
35
+ </div>
36
+
37
+ <!-- Sidebar: Properties and Settings -->
38
+ <div class="lg:col-span-5">
39
+ {% include "cpanel/scripts/_form_sidebar.html" %}
40
+ </div>
41
+ </div>
42
+ </form>
43
+ </div>
44
+ </div>
45
+ {% endblock %}
46
+
47
+ {% block extra_scripts %}
48
+ <script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script>
49
+ <script>
50
+ // Collapsible sections toggle
51
+ function toggleSection(sectionId) {
52
+ const content = document.getElementById(sectionId + '-content');
53
+ const chevron = document.getElementById(sectionId + '-chevron');
54
+
55
+ if (content && chevron) {
56
+ content.classList.toggle('hidden');
57
+ chevron.classList.toggle('rotate-180');
58
+ }
59
+ }
60
+
61
+ // Schedule options toggle
62
+ function toggleScheduleOptions(mode) {
63
+ const intervalOptions = document.getElementById('interval-options');
64
+ const dailyOptions = document.getElementById('daily-options');
65
+ const weeklyOptions = document.getElementById('weekly-options');
66
+ const monthlyOptions = document.getElementById('monthly-options');
67
+
68
+ if (intervalOptions) {
69
+ intervalOptions.classList.toggle('hidden', mode !== 'interval');
70
+ }
71
+ if (dailyOptions) {
72
+ dailyOptions.classList.toggle('hidden', mode !== 'daily');
73
+ }
74
+ if (weeklyOptions) {
75
+ weeklyOptions.classList.toggle('hidden', mode !== 'weekly');
76
+ }
77
+ if (monthlyOptions) {
78
+ monthlyOptions.classList.toggle('hidden', mode !== 'monthly');
79
+ }
80
+
81
+ // Update radio button styles
82
+ document.querySelectorAll('input[name="run_mode"]').forEach(radio => {
83
+ const label = radio.closest('label');
84
+ const dot = label.querySelector('span > span');
85
+
86
+ if (radio.checked) {
87
+ label.classList.add('border-code-accent', 'bg-code-accent/10');
88
+ label.classList.remove('border-code-border');
89
+ label.querySelector('span').classList.add('border-code-accent');
90
+ label.querySelector('span').classList.remove('border-code-border');
91
+ if (!dot) {
92
+ const indicator = label.querySelector('span.w-4');
93
+ indicator.innerHTML = '<span class="w-2 h-2 rounded-full bg-code-accent"></span>';
94
+ }
95
+ } else {
96
+ label.classList.remove('border-code-accent', 'bg-code-accent/10');
97
+ label.classList.add('border-code-border');
98
+ label.querySelector('span').classList.remove('border-code-accent');
99
+ label.querySelector('span').classList.add('border-code-border');
100
+ const indicator = label.querySelector('span.w-4');
101
+ indicator.innerHTML = '';
102
+ }
103
+ });
104
+ }
105
+
106
+ // Monaco Editor initialization
107
+ require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' } });
108
+
109
+ require(['vs/editor/editor.main'], function () {
110
+ const textarea = document.getElementById('id_code');
111
+ const container = document.getElementById('code-editor-container');
112
+
113
+ if (!textarea || !container) {
114
+ console.error('Editor elements not found');
115
+ return;
116
+ }
117
+
118
+ textarea.style.display = 'none';
119
+
120
+ const editor = monaco.editor.create(container, {
121
+ value: textarea.value,
122
+ language: 'python',
123
+ theme: 'vs-dark',
124
+ fontSize: 14,
125
+ fontFamily: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace",
126
+ minimap: { enabled: false },
127
+ scrollBeyondLastLine: false,
128
+ lineNumbers: 'on',
129
+ tabSize: 4,
130
+ insertSpaces: true,
131
+ automaticLayout: true
132
+ });
133
+
134
+ editor.onDidChangeModelContent(function () {
135
+ textarea.value = editor.getValue();
136
+ });
137
+ });
138
+
139
+ // Tag selection handling
140
+ document.querySelectorAll('.tag-label').forEach(label => {
141
+ label.addEventListener('click', function(e) {
142
+ if (e.target.tagName === 'INPUT') return;
143
+ e.preventDefault(); // Prevent browser's default toggle (input is inside label)
144
+ const checkbox = this.querySelector('input[type="checkbox"]');
145
+ checkbox.checked = !checkbox.checked;
146
+ updateTagStyle(this, checkbox.checked);
147
+ });
148
+ });
149
+
150
+ function updateTagStyle(label, isChecked) {
151
+ if (isChecked) {
152
+ label.classList.add('border-code-accent', 'bg-code-accent/20');
153
+ label.classList.remove('border-code-border', 'bg-code-bg');
154
+ } else {
155
+ label.classList.remove('border-code-accent', 'bg-code-accent/20');
156
+ label.classList.add('border-code-border', 'bg-code-bg');
157
+ }
158
+ }
159
+ </script>
160
+ {% endblock %}
templates/cpanel/scripts/list.html ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Scripts - PyRunner{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="flex">
7
+ {% include "cpanel/_sidebar.html" %}
8
+
9
+ <div class="flex-1 p-8">
10
+ <!-- Header -->
11
+ <div class="flex items-center justify-between mb-8">
12
+ <div>
13
+ <h1 class="text-2xl font-bold text-code-text">Scripts</h1>
14
+ <p class="text-code-muted">Manage your Python scripts</p>
15
+ </div>
16
+ <a href="{% url 'cpanel:script_create' %}"
17
+ class="px-4 py-2 bg-code-accent hover:bg-code-accent/90 text-white font-semibold rounded-lg transition-colors flex items-center space-x-2">
18
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
20
+ </svg>
21
+ <span>New Script</span>
22
+ </a>
23
+ </div>
24
+
25
+ <!-- Filters -->
26
+ <div class="mb-6 flex items-center flex-wrap gap-4">
27
+ <div class="flex items-center space-x-4">
28
+ <span class="text-code-muted text-sm">Status:</span>
29
+ <a href="{% url 'cpanel:script_list' %}{% if selected_tag %}?tag={{ selected_tag.pk }}{% endif %}"
30
+ class="px-3 py-1 text-sm rounded-lg transition-colors {% if not status_filter %}bg-code-accent/20 text-code-accent{% else %}text-code-muted hover:text-code-text hover:bg-code-bg{% endif %}">
31
+ All
32
+ </a>
33
+ <a href="{% url 'cpanel:script_list' %}?status=enabled{% if selected_tag %}&tag={{ selected_tag.pk }}{% endif %}"
34
+ class="px-3 py-1 text-sm rounded-lg transition-colors {% if status_filter == 'enabled' %}bg-code-green/20 text-code-green{% else %}text-code-muted hover:text-code-text hover:bg-code-bg{% endif %}">
35
+ Enabled
36
+ </a>
37
+ <a href="{% url 'cpanel:script_list' %}?status=disabled{% if selected_tag %}&tag={{ selected_tag.pk }}{% endif %}"
38
+ class="px-3 py-1 text-sm rounded-lg transition-colors {% if status_filter == 'disabled' %}bg-code-yellow/20 text-code-yellow{% else %}text-code-muted hover:text-code-text hover:bg-code-bg{% endif %}">
39
+ Disabled
40
+ </a>
41
+ <a href="{% url 'cpanel:script_list' %}?status=archived{% if selected_tag %}&tag={{ selected_tag.pk }}{% endif %}"
42
+ class="px-3 py-1 text-sm rounded-lg transition-colors {% if status_filter == 'archived' %}bg-code-red/20 text-code-red{% else %}text-code-muted hover:text-code-text hover:bg-code-bg{% endif %}">
43
+ Archived
44
+ </a>
45
+ </div>
46
+
47
+ {% if all_tags %}
48
+ <div class="flex items-center space-x-2">
49
+ <span class="text-code-muted text-sm">Tag:</span>
50
+ <select onchange="filterByTag(this.value)"
51
+ class="px-3 py-1 text-sm bg-code-bg border border-code-border rounded-lg text-code-text focus:outline-none focus:ring-2 focus:ring-code-accent/50">
52
+ <option value="">All Tags</option>
53
+ {% for tag in all_tags %}
54
+ <option value="{{ tag.pk }}" {% if selected_tag.pk == tag.pk %}selected{% endif %}>
55
+ {{ tag.name }}
56
+ </option>
57
+ {% endfor %}
58
+ </select>
59
+ {% if selected_tag %}
60
+ <a href="{% url 'cpanel:script_list' %}{% if status_filter %}?status={{ status_filter }}{% endif %}"
61
+ class="text-code-muted hover:text-code-text text-sm">
62
+ Clear
63
+ </a>
64
+ {% endif %}
65
+ </div>
66
+ {% endif %}
67
+ </div>
68
+
69
+ <script>
70
+ function filterByTag(tagId) {
71
+ const url = new URL(window.location.href);
72
+ if (tagId) {
73
+ url.searchParams.set('tag', tagId);
74
+ } else {
75
+ url.searchParams.delete('tag');
76
+ }
77
+ window.location.href = url.toString();
78
+ }
79
+ </script>
80
+
81
+ <!-- Scripts Table -->
82
+ {% if scripts %}
83
+ <div class="bg-code-surface border border-code-border rounded-xl overflow-hidden">
84
+ <table class="w-full">
85
+ <thead class="bg-code-bg border-b border-code-border">
86
+ <tr>
87
+ <th class="px-6 py-4 text-left text-xs font-semibold text-code-muted uppercase tracking-wider">Name</th>
88
+ <th class="px-6 py-4 text-left text-xs font-semibold text-code-muted uppercase tracking-wider">Environment</th>
89
+ <th class="px-6 py-4 text-left text-xs font-semibold text-code-muted uppercase tracking-wider">Tags</th>
90
+ <th class="px-6 py-4 text-left text-xs font-semibold text-code-muted uppercase tracking-wider">Status</th>
91
+ <th class="px-6 py-4 text-left text-xs font-semibold text-code-muted uppercase tracking-wider">Runs</th>
92
+ <th class="px-6 py-4 text-left text-xs font-semibold text-code-muted uppercase tracking-wider">Success Rate</th>
93
+ <th class="px-6 py-4 text-left text-xs font-semibold text-code-muted uppercase tracking-wider">Updated</th>
94
+ </tr>
95
+ </thead>
96
+ <tbody class="divide-y divide-code-border">
97
+ {% for script in scripts %}
98
+ <tr class="hover:bg-code-bg/50 transition-colors">
99
+ <td class="px-6 py-4">
100
+ <a href="{% url 'cpanel:script_detail' pk=script.pk %}" class="text-code-accent hover:underline font-medium">
101
+ {{ script.name }}
102
+ </a>
103
+ {% if script.description %}
104
+ <p class="text-code-muted text-sm truncate max-w-xs">{{ script.description|truncatechars:50 }}</p>
105
+ {% endif %}
106
+ </td>
107
+ <td class="px-6 py-4 text-code-text">
108
+ {{ script.environment.name }}
109
+ </td>
110
+ <td class="px-6 py-4">
111
+ <div class="flex flex-wrap gap-1">
112
+ {% for tag in script.tags.all %}
113
+ <a href="{% url 'cpanel:script_list' %}?tag={{ tag.pk }}{% if status_filter %}&status={{ status_filter }}{% endif %}"
114
+ class="inline-flex items-center px-2 py-0.5 text-xs rounded-full tag-{{ tag.color }} hover:opacity-80 transition-opacity">
115
+ {{ tag.name }}
116
+ </a>
117
+ {% empty %}
118
+ <span class="text-code-muted text-sm">-</span>
119
+ {% endfor %}
120
+ </div>
121
+ </td>
122
+ <td class="px-6 py-4">
123
+ {% if script.is_archived %}
124
+ <span class="px-2 py-1 text-xs rounded bg-code-red/20 text-code-red">Archived</span>
125
+ {% elif script.is_enabled %}
126
+ <span class="px-2 py-1 text-xs rounded bg-code-green/20 text-code-green">Enabled</span>
127
+ {% else %}
128
+ <span class="px-2 py-1 text-xs rounded bg-code-yellow/20 text-code-yellow">Disabled</span>
129
+ {% endif %}
130
+ </td>
131
+ <td class="px-6 py-4 text-code-text">
132
+ {{ script.run_count }}
133
+ </td>
134
+ <td class="px-6 py-4">
135
+ {% if script.run_count > 0 %}
136
+ <span class="{% if script.success_rate >= 80 %}text-code-green{% elif script.success_rate >= 50 %}text-code-yellow{% else %}text-code-red{% endif %}">
137
+ {{ script.success_rate|floatformat:0 }}%
138
+ </span>
139
+ {% else %}
140
+ <span class="text-code-muted">-</span>
141
+ {% endif %}
142
+ </td>
143
+ <td class="px-6 py-4 text-code-muted text-sm">
144
+ {{ script.updated_at|timesince }} ago
145
+ </td>
146
+ </tr>
147
+ {% endfor %}
148
+ </tbody>
149
+ </table>
150
+ </div>
151
+ {% else %}
152
+ <div class="bg-code-surface border border-code-border rounded-xl p-12 text-center">
153
+ <div class="w-16 h-16 bg-code-accent/20 rounded-full flex items-center justify-center mx-auto mb-4">
154
+ <svg class="w-8 h-8 text-code-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
155
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
156
+ </svg>
157
+ </div>
158
+ <h3 class="text-lg font-semibold text-code-text mb-2">No scripts yet</h3>
159
+ <p class="text-code-muted mb-6">Create your first Python script to get started.</p>
160
+ <a href="{% url 'cpanel:script_create' %}"
161
+ class="inline-flex items-center px-4 py-2 bg-code-accent hover:bg-code-accent/90 text-white font-semibold rounded-lg transition-colors">
162
+ <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
163
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
164
+ </svg>
165
+ Create Script
166
+ </a>
167
+ </div>
168
+ {% endif %}
169
+ </div>
170
+ </div>
171
+ {% endblock %}
templates/cpanel/scripts/schedule_history.html ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Schedule History - {{ script.name }} - PyRunner{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="flex">
7
+ {% include "cpanel/_sidebar.html" %}
8
+
9
+ <div class="flex-1 p-8">
10
+ <!-- Header -->
11
+ <div class="mb-8">
12
+ <nav class="flex items-center space-x-2 text-sm text-code-muted mb-4">
13
+ <a href="{% url 'cpanel:script_list' %}" class="hover:text-code-accent">Scripts</a>
14
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
15
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
16
+ </svg>
17
+ <a href="{% url 'cpanel:script_detail' pk=script.pk %}" class="hover:text-code-accent">{{ script.name }}</a>
18
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
20
+ </svg>
21
+ <span class="text-code-text">Schedule History</span>
22
+ </nav>
23
+ <h1 class="text-2xl font-bold text-code-text">Schedule History</h1>
24
+ <p class="text-code-muted">Changes to {{ script.name }}'s schedule configuration</p>
25
+ </div>
26
+
27
+ {% if history %}
28
+ <div class="bg-code-surface border border-code-border rounded-xl overflow-hidden">
29
+ <table class="w-full">
30
+ <thead class="bg-code-bg border-b border-code-border">
31
+ <tr>
32
+ <th class="px-6 py-3 text-left text-xs font-semibold text-code-muted uppercase tracking-wider">Change Type</th>
33
+ <th class="px-6 py-3 text-left text-xs font-semibold text-code-muted uppercase tracking-wider">Details</th>
34
+ <th class="px-6 py-3 text-left text-xs font-semibold text-code-muted uppercase tracking-wider">Changed By</th>
35
+ <th class="px-6 py-3 text-left text-xs font-semibold text-code-muted uppercase tracking-wider">When</th>
36
+ </tr>
37
+ </thead>
38
+ <tbody class="divide-y divide-code-border">
39
+ {% for entry in history %}
40
+ <tr class="hover:bg-code-bg/50 transition-colors">
41
+ <td class="px-6 py-4">
42
+ {% if entry.change_type == 'created' %}
43
+ <span class="px-2 py-1 text-xs rounded bg-code-green/20 text-code-green">Created</span>
44
+ {% elif entry.change_type == 'updated' %}
45
+ <span class="px-2 py-1 text-xs rounded bg-code-accent/20 text-code-accent">Updated</span>
46
+ {% elif entry.change_type == 'enabled' %}
47
+ <span class="px-2 py-1 text-xs rounded bg-code-green/20 text-code-green">Enabled</span>
48
+ {% elif entry.change_type == 'disabled' %}
49
+ <span class="px-2 py-1 text-xs rounded bg-code-yellow/20 text-code-yellow">Disabled</span>
50
+ {% elif entry.change_type == 'deleted' %}
51
+ <span class="px-2 py-1 text-xs rounded bg-code-red/20 text-code-red">Deleted</span>
52
+ {% endif %}
53
+ </td>
54
+ <td class="px-6 py-4 text-code-text text-sm">
55
+ {% if entry.new_config %}
56
+ {% if entry.new_config.run_mode %}
57
+ Mode: {{ entry.new_config.run_mode }}
58
+ {% endif %}
59
+ {% if entry.new_config.interval_minutes %}
60
+ | Interval: {{ entry.new_config.interval_minutes }} min
61
+ {% endif %}
62
+ {% if entry.new_config.daily_times %}
63
+ | Times: {{ entry.new_config.daily_times|join:", " }}
64
+ {% endif %}
65
+ {% else %}
66
+ -
67
+ {% endif %}
68
+ </td>
69
+ <td class="px-6 py-4 text-code-muted">
70
+ {% if entry.changed_by %}{{ entry.changed_by.email }}{% else %}System{% endif %}
71
+ </td>
72
+ <td class="px-6 py-4 text-code-muted text-sm">
73
+ {{ entry.created_at|date:"M d, Y H:i" }}
74
+ </td>
75
+ </tr>
76
+ {% endfor %}
77
+ </tbody>
78
+ </table>
79
+ </div>
80
+ {% else %}
81
+ <div class="bg-code-surface border border-code-border rounded-xl p-8 text-center">
82
+ <p class="text-code-muted">No schedule history yet.</p>
83
+ </div>
84
+ {% endif %}
85
+
86
+ <div class="mt-6">
87
+ <a href="{% url 'cpanel:script_detail' pk=script.pk %}" class="text-code-accent hover:underline">
88
+ &larr; Back to script
89
+ </a>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ {% endblock %}