Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
e66870e
feat: limit the number of jobs submitted
vtnphan Jul 14, 2026
c907149
ci: update main to dev in CI workflows
vtnphan Jul 14, 2026
13c6dfe
test: seqera services and job queue tests
vtnphan Jul 14, 2026
555f9a6
fix: hardcode max concurrent workflows from now
vtnphan Jul 14, 2026
14b382c
docs: update MAX_CONCURRENT_WORKFLOWS docs string
vtnphan Jul 15, 2026
dca5839
fix: count active workflows via totalSize
vtnphan Jul 15, 2026
c89a070
Merge pull request #101 from AustralianBioCommons/sbp-409
vtnphan Jul 15, 2026
c1a7cc9
feat: base64 encode utils
vtnphan Jul 15, 2026
b3d3717
feat: update cluster options
vtnphan Jul 15, 2026
c6e4bca
refactor: update project to GADI_PROJECT param
vtnphan Jul 15, 2026
9ad2654
tests: fix cluster options in tests
vtnphan Jul 15, 2026
b25f78f
fix: add load dotenv to run_scheduler.py
vtnphan Jul 16, 2026
ab7bd65
fix: remove timestamp, username, jobid
vtnphan Jul 16, 2026
3706861
docs: add load env docs in app scheduler
vtnphan Jul 16, 2026
12fa0c2
docs: add GADI_PROJECT in .env.example
vtnphan Jul 16, 2026
62a07bf
fix: remove unused user details
vtnphan Jul 16, 2026
90693ed
fix: remove unused details in executor and config
vtnphan Jul 16, 2026
5b24ac8
test: fix related tests
vtnphan Jul 16, 2026
ffe7706
chore: lint
vtnphan Jul 16, 2026
7638f71
Merge pull request #102 from AustralianBioCommons/sbp-481
vtnphan Jul 16, 2026
ffd4b89
feat: add tool, force repo-url, revision and
vtnphan Jul 17, 2026
bb01dfd
feat: add migration
vtnphan Jul 17, 2026
51361c1
tests: update all workflows in tests
vtnphan Jul 17, 2026
af16c33
feat: update schema diagram
vtnphan Jul 17, 2026
9a57bba
chore: lint
vtnphan Jul 17, 2026
43e5ad2
feat: update tool in admin dashboard
vtnphan Jul 17, 2026
6fb4f57
fix: update output to support cif file formats
minh-biocommons Jul 17, 2026
0b6442d
feat: add workflows name and tool constraint
vtnphan Jul 17, 2026
42cea4d
feat: query tool if workflows.tool is not null
vtnphan Jul 17, 2026
c1d18c8
test: add tool in tests
vtnphan Jul 17, 2026
1557646
Merge branch 'dev' of https://github.com/AustralianBioCommons/sbp-bac…
minh-biocommons Jul 17, 2026
aa81cf6
fix: remove CIF format support from AlphaFold and ColabFold
minh-biocommons Jul 17, 2026
e2f2b96
Merge pull request #103 from AustralianBioCommons/sbp-474
vtnphan Jul 17, 2026
359988d
fix: drop stale name-only unique index
vtnphan Jul 17, 2026
00c0514
Merge pull request #105 from AustralianBioCommons/sbp-474
vtnphan Jul 17, 2026
ccf7489
feat: proteindj config and executor
vtnphan Jul 21, 2026
82604b5
feat: wire proteindj workflow to de-novo-design
vtnphan Jul 21, 2026
0b7a2a6
feat: add single-prediction entity validation and related tests
minh-biocommons Jul 21, 2026
1e3ccd0
feat: add rfd_length params
vtnphan Jul 21, 2026
eb22765
feat: remove afd_length, update related tests
vtnphan Jul 21, 2026
5691624
fix: out_dir params
vtnphan Jul 21, 2026
2def2a2
fix: out_dir params
vtnphan Jul 21, 2026
c989aba
fix: black
minh-biocommons Jul 21, 2026
8a6881d
feat: add ProteinDjFromData
vtnphan Jul 22, 2026
76c6f1b
feat: add required fields to ProteinDJ form data
vtnphan Jul 22, 2026
36846ce
fix: refactor error handling
vtnphan Jul 22, 2026
22d1a12
feat: additional proteindj form data validation
vtnphan Jul 22, 2026
cd3d8eb
chore: lint
vtnphan Jul 22, 2026
ebcb54c
Merge pull request #107 from AustralianBioCommons/feat/sbp-486-rfdiff…
vtnphan Jul 22, 2026
18b2699
feat: encode email and ip for cluster options
vtnphan Jul 22, 2026
766acff
test: fix encoded email in cluster options
vtnphan Jul 22, 2026
b26d032
refactor: Pydantic types
minh-biocommons Jul 22, 2026
39f989a
Merge branch 'dev' of https://github.com/AustralianBioCommons/sbp-bac…
minh-biocommons Jul 22, 2026
536f9ce
Merge pull request #106 from AustralianBioCommons/sbp-483
minh-biocommons Jul 22, 2026
6a17515
Merge pull request #104 from AustralianBioCommons/sbp-451
minh-biocommons Jul 22, 2026
8d90f2a
refactor: rename 'alphafold2_random_seed' to 'random_seed' in tool pa…
minh-biocommons Jul 22, 2026
1a9806e
refactor: encode user email and IP address
vtnphan Jul 22, 2026
eaf7ca3
Merge branch 'dev' into sbp-481-extension
vtnphan Jul 22, 2026
adddbe6
chore: lint
vtnphan Jul 22, 2026
1ede066
Merge pull request #109 from AustralianBioCommons/sbp-484
minh-biocommons Jul 22, 2026
cc4b121
Merge pull request #108 from AustralianBioCommons/sbp-481-extension
vtnphan Jul 23, 2026
c2b59c9
feat: record system status incidents in the db
vtnphan Jul 24, 2026
379a7f9
feat: display historic health status
vtnphan Jul 24, 2026
484788b
chore: lint
vtnphan Jul 24, 2026
a26dd97
feat: fix summary time min width
vtnphan Jul 26, 2026
3362709
refactor: system status template
vtnphan Jul 26, 2026
65023ec
feat: detect time zone in system status
vtnphan Jul 27, 2026
1cdf0d3
Merge pull request #110 from AustralianBioCommons/feat/historic--heal…
vtnphan Jul 27, 2026
32eaf54
fix: fix out_dir in proteindj workflow
vtnphan Jul 27, 2026
cea3758
feat: rfdiffusion results
vtnphan Jul 27, 2026
a3536fb
feat: update nextflow module version
vtnphan Jul 27, 2026
4a1f521
fix: update to nextflow 25.10.4
vtnphan Jul 27, 2026
57ac01a
Merge pull request #111 from AustralianBioCommons/feat/update-nf-version
vtnphan Jul 27, 2026
a07b068
feat: max score, archive pdb file for rfdiffusion
vtnphan Jul 28, 2026
cacac08
chore: lint
vtnphan Jul 28, 2026
68f5601
Merge pull request #112 from AustralianBioCommons/sbp-475
vtnphan Jul 28, 2026
896ee39
feat: track service unit cost of runs in database (SBP-445) (#113)
marius-mather Jul 29, 2026
4f03a2b
chore: tweak workflow run admin to order by submission time (#114)
marius-mather Jul 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ COMPUTE_ID=compute-env-id
WORK_SPACE=workspace-id
WORK_DIR=s3://bucket/workdir

# NCI Gadi project code used for PBS submissions (clusterOptions) across all
# Nextflow workflows. Optional; defaults to "yz52" if unset.
GADI_PROJECT=yz52

# AWS S3 Configuration for file uploads
AWS_ACCESS_KEY_ID=your-access-key-id
AWS_SECRET_ACCESS_KEY=your-secret-access-key
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/check-migrations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Check Alembic Migrations

on:
pull_request:
branches: [main]
branches: [dev]
paths:
- "alembic/**"
- "alembic.ini"
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: Lint

on:
push:
branches: [main]
branches: [dev]
pull_request:
branches: [main]
branches: [dev]

jobs:
ruff:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: Coverage

on:
push:
branches: ["main"]
branches: ["dev"]
pull_request:
branches: ["main"]
branches: ["dev"]

jobs:
tests:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""add_tool_and_require_core_fields_on_workflows"""

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '7a377a05de6e'
down_revision = '74ab4858580d'
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('workflows', sa.Column('tool', sa.Text(), nullable=True))
op.alter_column('workflows', 'repo_url',
existing_type=sa.TEXT(),
nullable=False)
op.alter_column('workflows', 'default_revision',
existing_type=sa.TEXT(),
nullable=False)
op.alter_column('workflows', 'config_path',
existing_type=sa.TEXT(),
nullable=False)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('workflows', 'config_path',
existing_type=sa.TEXT(),
nullable=True)
op.alter_column('workflows', 'default_revision',
existing_type=sa.TEXT(),
nullable=True)
op.alter_column('workflows', 'repo_url',
existing_type=sa.TEXT(),
nullable=True)
op.drop_column('workflows', 'tool')
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""add_unique_constraints_for_workflow_name_tool"""

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '14cff803a3b9'
down_revision = '7616ae331fe5'
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_unique_constraint(op.f('uq_workflows_name'), 'workflows', ['name', 'tool'])
op.create_index('uq_workflows_name_null_tool', 'workflows', ['name'], unique=True, postgresql_where=sa.text('tool IS NULL'))
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('uq_workflows_name_null_tool', table_name='workflows', postgresql_where=sa.text('tool IS NULL'))
op.drop_constraint(op.f('uq_workflows_name'), 'workflows', type_='unique')
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""drop_stale_name_only_unique_index_on_workflows"""

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '7616ae331fe5'
down_revision = '7a377a05de6e'
branch_labels = None
depends_on = None


def upgrade() -> None:
# Some environments have a plain unique index named 'uq_workflows_name' on
# workflows.name alone, predating this feature and untracked by any prior
# migration (likely created out-of-band). It collides with the
# same-named (name, tool) constraint the next migration creates.
op.execute("DROP INDEX IF EXISTS uq_workflows_name")


def downgrade() -> None:
# The dropped index was untracked drift, not a schema feature; nothing to restore.
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""system_status_incidents"""

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'a6ee1af2c156'
down_revision = '14cff803a3b9'
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('system_status_incidents',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('component', sa.Text(), nullable=False),
sa.Column('status', sa.Text(), nullable=False),
sa.Column('started_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('ended_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('message', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id', name=op.f('pk_system_status_incidents'))
)
op.create_index('ix_system_status_incidents_component_started_at', 'system_status_incidents', ['component', 'started_at'], unique=False)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('ix_system_status_incidents_component_started_at', table_name='system_status_incidents')
op.drop_table('system_status_incidents')
# ### end Alembic commands ###
23 changes: 23 additions & 0 deletions alembic/versions/20260728_145217_service_usage_4f26ab5038ec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""service_usage"""

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '4f26ab5038ec'
down_revision = 'a6ee1af2c156'
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('workflow_runs', sa.Column('service_usage', sa.Float(), nullable=True))
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('workflow_runs', 'service_usage')
# ### end Alembic commands ###
16 changes: 10 additions & 6 deletions app/db/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ class WorkflowAdmin(ModelView):
fields = [
"id",
"name",
"tool",
"description",
"repo_url",
"default_revision",
Expand All @@ -135,6 +136,7 @@ class WorkflowAdmin(ModelView):

_NULLABLE_FIELDS = (
"description",
"tool",
"prerun_script_path",
)

Expand All @@ -158,20 +160,22 @@ async def repr(self, obj: Any, request: Request) -> str:

class WorkflowRunAdmin(ModelView):
fields = [
"id",
"submission_timestamp",
HasOne("workflow", identity="workflow"),
"workflow_id",
"tool",
"owner_user_id",
HasOne("workflow", identity="workflow"),
HasOne("owner", identity="app-user"),
"owner_user_id",
"seqera_run_id",
"run_name",
"binder_name",
"service_usage",
JSONField("submitted_form_data"),
"binder_name",
"work_dir",
"submission_timestamp",
"id",
]
exclude_fields_from_list = "submitted_form_data"
exclude_fields_from_list = ["submitted_form_data"]
fields_default_sort = [("submission_timestamp", True)]

async def repr(self, obj: Any, request: Request) -> str:
return f"{obj.run_name}"
Expand Down
28 changes: 25 additions & 3 deletions app/db/models/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
JSON,
BigInteger,
DateTime,
Float,
ForeignKey,
Index,
Numeric,
PrimaryKeyConstraint,
Text,
UniqueConstraint,
text,
)
from sqlalchemy.dialects.postgresql import INET, UUID
from sqlalchemy.orm import Mapped, Session, mapped_column, relationship
Expand Down Expand Up @@ -39,14 +42,32 @@ class AppUser(Base):

class Workflow(Base):
__tablename__ = "workflows"
__table_args__ = (
# At most one row per (name, tool) for workflows with tool-specific
# configs (e.g. de-novo-design: bindcraft vs rfdiffusion).
UniqueConstraint("name", "tool"),
# At most one generic (tool-independent) row per name. sqlite_where is
# needed alongside postgresql_where so the tests' SQLite schema (built
# via Base.metadata.create_all) enforces the same partial uniqueness
# as the Postgres migration; without it, SQLite silently drops the
# WHERE clause and the index becomes a full unique constraint on name.
Index(
"uq_workflows_name_null_tool",
"name",
unique=True,
postgresql_where=text("tool IS NULL"),
sqlite_where=text("tool IS NULL"),
),
)

id: Mapped[UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid4)
name: Mapped[str] = mapped_column(Text, nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
repo_url: Mapped[str | None] = mapped_column(Text, nullable=True)
default_revision: Mapped[str | None] = mapped_column(Text, nullable=True)
config_path: Mapped[str | None] = mapped_column(Text, nullable=True)
repo_url: Mapped[str] = mapped_column(Text, nullable=False)
default_revision: Mapped[str] = mapped_column(Text, nullable=False)
config_path: Mapped[str] = mapped_column(Text, nullable=False)
prerun_script_path: Mapped[str | None] = mapped_column(Text, nullable=True)
tool: Mapped[str | None] = mapped_column(Text, nullable=True)

runs: Mapped[list[WorkflowRun]] = relationship(back_populates="workflow")

Expand All @@ -72,6 +93,7 @@ class WorkflowRun(Base):
DateTime(timezone=True), nullable=True
)
tool: Mapped[str | None] = mapped_column(Text, nullable=True)
service_usage: Mapped[float | None] = mapped_column(Float, nullable=True)

owner: Mapped[AppUser] = relationship(back_populates="workflow_runs")
workflow: Mapped[Workflow | None] = relationship(back_populates="runs")
Expand Down
25 changes: 23 additions & 2 deletions app/db/models/system_status.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""Database-backed cache for runtime system health."""
"""Database-backed cache and history for runtime system health."""

from datetime import UTC, datetime
from typing import Any

from sqlalchemy import JSON, DateTime, Text
from sqlalchemy import JSON, DateTime, Index, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column

from ...schemas.health import SystemStatus
Expand Down Expand Up @@ -38,3 +38,24 @@ def get_status(self) -> SystemStatus:
Return a SystemStatus object from the cache payload.
"""
return SystemStatus.model_validate(self.payload)


class SystemStatusIncident(Base):
"""
One row per downtime period for a monitored component: opened when a probe
first reports ``degraded``/``unhealthy``, closed when it next reports
``healthy``. Healthy checks are never recorded, so storage scales with the
number of actual outages rather than with polling frequency.
"""

__tablename__ = "system_status_incidents"
__table_args__ = (
Index("ix_system_status_incidents_component_started_at", "component", "started_at"),
)

id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
component: Mapped[str] = mapped_column(Text, nullable=False)
status: Mapped[str] = mapped_column(Text, nullable=False)
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
ended_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
message: Mapped[str | None] = mapped_column(Text, nullable=True)
28 changes: 27 additions & 1 deletion app/routes/system_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@

from __future__ import annotations

from datetime import UTC, datetime, timedelta

from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session

from ..db.admin import require_admin_access
from ..schemas.health import SystemStatusAdminResponse
from ..schemas.health import SystemStatusAdminResponse, SystemStatusDowntimeResponse
from ..services import health
from .dependencies import get_db

Expand All @@ -25,6 +27,9 @@
dependencies=[Depends(require_admin_access)],
)

# Bounds how far back /system-status/history can be queried.
_MAX_HISTORY_HOURS = 24 * 30


@router.get("/system-status", response_model=SystemStatusAdminResponse)
async def get_admin_system_status(
Expand All @@ -37,3 +42,24 @@ async def get_admin_system_status(
"""Return verbose, admin-only runtime health of the submission components."""
status_obj = await health.get_system_status(db, force_refresh=refresh)
return SystemStatusAdminResponse.model_validate(health.to_admin_dict(status_obj))


@router.get("/system-status/history", response_model=SystemStatusDowntimeResponse)
async def get_admin_system_status_history(
hours: int = Query(
default=24,
ge=1,
le=_MAX_HISTORY_HOURS,
description="How many hours of downtime history to return",
),
component: str | None = Query(default=None, description="Restrict to a single component name"),
db: Session = Depends(get_db),
) -> SystemStatusDowntimeResponse:
"""Return per-component downtime incidents, so admins can see how frequently
and for how long a component has been degraded/unhealthy over time."""
until = datetime.now(UTC)
since = until - timedelta(hours=hours)
incidents = health.get_incidents(db, since=since, until=until, component=component)
return SystemStatusDowntimeResponse.model_validate(
health.to_downtime_dict(incidents, since=since, until=until, hours=hours)
)
Loading
Loading