diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 3bebee2..1c9b193 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -6,6 +6,10 @@ updates:
interval: "weekly"
cooldown:
default-days: 7
+ groups:
+ github-actions:
+ patterns: ["*"]
+ update-types: ["minor", "patch"]
- package-ecosystem: "uv"
directory: "/"
@@ -14,6 +18,10 @@ updates:
cooldown:
default-days: 7
semver-major-days: 14
+ groups:
+ python:
+ patterns: ["*"]
+ update-types: ["minor", "patch"]
- package-ecosystem: "npm"
directory: "/frontend/app"
@@ -22,6 +30,10 @@ updates:
cooldown:
default-days: 7
semver-major-days: 14
+ groups:
+ npm:
+ patterns: ["*"]
+ update-types: ["minor", "patch"]
- package-ecosystem: "docker"
directory: "/"
@@ -29,3 +41,7 @@ updates:
interval: "weekly"
cooldown:
default-days: 7
+ groups:
+ docker:
+ patterns: ["*"]
+ update-types: ["minor", "patch"]
diff --git a/.github/workflows/release-docker.yaml b/.github/workflows/release-docker.yaml
index 5e27530..fd3f62c 100644
--- a/.github/workflows/release-docker.yaml
+++ b/.github/workflows/release-docker.yaml
@@ -29,7 +29,7 @@ jobs:
fetch-depth: 0 # Fetch tags for setuptools_scm
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
+ uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Log in to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
diff --git a/.github/workflows/release-pypi.yaml b/.github/workflows/release-pypi.yaml
index e2885db..e00736d 100644
--- a/.github/workflows/release-pypi.yaml
+++ b/.github/workflows/release-pypi.yaml
@@ -22,24 +22,7 @@ jobs:
git merge-base --is-ancestor "$GITHUB_SHA" origin/main \
|| { echo "Tag $GITHUB_REF_NAME is not on main"; exit 1; }
- - uses: actions/setup-node@v4
- with:
- node-version: 22
- cache: npm
- cache-dependency-path: frontend/app/package-lock.json
-
- - name: Build SvelteKit frontend
- working-directory: frontend/app
- run: |
- npm ci
- npm run build
-
- - uses: hynek/build-and-inspect-python-package@v2
-
- - name: Verify frontend is bundled in the wheel
- run: |
- unzip -l dist/pypsa_app-*.whl | grep -q "static/app/index.html" \
- || { echo "wheel missing built frontend (static/app/index.html)"; exit 1; }
+ - uses: ./.github/actions/build-package
release:
name: Create GitHub release
diff --git a/frontend/app/src/lib/components/AppSidebar.svelte b/frontend/app/src/lib/components/AppSidebar.svelte
index ba78d17..7402998 100644
--- a/frontend/app/src/lib/components/AppSidebar.svelte
+++ b/frontend/app/src/lib/components/AppSidebar.svelte
@@ -1,19 +1,14 @@
-
-
- Networks
-
- {#if $loadingNetworks}
-
- {:else if $networksList.length === 0}
- No networks found
- {:else}
-
- {#each $networksList as network}
- {@const isSelected = $selectedNetworkIds.has(network.id)}
-
-
- {#snippet child({ props }: { props: Record })}
- {
- // Allow new-tab / new-window opens to fall through to the
- // browser's default behaviour — store will sync on the
- // new page's mount.
- if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
- e.preventDefault();
- handleNetworkClick(network.id);
- }}
- >
- {network.filename}
-
- {/snippet}
-
-
- {/each}
-
- {/if}
-
-
diff --git a/frontend/app/src/lib/types.ts b/frontend/app/src/lib/types.ts
index 74807ea..4321379 100644
--- a/frontend/app/src/lib/types.ts
+++ b/frontend/app/src/lib/types.ts
@@ -102,6 +102,7 @@ export interface RunNetwork {
id: string;
name: string | null;
filename: string;
+ source_path?: string | null;
}
export interface UserPublic {
diff --git a/frontend/app/src/routes/runs/[id]/+page.svelte b/frontend/app/src/routes/runs/[id]/+page.svelte
index 691eb90..16a8ec4 100644
--- a/frontend/app/src/routes/runs/[id]/+page.svelte
+++ b/frontend/app/src/routes/runs/[id]/+page.svelte
@@ -479,19 +479,6 @@ const workflowDisplay = $derived.by(() => {
{/if}
{/snippet}
- {#snippet networks()}
- {#if run && run.networks.length > 0}
-
-
- {/if}
- {/snippet}
diff --git a/frontend/app/src/routes/runs/components/RunHeader.svelte b/frontend/app/src/routes/runs/components/RunHeader.svelte
index 1105e2b..4dd3992 100644
--- a/frontend/app/src/routes/runs/components/RunHeader.svelte
+++ b/frontend/app/src/routes/runs/components/RunHeader.svelte
@@ -7,6 +7,8 @@
import Clock from '@lucide/svelte/icons/clock';
import Calendar from '@lucide/svelte/icons/calendar';
import Server from '@lucide/svelte/icons/server';
+ import Network from '@lucide/svelte/icons/network';
+ import ChevronRight from '@lucide/svelte/icons/chevron-right';
interface RunLike {
id: string;
@@ -28,7 +30,6 @@
progress?: { total: number; done: number; pct: number } | null;
actions?: Snippet;
extraChips?: Snippet;
- networks?: Snippet;
}
let {
@@ -40,7 +41,6 @@
progress = null,
actions,
extraChips,
- networks,
}: Props = $props();
@@ -97,17 +97,6 @@
{#if extraChips}
{@render extraChips()}
{/if}
- {#if networks}
- {@render networks()}
- {:else if run.networks.length > 0}
-
-
- {#each run.networks as network, i}
- {#if i > 0},{/if}
- {network.name || network.id.slice(0, 8)}
- {/each}
-
- {/if}
{#if run.owner}
{#if run.owner.avatar_url}
@@ -118,6 +107,25 @@
{/if}
+
+ {#if run.networks.length > 0}
+
+ {/if}
+
{#if !isTerminal && progress}
diff --git a/src/pypsa_app/backend/alembic/versions/0001_initial_schema.py b/src/pypsa_app/backend/alembic/versions/0001_initial_schema.py
index 1b535b0..30ab817 100644
--- a/src/pypsa_app/backend/alembic/versions/0001_initial_schema.py
+++ b/src/pypsa_app/backend/alembic/versions/0001_initial_schema.py
@@ -248,3 +248,8 @@ def downgrade() -> None:
op.drop_table("user_oauth_providers")
op.drop_table("users")
op.drop_table("snakedispatch_backends")
+
+ # Postgres: drop ENUM types so re-upgrade does not hit DuplicateObject
+ if op.get_bind().dialect.name == "postgresql":
+ for enum_name in ("network_visibility", "run_status", "user_role"):
+ op.execute(f"DROP TYPE IF EXISTS {enum_name}")
diff --git a/src/pypsa_app/backend/alembic/versions/0006_network_source_path.py b/src/pypsa_app/backend/alembic/versions/0006_network_source_path.py
new file mode 100644
index 0000000..4b3a32a
--- /dev/null
+++ b/src/pypsa_app/backend/alembic/versions/0006_network_source_path.py
@@ -0,0 +1,29 @@
+"""Add source_path column to networks table.
+
+Revision ID: 0006
+Revises: 0005
+Create Date: 2026-05-13
+
+"""
+
+from collections.abc import Sequence
+
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision: str = "0006"
+down_revision: str | None = "0005"
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
+
+
+def upgrade() -> None:
+ op.add_column(
+ "networks",
+ sa.Column("source_path", sa.Text(), nullable=True),
+ )
+
+
+def downgrade() -> None:
+ op.drop_column("networks", "source_path")
diff --git a/src/pypsa_app/backend/models.py b/src/pypsa_app/backend/models.py
index 70b26c5..578b5ef 100644
--- a/src/pypsa_app/backend/models.py
+++ b/src/pypsa_app/backend/models.py
@@ -233,6 +233,7 @@ class Network(Base):
# File information
filename: Mapped[str] = mapped_column(String(255))
file_path: Mapped[str] = mapped_column(Text, unique=True, index=True)
+ source_path: Mapped[str | None] = mapped_column(Text)
file_size: Mapped[int | None] = mapped_column(BigInteger)
file_hash: Mapped[str | None] = mapped_column(String(64))
# External: file lives outside data_dir (LOCAL_MODE only); do not unlink on delete
diff --git a/src/pypsa_app/backend/schemas/run.py b/src/pypsa_app/backend/schemas/run.py
index e7fce35..82dc1ca 100644
--- a/src/pypsa_app/backend/schemas/run.py
+++ b/src/pypsa_app/backend/schemas/run.py
@@ -44,6 +44,7 @@ class RunNetworkSummary(BaseModel):
id: uuid.UUID
name: str | None = None
filename: str
+ source_path: str | None = None
class RunCreate(BaseModel):
diff --git a/src/pypsa_app/backend/services/network.py b/src/pypsa_app/backend/services/network.py
index 66caa0b..daa7486 100644
--- a/src/pypsa_app/backend/services/network.py
+++ b/src/pypsa_app/backend/services/network.py
@@ -328,6 +328,7 @@ def import_network_file( # noqa: PLR0913
visibility: Visibility = Visibility.PRIVATE,
*,
is_external: bool = False,
+ source_path: str | None = None,
) -> Network:
"""Import a network file and create a DB record.
@@ -383,6 +384,7 @@ def import_network_file( # noqa: PLR0913
filename=original_filename,
file_path=str(dest),
is_external=is_external,
+ source_path=source_path,
)
_apply_network_metadata(network, dest, file_hash)
db.add(network)
diff --git a/src/pypsa_app/backend/settings.py b/src/pypsa_app/backend/settings.py
index 54b0496..bb1c0d4 100644
--- a/src/pypsa_app/backend/settings.py
+++ b/src/pypsa_app/backend/settings.py
@@ -255,6 +255,9 @@ def validate_local_mode(self) -> Self:
"Local mode is a single-user dashboard deployment."
)
raise ValueError(msg)
+ if self.local_mode and self.snakedispatch_backends:
+ msg = "SNAKEDISPATCH_BACKENDS is not yet implemented in LOCAL_MODE."
+ raise ValueError(msg)
return self
@model_validator(mode="after")
diff --git a/src/pypsa_app/backend/tasks.py b/src/pypsa_app/backend/tasks.py
index 0842c96..58d33d6 100644
--- a/src/pypsa_app/backend/tasks.py
+++ b/src/pypsa_app/backend/tasks.py
@@ -220,6 +220,7 @@ def import_run_outputs_task(self: Any, job_id: str) -> None: # noqa: PLR0915
db,
source_run_id=run.job_id,
visibility=run.visibility,
+ source_path=output_path,
)
logger.info(
"Imported network from run output",