diff --git a/CHANGELOG.md b/CHANGELOG.md
index 515b8ecf..cae71802 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file.
 - Add the "Badges" page in docs
 - Add support for multiple recipients in incoming email (#669)
 - Upgrade to fido2 1.0.0, requests 2.28.1, segno 1.5.2
+- Implement auto-refresh and running indicator in the My Projects page (#681)
 
 ### Bug Fixes
 - Fix the display of ignored pings with non-zero exit status
diff --git a/hc/accounts/models.py b/hc/accounts/models.py
index 54fb9784..1aa6f596 100644
--- a/hc/accounts/models.py
+++ b/hc/accounts/models.py
@@ -394,19 +394,6 @@ class Project(models.Model):
         for profile in q:
             profile.update_next_nag_date()
 
-    def overall_status(self):
-        if not hasattr(self, "_overall_status"):
-            self._overall_status = "up"
-            for check in self.check_set.all():
-                check_status = check.get_status()
-                if check_status == "grace" and self._overall_status == "up":
-                    self._overall_status = "grace"
-                elif check_status == "down":
-                    self._overall_status = "down"
-                    break
-
-        return self._overall_status
-
     def get_n_down(self):
         result = 0
         for check in self.check_set.all():
diff --git a/hc/front/views.py b/hc/front/views.py
index 137ce6e8..2e09710c 100644
--- a/hc/front/views.py
+++ b/hc/front/views.py
@@ -1,3 +1,4 @@
+from collections import defaultdict
 from datetime import timedelta as td
 import email
 import json
@@ -298,29 +299,51 @@ def switch_channel(request, code, channel_code):
     return HttpResponse()
 
 
+def _get_project_summary(profile):
+    statuses = defaultdict(lambda: {"status": "up", "started": False})
+    q = profile.checks_from_all_projects()
+    q = q.annotate(project_code=F("project__code"))
+    for check in q:
+        summary = statuses[check.project_code]
+        if check.last_start:
+            summary["started"] = True
+
+        if summary["status"] != "down":
+            status = check.get_status()
+            if status == "down" or (status == "grace" and summary["status"] == "up"):
+                summary["status"] = status
+
+    return statuses
+
+
 def index(request):
-    if request.user.is_authenticated:
-        project_ids = request.profile.projects().values("id")
+    if not request.user.is_authenticated:
+        return redirect("hc-login")
 
-        q = Project.objects.filter(id__in=project_ids)
-        q = q.annotate(n_checks=Count("check", distinct=True))
-        q = q.annotate(n_channels=Count("channel", distinct=True))
-        q = q.annotate(owner_email=F("owner__email"))
+    summary = _get_project_summary(request.profile)
+    if "refresh" in request.GET:
+        return JsonResponse({str(k): v for k, v in summary.items()})
 
-        projects = list(q)
-        # Primary sort key: projects with overall_status=down go first
-        # Secondary sort key: project's name
-        projects.sort(key=lambda p: (p.overall_status() != "down", p.name))
+    q = request.profile.projects()
+    q = q.annotate(n_checks=Count("check", distinct=True))
+    q = q.annotate(n_channels=Count("channel", distinct=True))
+    q = q.annotate(owner_email=F("owner__email"))
+    projects = list(q)
+    for project in projects:
+        project.overall_status = summary[project.code]["status"]
+        project.any_started = summary[project.code]["started"]
 
-        ctx = {
-            "page": "projects",
-            "projects": projects,
-            "last_project_id": request.session.get("last_project_id"),
-        }
+    # Primary sort key: projects with overall_status=down go first
+    # Secondary sort key: project's name
+    projects.sort(key=lambda p: (p.overall_status != "down", p.name))
 
-        return render(request, "front/projects.html", ctx)
+    ctx = {
+        "page": "projects",
+        "projects": projects,
+        "last_project_id": request.session.get("last_project_id"),
+    }
 
-    return redirect("hc-login")
+    return render(request, "front/projects.html", ctx)
 
 
 def dashboard(request):
diff --git a/static/css/projects.css b/static/css/projects.css
index 0bba2db3..1d872022 100644
--- a/static/css/projects.css
+++ b/static/css/projects.css
@@ -26,11 +26,6 @@
     text-overflow: ellipsis;
 }
 
-#project-selector .project .status {
-    position: absolute;
-    left: 24px;
-}
-
 #project-selector #add-project .project {
     border-style: dashed;
     padding: 0;
@@ -46,3 +41,8 @@
     color: var(--text-color);
 }
 
+.indicators {
+    position: absolute;
+    left: 24px;
+    width:  24px;
+}
\ No newline at end of file
diff --git a/static/js/projects.js b/static/js/projects.js
new file mode 100644
index 00000000..0a1c129d
--- /dev/null
+++ b/static/js/projects.js
@@ -0,0 +1,32 @@
+$(function () {
+    var base = document.getElementById("base-url").getAttribute("href").slice(0, -1);
+
+    // Schedule refresh to run every 3s when tab is visible and user
+    // is active, every 60s otherwise
+    var lastStatus = {};
+    var lastStarted = {};
+    function refreshStatus() {
+        $.ajax({
+            url: base + "?refresh=1",
+            dataType: "json",
+            timeout: 2000,
+            success: function(data) {
+                for (var code in data) {
+                    var el = data[code];
+
+                    if (el.status != lastStatus[code]) {
+                        $("#" + code + " div.status").attr("class", "status ic-" + el.status);
+                        lastStatus[code] = el.status;
+                    }
+
+                    if (el.started != lastStarted[code]) {
+                        $("#" + code + " div.spinner").toggleClass("started", el.started);
+                        lastStarted[code] = el.started;
+                    }
+                }
+            }
+        });
+    }
+
+    adaptiveSetInterval(refreshStatus);
+});
diff --git a/templates/front/projects.html b/templates/front/projects.html
index b3588c59..0edac1cd 100644
--- a/templates/front/projects.html
+++ b/templates/front/projects.html
@@ -14,9 +14,12 @@
         <div id="project-selector" class="row">
             {% for project in projects %}
             <a href="{% url 'hc-checks' project.code %}">
-                <div class="col-sm-6 col-md-4">
+                <div id="{{ project.code }}" class="col-sm-6 col-md-4">
                     <div class="panel project {% if project.id == last_project_id %}selected{% endif %}">
-                        <div class="status ic-{{ project.overall_status }}"></div>
+                        <div class="indicators">
+                            <div class="status ic-{{ project.overall_status }}"></div>
+                            <div class="spinner {% if project.any_started %}started{% endif %}"></div>
+                        </div>
                         <h4>{{ project }}</h4>
                         <div>
                             {{ project.n_checks }} check{{ project.n_checks|pluralize }},
@@ -51,6 +54,8 @@
 {% compress js %}
 <script src="{% static 'js/jquery-3.6.0.min.js' %}"></script>
 <script src="{% static 'js/bootstrap.min.js' %}"></script>
+<script src="{% static 'js/adaptive-setinterval.js' %}"></script>
 <script src="{% static 'js/add_project_modal.js' %}"></script>
+<script src="{% static 'js/projects.js' %}"></script>
 {% endcompress %}
 {% endblock %}