• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

scoringengine / scoringengine / 23880703624

02 Apr 2026 02:26AM UTC coverage: 90.624% (+0.01%) from 90.614%
23880703624

push

github

web-flow
Merge pull request #1195 from scoringengine/feature/inject-categories-1174

Add inject category field (Business/Technical/Incident Response)

7 of 9 new or added lines in 3 files covered. (77.78%)

3 existing lines in 1 file now uncovered.

4736 of 5226 relevant lines covered (90.62%)

0.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.71
/scoring_engine/web/views/api/admin.py
1
import html
1✔
2
import json
1✔
3
import os
1✔
4
import re
1✔
5
import time
1✔
6
from datetime import datetime, timezone
1✔
7

8
import pytz
1✔
9
from dateutil.parser import parse
1✔
10
from flask import flash, jsonify, redirect, request, url_for
1✔
11
from flask_login import current_user, login_required
1✔
12

13

14
def _ensure_utc_aware(dt):
1✔
15
    """Ensure datetime is timezone-aware in UTC. Handles both naive and aware datetimes."""
16
    if dt is None:
1✔
17
        return None
×
18
    if dt.tzinfo is None:
1✔
19
        # Naive datetime - assume UTC
20
        return pytz.utc.localize(dt)
1✔
21
    # Already aware - convert to UTC
22
    return dt.astimezone(pytz.utc)
×
23

24

25
from sqlalchemy.orm import joinedload
1✔
26
from sqlalchemy.orm.exc import NoResultFound
1✔
27
from sqlalchemy.sql import func
1✔
28

29
from scoring_engine.cache_helper import (
1✔
30
    update_all_cache,
31
    update_all_inject_data,
32
    update_inject_comments,
33
    update_inject_data,
34
    update_overview_data,
35
    update_scoreboard_data,
36
    update_service_data,
37
    update_services_data,
38
    update_services_navbar,
39
    update_team_stats,
40
)
41
from scoring_engine.events import publish_event
1✔
42
from scoring_engine.celery_stats import CeleryStats
1✔
43
from scoring_engine.config import config
1✔
44
from scoring_engine.db import db
1✔
45
from scoring_engine.engine.basic_check import CHECK_FAILURE_TEXT, CHECK_SUCCESS_TEXT, CHECK_TIMED_OUT_TEXT
1✔
46
from scoring_engine.engine.engine import Engine
1✔
47
from scoring_engine.engine.execute_command import execute_command
1✔
48
from scoring_engine.models.check import Check
1✔
49
from scoring_engine.models.environment import Environment
1✔
50
from scoring_engine.models.inject import Inject, InjectComment, InjectRubricScore, RubricItem, Template
1✔
51
from scoring_engine.models.kb import KB
1✔
52
from scoring_engine.models.property import Property
1✔
53
from scoring_engine.models.round import Round
1✔
54
from scoring_engine.models.service import Service
1✔
55
from scoring_engine.models.setting import Setting
1✔
56
from scoring_engine.models.team import Team
1✔
57
from scoring_engine.models.user import User
1✔
58
from scoring_engine.models.welcome import get_welcome_config, save_welcome_config
1✔
59
from scoring_engine.notifications import notify_inject_graded, notify_revision_requested
1✔
60

61
from . import mod
1✔
62

63

64
@mod.route("/api/admin/update_environment_info", methods=["POST"])
1✔
65
@login_required
1✔
66
def admin_update_environment():
1✔
67
    if current_user.is_white_team:
1✔
68
        if "name" in request.form and "value" in request.form and "pk" in request.form:
1✔
69
            environment = db.session.get(Environment, int(request.form["pk"]))
1✔
70
            if environment:
1✔
71
                if request.form["name"] in ("matching_content", "matching_content_reject"):
1✔
72
                    value = html.escape(request.form["value"])
1✔
73
                    if value:
1✔
74
                        try:
1✔
75
                            re.compile(value)
1✔
76
                        except re.error as e:
1✔
77
                            return jsonify({"error": "Invalid regex pattern: " + str(e)}), 400
1✔
78
                    setattr(environment, request.form["name"], value or None)
1✔
79
                db.session.add(environment)
1✔
80
                db.session.commit()
1✔
81
                return jsonify({"status": "Updated Environment Information"})
1✔
82
            return jsonify({"error": "Incorrect permissions"})
1✔
83
    return jsonify({"error": "Incorrect permissions"})
1✔
84

85

86
@mod.route("/api/admin/update_property", methods=["POST"])
1✔
87
@login_required
1✔
88
def admin_update_property():
1✔
89
    if current_user.is_white_team:
1✔
90
        if "name" in request.form and "value" in request.form and "pk" in request.form:
1✔
91
            property_obj = db.session.get(Property, int(request.form["pk"]))
1✔
92
            if property_obj:
1✔
93
                if request.form["name"] == "property_name":
1✔
94
                    property_obj.name = html.escape(request.form["value"])
1✔
95
                elif request.form["name"] == "property_value":
1✔
96
                    property_obj.value = html.escape(request.form["value"])
1✔
97
                db.session.add(property_obj)
1✔
98
                db.session.commit()
1✔
99
                return jsonify({"status": "Updated Property Information"})
1✔
100
            return jsonify({"error": "Incorrect permissions"})
1✔
101
    return jsonify({"error": "Incorrect permissions"})
1✔
102

103

104
@mod.route("/api/admin/update_check", methods=["POST"])
1✔
105
@login_required
1✔
106
def admin_update_check():
1✔
107
    if current_user.is_white_team:
1✔
108
        if "name" in request.form and "value" in request.form and "pk" in request.form:
1✔
109
            check = db.session.get(Check, int(request.form["pk"]))
1✔
110
            if check:
1✔
111
                modified_check = False
1✔
112
                if request.form["name"] == "check_value":
1✔
113
                    if request.form["value"] == "1":
1✔
114
                        check.result = True
1✔
115
                    elif request.form["value"] == "2":
1✔
116
                        check.result = False
1✔
117
                    modified_check = True
1✔
118
                elif request.form["name"] == "check_reason":
1✔
119
                    modified_check = True
1✔
120
                    check.reason = request.form["value"]
1✔
121
                if modified_check:
1✔
122
                    db.session.add(check)
1✔
123
                    db.session.commit()
1✔
124
                    update_scoreboard_data()
1✔
125
                    update_overview_data()
1✔
126
                    update_services_navbar(check.service.team.id)
1✔
127
                    update_team_stats(check.service.team.id)
1✔
128
                    update_services_data(check.service.team.id)
1✔
129
                    update_service_data(check.service.id)
1✔
130
                    return jsonify({"status": "Updated Property Information"})
1✔
131
            return jsonify({"error": "Incorrect permissions"})
1✔
132
    return jsonify({"error": "Incorrect permissions"})
1✔
133

134

135
@mod.route("/api/admin/update_host", methods=["POST"])
1✔
136
@login_required
1✔
137
def admin_update_host():
1✔
138
    if current_user.is_white_team:
1✔
139
        if "name" in request.form and "value" in request.form and "pk" in request.form:
1✔
140
            service = db.session.get(Service, int(request.form["pk"]))
1✔
141
            if service:
1✔
142
                if request.form["name"] == "host":
1✔
143
                    service.host = html.escape(request.form["value"])
1✔
144
                    db.session.add(service)
1✔
145
                    db.session.commit()
1✔
146
                    update_overview_data()
1✔
147
                    update_services_data(service.team.id)
1✔
148
                    update_service_data(service.id)
1✔
149
                    return jsonify({"status": "Updated Service Information"})
1✔
150
    return jsonify({"error": "Incorrect permissions"})
1✔
151

152

153
@mod.route("/api/admin/update_port", methods=["POST"])
1✔
154
@login_required
1✔
155
def admin_update_port():
1✔
156
    if current_user.is_white_team:
1✔
157
        if "name" in request.form and "value" in request.form and "pk" in request.form:
1✔
158
            service = db.session.get(Service, int(request.form["pk"]))
1✔
159
            if service:
1✔
160
                if request.form["name"] == "port":
1✔
161
                    service.port = int(request.form["value"])
1✔
162
                    db.session.add(service)
1✔
163
                    db.session.commit()
1✔
164
                    update_overview_data()
1✔
165
                    update_services_data(service.team.id)
1✔
166
                    update_service_data(service.id)
1✔
167
                    return jsonify({"status": "Updated Service Information"})
1✔
168
    return jsonify({"error": "Incorrect permissions"})
1✔
169

170

171
@mod.route("/api/admin/update_worker_queue", methods=["POST"])
1✔
172
@login_required
1✔
173
def admin_update_worker_queue():
1✔
174
    if current_user.is_white_team:
1✔
175
        if "name" in request.form and "value" in request.form and "pk" in request.form:
1✔
176
            service = db.session.get(Service, int(request.form["pk"]))
1✔
177
            if service:
1✔
178
                if request.form["name"] == "worker_queue":
1✔
179
                    service.worker_queue = request.form["value"]
1✔
180
                    db.session.add(service)
1✔
181
                    db.session.commit()
1✔
182
                    return jsonify({"status": "Updated Service Information"})
1✔
183
    return jsonify({"error": "Incorrect permissions"})
1✔
184

185

186
@mod.route("/api/admin/update_points", methods=["POST"])
1✔
187
@login_required
1✔
188
def admin_update_points():
1✔
189
    if current_user.is_white_team:
1✔
190
        if "name" in request.form and "value" in request.form and "pk" in request.form:
1✔
191
            service = db.session.get(Service, int(request.form["pk"]))
1✔
192
            if service:
1✔
193
                if request.form["name"] == "points":
1✔
194
                    service.points = int(request.form["value"])
1✔
195
                    db.session.add(service)
1✔
196
                    db.session.commit()
1✔
197
                    return jsonify({"status": "Updated Service Information"})
1✔
198
    return jsonify({"error": "Incorrect permissions"})
1✔
199

200

201
@mod.route("/api/admin/update_about_page_content", methods=["POST"])
1✔
202
@login_required
1✔
203
def admin_update_about_page_content():
1✔
204
    if current_user.is_white_team:
1✔
205
        if "about_page_content" in request.form:
1✔
206
            setting = Setting.get_setting("about_page_content")
1✔
207
            setting.value = request.form["about_page_content"]
1✔
208
            db.session.add(setting)
1✔
209
            db.session.commit()
1✔
210
            Setting.clear_cache("about_page_content")
1✔
211
            flash("About Page Content Successfully Updated.", "success")
1✔
212
            return redirect(url_for("admin.settings"))
1✔
213
        flash("Error: about_page_content not specified.", "danger")
1✔
214
        return redirect(url_for("admin.manage"))
1✔
215
    return {"status": "Unauthorized"}, 403
1✔
216

217

218
@mod.route("/api/admin/update_welcome_page_content", methods=["POST"])
1✔
219
@login_required
1✔
220
def admin_update_welcome_page_content():
1✔
221
    if current_user.is_white_team:
1✔
222
        if "welcome_page_content" in request.form:
1✔
223
            setting = Setting.get_setting("welcome_page_content")
1✔
224
            setting.value = request.form["welcome_page_content"]
1✔
225
            db.session.add(setting)
1✔
226
            db.session.commit()
1✔
227
            Setting.clear_cache("welcome_page_content")
1✔
228
            flash("Welcome Page Content Successfully Updated.", "success")
1✔
229
            return redirect(url_for("admin.settings"))
1✔
230
        flash("Error: welcome_page_content not specified.", "danger")
1✔
231
        return redirect(url_for("admin.manage"))
1✔
232
    return {"status": "Unauthorized"}, 403
1✔
233

234

235
@mod.route("/api/admin/update_target_round_time", methods=["POST"])
1✔
236
@login_required
1✔
237
def admin_update_target_round_time():
1✔
238
    if current_user.is_white_team:
1✔
239
        if "target_round_time" in request.form:
1✔
240
            setting = Setting.get_setting("target_round_time")
1✔
241
            input_time = request.form["target_round_time"]
1✔
242
            if not input_time.isdigit():
1✔
243
                flash("Error: Target Round Time must be an integer.", "danger")
1✔
244
                return redirect(url_for("admin.settings"))
1✔
245
            setting.value = input_time
1✔
246
            db.session.add(setting)
1✔
247
            db.session.commit()
1✔
248
            Setting.clear_cache("target_round_time")
1✔
249
            flash("Target Round Time Successfully Updated.", "success")
1✔
250
            return redirect(url_for("admin.settings"))
1✔
251
        flash("Error: target_round_time not specified.", "danger")
1✔
252
        return redirect(url_for("admin.settings"))
1✔
253
    return {"status": "Unauthorized"}, 403
1✔
254

255

256
@mod.route("/api/admin/update_worker_refresh_time", methods=["POST"])
1✔
257
@login_required
1✔
258
def admin_update_worker_refresh_time():
1✔
259
    if current_user.is_white_team:
1✔
260
        if "worker_refresh_time" in request.form:
1✔
261
            setting = Setting.get_setting("worker_refresh_time")
1✔
262
            input_time = request.form["worker_refresh_time"]
1✔
263
            if not input_time.isdigit():
1✔
264
                flash("Error: Worker Refresh Time must be an integer.", "danger")
1✔
265
                return redirect(url_for("admin.settings"))
1✔
266
            setting.value = input_time
1✔
267
            db.session.add(setting)
1✔
268
            db.session.commit()
1✔
269
            Setting.clear_cache("worker_refresh_time")
1✔
270
            flash("Worker Refresh Time Successfully Updated.", "success")
1✔
271
            return redirect(url_for("admin.settings"))
1✔
272
        flash("Error: worker_refresh_time not specified.", "danger")
1✔
273
        return redirect(url_for("admin.settings"))
1✔
274
    return {"status": "Unauthorized"}, 403
1✔
275

276

277
@mod.route("/api/admin/update_blueteam_edit_hostname", methods=["POST"])
1✔
278
@login_required
1✔
279
def admin_update_blueteam_edit_hostname():
1✔
280
    if current_user.is_white_team:
1✔
281
        setting = Setting.get_setting("blue_team_update_hostname")
1✔
282
        if setting.value is True:
1✔
283
            setting.value = False
1✔
284
        else:
285
            setting.value = True
1✔
286
        db.session.add(setting)
1✔
287
        db.session.commit()
1✔
288
        Setting.clear_cache("blue_team_update_hostname")
1✔
289
        return redirect(url_for("admin.permissions"))
1✔
290
    return {"status": "Unauthorized"}, 403
1✔
291

292

293
@mod.route("/api/admin/update_blueteam_edit_port", methods=["POST"])
1✔
294
@login_required
1✔
295
def admin_update_blueteam_edit_port():
1✔
296
    if current_user.is_white_team:
1✔
297
        setting = Setting.get_setting("blue_team_update_port")
1✔
298
        if setting.value is True:
1✔
299
            setting.value = False
1✔
300
        else:
301
            setting.value = True
1✔
302
        db.session.add(setting)
1✔
303
        db.session.commit()
1✔
304
        Setting.clear_cache("blue_team_update_port")
1✔
305
        return redirect(url_for("admin.permissions"))
1✔
306
    return {"status": "Unauthorized"}, 403
1✔
307

308

309
@mod.route("/api/admin/update_blueteam_edit_account_usernames", methods=["POST"])
1✔
310
@login_required
1✔
311
def admin_update_blueteam_edit_account_usernames():
1✔
312
    if current_user.is_white_team:
1✔
313
        setting = Setting.get_setting("blue_team_update_account_usernames")
1✔
314
        if setting.value is True:
1✔
315
            setting.value = False
1✔
316
        else:
317
            setting.value = True
1✔
318
        db.session.add(setting)
1✔
319
        db.session.commit()
1✔
320
        Setting.clear_cache("blue_team_update_account_usernames")
1✔
321
        return redirect(url_for("admin.permissions"))
1✔
322
    return {"status": "Unauthorized"}, 403
1✔
323

324

325
@mod.route("/api/admin/update_blueteam_edit_account_passwords", methods=["POST"])
1✔
326
@login_required
1✔
327
def admin_update_blueteam_edit_account_passwords():
1✔
328
    if current_user.is_white_team:
1✔
329
        setting = Setting.get_setting("blue_team_update_account_passwords")
1✔
330
        if setting.value is True:
1✔
331
            setting.value = False
1✔
332
        else:
333
            setting.value = True
1✔
334
        db.session.add(setting)
1✔
335
        db.session.commit()
1✔
336
        Setting.clear_cache("blue_team_update_account_passwords")
1✔
337
        return redirect(url_for("admin.permissions"))
1✔
338
    return {"status": "Unauthorized"}, 403
1✔
339

340

341
@mod.route("/api/admin/update_blueteam_view_check_output", methods=["POST"])
1✔
342
@login_required
1✔
343
def admin_update_blueteam_view_check_output():
1✔
344
    if current_user.is_white_team:
1✔
345
        setting = Setting.get_setting("blue_team_view_check_output")
1✔
346
        if setting.value is True:
1✔
347
            setting.value = False
1✔
348
        else:
349
            setting.value = True
1✔
350
        db.session.add(setting)
1✔
351
        db.session.commit()
1✔
352
        Setting.clear_cache("blue_team_view_check_output")
1✔
353
        return redirect(url_for("admin.permissions"))
1✔
354
    return {"status": "Unauthorized"}, 403
1✔
355

356

357
@mod.route("/api/admin/update_anonymize_team_names", methods=["POST"])
1✔
358
@login_required
1✔
359
def admin_update_anonymize_team_names():
1✔
360
    if current_user.is_white_team:
1✔
361
        setting = Setting.get_setting("anonymize_team_names")
1✔
362
        if setting is None:
1✔
363
            # Create the setting if it doesn't exist
364
            setting = Setting(name="anonymize_team_names", value=True)
×
365
        elif setting.value is True:
1✔
366
            setting.value = False
1✔
367
        else:
368
            setting.value = True
1✔
369
        db.session.add(setting)
1✔
370
        db.session.commit()
1✔
371
        Setting.clear_cache("anonymize_team_names")
1✔
372
        return redirect(url_for("admin.permissions"))
1✔
373
    return {"status": "Unauthorized"}, 403
1✔
374

375

376
@mod.route("/api/admin/check/<int:check_id>/full_output")
1✔
377
@login_required
1✔
378
def admin_get_check_full_output(check_id):
1✔
379
    if current_user.is_white_team:
1✔
380
        check = db.session.get(Check, check_id)
1✔
381
        if not check:
1✔
382
            return jsonify({"error": "Check not found"}), 404
1✔
383

384
        team_name = check.service.team.name
1✔
385
        service_name = check.service.name
1✔
386
        round_num = check.round.number
1✔
387

388
        # Try disk file first, fall back to DB output
389
        output_path = os.path.join(
1✔
390
            config.check_output_folder,
391
            team_name,
392
            service_name,
393
            f"round_{round_num}.txt",
394
        )
395

396
        if os.path.isfile(output_path):
1✔
397
            with open(output_path, "r") as f:
1✔
398
                content = f.read()
1✔
399
        else:
400
            content = check.output or ""
1✔
401

402
        return content, 200, {"Content-Type": "text/plain; charset=utf-8"}
1✔
403
    return {"status": "Unauthorized"}, 403
1✔
404

405

406
@mod.route("/api/admin/get_round_progress")
1✔
407
@login_required
1✔
408
def get_check_progress_total():
1✔
409
    if current_user.is_white_team:
×
410
        task_id_settings = db.session.query(KB).filter_by(name="task_ids").order_by(KB.round_num.desc()).first()
×
411
        total_stats = {}
×
412
        total_stats["finished"] = 0
×
413
        total_stats["pending"] = 0
×
414

415
        team_stats = {}
×
416
        if task_id_settings:
×
417
            task_dict = json.loads(task_id_settings.value)
×
418
            for team_name, task_ids in task_dict.items():
×
419
                for task_id in task_ids:
×
420
                    task = execute_command.AsyncResult(task_id)
×
421
                    if team_name not in team_stats:
×
422
                        team_stats[team_name] = {}
×
423
                        team_stats[team_name]["pending"] = 0
×
424
                        team_stats[team_name]["finished"] = 0
×
425

426
                    if task.state == "PENDING":
×
427
                        team_stats[team_name]["pending"] += 1
×
428
                        total_stats["pending"] += 1
×
429
                    else:
430
                        team_stats[team_name]["finished"] += 1
×
431
                        total_stats["finished"] += 1
×
432

433
        total_percentage = 0
×
434
        total_tasks = total_stats["finished"] + total_stats["pending"]
×
435
        if total_stats["finished"] == 0:
×
436
            total_percentage = 0
×
437
        elif total_tasks == 0:
×
438
            total_percentage = 100
×
439
        elif total_stats and total_stats["finished"]:
×
440
            total_percentage = int((total_stats["finished"] / total_tasks) * 100)
×
441

442
        output_dict = {"Total": total_percentage}
×
443
        for team_name, team_stat in team_stats.items():
×
444
            team_total_percentage = 0
×
445
            team_total_tasks = team_stat["finished"] + team_stat["pending"]
×
446
            if team_stat["finished"] == 0:
×
447
                team_total_percentage = 0
×
448
            elif team_total_tasks == 0:
×
449
                team_total_percentage = 100
×
450
            elif team_stat and team_stat["finished"]:
×
451
                team_total_percentage = int((team_stat["finished"] / team_total_tasks) * 100)
×
452
            output_dict[team_name] = team_total_percentage
×
453

454
        return json.dumps(output_dict)
×
455
    else:
456
        return {"status": "Unauthorized"}, 403
×
457

458

459
@mod.route("/api/admin/inject/<inject_id>/reopen", methods=["POST"])
1✔
460
@login_required
1✔
461
def admin_post_inject_reopen(inject_id):
1✔
462
    if current_user.is_white_team:
1✔
463
        inject = db.session.get(Inject, inject_id)
1✔
464
        if not inject:
1✔
465
            return jsonify({"status": "Invalid Inject ID"}), 400
1✔
466
        if inject.status != "Graded":
1✔
467
            return jsonify({"status": "Inject is not in Graded state"}), 400
1✔
468

469
        inject.status = "Submitted"
1✔
470
        inject.graded = None
1✔
471
        comment = InjectComment(
1✔
472
            f"Inject reopened for re-grading by {current_user.username}.",
473
            current_user,
474
            inject,
475
        )
476
        db.session.add(comment)
1✔
477
        db.session.commit()
1✔
478
        update_inject_data(inject_id)
1✔
479
        update_inject_comments(inject_id)
1✔
480
        update_scoreboard_data()
1✔
481
        publish_event("inject_update", {"inject_id": inject_id}, visibility="blue", team_id=inject.team_id)
1✔
482
        publish_event("inject_update", {"inject_id": inject_id}, visibility="white")
1✔
483
        return jsonify({"status": "Success"}), 200
1✔
484
    else:
485
        return {"status": "Unauthorized"}, 403
1✔
486

487

488
@mod.route("/api/admin/injects/templates/<template_id>")
1✔
489
@login_required
1✔
490
def admin_get_inject_templates_id(template_id):
1✔
491
    if current_user.is_white_team:
1✔
492
        template = db.session.get(Template, int(template_id))
1✔
493
        data = dict(
1✔
494
            id=template.id,
495
            title=template.title,
496
            scenario=template.scenario,
497
            deliverable=template.deliverable,
498
            category=template.category,
499
            max_score=template.max_score,
500
            start_time=_ensure_utc_aware(template.start_time).astimezone(pytz.timezone(config.timezone)).isoformat(),
501
            end_time=_ensure_utc_aware(template.end_time).astimezone(pytz.timezone(config.timezone)).isoformat(),
502
            enabled=template.enabled,
503
            rubric_items=[
504
                {"id": x.id, "title": x.title, "description": x.description, "points": x.points, "order": x.order}
505
                for x in template.rubric_items
506
            ],
507
            teams=[inject.team.name for inject in template.injects if inject.enabled and inject.team],
508
        )
509
        return jsonify(data)
1✔
510
    else:
511
        return jsonify({"status": "Unauthorized"}), 403
1✔
512

513

514
@mod.route("/api/admin/injects/templates/<template_id>", methods=["PUT"])
1✔
515
@login_required
1✔
516
def admin_put_inject_templates_id(template_id):
1✔
517
    if current_user.is_white_team:
1✔
518
        data = request.get_json()
1✔
519
        template = db.session.get(Template, int(template_id))
1✔
520
        if template:
1✔
521
            if data.get("title"):
1✔
522
                template.title = data["title"]
1✔
523
            if data.get("scenario"):
1✔
524
                template.scenario = data["scenario"]
1✔
525
            if data.get("deliverable"):
1✔
526
                template.deliverable = data["deliverable"]
1✔
527
            if data.get("start_time"):
1✔
528
                template.start_time = parse(data["start_time"]).astimezone(pytz.utc).replace(tzinfo=None)
×
529
            if data.get("end_time"):
1✔
530
                template.end_time = parse(data["end_time"]).astimezone(pytz.utc).replace(tzinfo=None)
×
531
            if "category" in data:
1✔
NEW
532
                template.category = data["category"] or None
×
533
            # TODO - Fix this to not be string values from javascript select
534
            if data.get("status") == "Enabled":
1✔
535
                template.enabled = True
×
536
            elif data.get("status") == "Disabled":
1✔
537
                template.enabled = False
1✔
538
            if "rubric_items" in data:
1✔
539
                # Update existing rubric items in place to preserve scores
540
                existing_by_id = {item.id: item for item in template.rubric_items}
1✔
541
                incoming_ids = set()
1✔
542
                for idx, ri in enumerate(data["rubric_items"]):
1✔
543
                    ri_id = ri.get("id")
1✔
544
                    if ri_id and ri_id in existing_by_id:
1✔
545
                        # Update existing item
546
                        item = existing_by_id[ri_id]
1✔
547
                        item.title = ri["title"]
1✔
548
                        item.points = ri["points"]
1✔
549
                        item.description = ri.get("description")
1✔
550
                        item.order = ri.get("order", idx)
1✔
551
                        incoming_ids.add(ri_id)
1✔
552
                    else:
553
                        # Create new item
554
                        rubric_item = RubricItem(
1✔
555
                            title=ri["title"],
556
                            points=ri["points"],
557
                            template=template,
558
                            description=ri.get("description"),
559
                            order=ri.get("order", idx),
560
                        )
561
                        db.session.add(rubric_item)
1✔
562
                # Delete only items that were removed from the payload
563
                for item_id, item in existing_by_id.items():
1✔
564
                    if item_id not in incoming_ids:
1✔
565
                        db.session.delete(item)
×
566
            if data.get("selectedTeams"):
1✔
567
                for team_name in data["selectedTeams"]:
1✔
568
                    inject = (
1✔
569
                        db.session.query(Inject)
570
                        .join(Template)
571
                        .join(Team)
572
                        .filter(Team.name == team_name)
573
                        .filter(Template.id == template_id)
574
                        .one_or_none()
575
                    )
576
                    # Update inject if it exists
577
                    if inject:
1✔
578
                        inject.enabled = True
×
579
                    # Otherwise, create the inject
580
                    else:
581
                        team = db.session.query(Team).filter(Team.name == team_name).first()
1✔
582
                        inject = Inject(
1✔
583
                            team=team,
584
                            template=template,
585
                        )
586
                        db.session.add(inject)
1✔
587
            if data.get("unselectedTeams"):
1✔
588
                injects = (
1✔
589
                    db.session.query(Inject)
590
                    .join(Template)
591
                    .join(Team)
592
                    .filter(Team.name.in_(data["unselectedTeams"]))
593
                    .filter(Template.id == template_id)
594
                    .all()
595
                )
596
                for inject in injects:
1✔
597
                    inject.enabled = False
1✔
598
            db.session.commit()
1✔
599
            return jsonify({"status": "Success"}), 200
1✔
600
        else:
601
            return jsonify({"status": "Error", "message": "Template not found"}), 400
1✔
602

603
    else:
604
        return jsonify({"status": "Unauthorized"}), 403
1✔
605

606

607
@mod.route("/api/admin/injects/templates/<template_id>", methods=["DELETE"])
1✔
608
@login_required
1✔
609
def admin_delete_inject_templates_id(template_id):
1✔
610
    if current_user.is_white_team:
1✔
611
        template = db.session.get(Template, int(template_id))
1✔
612
        if template:
1✔
613
            db.session.delete(template)
1✔
614
            db.session.commit()
1✔
615
            return jsonify({"status": "Success"}), 200
1✔
616
        else:
617
            return jsonify({"status": "Error", "message": "Template not found"}), 400
1✔
618
    else:
619
        return jsonify({"status": "Unauthorized"}), 403
1✔
620

621

622
@mod.route("/api/admin/injects/templates")
1✔
623
@login_required
1✔
624
def admin_get_inject_templates():
1✔
625
    if current_user.is_white_team:
1✔
626
        data = list()
1✔
627
        templates = db.session.query(Template).options(joinedload(Template.injects)).all()
1✔
628
        for template in templates:
1✔
629
            data.append(
1✔
630
                dict(
631
                    id=template.id,
632
                    title=template.title,
633
                    category=template.category,
634
                    scenario=template.scenario,
635
                    deliverable=template.deliverable,
636
                    max_score=template.max_score,
637
                    start_time=template.start_time.astimezone(pytz.timezone(config.timezone)).isoformat(),
638
                    end_time=template.end_time.astimezone(pytz.timezone(config.timezone)).isoformat(),
639
                    enabled=template.enabled,
640
                    rubric_items=[
641
                        {
642
                            "id": x.id,
643
                            "title": x.title,
644
                            "description": x.description,
645
                            "points": x.points,
646
                            "order": x.order,
647
                        }
648
                        for x in template.rubric_items
649
                    ],
650
                    teams=[inject.team.name for inject in template.injects if inject if inject.enabled if inject.team],
651
                )
652
            )
653
        return jsonify(data=data)
1✔
654
    else:
655
        return {"status": "Unauthorized"}, 403
1✔
656

657

658
@mod.route("/api/admin/inject/<inject_id>/grade", methods=["POST"])
1✔
659
@login_required
1✔
660
def admin_post_inject_grade(inject_id):
1✔
661
    if current_user.is_white_team:
1✔
662
        data = request.get_json()
1✔
663
        if "rubric_scores" not in data or not data["rubric_scores"]:
1✔
664
            return jsonify({"status": "Invalid Score Provided"}), 400
1✔
665
        inject = db.session.get(Inject, inject_id)
1✔
666
        if not inject:
1✔
667
            return jsonify({"status": "Invalid Inject ID"}), 400
1✔
668

669
        # Validate rubric items belong to the template
670
        template_item_ids = {item.id for item in inject.template.rubric_items}
1✔
671
        template_item_max = {item.id: item.points for item in inject.template.rubric_items}
1✔
672
        for rs in data["rubric_scores"]:
1✔
673
            if rs["rubric_item_id"] not in template_item_ids:
1✔
674
                return jsonify({"status": "Invalid rubric item ID"}), 400
1✔
675
            if rs["score"] > template_item_max[rs["rubric_item_id"]]:
1✔
676
                return jsonify({"status": "Score exceeds rubric item max"}), 400
1✔
677

678
        # Delete existing scores and create new ones
679
        for old_score in list(inject.rubric_scores):
1✔
680
            db.session.delete(old_score)
×
681
        db.session.flush()
1✔
682

683
        for rs in data["rubric_scores"]:
1✔
684
            rubric_item = db.session.get(RubricItem, rs["rubric_item_id"])
1✔
685
            score_obj = InjectRubricScore(
1✔
686
                score=rs["score"],
687
                inject=inject,
688
                rubric_item=rubric_item,
689
                grader=current_user,
690
            )
691
            db.session.add(score_obj)
1✔
692

693
        inject.graded = datetime.now(timezone.utc)
1✔
694
        inject.status = "Graded"
1✔
695
        score = sum(rs["score"] for rs in data["rubric_scores"])
1✔
696
        comment = InjectComment(
1✔
697
            f"Inject graded by {current_user.username}. Score: {score}/{inject.template.max_score}.",
698
            current_user,
699
            inject,
700
        )
701
        db.session.add(comment)
1✔
702
        db.session.commit()
1✔
703
        update_inject_data(inject_id)
1✔
704
        update_inject_comments(inject_id)
1✔
705
        update_scoreboard_data()
1✔
706
        publish_event("inject_update", {"inject_id": inject_id}, visibility="blue", team_id=inject.team_id)
1✔
707
        publish_event("inject_update", {"inject_id": inject_id}, visibility="white")
1✔
708
        notify_inject_graded(inject)
1✔
709
        return jsonify({"status": "Success"}), 200
1✔
710
    else:
711
        return {"status": "Unauthorized"}, 403
1✔
712

713

714
@mod.route("/api/admin/inject/<inject_id>/request-revision", methods=["POST"])
1✔
715
@login_required
1✔
716
def admin_post_inject_request_revision(inject_id):
1✔
717
    if current_user.is_white_team:
1✔
718
        inject = db.session.get(Inject, inject_id)
1✔
719
        if not inject:
1✔
720
            return jsonify({"status": "Invalid Inject ID"}), 400
1✔
721
        if inject.status not in ("Submitted", "Resubmitted"):
1✔
722
            return jsonify({"status": "Inject is not in a submittable state"}), 400
1✔
723

724
        data = request.get_json() or {}
1✔
725
        reason = data.get("reason", "Revision requested by grader.")
1✔
726

727
        inject.status = "Revision Requested"
1✔
728
        # Create a system comment with the reason
729
        comment = InjectComment(content=reason, user=current_user, inject=inject)
1✔
730
        db.session.add(comment)
1✔
731
        db.session.commit()
1✔
732
        update_inject_data(inject_id)
1✔
733
        update_inject_comments(inject_id)
1✔
734
        publish_event("inject_update", {"inject_id": inject_id}, visibility="blue", team_id=inject.team_id)
1✔
735
        publish_event("inject_update", {"inject_id": inject_id}, visibility="white")
1✔
736
        notify_revision_requested(inject)
1✔
737
        return jsonify({"status": "Success"}), 200
1✔
738
    else:
739
        return {"status": "Unauthorized"}, 403
1✔
740

741

742
@mod.route("/api/admin/injects/template/<int:template_id>/submissions")
1✔
743
@login_required
1✔
744
def admin_get_template_submissions(template_id):
1✔
745
    if current_user.is_white_team:
1✔
746
        template = db.session.get(Template, template_id)
1✔
747
        if not template:
1✔
748
            return jsonify({"status": "Template not found"}), 404
1✔
749

750
        submissions = []
1✔
751
        for inject in template.injects:
1✔
752
            if not inject.team or not inject.enabled:
1✔
753
                continue
×
754
            submissions.append(
1✔
755
                {
756
                    "id": inject.id,
757
                    "team": inject.team.name,
758
                    "status": inject.status,
759
                    "score": inject.score,
760
                    "max_score": template.max_score,
761
                    "submitted": inject.submitted.isoformat() if inject.submitted else None,
762
                    "graded": inject.graded.isoformat() if inject.graded else None,
763
                }
764
            )
765
        return jsonify(data=submissions), 200
1✔
766
    else:
767
        return {"status": "Unauthorized"}, 403
1✔
768

769

770
@mod.route("/api/admin/injects/templates", methods=["POST"])
1✔
771
@login_required
1✔
772
def admin_post_inject_templates():
1✔
773
    if current_user.is_white_team:
1✔
774
        data = request.get_json()
1✔
775
        if (
1✔
776
            data.get("title")
777
            and data.get("scenario")
778
            and data.get("deliverable")
779
            and data.get("start_time")
780
            and data.get("end_time")
781
        ):
782
            template = Template(
1✔
783
                title=data["title"],
784
                scenario=data["scenario"],
785
                deliverable=data["deliverable"],
786
                start_time=parse(data["start_time"]).astimezone(pytz.utc).replace(tzinfo=None),
787
                end_time=parse(data["end_time"]).astimezone(pytz.utc).replace(tzinfo=None),
788
                category=data.get("category"),
789
            )
790
            db.session.add(template)
1✔
791
            db.session.flush()
1✔
792
            # Create rubric items
793
            if data.get("rubric_items"):
1✔
794
                for i, item_data in enumerate(data["rubric_items"]):
1✔
795
                    rubric_item = RubricItem(
1✔
796
                        title=item_data["title"],
797
                        description=item_data.get("description", ""),
798
                        points=item_data["points"],
799
                        order=item_data.get("order", i),
800
                        template=template,
801
                    )
802
                    db.session.add(rubric_item)
1✔
803
            db.session.commit()
1✔
804
            # TODO - Fix this to not be string values from javascript select
805
            if data.get("status") == "Enabled":
1✔
806
                template.enabled = True
1✔
807
            elif data.get("status") == "Disabled":
1✔
808
                template.enabled = False
×
809
            if data.get("selectedTeams"):
1✔
810
                for team_name in data["selectedTeams"]:
1✔
811
                    inject = (
1✔
812
                        db.session.query(Inject)
813
                        .join(Template)
814
                        .join(Team)
815
                        .filter(Team.name == team_name)
816
                        .filter(Template.id == template.id)
817
                        .one_or_none()
818
                    )
819
                    # Update inject if it exists
820
                    if inject:
1✔
821
                        inject.enabled = True
×
822
                    # Otherwise, create the inject
823
                    else:
824
                        team = db.session.query(Team).filter(Team.name == team_name).first()
1✔
825
                        inject = Inject(
1✔
826
                            team=team,
827
                            template=template,
828
                        )
829
                        db.session.add(inject)
1✔
830
            if data.get("unselectedTeams"):
1✔
831
                injects = (
×
832
                    db.session.query(Inject)
833
                    .join(Template)
834
                    .join(Team)
835
                    .filter(Team.name.in_(data["unselectedTeams"]))
836
                    .filter(Template.id == template.id)
837
                    .all()
838
                )
839
                for inject in injects:
×
840
                    inject.enabled = False
×
841
            db.session.commit()
1✔
842
            return jsonify({"status": "Success"}), 200
1✔
843
        else:
844
            return jsonify({"status": "Error", "message": "Missing Data"}), 400
1✔
845
    else:
846
        return {"status": "Unauthorized"}, 403
1✔
847

848

849
# @mod.route("/api/admin/injects/templates/export")
850
# @login_required
851
# def admin_export_inject_templates():
852
#     if current_user.is_white_team:
853
#         data = []
854
#         templates = db.session.query(Template).all()
855
#         for template in templates:
856
#             data.append(
857
#                 dict(
858
#                     id=template.id,
859
#                     title=template.title,
860
#                     scenario=template.scenario,
861
#                     deliverable=template.deliverable,
862
#                     start_time=template.start_time,
863
#                     end_time=template.end_time,
864
#                     enabled=template.enabled,
865
#                     rubric=[
866
#                         {"id": x.id, "value": x.value, "deliverable": x.deliverable}
867
#                         for x in template.rubric
868
#                     ],
869
#                     # TODO - export teams
870
#                 )
871
#             )
872
#         return jsonify(data=data)
873
#     else:
874
#         return {"status": "Unauthorized"}, 403
875

876

877
# TODO - Generate injects from templates
878
@mod.route("/api/admin/injects/templates/import", methods=["POST"])
1✔
879
@login_required
1✔
880
def admin_import_inject_templates():
1✔
881
    if current_user.is_white_team:
1✔
882
        data = request.get_json()
1✔
883
        if data:
1✔
884
            for d in data:
1✔
885
                if d.get("id"):
1✔
886
                    template_id = d["id"]
1✔
887
                    t = db.session.get(Template, int(template_id))
1✔
888
                    # Update template if it exists
889
                    if t:
1✔
890
                        if d.get("title"):
1✔
891
                            t.title = d["title"]
1✔
892
                        if d.get("scenario"):
1✔
893
                            t.scenario = d["scenario"]
1✔
894
                        if d.get("deliverable"):
1✔
895
                            t.deliverable = d["deliverable"]
1✔
896
                        if d.get("start_time"):
1✔
897
                            t.start_time = parse(d["start_time"]).astimezone(pytz.utc).replace(tzinfo=None)
1✔
898
                        if d.get("end_time"):
1✔
899
                            t.end_time = parse(d["end_time"]).astimezone(pytz.utc).replace(tzinfo=None)
1✔
900
                        if "category" in d:
1✔
NEW
901
                            t.category = d["category"] or None
×
902
                        if d.get("enabled"):
1✔
903
                            t.enabled = True
1✔
904
                        else:
905
                            t.enabled = False
×
906
                        # for rubric in d["rubric"]:
907
                        #     if rubric.get("id"):
908
                        #         rubric_id = rubric["id"]
909
                        #     r = db.session.get(Rubric, int(rubric_id))
910
                        #     # Update rubric if it exists
911
                        #     if r:
912
                        #         if rubric.get("value"):
913
                        #             r.value = rubric["value"]
914
                        #         if rubric.get("deliverable"):
915
                        #             r.deliverable = rubric["deliverable"]
916
                        #     # Otherwise, create the rubric
917
                        #     else:
918
                        #         r = Rubric(
919
                        #             value=rubric["value"],
920
                        #             deliverable=rubric["deliverable"],
921
                        #             template=template,
922
                        #         )
923
                        #         db.session.add(r)
924
                        # Generate injects from template
925
                        if d.get("selectedTeams"):
1✔
926
                            for team_name in data["selectedTeams"]:
×
927
                                inject = (
×
928
                                    db.session.query(Inject)
929
                                    .join(Template)
930
                                    .join(Team)
931
                                    .filter(Team.name == team_name)
932
                                    .filter(Template.id == template_id)
933
                                    .one_or_none()
934
                                )
935
                                # Update inject if it exists
936
                                if inject:
×
937
                                    inject.enabled = True
×
938
                                # Otherwise, create the inject
939
                                else:
940
                                    team = db.session.query(Team).filter(Team.name == team_name).first()
×
941
                                    inject = Inject(
×
942
                                        team=team,
943
                                        template=template,
944
                                    )
945
                                    db.session.add(inject)
×
946
                        if d.get("unselectedTeams"):
1✔
947
                            injects = (
×
948
                                db.session.query(Inject)
949
                                .join(Template)
950
                                .join(Team)
951
                                .filter(Team.name.in_(data["unselectedTeams"]))
952
                                .filter(Template.id == template_id)
953
                                .all()
954
                            )
955
                            for inject in injects:
×
956
                                inject.enabled = False
×
957

958
                    else:
959
                        # Template ID doesn't exist, fall through to create a new one
960
                        d.pop("id")
1✔
961
                if not d.get("id"):
1✔
962
                    # Create the template
963
                    t = Template(
1✔
964
                        title=d["title"],
965
                        scenario=d["scenario"],
966
                        deliverable=d["deliverable"],
967
                        start_time=parse(d["start_time"]).astimezone(pytz.utc).replace(tzinfo=None),
968
                        end_time=parse(d["end_time"]).astimezone(pytz.utc).replace(tzinfo=None),
969
                        enabled=d["enabled"],
970
                    )
971
                    db.session.add(t)
1✔
972
                    db.session.flush()
1✔
973
                    # Create rubric items (backward compat: if only "score" exists, create default item)
974
                    if d.get("rubric_items"):
1✔
975
                        for idx, item_data in enumerate(d["rubric_items"]):
1✔
976
                            rubric_item = RubricItem(
1✔
977
                                title=item_data["title"],
978
                                description=item_data.get("description", ""),
979
                                points=item_data["points"],
980
                                order=item_data.get("order", idx),
981
                                template=t,
982
                            )
983
                            db.session.add(rubric_item)
1✔
984
                    elif d.get("score"):
1✔
985
                        rubric_item = RubricItem(
1✔
986
                            title="Overall Quality",
987
                            description="",
988
                            points=int(d["score"]),
989
                            order=0,
990
                            template=t,
991
                        )
992
                        db.session.add(rubric_item)
1✔
993
                    for team_name in d["teams"]:
1✔
994
                        inject = (
1✔
995
                            db.session.query(Inject)
996
                            .join(Template)
997
                            .join(Team)
998
                            .filter(Team.name == team_name)
999
                            .filter(Template.id == t.id)
1000
                            .one_or_none()
1001
                        )
1002
                        # Update inject if it exists
1003
                        if inject:
1✔
1004
                            inject.enabled = True
×
1005
                        # Otherwise, create the inject
1006
                        else:
1007
                            team = db.session.query(Team).filter(Team.name == team_name).first()
1✔
1008
                            if team:
1✔
1009
                                inject = Inject(
1✔
1010
                                    team=team,
1011
                                    template=t,
1012
                                )
1013
                                db.session.add(inject)
1✔
1014
            db.session.commit()
1✔
1015
            return jsonify({"status": "Success"}), 200
1✔
1016
        else:
1017
            return jsonify({"status": "Error", "message": "Invalid Data"}), 400
1✔
1018
    else:
1019
        return {"status": "Unauthorized"}, 403
1✔
1020

1021

1022
# TODO - This is way too many database queries
1023
@mod.route("/api/admin/injects/scores")
1✔
1024
@login_required
1✔
1025
def admin_inject_scores():
1✔
1026
    if current_user.is_white_team:
1✔
1027
        data = {}
1✔
1028

1029
        injects = (
1✔
1030
            db.session.query(Inject)
1031
            .options(joinedload(Inject.template), joinedload(Inject.team))
1032
            .order_by(Inject.template_id)
1033
            .order_by(Inject.team_id)
1034
            .all()
1035
        )
1036

1037
        for inject in injects:
1✔
1038
            if inject.team is None:
1✔
1039
                continue
1✔
1040
            if inject.template.id not in data:
1✔
1041
                data[inject.template.id] = {
1✔
1042
                    "title": inject.template.title,
1043
                    "end_time": inject.template.end_time,
1044
                }
1045
            if inject.team.name not in data[inject.template.id]:
1✔
1046
                data[inject.template.id][inject.team.name] = {
1✔
1047
                    "id": inject.id,
1048
                    "score": inject.score,
1049
                    "status": inject.status,
1050
                    "max_score": inject.template.max_score,
1051
                }
1052

1053
        # Rewrite data to be in the format expected by the frontend
1054
        score_data = []
1✔
1055
        for x in data.items():
1✔
1056
            score_data.append(x[1])
1✔
1057

1058
        return jsonify(data=score_data), 200
1✔
1059
    else:
1060
        return {"status": "Unauthorized"}, 403
1✔
1061

1062

1063
@mod.route("/api/admin/injects/get_bar_chart")
1✔
1064
@login_required
1✔
1065
def admin_injects_bar():
1✔
1066
    if current_user.is_white_team:
1✔
1067
        inject_scores = dict(
1✔
1068
            db.session.query(Inject.team_id, func.sum(InjectRubricScore.score))
1069
            .join(InjectRubricScore)
1070
            .filter(Inject.status == "Graded")
1071
            .group_by(Inject.team_id)
1072
            .all()
1073
        )
1074

1075
        team_data = {}
1✔
1076
        team_labels = []
1✔
1077
        team_inject_scores = []
1✔
1078
        blue_teams = db.session.query(Team).filter(Team.color == "Blue").order_by(Team.name).all()
1✔
1079
        for blue_team in blue_teams:
1✔
1080
            team_labels.append(blue_team.name)
1✔
1081
            team_inject_scores.append(str(inject_scores.get(blue_team.id, 0)))
1✔
1082

1083
        team_data["labels"] = team_labels
1✔
1084
        team_data["inject_scores"] = team_inject_scores
1✔
1085

1086
        return jsonify(team_data), 200
1✔
1087
    else:
1088
        return {"status": "Unauthorized"}, 403
1✔
1089

1090

1091
@mod.route("/api/admin/admin_update_template", methods=["POST"])
1✔
1092
@login_required
1✔
1093
def admin_update_template():
1✔
1094
    if current_user.is_white_team:
1✔
1095
        if "name" in request.form and "value" in request.form and "pk" in request.form:
1✔
1096
            template = db.session.get(Template, int(request.form["pk"]))
1✔
1097
            if template:
1✔
1098
                modified_check = False
1✔
1099
                if request.form["name"] == "template_state":
1✔
1100
                    template.state = request.form["value"]
1✔
1101
                    modified_check = True
1✔
1102
                elif request.form["name"] == "template_points":
1✔
1103
                    template.points = request.form["value"]
1✔
1104
                    modified_check = True
1✔
1105
                if modified_check:
1✔
1106
                    db.session.add(template)
1✔
1107
                    db.session.commit()
1✔
1108
                    # update_scoreboard_data()
1109
                    # update_overview_data()
1110
                    # update_services_navbar(check.service.team.id)
1111
                    # update_team_stats(check.service.team.id)
1112
                    # update_services_data(check.service.team.id)
1113
                    # update_service_data(check.service.id)
1114
                    return jsonify({"status": "Updated Property Information"})
1✔
1115
            return jsonify({"error": "Template Not Found"})
1✔
1116
    return jsonify({"error": "Incorrect permissions"})
1✔
1117

1118

1119
# @mod.route("/api/admin/injects/team/<team_id>")
1120
# @login_required
1121
# def admin_get_team_injects(team_id):
1122
#     if current_user.is_white_team:
1123
#         injects = db.session.query(Inject).filter(team_id == team_id).all()
1124
#         return jsonify(data=injects)
1125
#     else:
1126
#         return {"status": "Unauthorized"}, 403
1127

1128

1129
@mod.route("/api/admin/get_teams")
1✔
1130
@login_required
1✔
1131
def admin_get_teams():
1✔
1132
    if current_user.is_white_team:
1✔
1133
        all_teams = db.session.query(Team).all()
1✔
1134
        data = []
1✔
1135
        for team in all_teams:
1✔
1136
            users = {}
1✔
1137
            for user in team.users:
1✔
1138
                users[user.username] = [user.password, str(user.authenticated).title()]
1✔
1139
            data.append({"name": team.name, "color": team.color, "users": users})
1✔
1140
        return jsonify(data=data)
1✔
1141
    else:
1142
        return {"status": "Unauthorized"}, 403
1✔
1143

1144

1145
@mod.route("/api/admin/update_password", methods=["POST"])
1✔
1146
@login_required
1✔
1147
def admin_update_password():
1✔
1148
    if current_user.is_white_team:
1✔
1149
        if "user_id" in request.form and "password" in request.form:
1✔
1150
            try:
1✔
1151
                user_obj = db.session.query(User).filter(User.id == request.form["user_id"]).one()
1✔
1152
            except NoResultFound:
1✔
1153
                return redirect(url_for("auth.login"))
1✔
1154
            user_obj.update_password(html.escape(request.form["password"]))
1✔
1155
            user_obj.authenticated = False
1✔
1156
            db.session.add(user_obj)
1✔
1157
            db.session.commit()
1✔
1158
            flash("Password Successfully Updated.", "success")
1✔
1159
            return redirect(url_for("admin.manage"))
1✔
1160
        else:
1161
            flash("Error: user_id or password not specified.", "danger")
1✔
1162
            return redirect(url_for("admin.manage"))
1✔
1163
    else:
1164
        return {"status": "Unauthorized"}, 403
1✔
1165

1166

1167
@mod.route("/api/admin/add_user", methods=["POST"])
1✔
1168
@login_required
1✔
1169
def admin_add_user():
1✔
1170
    if current_user.is_white_team:
1✔
1171
        if "username" in request.form and "password" in request.form and "team_id" in request.form:
1✔
1172
            team_obj = db.session.query(Team).filter(Team.id == request.form["team_id"]).one()
1✔
1173
            user_obj = User(
1✔
1174
                username=html.escape(request.form["username"]),
1175
                password=html.escape(request.form["password"]),
1176
                team=team_obj,
1177
            )
1178
            db.session.add(user_obj)
1✔
1179
            db.session.commit()
1✔
1180
            flash("User successfully added.", "success")
1✔
1181
            return redirect(url_for("admin.manage"))
1✔
1182
        else:
1183
            flash("Error: Username, Password, or Team ID not specified.", "danger")
1✔
1184
            return redirect(url_for("admin.manage"))
1✔
1185
    else:
1186
        return {"status": "Unauthorized"}, 403
1✔
1187

1188

1189
@mod.route("/api/admin/add_team", methods=["POST"])
1✔
1190
@login_required
1✔
1191
def admin_add_team():
1✔
1192
    if current_user.is_white_team:
1✔
1193
        if "name" in request.form and "color" in request.form:
1✔
1194
            team_obj = Team(html.escape(request.form["name"]), html.escape(request.form["color"]))
1✔
1195
            db.session.add(team_obj)
1✔
1196
            db.session.commit()
1✔
1197
            flash("Team successfully added.", "success")
1✔
1198
            return redirect(url_for("admin.manage"))
1✔
1199
        else:
1200
            flash("Error: Team name or color not defined.", "danger")
1✔
1201
            return redirect(url_for("admin.manage"))
1✔
1202
    else:
1203
        return {"status": "Unauthorized"}, 403
1✔
1204

1205

1206
@mod.route("/api/admin/toggle_inject_scores_visible", methods=["POST"])
1✔
1207
@login_required
1✔
1208
def admin_toggle_inject_scores_visible():
1✔
1209
    if current_user.is_white_team:
1✔
1210
        Setting.clear_cache("inject_scores_visible")
1✔
1211
        setting = Setting.get_setting("inject_scores_visible")
1✔
1212
        setting.value = not setting.value
1✔
1213
        db.session.add(setting)
1✔
1214
        db.session.commit()
1✔
1215
        Setting.clear_cache("inject_scores_visible")
1✔
1216
        update_scoreboard_data()
1✔
1217
        update_all_inject_data()
1✔
1218
        update_overview_data()
1✔
1219
        publish_event("settings_changed", {"setting": "inject_scores_visible"})
1✔
1220
        return {"status": "Success"}
1✔
1221
    else:
1222
        return {"status": "Unauthorized"}, 403
1✔
1223

1224

1225
@mod.route("/api/admin/toggle_engine", methods=["POST"])
1✔
1226
@login_required
1✔
1227
def admin_toggle_engine():
1✔
1228
    if current_user.is_white_team:
1✔
1229
        Setting.clear_cache("engine_paused")
1✔
1230
        setting = Setting.get_setting("engine_paused")
1✔
1231
        setting.value = not setting.value
1✔
1232
        db.session.add(setting)
1✔
1233
        db.session.commit()
1✔
1234
        Setting.clear_cache("engine_paused")
1✔
1235
        publish_event("settings_changed", {"setting": "engine_paused"})
1✔
1236
        return {"status": "Success"}
1✔
1237
    else:
1238
        return {"status": "Unauthorized"}, 403
1✔
1239

1240

1241
@mod.route("/api/admin/get_engine_stats")
1✔
1242
@login_required
1✔
1243
def admin_get_engine_stats():
1✔
1244
    if current_user.is_white_team:
1✔
1245
        engine_stats = {}
1✔
1246
        engine_stats["round_number"] = Round.get_last_round_num()
1✔
1247
        engine_stats["num_passed_checks"] = db.session.query(Check).filter_by(result=True).count()
1✔
1248
        engine_stats["num_failed_checks"] = db.session.query(Check).filter_by(result=False).count()
1✔
1249
        engine_stats["total_checks"] = db.session.query(Check).count()
1✔
1250
        return jsonify(engine_stats)
1✔
1251
    else:
1252
        return {"status": "Unauthorized"}, 403
1✔
1253

1254

1255
@mod.route("/api/admin/get_engine_paused")
1✔
1256
@login_required
1✔
1257
def admin_get_engine_status():
1✔
1258
    if current_user.is_white_team:
1✔
1259
        return jsonify({"paused": Setting.get_setting("engine_paused").value})
1✔
1260
    else:
1261
        return {"status": "Unauthorized"}, 403
1✔
1262

1263

1264
@mod.route("/api/admin/get_worker_stats")
1✔
1265
@login_required
1✔
1266
def admin_get_worker_stats():
1✔
1267
    if current_user.is_white_team:
1✔
1268
        worker_stats = CeleryStats.get_worker_stats()
1✔
1269
        return jsonify(data=worker_stats)
1✔
1270
    else:
1271
        return {"status": "Unauthorized"}, 403
1✔
1272

1273

1274
@mod.route("/api/admin/get_queue_stats")
1✔
1275
@login_required
1✔
1276
def admin_get_queue_stats():
1✔
1277
    if current_user.is_white_team:
1✔
1278
        queue_stats = CeleryStats.get_queue_stats()
1✔
1279
        return jsonify(data=queue_stats)
1✔
1280
    else:
1281
        return {"status": "Unauthorized"}, 403
1✔
1282

1283

1284
@mod.route("/api/admin/get_competition_summary")
1✔
1285
@login_required
1✔
1286
def admin_get_competition_summary():
1✔
1287
    if current_user.is_white_team:
1✔
1288
        blue_teams = db.session.query(Team).filter(Team.color == "Blue").all()
1✔
1289
        total_services = db.session.query(Service).join(Team).filter(Team.color == "Blue").count()
1✔
1290
        total_checks = db.session.query(Check).count()
1✔
1291
        passed_checks = db.session.query(Check).filter_by(result=True).count()
1✔
1292

1293
        overall_uptime = 0.0
1✔
1294
        if total_checks > 0:
1✔
1295
            overall_uptime = round((passed_checks / total_checks) * 100, 1)
1✔
1296

1297
        last_round = Round.get_last_round_num()
1✔
1298
        currently_passing = 0
1✔
1299
        if last_round > 0:
1✔
1300
            currently_passing = (
1✔
1301
                db.session.query(Check).join(Round).filter(Round.number == last_round, Check.result == True).count()
1302
            )
1303

1304
        return jsonify(
1✔
1305
            {
1306
                "blue_teams": len(blue_teams),
1307
                "total_services": total_services,
1308
                "currently_passing": currently_passing,
1309
                "overall_uptime": overall_uptime,
1310
            }
1311
        )
1312
    else:
1313
        return {"status": "Unauthorized"}, 403
1✔
1314

1315

1316
@mod.route("/api/admin/welcome/config", methods=["GET"])
1✔
1317
@login_required
1✔
1318
def admin_get_welcome_config():
1✔
1319
    """Get the welcome page configuration."""
1320
    if current_user.is_white_team:
1✔
1321
        config = get_welcome_config()
1✔
1322
        return jsonify(data=config)
1✔
1323
    else:
1324
        return {"status": "Unauthorized"}, 403
1✔
1325

1326

1327
@mod.route("/api/admin/welcome/config", methods=["PUT"])
1✔
1328
@login_required
1✔
1329
def admin_update_welcome_config():
1✔
1330
    """Update the welcome page configuration."""
1331
    if current_user.is_white_team:
1✔
1332
        data = request.get_json()
1✔
1333
        if not data:
1✔
1334
            return (
1✔
1335
                jsonify(
1336
                    {
1337
                        "status": "Error",
1338
                        "message": "No data provided",
1339
                    }
1340
                ),
1341
                400,
1342
            )
1343

1344
        try:
1✔
1345
            config = save_welcome_config(data)
1✔
1346
            return jsonify({"status": "Success", "data": config})
1✔
1347
        except ValueError as e:
1✔
1348
            return jsonify({"status": "Error", "message": str(e)}), 400
1✔
1349
    else:
1350
        return {"status": "Unauthorized"}, 403
1✔
1351

1352

1353
@mod.route("/api/admin/welcome/upload_logo", methods=["POST"])
1✔
1354
@login_required
1✔
1355
def admin_upload_sponsor_logo():
1✔
1356
    """Upload a sponsor logo image."""
1357
    import os
×
1358

1359
    from werkzeug.utils import secure_filename
×
1360

1361
    if not current_user.is_white_team:
×
1362
        return jsonify({"status": "Unauthorized"}), 403
×
1363

1364
    if "file" not in request.files:
×
1365
        return (
×
1366
            jsonify(
1367
                {
1368
                    "status": "Error",
1369
                    "message": "No file provided",
1370
                }
1371
            ),
1372
            400,
1373
        )
1374

1375
    file = request.files["file"]
×
1376
    if file.filename == "":
×
1377
        return (
×
1378
            jsonify(
1379
                {
1380
                    "status": "Error",
1381
                    "message": "No file selected",
1382
                }
1383
            ),
1384
            400,
1385
        )
1386

1387
    # Validate file extension
1388
    allowed = {"png", "jpg", "jpeg", "gif", "svg", "webp"}
×
1389
    ext = file.filename.rsplit(".", 1)[-1].lower() if "." in file.filename else ""
×
1390
    if ext not in allowed:
×
1391
        return (
×
1392
            jsonify(
1393
                {
1394
                    "status": "Error",
1395
                    "message": "File type not allowed. Use: " + ", ".join(sorted(allowed)),
1396
                }
1397
            ),
1398
            400,
1399
        )
1400

1401
    filename = secure_filename(file.filename)
×
1402

1403
    # Save to the shared upload folder (Docker volume), not the static dir.
1404
    # Static files are served by nginx from a separate container/mount,
1405
    # so uploads to the web container's static/ dir would be inaccessible.
1406
    sponsors_dir = os.path.join(config.upload_folder, "sponsors")
×
1407
    os.makedirs(sponsors_dir, exist_ok=True)
×
1408

1409
    # Avoid overwriting: add suffix if file exists
1410
    filepath = os.path.join(sponsors_dir, filename)
×
1411
    if os.path.exists(filepath):
×
1412
        name, dot_ext = os.path.splitext(filename)
×
1413
        counter = 1
×
1414
        while os.path.exists(filepath):
×
1415
            filename = f"{name}_{counter}{dot_ext}"
×
1416
            filepath = os.path.join(sponsors_dir, filename)
×
1417
            counter += 1
×
1418

1419
    file.save(filepath)
×
1420

1421
    url = f"/api/admin/welcome/sponsor_logo/{filename}"
×
1422
    return jsonify({"status": "Success", "url": url})
×
1423

1424

1425
@mod.route("/api/admin/welcome/sponsor_logo/<filename>")
1✔
1426
def serve_sponsor_logo(filename):
1✔
1427
    """Serve a sponsor logo from the upload folder."""
1428
    import os
×
1429

1430
    from flask import abort, send_from_directory
×
1431
    from werkzeug.utils import secure_filename
×
1432

1433
    safe_filename = secure_filename(filename)
×
1434
    if not safe_filename:
×
1435
        abort(404)
×
1436
    sponsors_dir = os.path.join(config.upload_folder, "sponsors")
×
1437
    return send_from_directory(sponsors_dir, safe_filename)
×
1438

1439

1440
# Global cache for loaded check classes (loaded once per app lifecycle)
1441
_check_classes = None
1✔
1442

1443

1444
def _get_check_classes():
1✔
1445
    """Load and cache all check classes."""
1446
    global _check_classes
1447
    if _check_classes is None:
×
1448
        _check_classes = {}
×
1449
        loaded_checks = Engine.load_check_files(config.checks_location)
×
1450
        for check_class in loaded_checks:
×
1451
            _check_classes[check_class.__name__] = check_class
×
1452
    return _check_classes
×
1453

1454

1455
@mod.route("/api/admin/check/dry_run", methods=["POST"])
1✔
1456
@login_required
1✔
1457
def admin_check_dry_run():
1✔
1458
    """
1459
    Run a service check without recording results to the database.
1460
    Dispatches to a Celery worker so the check runs in an environment
1461
    that has the required tools (SSH client, DNS utils, etc.).
1462
    """
1463
    if not current_user.is_white_team:
1✔
1464
        return jsonify({"status": "Unauthorized"}), 403
1✔
1465

1466
    data = request.get_json()
1✔
1467
    if not data or "service_id" not in data:
1✔
1468
        return jsonify({"status": "error", "message": "service_id is required"}), 400
1✔
1469

1470
    service_id = data.get("service_id")
1✔
1471
    environment_id = data.get("environment_id")
1✔
1472

1473
    service = db.session.get(Service, int(service_id))
1✔
1474
    if not service:
1✔
1475
        return jsonify({"status": "error", "message": f"Service with id {service_id} not found"}), 404
1✔
1476

1477
    # Get environment (specific or random)
1478
    if environment_id:
1✔
1479
        environment = db.session.get(Environment, int(environment_id))
1✔
1480
        if not environment or environment.service_id != service.id:
1✔
1481
            return jsonify({"status": "error", "message": f"Environment {environment_id} not found for service"}), 404
×
1482
    else:
1483
        if not service.environments:
1✔
1484
            return jsonify({"status": "error", "message": "Service has no environments configured"}), 400
×
1485
        import random
1✔
1486

1487
        environment = random.choice(service.environments)
1✔
1488

1489
    # Load check class and generate command
1490
    check_classes = _get_check_classes()
1✔
1491
    check_class = check_classes.get(service.check_name)
1✔
1492
    if not check_class:
1✔
1493
        return jsonify({"status": "error", "message": f"Check class '{service.check_name}' not found"}), 404
1✔
1494

1495
    try:
1✔
1496
        check_obj = check_class(environment)
1✔
1497
        command = check_obj.command()
1✔
1498
    except LookupError as e:
×
1499
        return jsonify({"status": "error", "message": f"Check configuration error: {str(e)}"}), 400
×
1500
    except Exception as e:
×
1501
        return jsonify({"status": "error", "message": f"Error instantiating check: {str(e)}"}), 500
×
1502

1503
    # Dispatch to a Celery worker via the service's worker queue
1504
    import time
1✔
1505

1506
    job = {"command": command}
1✔
1507
    start_time = time.time()
1✔
1508
    try:
1✔
1509
        result = execute_command.apply_async(args=[job], queue=service.worker_queue)
1✔
1510
        completed_job = result.get(timeout=35)
1✔
1511
    except Exception as e:
×
1512
        return jsonify({"status": "error", "message": f"Worker execution failed: {str(e)}"}), 500
×
1513
    execution_time_ms = int((time.time() - start_time) * 1000)
1✔
1514

1515
    # Evaluate result against matching_content
1516
    if completed_job.get("errored_out"):
1✔
1517
        check_result = False
1✔
1518
        reason = CHECK_TIMED_OUT_TEXT
1✔
1519
    else:
1520
        output = completed_job.get("output", "")
1✔
1521
        if re.search(environment.matching_content, output):
1✔
1522
            check_result = True
1✔
1523
            reason = CHECK_SUCCESS_TEXT
1✔
1524
        else:
1525
            check_result = False
1✔
1526
            reason = CHECK_FAILURE_TEXT
1✔
1527

1528
    return jsonify(
1✔
1529
        {
1530
            "status": "success",
1531
            "result": check_result,
1532
            "reason": reason,
1533
            "output": completed_job.get("output", "")[:35000],
1534
            "command": command,
1535
            "execution_time_ms": execution_time_ms,
1536
            "service_name": service.name,
1537
            "team_name": service.team.name,
1538
            "host": service.host,
1539
            "port": service.port,
1540
            "matching_content": environment.matching_content,
1541
            "environment_id": environment.id,
1542
        }
1543
    )
1544

1545

1546
@mod.route("/api/admin/rollback", methods=["POST"])
1✔
1547
@login_required
1✔
1548
def admin_rollback():
1✔
1549
    """
1550
    Rollback competition data to a specific round.
1551

1552
    Deletes all rounds, checks, and KB entries from the specified round onwards.
1553
    This is a destructive operation - use with caution.
1554

1555
    Request JSON:
1556
    {
1557
        "round_number": 10,  // Delete this round and all after it
1558
        "confirm": true      // Required to confirm destructive action
1559
    }
1560
    """
1561
    if not current_user.is_white_team:
1✔
1562
        return jsonify({"status": "Unauthorized"}), 403
1✔
1563

1564
    data = request.get_json()
1✔
1565
    if not data:
1✔
1566
        return jsonify({"status": "error", "message": "Request body required"}), 400
×
1567

1568
    round_number = data.get("round_number")
1✔
1569
    confirm = data.get("confirm", False)
1✔
1570

1571
    if round_number is None:
1✔
1572
        return jsonify({"status": "error", "message": "round_number is required"}), 400
1✔
1573

1574
    try:
1✔
1575
        round_number = int(round_number)
1✔
1576
    except (ValueError, TypeError):
1✔
1577
        return jsonify({"status": "error", "message": "round_number must be an integer"}), 400
1✔
1578

1579
    if round_number < 1:
1✔
1580
        return jsonify({"status": "error", "message": "round_number must be >= 1"}), 400
1✔
1581

1582
    if not confirm:
1✔
1583
        return jsonify({"status": "error", "message": "confirm=true required for destructive operation"}), 400
1✔
1584

1585
    # Get current round count for validation
1586
    current_round = Round.get_last_round_num()
1✔
1587
    if current_round == 0:
1✔
1588
        return jsonify({"status": "error", "message": "No rounds exist to rollback"}), 400
1✔
1589

1590
    if round_number > current_round:
1✔
1591
        return jsonify(
1✔
1592
            {"status": "error", "message": f"round_number ({round_number}) exceeds current round ({current_round})"}
1593
        ), 400
1594

1595
    # Pause the engine to prevent race conditions during rollback
1596
    was_paused = Setting.get_setting("engine_paused").value
1✔
1597
    if not was_paused:
1✔
1598
        setting = Setting.get_setting("engine_paused")
1✔
1599
        setting.value = True
1✔
1600
        db.session.add(setting)
1✔
1601
        db.session.commit()
1✔
1602
        Setting.clear_cache("engine_paused")
1✔
1603
        # Wait for the engine to finish its current round and notice the pause
1604
        time.sleep(5)
1✔
1605

1606
    try:
1✔
1607
        # Revoke any pending Celery tasks for rounds being rolled back
1608
        task_kb_entries = (
1✔
1609
            db.session.query(KB)
1610
            .filter(KB.round_num >= round_number, KB.name == "task_ids")
1611
            .all()
1612
        )
1613
        revoked_count = 0
1✔
1614
        for kb_entry in task_kb_entries:
1✔
1615
            try:
1✔
1616
                task_dict = json.loads(kb_entry.value)
1✔
1617
                for team_name, task_id_list in task_dict.items():
1✔
1618
                    for task_id in task_id_list:
×
1619
                        execute_command.AsyncResult(task_id).revoke(terminate=True)
×
1620
                        revoked_count += 1
×
1621
            except (json.JSONDecodeError, TypeError):
×
1622
                pass
×
1623

1624
        # Get rounds to delete
1625
        rounds_to_delete = db.session.query(Round).filter(Round.number >= round_number).all()
1✔
1626
        round_ids = [r.id for r in rounds_to_delete]
1✔
1627

1628
        # Count checks to delete
1629
        checks_count = db.session.query(Check).filter(Check.round_id.in_(round_ids)).count()
1✔
1630

1631
        # Count KB entries to delete
1632
        kb_count = db.session.query(KB).filter(KB.round_num >= round_number).count()
1✔
1633

1634
        # Delete checks in batches to avoid lock wait timeout
1635
        BATCH_SIZE = 500
1✔
1636
        for i in range(0, len(round_ids), BATCH_SIZE):
1✔
1637
            batch_ids = round_ids[i : i + BATCH_SIZE]
1✔
1638
            db.session.query(Check).filter(Check.round_id.in_(batch_ids)).delete(synchronize_session=False)
1✔
1639
            db.session.commit()
1✔
1640

1641
        # Delete KB entries
1642
        db.session.query(KB).filter(KB.round_num >= round_number).delete(synchronize_session=False)
1✔
1643
        db.session.commit()
1✔
1644

1645
        # Delete rounds
1646
        rounds_count = len(rounds_to_delete)
1✔
1647
        db.session.query(Round).filter(Round.number >= round_number).delete(synchronize_session=False)
1✔
1648
        db.session.commit()
1✔
1649

1650
    finally:
1651
        # Unpause engine if we paused it (even on error)
1652
        if not was_paused:
1✔
1653
            Setting.clear_cache("engine_paused")
1✔
1654
            setting = Setting.get_setting("engine_paused")
1✔
1655
            setting.value = False
1✔
1656
            db.session.add(setting)
1✔
1657
            db.session.commit()
1✔
1658
            Setting.clear_cache("engine_paused")
1✔
1659

1660
    # Clear all caches to force recalculation
1661
    from flask import current_app
1✔
1662

1663
    update_all_cache(current_app)
1✔
1664

1665
    return jsonify(
1✔
1666
        {
1667
            "status": "success",
1668
            "message": f"Rolled back to before round {round_number}",
1669
            "deleted": {
1670
                "rounds": rounds_count,
1671
                "checks": checks_count,
1672
                "kb_entries": kb_count,
1673
            },
1674
            "new_current_round": Round.get_last_round_num(),
1675
        }
1676
    )
1677

1678

1679
@mod.route("/api/admin/rollback/preview", methods=["POST"])
1✔
1680
@login_required
1✔
1681
def admin_rollback_preview():
1✔
1682
    """Preview what would be deleted by a rollback operation."""
1683
    if not current_user.is_white_team:
1✔
1684
        return jsonify({"status": "Unauthorized"}), 403
1✔
1685

1686
    data = request.get_json()
1✔
1687
    if not data:
1✔
1688
        return jsonify({"status": "error", "message": "Request body required"}), 400
1✔
1689

1690
    round_number = data.get("round_number")
1✔
1691
    if round_number is None:
1✔
1692
        return jsonify({"status": "error", "message": "round_number is required"}), 400
×
1693

1694
    try:
1✔
1695
        round_number = int(round_number)
1✔
1696
    except (ValueError, TypeError):
×
1697
        return jsonify({"status": "error", "message": "round_number must be an integer"}), 400
×
1698

1699
    if round_number < 1:
1✔
1700
        return jsonify({"status": "error", "message": "round_number must be >= 1"}), 400
×
1701

1702
    current_round = Round.get_last_round_num()
1✔
1703
    if current_round == 0:
1✔
1704
        return jsonify(
1✔
1705
            {
1706
                "status": "success",
1707
                "current_round": 0,
1708
                "round_number": round_number,
1709
                "will_delete": {"rounds": 0, "checks": 0, "kb_entries": 0},
1710
            }
1711
        )
1712

1713
    # Get rounds that would be deleted
1714
    rounds_to_delete = db.session.query(Round).filter(Round.number >= round_number).all()
1✔
1715
    round_ids = [r.id for r in rounds_to_delete]
1✔
1716

1717
    # Count checks
1718
    checks_count = db.session.query(Check).filter(Check.round_id.in_(round_ids)).count() if round_ids else 0
1✔
1719

1720
    # Count KB entries
1721
    kb_count = db.session.query(KB).filter(KB.round_num >= round_number).count()
1✔
1722

1723
    return jsonify(
1✔
1724
        {
1725
            "status": "success",
1726
            "current_round": current_round,
1727
            "round_number": round_number,
1728
            "will_delete": {
1729
                "rounds": len(rounds_to_delete),
1730
                "checks": checks_count,
1731
                "kb_entries": kb_count,
1732
            },
1733
        }
1734
    )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc