diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b2aea3a..822e6e8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,7 @@ jobs: docker run --rm \ -v "${{ github.workspace }}:/src" -w /src \ mcr.microsoft.com/dotnet/sdk:10.0 \ - ./scripts/publish-self-contained.sh ${{ matrix.rid }} + ./scripts/publish-self-contained.sh ${{ matrix.rid }} ${{ github.ref_name }} - name: Setup .NET if: matrix.rid == 'linux-musl-x64' || matrix.rid == 'linux-musl-arm64' @@ -40,7 +40,7 @@ jobs: - name: Publish for ${{ matrix.rid }} (musl on host) if: matrix.rid == 'linux-musl-x64' || matrix.rid == 'linux-musl-arm64' - run: ./scripts/publish-self-contained.sh ${{ matrix.rid }} + run: ./scripts/publish-self-contained.sh ${{ matrix.rid }} ${{ github.ref_name }} - name: Zip publish output run: | diff --git a/scripts/publish-self-contained.sh b/scripts/publish-self-contained.sh index c382aca..28791fa 100755 --- a/scripts/publish-self-contained.sh +++ b/scripts/publish-self-contained.sh @@ -59,14 +59,17 @@ default_rid() { esac } +# Optional version override (e.g. v0.0.4 from CI tag). When set, passed to dotnet publish for assembly metadata. +VERSION_OVERRIDE="" + while [ $# -gt 0 ]; do case "$1" in -h|-?|--help) - echo "Usage: $0 [OPTIONS] [RID]" + echo "Usage: $0 [OPTIONS] [RID] [VERSION]" echo "" echo "Build a self-contained folder publish for Linux. If RID is omitted, the script" echo "auto-detects from the current OS and architecture (recommended on Ubuntu and Alpine)." - echo "Pass options first, then RID if desired (e.g. $0 --trim linux-x64)." + echo "Pass options first, then RID, then optional VERSION (e.g. v0.0.4 from a release tag)." echo "" echo "Options:" echo " --trim Enable trimming (smaller output; can cause 404/routing issues with Blazor)" @@ -112,6 +115,10 @@ while [ $# -gt 0 ]; do *) RID="$1" shift + if [ $# -gt 0 ] && [ -n "$1" ] && [ "${1#-}" = "$1" ]; then + VERSION_OVERRIDE="$1" + shift + fi break ;; esac @@ -148,19 +155,29 @@ if [ "$CLEAN" = true ]; then dotnet clean "$PROJECT" -c Release -nologo -v q fi +VERSION_ARGS="" +if [ -n "$VERSION_OVERRIDE" ]; then + # Strip leading 'v' (e.g. v0.0.4 -> 0.0.4) for assembly metadata + VER="${VERSION_OVERRIDE#v}" + VERSION_ARGS="-p:Version=$VER -p:InformationalVersion=$VER" + echo "Using version: $VER (from $VERSION_OVERRIDE)" +fi + if [ "$TRIM" = true ]; then echo "Publishing self-contained (trimmed, folder) for $RID..." dotnet publish "$PROJECT" \ -c Release \ -r "$RID" \ --self-contained true \ - -p:PublishTrimmed=true + -p:PublishTrimmed=true \ + $VERSION_ARGS else echo "Publishing self-contained (no trim, folder) for $RID..." dotnet publish "$PROJECT" \ -c Release \ -r "$RID" \ - --self-contained true + --self-contained true \ + $VERSION_ARGS fi OUT_DIR="$REPO_ROOT/src/DnsmasqWebUI/bin/Release/net10.0/$RID/publish" diff --git a/src/DnsmasqWebUI/Components/App.razor b/src/DnsmasqWebUI/Components/App.razor index 4504bb7..358e1a7 100644 --- a/src/DnsmasqWebUI/Components/App.razor +++ b/src/DnsmasqWebUI/Components/App.razor @@ -1,3 +1,4 @@ +@using System.Reflection @inject IOptions AppOptions @@ -6,16 +7,16 @@ - - - - - - - - - - + + + + + + + + + + @_appTitle @@ -27,5 +28,7 @@ @code { + private static readonly string _cacheBust = typeof(App).Assembly + .GetCustomAttribute()?.InformationalVersion ?? "0"; private string? _appTitle => AppOptions?.Value?.EffectiveTitle; } diff --git a/src/DnsmasqWebUI/Components/Layout/MainLayout.razor.css b/src/DnsmasqWebUI/Components/Layout/MainLayout.razor.css index 901704d..450617e 100644 --- a/src/DnsmasqWebUI/Components/Layout/MainLayout.razor.css +++ b/src/DnsmasqWebUI/Components/Layout/MainLayout.razor.css @@ -29,6 +29,8 @@ main.main { } .sidebar { + display: flex; + flex-direction: column; background: linear-gradient(180deg, #052767 0%, #3a0647 70%); } diff --git a/src/DnsmasqWebUI/Components/Layout/NavMenu.razor b/src/DnsmasqWebUI/Components/Layout/NavMenu.razor index 56249ff..b933796 100644 --- a/src/DnsmasqWebUI/Components/Layout/NavMenu.razor +++ b/src/DnsmasqWebUI/Components/Layout/NavMenu.razor @@ -1,4 +1,5 @@ @namespace DnsmasqWebUI.Components.Layout +@using System.Reflection @inject IOptions AppOptions @inject ISettingsModalService SettingsModalService @@ -35,9 +36,27 @@ + @code { + private const string ReleasesBaseUrl = "https://github.com/alexhopeoconnor/dnsmasq-webui/releases"; + private static readonly string Version = typeof(NavMenu).Assembly + .GetCustomAttribute()?.InformationalVersion ?? "?"; + /// Base version without build metadata (e.g. 0.0.4+abc123 -> 0.0.4) for GitHub tag URLs. + private static string BaseVersion(string v) + { + if (string.IsNullOrEmpty(v) || v == "?") return v; + var i = v.IndexOf('+'); + return i >= 0 ? v[..i] : v; + } + private string ReleaseUrl => Version != "?" + ? $"{ReleasesBaseUrl}/tag/v{BaseVersion(Version)}" + : ReleasesBaseUrl; private string? AppTitle => AppOptions?.Value?.EffectiveTitle; private void OpenAllSettings() => SettingsModalService.Open(SettingsModalContext.All, "Settings"); diff --git a/src/DnsmasqWebUI/Components/Layout/NavMenu.razor.css b/src/DnsmasqWebUI/Components/Layout/NavMenu.razor.css index 096cd3b..b43b067 100644 --- a/src/DnsmasqWebUI/Components/Layout/NavMenu.razor.css +++ b/src/DnsmasqWebUI/Components/Layout/NavMenu.razor.css @@ -131,13 +131,29 @@ color: white; } +.nav-footer { + margin-top: auto; + flex-shrink: 0; + font-size: 0.8125rem; +} + +.nav-footer-link { + color: rgba(255, 255, 255, 0.65) !important; + text-decoration: none; +} + +.nav-footer-link:hover { + color: rgba(255, 255, 255, 0.9) !important; +} + .nav-scrollable { display: none; + flex-direction: column; } /* Toggler is inside top-row; nav-scrollable is sibling of top-row */ .top-row:has(.navbar-toggler:checked) ~ .nav-scrollable { - display: block; + display: flex; } @media (min-width: 641px) { @@ -146,11 +162,10 @@ } .nav-scrollable { - /* Never collapse the sidebar for wide screens */ - display: block; - - /* Allow sidebar to scroll for tall menus */ - height: calc(100vh - 3.5rem); + /* Never collapse the sidebar for wide screens; flex to fill remaining sidebar height */ + display: flex; + flex: 1; + min-height: 0; overflow-y: auto; } } diff --git a/src/DnsmasqWebUI/Components/Modals/SettingsModal.razor b/src/DnsmasqWebUI/Components/Modals/SettingsModal.razor index b601a61..c77c0a2 100644 --- a/src/DnsmasqWebUI/Components/Modals/SettingsModal.razor +++ b/src/DnsmasqWebUI/Components/Modals/SettingsModal.razor @@ -33,77 +33,86 @@ @bind="_searchTerm" @bind:event="oninput" @bind:after="FilterSettings" /> } - @if (ShouldShowSection(SettingsModalSections.ServiceStatus)) + @foreach (var group in GetVisibleGroups()) { - var meta = SettingsModalSections.All[SettingsModalSections.ServiceStatus]; -
-
@meta.DisplayName
- - -
- } - @if (ShouldShowSection(SettingsModalSections.Logs)) - { - var meta = SettingsModalSections.All[SettingsModalSections.Logs]; -
-
@meta.DisplayName
- - -
- } - @if (ShouldShowSection(SettingsModalSections.AppLogs)) - { - var meta = SettingsModalSections.All[SettingsModalSections.AppLogs]; -
-
@meta.DisplayName
- - -
- } - @if (ShouldShowSection(SettingsModalSections.Leases)) - { - var meta = SettingsModalSections.All[SettingsModalSections.Leases]; -
-
@meta.DisplayName
- - -
- } - @if (ShouldShowSection(SettingsModalSections.RecentLogsDisplay)) - { - var meta = SettingsModalSections.All[SettingsModalSections.RecentLogsDisplay]; -
-
@meta.DisplayName
-
- - + var isExpanded = IsGroupExpanded(group); +
+ +
+ @foreach (var key in group.SectionKeys) + { + @if (ShouldShowSection(key)) + { +
+ @switch (key) + { + case SettingsModalSections.ServiceStatus: + { + var meta = SettingsModalSections.All[SettingsModalSections.ServiceStatus]; + + + break; + } + case SettingsModalSections.Logs: + { + var meta = SettingsModalSections.All[SettingsModalSections.Logs]; + + + break; + } + case SettingsModalSections.AppLogs: + { + var meta = SettingsModalSections.All[SettingsModalSections.AppLogs]; + + + break; + } + case SettingsModalSections.Leases: + { + var meta = SettingsModalSections.All[SettingsModalSections.Leases]; + + + break; + } + case SettingsModalSections.RecentLogsDisplay: +
+ + +
+
+ + +
+ break; + case SettingsModalSections.AppLogsDisplay: +
+ + +
+
+ + +
+ break; + } +
+ } + }
-
- - -
-
- } - @if (ShouldShowSection(SettingsModalSections.AppLogsDisplay)) - { - var meta = SettingsModalSections.All[SettingsModalSections.AppLogsDisplay]; -
-
@meta.DisplayName
-
- - -
-
- - -
-
+ }