modified
accounts/models.py
@@ -29,13 +29,3 @@ class User(AbstractUser): for property in self.properties.all(): total_properties_down += property.current_status != 200 return total_properties_down @property def avg_response_time(self): avg_response_times = [] for property in self.properties.all(): avg_response_times.append(property.avg_response_time) try: return int(sum(avg_response_times) / len(avg_response_times)) except ZeroDivisionError: return 0
modified
properties/management/commands/scheduler.py
@@ -19,19 +19,24 @@ class Command(BaseCommand): def thread_target(self, property_id): property = Property.objects.get(id=property_id) self.stdout.write("[Scheduler] Checking status {}".format(property.url)) property.process_check() self.stdout.write("[Scheduler] Checked {}".format(property.url)) def thread_target_lighthouse(self, property_id): property = Property.objects.get(id=property_id) self.stdout.write("[Scheduler] Checking lighthouse {}".format(property.url)) property.process_check_lighthouse() def handle(self, *args, **options): self.stdout.write("[Scheduler] Starting scheduler...") while True: # Do our standard checks properties = [p for p in Property.objects.all() if p.should_check()] for p in properties: p.next_run_at = p.get_next_run_at() p.last_run_at = timezone.now() p.save() p.save(update_fields=["next_run_at", "last_run_at"]) properties = [p.id for p in properties] db.connections.close_all()
@@ -42,6 +47,22 @@ class Command(BaseCommand): self.clean_checks() # Do our daily lighthouse checks # Only run 1 of these checks per loop to avoid overloading the server properties = [p for p in Property.objects.all() if p.should_check_lighthouse()] properties = properties[:1] for p in properties: p.next_lighthouse_run_at = p.get_next_run_at_lighthouse() p.last_lighthouse_run_at = timezone.now() p.save(update_fields=["next_lighthouse_run_at", "last_lighthouse_run_at"]) properties = [p.id for p in properties] db.connections.close_all() for p_id in properties: t = threading.Thread(target=self.thread_target_lighthouse, args=(p_id,)) t.daemon = True t.start() self.stdout.write("[Scheduler] Sleeping scheduler for 30 seconds...") try: time.sleep(30)
added
properties/migrations/0002_property_last_lighthouse_run_at_and_more.py
@@ -0,0 +1,28 @@# Generated by Django 4.0.6 on 2022-07-15 17:23from django.db import migrations, modelsclass Migration(migrations.Migration): dependencies = [ ('properties', '0001_initial'), ] operations = [ migrations.AddField( model_name='property', name='last_lighthouse_run_at', field=models.DateTimeField(blank=True, null=True), ), migrations.AddField( model_name='property', name='lighthouse_scores', field=models.JSONField(blank=True, null=True), ), migrations.AddField( model_name='property', name='next_lighthouse_run_at', field=models.DateTimeField(blank=True, null=True), ), ]
modified
properties/models.py
@@ -9,6 +9,8 @@ from django.template.loader import render_to_stringfrom django.utils import timezonefrom django.utils.functional import cached_propertyfrom status.lighthouse import fetch_lighthouse_results, parse_lighthouse_resultsUser = get_user_model()
@@ -154,6 +156,10 @@ class Property(AlertsMixin, SecurityMixin, models.Model): last_run_at = models.DateTimeField(blank=True, null=True) next_run_at = models.DateTimeField(blank=True, null=True) lighthouse_scores = models.JSONField(blank=True, null=True) last_lighthouse_run_at = models.DateTimeField(blank=True, null=True) next_lighthouse_run_at = models.DateTimeField(blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True)
@@ -236,6 +242,35 @@ class Property(AlertsMixin, SecurityMixin, models.Model): if check.status_code != 200: self.send_alerts() def get_next_run_at_lighthouse(self): """ Should check daily. """ return timezone.now() + timezone.timedelta(days=1) def should_check_lighthouse(self): now = timezone.now() if self.lighthouse_scores is None: return True if self.last_lighthouse_run_at is None: return True if self.next_lighthouse_run_at is None: return True return self.next_lighthouse_run_at <= now def process_check_lighthouse(self): self.run_check_lighthouse() def run_check_lighthouse(self): try: results = fetch_lighthouse_results(self.url) scores = parse_lighthouse_results(results) if scores: self.lighthouse_scores = scores self.save(update_fields=["lighthouse_scores"]) except Exception: pass @property def total_checks(self): return self.statuses.count()
@@ -268,6 +303,11 @@ class Property(AlertsMixin, SecurityMixin, models.Model): except Check.DoesNotExist: return {} @cached_property def avg_lighthouse_score(self): if self.lighthouse_scores: scores = [score for score in self.lighthouse_scores.values()] return round(sum(scores) / len(scores))class Check(models.Model): property = models.ForeignKey(
modified
properties/templates/properties/properties.html
@@ -94,12 +94,6 @@ <p class="card-text">Properties down</p> </div> </div> <div class="col-6 col-md-2 d-flex align-items-center"> <div class="card-body py-1"> <div class="card-title h3 mb-0">{{ user.avg_response_time }}</div> <p class="card-text">Avg. response time</p> </div> </div> <div class="col-6 col-md-2 d-flex align-items-center"> <div class="card-body py-1"> <div class="card-title h3 mb-0">{{ user.total_checks }}</div>
@@ -121,7 +115,7 @@ Delete </button> </div> <div class="modal fade" id="delete-modal-{{ property.id }}" tabindex="-1" aria-labelledby="delete-modal-{{ property.id }}-label" aria-hidden="true"> <div class="modal fade text-dark" id="delete-modal-{{ property.id }}" tabindex="-1" aria-labelledby="delete-modal-{{ property.id }}-label" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header">
@@ -159,10 +153,16 @@ <div class="card-text text-truncate small">Security</div> </div> </div> <div class="col-6 col-md-2 d-none d-md-flex d-flex align-items-center text-white {% if property.avg_response_time > 500 %}bg-danger{% else %}bg-success{% endif %}"> <div class="col-6 col-md-2 d-none d-md-flex d-flex align-items-center text-white {% if property.avg_lighthouse_score < 70 %}bg-warning{% else %}bg-success{% endif %}"> <div class="card-body"> <div class="card-title h4">{% if property.avg_response_time > 500 %}Unhealthy{% else %}Ok{% endif %}</div> <div class="card-text text-truncate small">Response time</div> <div class="card-title h4"> {% if property.avg_lighthouse_score %} {{ property.avg_lighthouse_score }}% {% else %} Checking... {% endif %} </div> <div class="card-text text-truncate small">Avg. LH score</div> </div> </div> </div>
modified
properties/templates/properties/property.html
@@ -56,7 +56,7 @@ <button type="button" class="btn btn-sm btn-outline-danger ms-1 ms-lg-3 my-1" data-bs-toggle="modal" data-bs-target="#delete-modal-{{ property.id }}"> Delete </button> <div class="modal fade" id="delete-modal-{{ property.id }}" tabindex="-1" aria-labelledby="delete-modal-{{ property.id }}-label" aria-hidden="true"> <div class="modal fade text-dark" id="delete-modal-{{ property.id }}" tabindex="-1" aria-labelledby="delete-modal-{{ property.id }}-label" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header">
@@ -81,27 +81,27 @@</div><div class="container-fluid"> <div class="row mb-4"> <div class="row {% if not property.lighthouse_scores %}mb-4{% endif %}"> <div class="col-6 col-md-3 d-flex align-items-center text-white {% if property.current_status == 200 %}bg-success{% else %}bg-danger{% endif %}"> <div class="card-body"> <div class="card-body text-center"> <div class="card-title h4">{% if property.current_status == 200 %}Ok{% else %}Failed{% endif %}</div> <div class="card-text text-truncate small">Current status</div> </div> </div> <div class="col-6 col-md-3 d-flex align-items-center text-white {% if not property.invalid_cert %}bg-success{% else %}bg-danger{% endif %}"> <div class="card-body"> <div class="card-body text-center"> <div class="card-title h4">{% if not property.invalid_cert %}Ok{% else %}Unhealthy{% endif %}</div> <div class="card-text text-truncate small">Certificate</div> </div> </div> <div class="col-6 col-md-3 d-none d-md-flex d-flex align-items-center text-white {% if not property.has_security_issue %}bg-success{% else %}bg-danger{% endif %}"> <div class="card-body"> <div class="card-body text-center"> <div class="card-title h4">{% if not property.has_security_issue %}Ok{% else %}Failed{% endif %}</div> <div class="card-text text-truncate small">Security</div> </div> </div> <div class="col-6 col-md-3 d-none d-md-flex d-flex align-items-center text-white {% if property.avg_response_time > 500 %}bg-danger{% else %}bg-success{% endif %}"> <div class="card-body"> <div class="card-body text-center"> <div class="card-title h4">{% if property.avg_response_time > 500 %}Unhealthy{% else %}Ok{% endif %}</div> <div class="card-text text-truncate small">Response time</div> </div>
@@ -109,6 +109,21 @@ </div></div>{% if property.lighthouse_scores %}<div class="container-fluid"> <div class="row mb-4"> {% for category, score in property.lighthouse_scores.items %} <div class="col-6 col-md-3 d-flex align-items-center text-white {% if score > 70 %}bg-success{% else %}bg-warning{% endif %}"> <div class="card-body text-center"> <div class="card-title h4">{{ score }}</div> <div class="card-text text-truncate small">{{ category }}</div> </div> </div> {% endfor %} </div></div>{% endif %}<div class="container mt-4"> <div class="row"> <div class="col-12 col-md-8">
modified
status/lighthouse.py
@@ -11,9 +11,10 @@ def fetch_lighthouse_results(url): command = [ f"{settings.BASE_DIR}/node_modules/.bin/lighthouse", url, '--chrome-flags="--headless --no-sandbox"', '--chrome-flags="--headless --no-sandbox --disable-dev-shm-usage"', '--output="json"', '--output-path="stdout"', '--max-wait-for-load=5000', "--quiet", ] try:
@@ -26,10 +27,10 @@ def fetch_lighthouse_results(url):def parse_lighthouse_results(results): try: scores = { "performance": results["categories"]["performance"]["score"], "accessibility": results["categories"]["accessibility"]["score"], "best-practices": results["categories"]["best-practices"]["score"], "seo": results["categories"]["seo"]["score"], "Performance": results["categories"]["performance"]["score"], "Accessibility": results["categories"]["accessibility"]["score"], "Best practices": results["categories"]["best-practices"]["score"], "SEO": results["categories"]["seo"]["score"], } except KeyError: return None