diff --git a/README.md b/README.md
index a265e029..ea2c7d20 100644
--- a/README.md
+++ b/README.md
@@ -78,6 +78,11 @@ environment and define a default project.
* Go to `http://localhost:8000/admin/codespeed/project/`
and create a project.
+An environment that no longer receives results can be marked as historical
+by unticking its "Active" field in the admin. Historical environments keep
+their data and permalinks, but are listed in a folded "Historical" group in
+the Changes, Timeline and Comparison views and are not selected by default.
+
Check the field "Track changes" and, in case you want version control
integration, configure the relevant fields.
@@ -192,6 +197,7 @@ several parameters (the file includes comments with full examples).
the Timeline and Changes views.
* `DEF_ENVIRONMENT`: Defines which environment should be selected as default
in the Changes and Timeline views.
+ Without it, the first *active* environment is used.
* `CHANGE_THRESHOLD`
* `TREND_THRESHOLD`
@@ -229,8 +235,9 @@ The main customization for the homepage is to display either the reports
* `NORMALIZATION`: Defines whether normalization should be enabled as default
in the Comparison view.
* `CHART_ORIENTATION`: horizontal or vertical
-* `COMP_ENVIRONMENTS_DEFAULT`: `all` (the default) checks every environment;
- `default` checks only `DEF_ENVIRONMENT`, or the first environment.
+* `COMP_ENVIRONMENTS_DEFAULT`: `all` (the default) checks every active
+ environment; `default` checks only `DEF_ENVIRONMENT`, or the first active
+ environment.
* `COMP_EXECUTABLES`: per default all executables will be checked. When there
are a large number of tags or executables, it is better to only select a few
so that the plots are not too cluttered.
diff --git a/codespeed/admin.py b/codespeed/admin.py
index 678cf3fd..681b8961 100644
--- a/codespeed/admin.py
+++ b/codespeed/admin.py
@@ -76,8 +76,10 @@ class BenchmarkAdmin(admin.ModelAdmin):
@admin.register(Environment)
class EnvironmentAdmin(admin.ModelAdmin):
- list_display = ('name', 'cpu', 'memory', 'os', 'kernel')
- ordering = ['name']
+ list_display = ('name', 'cpu', 'memory', 'os', 'kernel', 'active')
+ list_editable = ('active',)
+ list_filter = ('active',)
+ ordering = ['-active', 'name']
search_fields = ('name', 'cpu', 'memory', 'os', 'kernel')
diff --git a/codespeed/management/commands/import_sqlite_dump.py b/codespeed/management/commands/import_sqlite_dump.py
index 9aba6198..752da791 100644
--- a/codespeed/management/commands/import_sqlite_dump.py
+++ b/codespeed/management/commands/import_sqlite_dump.py
@@ -19,6 +19,7 @@
_BOOL_COLS = {
'codespeed_project': {'track'},
'codespeed_branch': {'display_on_comparison_page'},
+ 'codespeed_environment': {'active'},
'codespeed_benchmark': {'lessisbetter', 'default_on_comparison'},
}
diff --git a/codespeed/migrations/0007_environment_active.py b/codespeed/migrations/0007_environment_active.py
new file mode 100644
index 00000000..a864dedb
--- /dev/null
+++ b/codespeed/migrations/0007_environment_active.py
@@ -0,0 +1,18 @@
+# Generated by Django 6.1.2 on 2026-11-13 18:18
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('codespeed', '0006_alter_benchmark_name_alter_benchmark_unique_together'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='environment',
+ name='active',
+ field=models.BooleanField(db_default=True, default=True, help_text='Untick for a historical environment that no longer receives results. It is then listed in a folded "Historical" group and not selected by default.'),
+ ),
+ ]
diff --git a/codespeed/models.py b/codespeed/models.py
index 12a6f4aa..c5d740de 100644
--- a/codespeed/models.py
+++ b/codespeed/models.py
@@ -214,6 +214,11 @@ class Environment(models.Model):
memory = models.CharField(max_length=100, blank=True)
os = models.CharField(max_length=100, blank=True)
kernel = models.CharField(max_length=100, blank=True)
+ active = models.BooleanField(
+ default=True, db_default=True,
+ help_text="Untick for a historical environment that no longer "
+ "receives results. It is then listed in a folded "
+ "\"Historical\" group and not selected by default.")
def __str__(self):
return self.name
diff --git a/codespeed/static/css/main.css b/codespeed/static/css/main.css
index 3b3700de..da548606 100644
--- a/codespeed/static/css/main.css
+++ b/codespeed/static/css/main.css
@@ -379,7 +379,7 @@ div.about_content p, div.about_content li, div.about_content h2, div.about_conte
.compplot-wrap { display: inline-block; }
a.togglefold { color: var(--text); font-weight: 500; }
-a.togglefold:hover { text-decoration: none; }
+a.togglefold:hover, a.togglefold:focus { text-decoration: none; }
a.togglefold::before {
/* Bootstrap Icons chevron-right (MIT, twbs/icons), coloured via mask */
content: '';
diff --git a/codespeed/static/js/changes.js b/codespeed/static/js/changes.js
index 2c93f153..fc612580 100644
--- a/codespeed/static/js/changes.js
+++ b/codespeed/static/js/changes.js
@@ -177,6 +177,7 @@ function init(defaults) {
$("input[name='executable']").change(changeRevisions);
$("#env" + defaults.environment).prop('checked', true);
+ unfoldGroups($("input[name='environment']:checked"));
$("input[name='environment']").change(function() {
updateRevisionMarkers($("input[name='environment']:checked").val());
refreshContent();
diff --git a/codespeed/static/js/codespeed.js b/codespeed/static/js/codespeed.js
index a31111d6..1c1fafb6 100644
--- a/codespeed/static/js/codespeed.js
+++ b/codespeed/static/js/codespeed.js
@@ -23,6 +23,14 @@ function getLoadText(text, h) {
return loadtext;
}
+// Unfold the .togglefold groups holding the given inputs.
+function unfoldGroups(inputs) {
+ inputs.closest("ul").each(function() {
+ $(this).children("li").show();
+ $(this).children("a.togglefold").removeClass('folded');
+ });
+}
+
$(function() {
// Check all and none links
$('.checkall').each(function() {
diff --git a/codespeed/static/js/timeline.js b/codespeed/static/js/timeline.js
index 50ed2352..55b60d0e 100644
--- a/codespeed/static/js/timeline.js
+++ b/codespeed/static/js/timeline.js
@@ -625,7 +625,7 @@ function setValuesOfInputFields(params) {
}
benchRadio.prop('checked', true);
// reveal the suite accordion section that holds the selected benchmark
- benchRadio.closest("ul").show().children("a.togglefold").removeClass('folded');
+ unfoldGroups(benchRadio);
var envDefault = (defaults.environments || []).map(String).join(',');
var envIds = valueOrDefault(params.env, envDefault).split(',').filter(Boolean);
@@ -633,6 +633,7 @@ function setValuesOfInputFields(params) {
envIds.forEach(function(envId) {
envSel.filter("[value='" + envId.trim() + "']").prop('checked', true);
});
+ unfoldGroups(envSel.filter(':checked'));
updateEnvIndicators();
updateExeColors();
$("#baselinecolor").css("background-color", baselineColor);
diff --git a/codespeed/templates/codespeed/changes.html b/codespeed/templates/codespeed/changes.html
index c55ee513..0d9ffa96 100644
--- a/codespeed/templates/codespeed/changes.html
+++ b/codespeed/templates/codespeed/changes.html
@@ -22,7 +22,15 @@
{% endfor %}
-
+ {% if historical_environments %}
+
Historical
+ {% for env in historical_environments %}
+