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} -
-
- {#each run.networks as network, i} - {#if i > 0},{/if} - - {network.filename} - - {/each} -
- {/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} +
+

Output networks

+
+ {#each run.networks as network} + + + {network.source_path ?? network.filename} + + + {/each} +
+
+ {/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",