From 28f6cb1b5fbda9b9f0e8474e4b42934e493e4d04 Mon Sep 17 00:00:00 2001 From: James Collier Date: Thu, 18 Sep 2025 12:17:47 +0200 Subject: [PATCH 01/26] Create a reusable database view into gauge metabolite labelling --- server/alembic.ini | 7 ++- server/migrations/env.py | 7 ++- .../c24eaab852f4_add_a_gauge_labels_view.py | 62 +++++++++++++++++++ ...add_foreign_key_constraint_to_reaction_.py | 3 +- server/poetry.lock | 48 +++++++++++++- server/pyproject.toml | 1 + server/ttfd/models.py | 29 +++++++++ 7 files changed, 151 insertions(+), 6 deletions(-) create mode 100644 server/migrations/versions/c24eaab852f4_add_a_gauge_labels_view.py diff --git a/server/alembic.ini b/server/alembic.ini index a0f547c6..890ba88c 100644 --- a/server/alembic.ini +++ b/server/alembic.ini @@ -76,7 +76,7 @@ sqlalchemy.url = driver://user:pass@localhost/dbname # Logging configuration [loggers] -keys = root,sqlalchemy,alembic +keys = root,sqlalchemy,alembic,alembic_utils [handlers] keys = console @@ -99,6 +99,11 @@ level = INFO handlers = qualname = alembic +[logger_alembic_utils] +level = INFO +handlers = +qualname = alembic_utils + [handler_console] class = StreamHandler args = (sys.stderr,) diff --git a/server/migrations/env.py b/server/migrations/env.py index 55b8c585..2f5007c0 100644 --- a/server/migrations/env.py +++ b/server/migrations/env.py @@ -1,12 +1,12 @@ from logging.config import fileConfig -from sqlalchemy import create_engine - from alembic import context +from alembic_utils.replaceable_entity import register_entities +from sqlalchemy import create_engine from ttfd.config import settings from ttfd.database import engine -from ttfd.models import Base +from ttfd.models import Base, gauge_labelling, intarray_extension, pg_trgm_extension # this is the Alembic Config object, which provides # access to the values within the .ini file in use. @@ -27,6 +27,7 @@ # can be acquired: # my_important_option = config.get_main_option("my_important_option") # ... etc. +register_entities([gauge_labelling, pg_trgm_extension, intarray_extension]) def run_migrations_offline() -> None: diff --git a/server/migrations/versions/c24eaab852f4_add_a_gauge_labels_view.py b/server/migrations/versions/c24eaab852f4_add_a_gauge_labels_view.py new file mode 100644 index 00000000..488ee4b9 --- /dev/null +++ b/server/migrations/versions/c24eaab852f4_add_a_gauge_labels_view.py @@ -0,0 +1,62 @@ +"""Add a gauge labels view. + +Revision ID: c24eaab852f4 +Revises: 43b46c8fe17c +Create Date: 2025-09-18 11:56:02.041315 + +""" + +from alembic import op +from alembic_utils.pg_extension import PGExtension +from alembic_utils.pg_view import PGView + +# revision identifiers, used by Alembic. +revision = "c24eaab852f4" +down_revision = "43b46c8fe17c" +branch_labels = None +depends_on = None + +public_gauge_labelling = PGView( + schema="public", + signature="gauge_labelling", + definition=""" + SELECT rp.id as "path", + l.labeling as "labelling", + ARRAY_AGG(l."label" ORDER BY l."position" ASC) as "labels", + ARRAY_AGG(l."position" ORDER BY l."position" ASC) as "positions" + FROM labels l + JOIN reaction_paths rp ON l."path" = rp.id + JOIN pathways p ON rp.id = p."path" + WHERE rp.end_metabolite = p.metabolite AND p."next" AND l."index" = p."index" + GROUP BY rp.id, l.labeling + """, +) +public_intarray = PGExtension(schema="public", signature="intarray") + + +def upgrade() -> None: + with op.batch_alter_table("sessions", schema=None) as batch_op: + batch_op.drop_index(batch_op.f("ix_api_keys_token")) + batch_op.drop_index(batch_op.f("ix_sessions_token")) + batch_op.create_index(batch_op.f("ix_sessions_token"), ["token"], unique=False) + + with op.batch_alter_table("api_keys", schema=None) as batch_op: + batch_op.create_index(batch_op.f("ix_api_keys_token"), ["token"], unique=False) + + op.create_entity(public_gauge_labelling) # type: ignore [attr-defined] + + op.create_entity(public_intarray) # type: ignore [attr-defined] + + +def downgrade() -> None: + op.drop_entity(public_intarray) # type: ignore [attr-defined] + + op.drop_entity(public_gauge_labelling) # type: ignore [attr-defined] + + with op.batch_alter_table("api_keys", schema=None) as batch_op: + batch_op.drop_index(batch_op.f("ix_api_keys_token")) + + with op.batch_alter_table("sessions", schema=None) as batch_op: + batch_op.drop_index(batch_op.f("ix_sessions_token")) + batch_op.create_index(batch_op.f("ix_sessions_token"), ["token"], unique=True) + batch_op.create_index(batch_op.f("ix_api_keys_token"), ["token"], unique=True) diff --git a/server/migrations/versions/d468630d342c_add_foreign_key_constraint_to_reaction_.py b/server/migrations/versions/d468630d342c_add_foreign_key_constraint_to_reaction_.py index 948ecb9a..8ee96c73 100644 --- a/server/migrations/versions/d468630d342c_add_foreign_key_constraint_to_reaction_.py +++ b/server/migrations/versions/d468630d342c_add_foreign_key_constraint_to_reaction_.py @@ -20,7 +20,7 @@ def upgrade() -> None: """Add a foreign key constraint to reaction_paths.""" - op.execute(sa.text("CREATE EXTENSION pg_trgm;")) + op.execute(sa.text("CREATE EXTENSION IF NOT EXISTS pg_trgm;")) with op.batch_alter_table("metabolites", schema=None) as batch_op: batch_op.alter_column( "mol", @@ -77,3 +77,4 @@ def downgrade() -> None: type_=sa.VARCHAR(), existing_nullable=False, ) + op.execute(sa.text("DROP EXTENSION pg_trgm;")) diff --git a/server/poetry.lock b/server/poetry.lock index cb32d287..d5ed36a6 100644 --- a/server/poetry.lock +++ b/server/poetry.lock @@ -20,6 +20,25 @@ typing-extensions = ">=4.12" [package.extras] tz = ["tzdata"] +[[package]] +name = "alembic-utils" +version = "0.8.8" +description = "A sqlalchemy/alembic extension for migrating procedures and views" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "alembic_utils-0.8.8-py3-none-any.whl", hash = "sha256:2c2545dc545833c5deb63bce2c3cde01c1807bf99da5efab2497bc8d817cb86e"}, + {file = "alembic_utils-0.8.8.tar.gz", hash = "sha256:99de5d13194f26536bc0322f0c1660020a305015700d8447ccfc20e7d1494e5b"}, +] + +[package.dependencies] +alembic = ">=1.9" +flupy = "*" +parse = ">=1.8.4" +sqlalchemy = ">=1.4" +typing_extensions = ">=0.1.0" + [[package]] name = "allpairspy" version = "2.5.1" @@ -682,6 +701,21 @@ typing-extensions = ">=4.8.0" all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] +[[package]] +name = "flupy" +version = "1.2.3" +description = "Fluent data processing in Python - a chainable stream processing library for expressive data manipulation using method chaining" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "flupy-1.2.3-py3-none-any.whl", hash = "sha256:be0f5a393bad2b3534697fbab17081993cd3f5817169dd3a61e8b2e0887612e6"}, + {file = "flupy-1.2.3.tar.gz", hash = "sha256:220b6d40dea238cd2d66784c0d4d2a5483447a48acd343385768e0c740af9609"}, +] + +[package.dependencies] +typing_extensions = ">=4" + [[package]] name = "greenlet" version = "3.2.3" @@ -1365,6 +1399,18 @@ files = [ {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] +[[package]] +name = "parse" +version = "1.20.2" +description = "parse() is the opposite of format()" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558"}, + {file = "parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce"}, +] + [[package]] name = "pathspec" version = "0.12.1" @@ -2501,4 +2547,4 @@ h11 = ">=0.9.0,<1" [metadata] lock-version = "2.1" python-versions = ">=3.12,<3.14" -content-hash = "64178e96b2304ab1e55693c8a90401b8df8317370413664cb7264848a7bc9ffe" +content-hash = "13d3ffd90c969d089df5815e4af81560e2a296e67d0c15e0007a0af17f7423fb" diff --git a/server/pyproject.toml b/server/pyproject.toml index 31eace87..8aa5579c 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -29,6 +29,7 @@ apscheduler = "^3.11.0" httpx = "^0.28.1" approvaltests = "^15.1.0" pytest-approvaltests = "^0.2.4" +alembic-utils = "^0.8.8" [tool.poetry.group.dev.dependencies] mypy = "^1.5.1" diff --git a/server/ttfd/models.py b/server/ttfd/models.py index 09f5a517..c400713e 100644 --- a/server/ttfd/models.py +++ b/server/ttfd/models.py @@ -5,6 +5,8 @@ import datetime from typing import Any +from alembic_utils.pg_extension import PGExtension +from alembic_utils.pg_view import PGView from sqlalchemy import ( Boolean, DateTime, @@ -23,6 +25,16 @@ class Base(DeclarativeBase): """Set up for sqlalchemy.""" +pg_trgm_extension = PGExtension( + schema="public", + signature="pg_trgm", +) +intarray_extension = PGExtension( + schema="public", + signature="intarray", +) + + class TZDateTime(TypeDecorator): # type: ignore[type-arg] """Store timezone aware timestamps as timezone naive UTC. @@ -115,6 +127,23 @@ class APIKey(Base): # TTFD Business tables +gauge_labelling = PGView( + schema="public", + signature="gauge_labelling", + definition=""" + SELECT rp.id as "path", + l.labeling as "labelling", + ARRAY_AGG(l."label" ORDER BY l."position" ASC) as "labels", + ARRAY_AGG(l."position" ORDER BY l."position" ASC) as "positions" + FROM labels l + JOIN reaction_paths rp ON l."path" = rp.id + JOIN pathways p ON rp.id = p."path" + WHERE rp.end_metabolite = p.metabolite AND p."next" AND l."index" = p."index" + GROUP BY rp.id, l.labeling + """, +) + + class Label(Base): """Records individual atomic labelings. From f0e8de03854f9fb098b6069bddccc66444c20bc8 Mon Sep 17 00:00:00 2001 From: James Collier Date: Thu, 18 Sep 2025 17:15:26 +0200 Subject: [PATCH 02/26] Draft the database query for the "tracer labelling" filter step --- server/migrations/env.py | 13 ++++++- server/ttfd/crud.py | 80 +++++++++++++++++++++++++++++++++++++++- server/ttfd/models.py | 14 +++++++ 3 files changed, 103 insertions(+), 4 deletions(-) diff --git a/server/migrations/env.py b/server/migrations/env.py index 2f5007c0..36382cae 100644 --- a/server/migrations/env.py +++ b/server/migrations/env.py @@ -30,6 +30,11 @@ register_entities([gauge_labelling, pg_trgm_extension, intarray_extension]) +def include_object(object, name, type_, reflected, compare_to): + """Exclude views from Alembic's consideration.""" + return not object.info.get("is_view", False) + + def run_migrations_offline() -> None: """Run migrations in 'offline' mode. @@ -47,7 +52,8 @@ def run_migrations_offline() -> None: target_metadata=target_metadata, literal_binds=True, dialect_opts={"paramstyle": "named"}, - render_as_batch=True, + render_as_batch=False, + include_object=include_object, ) with context.begin_transaction(): @@ -63,7 +69,10 @@ def run_migrations_online() -> None: """ with engine.connect() as connection: context.configure( - connection=connection, target_metadata=target_metadata, render_as_batch=True + connection=connection, + target_metadata=target_metadata, + render_as_batch=False, + include_object=include_object, ) with context.begin_transaction(): diff --git a/server/ttfd/crud.py b/server/ttfd/crud.py index d2d50ebc..a7d9ba37 100644 --- a/server/ttfd/crud.py +++ b/server/ttfd/crud.py @@ -10,7 +10,8 @@ from operator import itemgetter from typing import TYPE_CHECKING -from sqlalchemy import delete, select, text, update +from pydantic.types import NonNegativeInt +from sqlalchemy import delete, select, text, true, update from sqlalchemy.dialects.postgresql import aggregate_order_by, insert from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.sql import and_, distinct, func, or_ @@ -19,6 +20,7 @@ from ttfd.models import ( APIKey, Branch, + GaugeLabellingView, GaugeMapping, Label, Loop, @@ -45,7 +47,9 @@ from sqlalchemy.orm import Session - +type Count = NonNegativeInt +type Position = NonNegativeInt +type LabelValue = NonNegativeInt logger = logging.getLogger(__name__) @@ -717,6 +721,78 @@ def count_gauge_labeled_positions( ] +def count_tracer_labelled_positions( + session: Session, + tracer: str, + gauge: str, + label_count: int | None, + gauge_positions: list[int] | None, +) -> list[ + tuple[Count, list[LabelValue], list[Position], list[LabelValue], list[Position]] +]: + """Count the number of pathways for every tracer/gauge label position pair.""" + if gauge_positions is not None: + gauge_filter = func.sort(GaugeLabellingView.positions) == gauge_positions + elif label_count is not None: + gauge_filter = func.array_length(GaugeLabellingView.positions, 1) == label_count + else: + gauge_filter = true() + + tracer_labellings = ( + select( + ReactionPath.id.label("path"), + Label.labeling, + func.array_agg(aggregate_order_by(Label.label, Label.position.asc())).label( + "tracer_labels" + ), + func.array_agg(aggregate_order_by(Label.label, Label.position.asc())).label( + "tracer_positions" + ), + ) + .join(ReactionPath) + .join( + GaugeLabellingView, + and_( + GaugeLabellingView.path == Label.path, + GaugeLabellingView.labelling == Label.labeling, + ), + ) + .where( + and_( + ReactionPath.start_metabolite == tracer, + ReactionPath.end_metabolite == gauge, + Label.index == 0, + gauge_filter, + ) + ) + .group_by(ReactionPath.id, Label.labeling) + ).subquery() + + query = ( + select( + func.count(), + tracer_labellings.c.tracer_labels, + tracer_labellings.c.tracer_positions, + GaugeLabellingView.labels.label("gauge_labels"), + GaugeLabellingView.positions.label("gauge_positions"), + ) + .join( + GaugeLabellingView, + and_( + GaugeLabellingView.path == tracer_labellings.c.path, + GaugeLabellingView.labelling == tracer_labellings.c.labeling, + ), + ) + .group_by( + tracer_labellings.c.tracer_labels, + tracer_labellings.c.tracer_positions, + GaugeLabellingView.labels, + GaugeLabellingView.positions, + ) + ) + return list(session.execute(query).tuples()) + + def count_number_pathways( database: Session, tracer: str, diff --git a/server/ttfd/models.py b/server/ttfd/models.py index c400713e..add81e0a 100644 --- a/server/ttfd/models.py +++ b/server/ttfd/models.py @@ -144,6 +144,20 @@ class APIKey(Base): ) +class GaugeLabellingView(Base): + """View on the labelling pattern of just gauge metabolites in each path.""" + + __tablename__ = "gauge_labelling" + __table_args__ = {"info": {"is_view": True}} + + path: Mapped[int] = mapped_column(Integer, nullable=False, primary_key=True) + labelling: Mapped[int] = mapped_column(Integer, nullable=False, primary_key=True) + labels: Mapped[list[int]] = mapped_column(postgresql.ARRAY(Integer), nullable=False) + positions: Mapped[list[int]] = mapped_column( + postgresql.ARRAY(Integer), nullable=False + ) + + class Label(Base): """Records individual atomic labelings. From 307c7ee1664c578a1664a26b5fa03bd9aede55bd Mon Sep 17 00:00:00 2001 From: James Collier Date: Mon, 22 Sep 2025 15:15:53 +0200 Subject: [PATCH 03/26] Display of tracer labeling is now working --- server/static/atom-hover.js | 27 +++ server/static/styles.css | 114 +---------- server/templates/_base.html | 4 +- server/templates/gauge-labels-table.html | 4 + server/templates/table.html | 2 +- server/templates/tracer-labels-table.html | 24 +++ server/ttfd/html_controllers.py | 225 ++++++++++++++++++++-- server/ttfd/molecule.py | 11 +- 8 files changed, 274 insertions(+), 137 deletions(-) create mode 100644 server/static/atom-hover.js create mode 100644 server/templates/tracer-labels-table.html diff --git a/server/static/atom-hover.js b/server/static/atom-hover.js new file mode 100644 index 00000000..bbe17fdd --- /dev/null +++ b/server/static/atom-hover.js @@ -0,0 +1,27 @@ +document.querySelectorAll(".ttfd-table-row")?.forEach((element) => { + element.addEventListener("mouseover", (e) => { + const gaugeIds = e.currentTarget.dataset["gaugeIds"]?.split(","); + if (gaugeIds) { + const selector = gaugeIds.map((id) => `g#gauge-${id}`).join(", "); + document.querySelectorAll(selector).forEach((el) => el.classList.add("marked")); + } + const tracerIds = e.currentTarget.dataset["tracerIds"]?.split(","); + console.log(tracerIds); + if (tracerIds) { + const selector = tracerIds.map((id) => `g#tracer-${id}`).join(", "); + document.querySelectorAll(selector).forEach((el) => el.classList.add("marked")); + } + }); + element.addEventListener("mouseout", (e) => { + const gaugeIds = e.currentTarget.dataset["gaugeIds"]?.split(","); + if (gaugeIds) { + const selector = gaugeIds.map((id) => `g#gauge-${id}`).join(", "); + document.querySelectorAll(selector).forEach((el) => el.classList.remove("marked")); + } + const tracerIds = e.currentTarget.dataset["tracerIds"]?.split(","); + if (tracerIds) { + const selector = tracerIds.map((id) => `g#tracer-${id}`).join(", "); + document.querySelectorAll(selector).forEach((el) => el.classList.remove("marked")); + } + }); +}) diff --git a/server/static/styles.css b/server/static/styles.css index 0adb0416..a0c4da56 100644 --- a/server/static/styles.css +++ b/server/static/styles.css @@ -182,10 +182,9 @@ span.marked { border-radius: 4px; border: 1px solid rgba(0, 0, 0, 0.12); overflow: hidden; - padding: 16px; width: 220px; height: 220px; - padding: 10px; + padding: 0px; display: flex; flex-direction: column; justify-content: space-between; @@ -617,119 +616,14 @@ input#ttfd-filter-pathways:checked ~ div#ttfd-filter-pathways-tab { } /* Atom highlight hovering and animations */ -/* Atom 0 */ -.ttfd-table:has(.ttfd-hover-gauge-0:hover) ~.ttfd-summary g#gauge-0 > circle { +.ttfd-summary g[class="marked"] > circle { fill: #1cbbba; } -.ttfd-table:has(.ttfd-hover-gauge-0:hover) ~.ttfd-summary g#gauge-0 > text { +.ttfd-summary g[class="marked"] > text { fill: white; } -.ttfd-table:has(.ttfd-hover-gauge-0:hover) ~.ttfd-summary g#gauge-0 > text.ttfd-atom-index { - fill: #1cbbba; -} - -/* Atom 1 */ -.ttfd-table:has(.ttfd-hover-gauge-1:hover) ~.ttfd-summary g#gauge-1 > circle { - fill: #1cbbba; -} - -.ttfd-table:has(.ttfd-hover-gauge-1:hover) ~.ttfd-summary g#gauge-1 > text { - fill: white; -} - -.ttfd-table:has(.ttfd-hover-gauge-1:hover) ~.ttfd-summary g#gauge-1 > text.ttfd-atom-index { - fill: #1cbbba; -} - -/* Atom 2 */ -.ttfd-table:has(.ttfd-hover-gauge-2:hover) ~.ttfd-summary g#gauge-2 > circle { - fill: #1cbbba; -} - -.ttfd-table:has(.ttfd-hover-gauge-2:hover) ~.ttfd-summary g#gauge-2 > text { - fill: white; -} - -.ttfd-table:has(.ttfd-hover-gauge-2:hover) ~.ttfd-summary g#gauge-2 > text.ttfd-atom-index { - fill: #1cbbba; -} - -/* Atom 3 */ -.ttfd-table:has(.ttfd-hover-gauge-3:hover) ~.ttfd-summary g#gauge-3 > circle { - fill: #1cbbba; -} - -.ttfd-table:has(.ttfd-hover-gauge-3:hover) ~.ttfd-summary g#gauge-3 > text { - fill: white; -} - -.ttfd-table:has(.ttfd-hover-gauge-3:hover) ~.ttfd-summary g#gauge-3 > text.ttfd-atom-index { - fill: #1cbbba; -} - -/* Atom 4 */ -.ttfd-table:has(.ttfd-hover-gauge-4:hover) ~.ttfd-summary g#gauge-4 > circle { - fill: #1cbbba; -} - -.ttfd-table:has(.ttfd-hover-gauge-4:hover) ~.ttfd-summary g#gauge-4 > text { - fill: white; -} - -.ttfd-table:has(.ttfd-hover-gauge-4:hover) ~.ttfd-summary g#gauge-4 > text.ttfd-atom-index { - fill: #1cbbba; -} - -/* Atom 5 */ -.ttfd-table:has(.ttfd-hover-gauge-5:hover) ~.ttfd-summary g#gauge-5 > circle { - fill: #1cbbba; -} - -.ttfd-table:has(.ttfd-hover-gauge-5:hover) ~.ttfd-summary g#gauge-5 > text { - fill: white; -} - -.ttfd-table:has(.ttfd-hover-gauge-5:hover) ~.ttfd-summary g#gauge-5 > text.ttfd-atom-index { - fill: #1cbbba; -} - -/* Atom 6 */ -.ttfd-table:has(.ttfd-hover-gauge-6:hover) ~.ttfd-summary g#gauge-6 > circle { - fill: #1cbbba; -} - -.ttfd-table:has(.ttfd-hover-gauge-6:hover) ~.ttfd-summary g#gauge-6 > text { - fill: white; -} - -.ttfd-table:has(.ttfd-hover-gauge-6:hover) ~.ttfd-summary g#gauge-6 > text.ttfd-atom-index { - fill: #1cbbba; -} - -/* Atom 7 */ -.ttfd-table:has(.ttfd-hover-gauge-7:hover) ~.ttfd-summary g#gauge-7 > circle { - fill: #1cbbba; -} - -.ttfd-table:has(.ttfd-hover-gauge-7:hover) ~.ttfd-summary g#gauge-7 > text { - fill: white; -} - -.ttfd-table:has(.ttfd-hover-gauge-7:hover) ~.ttfd-summary g#gauge-7 > text.ttfd-atom-index { - fill: #1cbbba; -} - -/* Atom 8 */ -.ttfd-table:has(.ttfd-hover-gauge-8:hover) ~.ttfd-summary g#gauge-8 > circle { - fill: #1cbbba; -} - -.ttfd-table:has(.ttfd-hover-gauge-8:hover) ~.ttfd-summary g#gauge-8 > text { - fill: white; -} - -.ttfd-table:has(.ttfd-hover-gauge-8:hover) ~.ttfd-summary g#gauge-8 > text.ttfd-atom-index { +.ttfd-summary g[class="marked"] > text.ttfd-atom-index { fill: #1cbbba; } diff --git a/server/templates/_base.html b/server/templates/_base.html index 1236f78e..fcc461dd 100644 --- a/server/templates/_base.html +++ b/server/templates/_base.html @@ -82,11 +82,11 @@ {% if ttfd.maintenance_mode %} {% else %} - {% include "_summary.html" %}
{% block main required %} {% endblock %}
+ {% include "_summary.html" %} {% endif %}