diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml new file mode 100644 index 0000000..4859233 --- /dev/null +++ b/.github/workflows/staging.yml @@ -0,0 +1,100 @@ +name: Staging Deploy (Beta) + +on: + push: + branches: [beta] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + ref: beta + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Publish + run: | + dotnet publish src/SpoolManager.Server/SpoolManager.Server.csproj \ + -c Release \ + -o ./publish \ + --no-self-contained + + - name: Create archive + run: cd publish && zip -r ../spoolhero-beta.zip . + + - uses: actions/upload-artifact@v4 + with: + name: spoolhero-beta + path: spoolhero-beta.zip + retention-days: 3 + + deploy: + runs-on: ubuntu-latest + needs: build + + steps: + - uses: actions/download-artifact@v4 + with: + name: spoolhero-beta + + - name: Stop service + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USER }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + script: systemctl stop spoolhero-beta || true + + - name: Upload archive + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USER }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + source: "spoolhero-beta.zip" + target: "/tmp/" + + - name: Deploy and start + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USER }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + script: | + rm -rf /opt/spoolhero-beta + mkdir -p /opt/spoolhero-beta + unzip /tmp/spoolhero-beta.zip -d /opt/spoolhero-beta + rm /tmp/spoolhero-beta.zip + cp /opt/appsettings-beta.json /opt/spoolhero-beta/appsettings.json + chown -R www-data:www-data /opt/spoolhero-beta + + # Create systemd service if it doesn't exist yet + if [ ! -f /etc/systemd/system/spoolhero-beta.service ]; then + cat > /etc/systemd/system/spoolhero-beta.service << 'EOF' + [Unit] + Description=SpoolHero Beta Staging + After=network.target + + [Service] + WorkingDirectory=/opt/spoolhero-beta + ExecStart=/usr/bin/dotnet /opt/spoolhero-beta/SpoolManager.Server.dll + Restart=always + RestartSec=5 + User=www-data + Environment=ASPNETCORE_ENVIRONMENT=Staging + Environment=ASPNETCORE_URLS=http://localhost:5100 + + [Install] + WantedBy=multi-user.target + EOF + systemctl daemon-reload + systemctl enable spoolhero-beta + fi + + systemctl start spoolhero-beta diff --git a/src/SpoolManager.Client/Components/FindSpoolModal.razor b/src/SpoolManager.Client/Components/FindSpoolModal.razor new file mode 100644 index 0000000..e263b64 --- /dev/null +++ b/src/SpoolManager.Client/Components/FindSpoolModal.razor @@ -0,0 +1,157 @@ +@implements IAsyncDisposable +@inject NfcService Nfc +@inject LocalizationService L + +@if (_visible) +{ + +} + +@code { + [Parameter, EditorRequired] public SpoolDto Spool { get; set; } = default!; + + private enum FindState { Idle, Scanning, Mismatch, Found, NoTag } + + private bool _visible; + private FindState _state = FindState.Idle; + private DotNetObjectReference? _ref; + private System.Timers.Timer? _resetTimer; + + public async Task OpenAsync() + { + if (string.IsNullOrWhiteSpace(Spool.RfidTagUid)) + { + _state = FindState.NoTag; + _visible = true; + StateHasChanged(); + return; + } + + _state = FindState.Scanning; + _visible = true; + StateHasChanged(); + + _ref ??= DotNetObjectReference.Create(this); + await Nfc.StartReadAsync(_ref); + } + + [JSInvokable] + public void OnScanStarted() { } + + [JSInvokable] + public async void OnTagRead(string json, string serialNumber) + { + _resetTimer?.Stop(); + + var uid = serialNumber.Trim().ToLowerInvariant().Replace(":", "").Replace("-", ""); + var expected = (Spool.RfidTagUid ?? "").Trim().ToLowerInvariant().Replace(":", "").Replace("-", ""); + + if (uid == expected) + { + _state = FindState.Found; + await InvokeAsync(StateHasChanged); + await Nfc.StopReadAsync(); + await Task.Delay(2000); + await CloseAsync(); + } + else + { + _state = FindState.Mismatch; + await InvokeAsync(StateHasChanged); + + _resetTimer = new System.Timers.Timer(2000); + _resetTimer.AutoReset = false; + _resetTimer.Elapsed += async (_, _) => + { + _state = FindState.Scanning; + await InvokeAsync(StateHasChanged); + }; + _resetTimer.Start(); + } + } + + [JSInvokable] + public void OnReadError(string message) + { + _state = FindState.Scanning; + InvokeAsync(StateHasChanged); + } + + [JSInvokable] + public void OnWriteSuccess() { } + + [JSInvokable] + public void OnWriteError(string message) { } + + private async Task CloseAsync() + { + _resetTimer?.Stop(); + await Nfc.StopReadAsync(); + _state = FindState.Idle; + _visible = false; + await InvokeAsync(StateHasChanged); + } + + public async ValueTask DisposeAsync() + { + _resetTimer?.Dispose(); + await Nfc.StopReadAsync(); + _ref?.Dispose(); + } +} diff --git a/src/SpoolManager.Client/Layout/MainLayout.razor b/src/SpoolManager.Client/Layout/MainLayout.razor index 9b79d2c..9891409 100644 --- a/src/SpoolManager.Client/Layout/MainLayout.razor +++ b/src/SpoolManager.Client/Layout/MainLayout.razor @@ -135,6 +135,7 @@ else
  • @L["nav.settings.account"]
  • @L["auth.change.password"]
  • @L["settings.notifications.title"]
  • +
  • @L["nav.settings.spoolman"]
  • +