Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
100 changes: 100 additions & 0 deletions .github/workflows/staging.yml
Original file line number Diff line number Diff line change
@@ -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
157 changes: 157 additions & 0 deletions src/SpoolManager.Client/Components/FindSpoolModal.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
@implements IAsyncDisposable
@inject NfcService Nfc
@inject LocalizationService L

@if (_visible)
{
<div class="modal fade show d-block" tabindex="-1" style="background:rgba(0,0,0,.55)">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi bi-search me-2"></i>@L["spool.find.title"]
</h5>
<button type="button" class="btn-close" @onclick="CloseAsync"></button>
</div>
<div class="modal-body text-center py-4">
<div class="d-flex align-items-center justify-content-center gap-2 mb-4">
<div class="color-dot" style="background-color: #@Spool.MaterialColorHex; width:1.1rem; height:1.1rem; border-radius:50%"></div>
<span class="fw-semibold">@Spool.MaterialBrand @Spool.MaterialType</span>
</div>

@if (_state == FindState.Found)
{
<div class="text-success mb-2">
<i class="bi bi-check-circle-fill" style="font-size:3.5rem"></i>
</div>
<p class="fw-semibold text-success mb-0">@L["spool.find.found"]</p>
}
else if (_state == FindState.NoTag)
{
<div class="text-warning mb-2">
<i class="bi bi-exclamation-triangle-fill" style="font-size:3rem"></i>
</div>
<p class="text-muted mb-0">@L["spool.find.no.tag"]</p>
}
else
{
<div class="mb-3">
@if (_state == FindState.Mismatch)
{
<i class="bi bi-x-circle-fill text-danger" style="font-size:3rem"></i>
<p class="text-danger fw-semibold mt-2 mb-0">@L["spool.find.wrong"]</p>
<small class="text-muted">@L["spool.find.retry"]</small>
}
else
{
<div class="nfc-pulse-icon mb-2">
<i class="bi bi-broadcast" style="font-size:3rem; color: var(--bs-primary)"></i>
</div>
<p class="text-muted mb-0">@L["spool.find.scanning"]</p>
}
</div>
}
</div>
@if (_state != FindState.Found)
{
<div class="modal-footer justify-content-center">
<button class="btn btn-secondary" @onclick="CloseAsync">@L["common.cancel"]</button>
</div>
}
</div>
</div>
</div>
}

@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<FindSpoolModal>? _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();
}
}
6 changes: 6 additions & 0 deletions src/SpoolManager.Client/Layout/MainLayout.razor
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ else
<li><a class="dropdown-item" href="/settings/account"><i class="bi bi-person-gear me-2"></i>@L["nav.settings.account"]</a></li>
<li><a class="dropdown-item" href="/settings/password"><i class="bi bi-key me-2"></i>@L["auth.change.password"]</a></li>
<li><a class="dropdown-item" href="/settings/notifications"><i class="bi bi-bell me-2"></i>@L["settings.notifications.title"]</a></li>
<li><a class="dropdown-item" href="/settings/spoolman"><i class="bi bi-plug me-2"></i>@L["nav.settings.spoolman"]</a></li>
<li><hr class="dropdown-divider"></li>
<li>
<button class="dropdown-item text-danger" @onclick="LogoutAsync">
Expand Down Expand Up @@ -254,6 +255,11 @@ else
<i class="bi bi-bell me-2"></i>@L["settings.notifications.title"]
</a>
</li>
<li class="nav-item mt-1">
<a class="btn btn-outline-secondary w-100" href="/settings/spoolman" @onclick="CloseOffcanvas">
<i class="bi bi-plug me-2"></i>@L["nav.settings.spoolman"]
</a>
</li>
<li class="nav-item mt-2">
<button class="btn btn-outline-danger w-100" @onclick="LogoutAsync">
<i class="bi bi-box-arrow-right me-2"></i>@L["nav.logout"]
Expand Down
Loading
Loading