diff --git a/.github/ISSUE_TEMPLATE/internal.md b/.github/ISSUE_TEMPLATE/internal.md
new file mode 100644
index 00000000..acc76d64
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/internal.md
@@ -0,0 +1,10 @@
+---
+name: "🛠️ Internal / Developer Issue"
+about: "Unstructured issue for project members only. Outside contributors: please use a standard template."
+title: "[INT]: "
+labels: ["internal"]
+assignees: []
+---
+
+**Summary:**
+[Write here]
\ No newline at end of file
diff --git a/.github/scripts/upgrade-test/README.md b/.github/scripts/upgrade-test/README.md
new file mode 100644
index 00000000..bb4519c8
--- /dev/null
+++ b/.github/scripts/upgrade-test/README.md
@@ -0,0 +1,259 @@
+# HomeBox Upgrade Testing Workflow
+
+This document describes the automated upgrade testing workflow for HomeBox.
+
+## Overview
+
+The upgrade test workflow is designed to ensure data integrity and functionality when upgrading HomeBox from one version to another. It automatically:
+
+1. Deploys a stable version of HomeBox
+2. Creates test data (users, items, locations, labels, notifiers, attachments)
+3. Upgrades to the latest version from the main branch
+4. Verifies all data and functionality remain intact
+
+## Workflow File
+
+**Location**: `.github/workflows/upgrade-test.yaml`
+
+## Trigger Conditions
+
+The workflow runs:
+- **Daily**: Automatically at 2 AM UTC (via cron schedule)
+- **Manual**: Can be triggered manually via GitHub Actions UI
+- **On Push**: When changes are made to the workflow files or test scripts
+
+## Test Scenarios
+
+### 1. Environment Setup
+- Pulls the latest stable HomeBox Docker image from GHCR
+- Starts the application with test configuration
+- Ensures the service is healthy and ready
+
+### 2. Data Creation
+
+The workflow creates comprehensive test data using the `create-test-data.sh` script:
+
+#### Users and Groups
+- **Group 1**: 5 users (user1@homebox.test through user5@homebox.test)
+- **Group 2**: 2 users (user6@homebox.test and user7@homebox.test)
+- All users have password: `TestPassword123!`
+
+#### Locations
+- **Group 1**: Living Room, Garage
+- **Group 2**: Home Office
+
+#### Labels
+- **Group 1**: Electronics, Important
+- **Group 2**: Work Equipment
+
+#### Items
+- **Group 1**: 5 items (Laptop Computer, Power Drill, TV Remote, Tool Box, Coffee Maker)
+- **Group 2**: 2 items (Monitor, Keyboard)
+
+#### Attachments
+- Multiple attachments added to various items (receipts, manuals, warranties)
+
+#### Notifiers
+- **Group 1**: Test notifier named "TESTING"
+
+### 3. Upgrade Process
+
+1. Stops the stable version container
+2. Builds a fresh image from the current main branch
+3. Copies the database to a new location
+4. Starts the new version with the existing data
+
+### 4. Verification Tests
+
+The Playwright test suite (`upgrade-verification.spec.ts`) verifies:
+
+- ✅ **User Authentication**: All 7 users can log in with their credentials
+- ✅ **Data Persistence**: All items, locations, and labels are present
+- ✅ **Attachments**: File attachments are correctly associated with items
+- ✅ **Notifiers**: The "TESTING" notifier is still configured
+- ✅ **UI Functionality**: Version display, theme switching work correctly
+- ✅ **Data Isolation**: Groups can only see their own data
+
+## Test Data File
+
+The setup script generates a JSON file at `/tmp/test-users.json` containing:
+
+```json
+{
+ "users": [
+ {
+ "email": "user1@homebox.test",
+ "password": "TestPassword123!",
+ "token": "...",
+ "group": "1"
+ },
+ ...
+ ],
+ "locations": {
+ "group1": ["location-id-1", "location-id-2"],
+ "group2": ["location-id-3"]
+ },
+ "labels": {...},
+ "items": {...},
+ "notifiers": {...}
+}
+```
+
+This file is used by the Playwright tests to verify data integrity.
+
+## Scripts
+
+### create-test-data.sh
+
+**Location**: `.github/scripts/upgrade-test/create-test-data.sh`
+
+**Purpose**: Creates all test data via the HomeBox REST API
+
+**Environment Variables**:
+- `HOMEBOX_URL`: Base URL of the HomeBox instance (default: http://localhost:7745)
+- `TEST_DATA_FILE`: Path to output JSON file (default: /tmp/test-users.json)
+
+**Requirements**:
+- `curl`: For API calls
+- `jq`: For JSON processing
+
+**Usage**:
+```bash
+export HOMEBOX_URL=http://localhost:7745
+./.github/scripts/upgrade-test/create-test-data.sh
+```
+
+## Running Tests Locally
+
+To run the upgrade tests locally:
+
+### Prerequisites
+```bash
+# Install dependencies
+sudo apt-get install -y jq curl docker.io
+
+# Install pnpm and Playwright
+cd frontend
+pnpm install
+pnpm exec playwright install --with-deps chromium
+```
+
+### Run the test
+```bash
+# Start stable version
+docker run -d \
+ --name homebox-test \
+ -p 7745:7745 \
+ -e HBOX_OPTIONS_ALLOW_REGISTRATION=true \
+ -v /tmp/homebox-data:/data \
+ ghcr.io/sysadminsmedia/homebox:latest
+
+# Wait for startup
+sleep 10
+
+# Create test data
+export HOMEBOX_URL=http://localhost:7745
+./.github/scripts/upgrade-test/create-test-data.sh
+
+# Stop container
+docker stop homebox-test
+docker rm homebox-test
+
+# Build new version
+docker build -t homebox:test .
+
+# Start new version with existing data
+docker run -d \
+ --name homebox-test \
+ -p 7745:7745 \
+ -e HBOX_OPTIONS_ALLOW_REGISTRATION=true \
+ -v /tmp/homebox-data:/data \
+ homebox:test
+
+# Wait for startup
+sleep 10
+
+# Run verification tests
+cd frontend
+TEST_DATA_FILE=/tmp/test-users.json \
+E2E_BASE_URL=http://localhost:7745 \
+pnpm exec playwright test \
+ --project=chromium \
+ test/upgrade/upgrade-verification.spec.ts
+
+# Cleanup
+docker stop homebox-test
+docker rm homebox-test
+```
+
+## Artifacts
+
+The workflow produces several artifacts:
+
+1. **playwright-report-upgrade-test**: HTML report of test results
+2. **playwright-traces**: Detailed traces for debugging failures
+3. **Docker logs**: Collected on failure for troubleshooting
+
+## Failure Scenarios
+
+The workflow will fail if:
+- The stable version fails to start
+- Test data creation fails
+- The new version fails to start with existing data
+- Any verification test fails
+- Database migrations fail
+
+## Troubleshooting
+
+### Test Data Creation Fails
+
+Check the Docker logs:
+```bash
+docker logs homebox-old
+```
+
+Verify the API is accessible:
+```bash
+curl http://localhost:7745/api/v1/status
+```
+
+### Verification Tests Fail
+
+1. Download the Playwright report from GitHub Actions artifacts
+2. Review the HTML report for detailed failure information
+3. Check traces for visual debugging
+
+### Database Issues
+
+If migrations fail:
+```bash
+# Check database file
+ls -lh /tmp/homebox-data-new/homebox.db
+
+# Check Docker logs for migration errors
+docker logs homebox-new
+```
+
+## Future Enhancements
+
+Potential improvements:
+- [ ] Test multiple upgrade paths (e.g., v0.10 → v0.11 → v0.12)
+- [ ] Test with PostgreSQL backend in addition to SQLite
+- [ ] Add performance benchmarks
+- [ ] Test with larger datasets
+- [ ] Add API-level verification in addition to UI tests
+- [ ] Test backup and restore functionality
+
+## Related Files
+
+- `.github/workflows/upgrade-test.yaml` - Main workflow definition
+- `.github/scripts/upgrade-test/create-test-data.sh` - Data generation script
+- `frontend/test/upgrade/upgrade-verification.spec.ts` - Playwright verification tests
+- `.github/workflows/e2e-partial.yaml` - Standard E2E test workflow (for reference)
+
+## Support
+
+For issues or questions about this workflow:
+1. Check the GitHub Actions run logs
+2. Review this documentation
+3. Open an issue in the repository
diff --git a/.github/scripts/upgrade-test/create-test-data.sh b/.github/scripts/upgrade-test/create-test-data.sh
new file mode 100755
index 00000000..07bc7a8e
--- /dev/null
+++ b/.github/scripts/upgrade-test/create-test-data.sh
@@ -0,0 +1,413 @@
+#!/bin/bash
+
+# Script to create test data in HomeBox for upgrade testing
+# This script creates users, items, attachments, notifiers, locations, and labels
+
+set -e
+
+HOMEBOX_URL="${HOMEBOX_URL:-http://localhost:7745}"
+API_URL="${HOMEBOX_URL}/api/v1"
+TEST_DATA_FILE="${TEST_DATA_FILE:-/tmp/test-users.json}"
+
+echo "Creating test data in HomeBox at $HOMEBOX_URL"
+
+# Function to make API calls with error handling
+api_call() {
+ local method=$1
+ local endpoint=$2
+ local data=$3
+ local token=$4
+
+ if [ -n "$token" ]; then
+ if [ -n "$data" ]; then
+ curl -s -X "$method" \
+ -H "Authorization: Bearer $token" \
+ -H "Content-Type: application/json" \
+ -d "$data" \
+ "$API_URL$endpoint"
+ else
+ curl -s -X "$method" \
+ -H "Authorization: Bearer $token" \
+ -H "Content-Type: application/json" \
+ "$API_URL$endpoint"
+ fi
+ else
+ if [ -n "$data" ]; then
+ curl -s -X "$method" \
+ -H "Content-Type: application/json" \
+ -d "$data" \
+ "$API_URL$endpoint"
+ else
+ curl -s -X "$method" \
+ -H "Content-Type: application/json" \
+ "$API_URL$endpoint"
+ fi
+ fi
+}
+
+# Function to register a user and get token
+register_user() {
+ local email=$1
+ local name=$2
+ local password=$3
+ local group_token=$4
+
+ echo "Registering user: $email"
+
+ local payload="{\"email\":\"$email\",\"name\":\"$name\",\"password\":\"$password\""
+
+ if [ -n "$group_token" ]; then
+ payload="$payload,\"groupToken\":\"$group_token\""
+ fi
+
+ payload="$payload}"
+
+ local response=$(curl -s -X POST \
+ -H "Content-Type: application/json" \
+ -d "$payload" \
+ "$API_URL/users/register")
+
+ echo "$response"
+}
+
+# Function to login and get token
+login_user() {
+ local email=$1
+ local password=$2
+
+ echo "Logging in user: $email" >&2
+
+ local response=$(curl -s -X POST \
+ -H "Content-Type: application/json" \
+ -d "{\"username\":\"$email\",\"password\":\"$password\"}" \
+ "$API_URL/users/login")
+
+ echo "$response" | jq -r '.token // empty'
+}
+
+# Function to create an item
+create_item() {
+ local token=$1
+ local name=$2
+ local description=$3
+ local location_id=$4
+
+ echo "Creating item: $name" >&2
+
+ local payload="{\"name\":\"$name\",\"description\":\"$description\""
+
+ if [ -n "$location_id" ]; then
+ payload="$payload,\"locationId\":\"$location_id\""
+ fi
+
+ payload="$payload}"
+
+ local response=$(curl -s -X POST \
+ -H "Authorization: Bearer $token" \
+ -H "Content-Type: application/json" \
+ -d "$payload" \
+ "$API_URL/items")
+
+ echo "$response"
+}
+
+# Function to create a location
+create_location() {
+ local token=$1
+ local name=$2
+ local description=$3
+
+ echo "Creating location: $name" >&2
+
+ local response=$(curl -s -X POST \
+ -H "Authorization: Bearer $token" \
+ -H "Content-Type: application/json" \
+ -d "{\"name\":\"$name\",\"description\":\"$description\"}" \
+ "$API_URL/locations")
+
+ echo "$response"
+}
+
+# Function to create a label
+create_label() {
+ local token=$1
+ local name=$2
+ local description=$3
+
+ echo "Creating label: $name" >&2
+
+ local response=$(curl -s -X POST \
+ -H "Authorization: Bearer $token" \
+ -H "Content-Type: application/json" \
+ -d "{\"name\":\"$name\",\"description\":\"$description\"}" \
+ "$API_URL/labels")
+
+ echo "$response"
+}
+
+# Function to create a notifier
+create_notifier() {
+ local token=$1
+ local name=$2
+ local url=$3
+
+ echo "Creating notifier: $name" >&2
+
+ local response=$(curl -s -X POST \
+ -H "Authorization: Bearer $token" \
+ -H "Content-Type: application/json" \
+ -d "{\"name\":\"$name\",\"url\":\"$url\",\"isActive\":true}" \
+ "$API_URL/groups/notifiers")
+
+ echo "$response"
+}
+
+# Function to attach a file to an item (creates a dummy attachment)
+attach_file_to_item() {
+ local token=$1
+ local item_id=$2
+ local filename=$3
+
+ echo "Creating attachment for item: $item_id" >&2
+
+ # Create a temporary file with some content
+ local temp_file=$(mktemp)
+ echo "This is a test attachment for $filename" > "$temp_file"
+
+ local response=$(curl -s -X POST \
+ -H "Authorization: Bearer $token" \
+ -F "file=@$temp_file" \
+ -F "type=attachment" \
+ -F "name=$filename" \
+ "$API_URL/items/$item_id/attachments")
+
+ rm -f "$temp_file"
+
+ echo "$response"
+}
+
+# Initialize test data storage
+echo "{\"users\":[]}" > "$TEST_DATA_FILE"
+
+echo "=== Step 1: Create first group with 5 users ==="
+
+# Register first user (creates a new group)
+user1_response=$(register_user "user1@homebox.test" "User One" "TestPassword123!")
+user1_token=$(echo "$user1_response" | jq -r '.token // empty')
+group_token=$(echo "$user1_response" | jq -r '.group.inviteToken // empty')
+
+if [ -z "$user1_token" ]; then
+ echo "Failed to register first user"
+ echo "Response: $user1_response"
+ exit 1
+fi
+
+echo "First user registered with token. Group token: $group_token"
+
+# Store user1 data
+jq --arg email "user1@homebox.test" \
+ --arg password "TestPassword123!" \
+ --arg token "$user1_token" \
+ --arg group "1" \
+ '.users += [{"email":$email,"password":$password,"token":$token,"group":$group}]' \
+ "$TEST_DATA_FILE" > "$TEST_DATA_FILE.tmp" && mv "$TEST_DATA_FILE.tmp" "$TEST_DATA_FILE"
+
+# Register 4 more users in the same group
+for i in {2..5}; do
+ echo "Registering user$i in group 1..."
+ user_response=$(register_user "user${i}@homebox.test" "User $i" "TestPassword123!" "$group_token")
+ user_token=$(echo "$user_response" | jq -r '.token // empty')
+
+ if [ -z "$user_token" ]; then
+ echo "Failed to register user$i"
+ echo "Response: $user_response"
+ else
+ echo "user$i registered successfully"
+ # Store user data
+ jq --arg email "user${i}@homebox.test" \
+ --arg password "TestPassword123!" \
+ --arg token "$user_token" \
+ --arg group "1" \
+ '.users += [{"email":$email,"password":$password,"token":$token,"group":$group}]' \
+ "$TEST_DATA_FILE" > "$TEST_DATA_FILE.tmp" && mv "$TEST_DATA_FILE.tmp" "$TEST_DATA_FILE"
+ fi
+done
+
+echo "=== Step 2: Create second group with 2 users ==="
+
+# Register first user of second group
+user6_response=$(register_user "user6@homebox.test" "User Six" "TestPassword123!")
+user6_token=$(echo "$user6_response" | jq -r '.token // empty')
+group2_token=$(echo "$user6_response" | jq -r '.group.inviteToken // empty')
+
+if [ -z "$user6_token" ]; then
+ echo "Failed to register user6"
+ echo "Response: $user6_response"
+ exit 1
+fi
+
+echo "user6 registered with token. Group 2 token: $group2_token"
+
+# Store user6 data
+jq --arg email "user6@homebox.test" \
+ --arg password "TestPassword123!" \
+ --arg token "$user6_token" \
+ --arg group "2" \
+ '.users += [{"email":$email,"password":$password,"token":$token,"group":$group}]' \
+ "$TEST_DATA_FILE" > "$TEST_DATA_FILE.tmp" && mv "$TEST_DATA_FILE.tmp" "$TEST_DATA_FILE"
+
+# Register second user in group 2
+user7_response=$(register_user "user7@homebox.test" "User Seven" "TestPassword123!" "$group2_token")
+user7_token=$(echo "$user7_response" | jq -r '.token // empty')
+
+if [ -z "$user7_token" ]; then
+ echo "Failed to register user7"
+ echo "Response: $user7_response"
+else
+ echo "user7 registered successfully"
+ # Store user7 data
+ jq --arg email "user7@homebox.test" \
+ --arg password "TestPassword123!" \
+ --arg token "$user7_token" \
+ --arg group "2" \
+ '.users += [{"email":$email,"password":$password,"token":$token,"group":$group}]' \
+ "$TEST_DATA_FILE" > "$TEST_DATA_FILE.tmp" && mv "$TEST_DATA_FILE.tmp" "$TEST_DATA_FILE"
+fi
+
+echo "=== Step 3: Create locations for each group ==="
+
+# Create locations for group 1 (using user1's token)
+location1=$(create_location "$user1_token" "Living Room" "Main living area")
+location1_id=$(echo "$location1" | jq -r '.id // empty')
+echo "Created location: Living Room (ID: $location1_id)"
+
+location2=$(create_location "$user1_token" "Garage" "Storage and tools")
+location2_id=$(echo "$location2" | jq -r '.id // empty')
+echo "Created location: Garage (ID: $location2_id)"
+
+# Create location for group 2 (using user6's token)
+location3=$(create_location "$user6_token" "Home Office" "Work from home space")
+location3_id=$(echo "$location3" | jq -r '.id // empty')
+echo "Created location: Home Office (ID: $location3_id)"
+
+# Store locations
+jq --arg loc1 "$location1_id" \
+ --arg loc2 "$location2_id" \
+ --arg loc3 "$location3_id" \
+ '.locations = {"group1":[$loc1,$loc2],"group2":[$loc3]}' \
+ "$TEST_DATA_FILE" > "$TEST_DATA_FILE.tmp" && mv "$TEST_DATA_FILE.tmp" "$TEST_DATA_FILE"
+
+echo "=== Step 4: Create labels for each group ==="
+
+# Create labels for group 1
+label1=$(create_label "$user1_token" "Electronics" "Electronic devices")
+label1_id=$(echo "$label1" | jq -r '.id // empty')
+echo "Created label: Electronics (ID: $label1_id)"
+
+label2=$(create_label "$user1_token" "Important" "High priority items")
+label2_id=$(echo "$label2" | jq -r '.id // empty')
+echo "Created label: Important (ID: $label2_id)"
+
+# Create label for group 2
+label3=$(create_label "$user6_token" "Work Equipment" "Items for work")
+label3_id=$(echo "$label3" | jq -r '.id // empty')
+echo "Created label: Work Equipment (ID: $label3_id)"
+
+# Store labels
+jq --arg lab1 "$label1_id" \
+ --arg lab2 "$label2_id" \
+ --arg lab3 "$label3_id" \
+ '.labels = {"group1":[$lab1,$lab2],"group2":[$lab3]}' \
+ "$TEST_DATA_FILE" > "$TEST_DATA_FILE.tmp" && mv "$TEST_DATA_FILE.tmp" "$TEST_DATA_FILE"
+
+echo "=== Step 5: Create test notifier ==="
+
+# Create notifier for group 1
+notifier1=$(create_notifier "$user1_token" "TESTING" "https://example.com/webhook")
+notifier1_id=$(echo "$notifier1" | jq -r '.id // empty')
+echo "Created notifier: TESTING (ID: $notifier1_id)"
+
+# Store notifier
+jq --arg not1 "$notifier1_id" \
+ '.notifiers = {"group1":[$not1]}' \
+ "$TEST_DATA_FILE" > "$TEST_DATA_FILE.tmp" && mv "$TEST_DATA_FILE.tmp" "$TEST_DATA_FILE"
+
+echo "=== Step 6: Create items for all users ==="
+
+# Create items for users in group 1
+declare -A user_tokens
+user_tokens[1]=$user1_token
+user_tokens[2]=$(echo "$user1_token") # Users in same group share data, but we'll use user1 token
+user_tokens[3]=$(echo "$user1_token")
+user_tokens[4]=$(echo "$user1_token")
+user_tokens[5]=$(echo "$user1_token")
+
+# Items for group 1 users
+echo "Creating items for group 1..."
+item1=$(create_item "$user1_token" "Laptop Computer" "Dell XPS 15 for work" "$location1_id")
+item1_id=$(echo "$item1" | jq -r '.id // empty')
+echo "Created item: Laptop Computer (ID: $item1_id)"
+
+item2=$(create_item "$user1_token" "Power Drill" "DeWalt 20V cordless drill" "$location2_id")
+item2_id=$(echo "$item2" | jq -r '.id // empty')
+echo "Created item: Power Drill (ID: $item2_id)"
+
+item3=$(create_item "$user1_token" "TV Remote" "Samsung TV remote control" "$location1_id")
+item3_id=$(echo "$item3" | jq -r '.id // empty')
+echo "Created item: TV Remote (ID: $item3_id)"
+
+item4=$(create_item "$user1_token" "Tool Box" "Red metal tool box with tools" "$location2_id")
+item4_id=$(echo "$item4" | jq -r '.id // empty')
+echo "Created item: Tool Box (ID: $item4_id)"
+
+item5=$(create_item "$user1_token" "Coffee Maker" "Breville espresso machine" "$location1_id")
+item5_id=$(echo "$item5" | jq -r '.id // empty')
+echo "Created item: Coffee Maker (ID: $item5_id)"
+
+# Items for group 2 users
+echo "Creating items for group 2..."
+item6=$(create_item "$user6_token" "Monitor" "27 inch 4K monitor" "$location3_id")
+item6_id=$(echo "$item6" | jq -r '.id // empty')
+echo "Created item: Monitor (ID: $item6_id)"
+
+item7=$(create_item "$user6_token" "Keyboard" "Mechanical keyboard" "$location3_id")
+item7_id=$(echo "$item7" | jq -r '.id // empty')
+echo "Created item: Keyboard (ID: $item7_id)"
+
+# Store items
+jq --argjson group1_items "[\"$item1_id\",\"$item2_id\",\"$item3_id\",\"$item4_id\",\"$item5_id\"]" \
+ --argjson group2_items "[\"$item6_id\",\"$item7_id\"]" \
+ '.items = {"group1":$group1_items,"group2":$group2_items}' \
+ "$TEST_DATA_FILE" > "$TEST_DATA_FILE.tmp" && mv "$TEST_DATA_FILE.tmp" "$TEST_DATA_FILE"
+
+echo "=== Step 7: Add attachments to items ==="
+
+# Add attachments for group 1 items
+echo "Adding attachments to group 1 items..."
+attach_file_to_item "$user1_token" "$item1_id" "laptop-receipt.pdf"
+attach_file_to_item "$user1_token" "$item1_id" "laptop-warranty.pdf"
+attach_file_to_item "$user1_token" "$item2_id" "drill-manual.pdf"
+attach_file_to_item "$user1_token" "$item3_id" "remote-guide.pdf"
+attach_file_to_item "$user1_token" "$item4_id" "toolbox-inventory.txt"
+
+# Add attachments for group 2 items
+echo "Adding attachments to group 2 items..."
+attach_file_to_item "$user6_token" "$item6_id" "monitor-receipt.pdf"
+attach_file_to_item "$user6_token" "$item7_id" "keyboard-manual.pdf"
+
+echo "=== Test Data Creation Complete ==="
+echo "Test data file saved to: $TEST_DATA_FILE"
+echo "Summary:"
+echo " - Users created: 7 (5 in group 1, 2 in group 2)"
+echo " - Locations created: 3"
+echo " - Labels created: 3"
+echo " - Notifiers created: 1"
+echo " - Items created: 7"
+echo " - Attachments created: 7"
+
+# Display the test data file for verification
+echo ""
+echo "Test data:"
+cat "$TEST_DATA_FILE" | jq '.'
+
+exit 0
diff --git a/.github/workflows/binaries-publish.yaml b/.github/workflows/binaries-publish.yaml
index 84f2f1a8..707b297b 100644
--- a/.github/workflows/binaries-publish.yaml
+++ b/.github/workflows/binaries-publish.yaml
@@ -17,19 +17,17 @@ jobs:
id-token: write
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
with:
fetch-depth: 0
- name: Set up Go
- uses: actions/setup-go@v5
+ uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c
with:
go-version: "1.24"
cache-dependency-path: backend/go.mod
- - uses: pnpm/action-setup@v2
- with:
- version: 9.15.3
+ - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
- name: Build Frontend and Copy to Backend
working-directory: frontend
@@ -51,7 +49,7 @@ jobs:
- name: Run GoReleaser
id: releaser
if: startsWith(github.ref, 'refs/tags/')
- uses: goreleaser/goreleaser-action@v5
+ uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a
with:
workdir: "backend"
distribution: goreleaser
@@ -75,7 +73,7 @@ jobs:
- name: Run GoReleaser No Release
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
- uses: goreleaser/goreleaser-action@v5
+ uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a
with:
workdir: "backend"
distribution: goreleaser
@@ -93,7 +91,7 @@ jobs:
actions: read # To read the workflow path.
id-token: write # To sign the provenance.
contents: write # To add assets to a release.
- uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.9.0
+ uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a
with:
base64-subjects: "${{ needs.goreleaser.outputs.hashes }}"
upload-assets: true # upload to a new release
@@ -105,7 +103,7 @@ jobs:
permissions: read-all
steps:
- name: Install the verifier
- uses: slsa-framework/slsa-verifier/actions/installer@v2.4.0
+ uses: slsa-framework/slsa-verifier/actions/installer@ea584f4502babc6f60d9bc799dbbb13c1caa9ee6
- name: Download assets
env:
diff --git a/.github/workflows/clear-stale-docker-images.yml b/.github/workflows/clear-stale-docker-images.yml
index 6811a5ff..928a5744 100644
--- a/.github/workflows/clear-stale-docker-images.yml
+++ b/.github/workflows/clear-stale-docker-images.yml
@@ -12,7 +12,7 @@ jobs:
permissions:
packages: write
steps:
- - uses: dataaxiom/ghcr-cleanup-action@v1
+ - uses: dataaxiom/ghcr-cleanup-action@cd0cdb900b5dbf3a6f2cc869f0dbb0b8211f50c4
with:
dry-run: true
delete-ghost-images: true
@@ -32,7 +32,7 @@ jobs:
permissions:
packages: write
steps:
- - uses: dataaxiom/ghcr-cleanup-action@v1
+ - uses: dataaxiom/ghcr-cleanup-action@cd0cdb900b5dbf3a6f2cc869f0dbb0b8211f50c4
with:
dry-run: false
delete-untagged: true
diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml
index 7a9cd774..bd88e606 100644
--- a/.github/workflows/copilot-setup-steps.yml
+++ b/.github/workflows/copilot-setup-steps.yml
@@ -26,25 +26,23 @@ jobs:
# If you do not check out your code, Copilot will do this for you.
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
- name: Set up Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
with:
- node-version: "22"
+ node-version: "24"
- - uses: pnpm/action-setup@v3.0.0
- with:
- version: 9.12.2
+ - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
- name: Set up Go
- uses: actions/setup-go@v5
+ uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c
with:
go-version: "1.24"
cache-dependency-path: backend/go.mod
- name: Install Task
- uses: arduino/setup-task@v1
+ uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/docker-publish-hardened.yaml b/.github/workflows/docker-publish-hardened.yaml
index ac8547df..5b08fa28 100644
--- a/.github/workflows/docker-publish-hardened.yaml
+++ b/.github/workflows/docker-publish-hardened.yaml
@@ -33,7 +33,7 @@ env:
jobs:
build:
- runs-on: ubuntu-latest
+ runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
@@ -43,10 +43,11 @@ jobs:
strategy:
fail-fast: false
matrix:
- platform:
- - linux/amd64
- - linux/arm64
- - linux/arm/v7
+ include:
+ - platform: linux/amd64
+ runner: ubuntu-latest
+ - platform: linux/arm64
+ runner: ubuntu-24.04-arm
steps:
- name: Enable Debug Logs
@@ -56,7 +57,7 @@ jobs:
ACTIONS_STEP_DEBUG: true
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
- name: Prepare
run: |
@@ -123,7 +124,7 @@ jobs:
annotations: ${{ steps.meta.outputs.annotations }}
- name: Attest platform-specific images
- uses: actions/attest-build-provenance@v1
+ uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
if: github.event_name != 'pull_request'
with:
subject-name: ${{ env.GHCR_REPO }}
@@ -216,7 +217,7 @@ jobs:
echo "digest=$digest" >> $GITHUB_OUTPUT
- name: Attest GHCR images
- uses: actions/attest-build-provenance@v1
+ uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
if: github.event_name != 'pull_request'
with:
subject-name: ${{ env.GHCR_REPO }}
@@ -240,7 +241,7 @@ jobs:
echo "digest=$digest" >> $GITHUB_OUTPUT
- name: Attest Dockerhub images
- uses: actions/attest-build-provenance@v1
+ uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
with:
subject-name: docker.io/${{ env.DOCKERHUB_REPO }}
diff --git a/.github/workflows/docker-publish-rootless.yaml b/.github/workflows/docker-publish-rootless.yaml
index 004a4040..e20e4035 100644
--- a/.github/workflows/docker-publish-rootless.yaml
+++ b/.github/workflows/docker-publish-rootless.yaml
@@ -37,7 +37,7 @@ env:
jobs:
build:
- runs-on: ubuntu-latest
+ runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
@@ -47,10 +47,11 @@ jobs:
strategy:
fail-fast: false
matrix:
- platform:
- - linux/amd64
- - linux/arm64
- - linux/arm/v7
+ include:
+ - platform: linux/amd64
+ runner: ubuntu-latest
+ - platform: linux/arm64
+ runner: ubuntu-24.04-arm
steps:
- name: Enable Debug Logs
@@ -60,7 +61,7 @@ jobs:
ACTIONS_STEP_DEBUG: true
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
- name: Prepare
run: |
@@ -75,40 +76,40 @@ jobs:
- name: Docker meta
id: meta
- uses: docker/metadata-action@v5
+ uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
with:
images: |
name=${{ env.DOCKERHUB_REPO }},enable=${{ github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') }}
name=${{ env.GHCR_REPO }}
- name: Login to Docker Hub
- uses: docker/login-action@v3
+ uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GHCR
- uses: docker/login-action@v3
+ uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
- uses: docker/setup-qemu-action@v3
+ uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392
with:
image: ghcr.io/sysadminsmedia/binfmt:latest
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
+ uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
with:
driver-opts: |
image=ghcr.io/sysadminsmedia/buildkit:master
- name: Build and push by digest
id: build
- uses: docker/build-push-action@v6
+ uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
with:
context: . # Explicitly specify the build context
file: ./Dockerfile.rootless # Explicitly specify the Dockerfile
@@ -125,7 +126,7 @@ jobs:
annotations: ${{ steps.meta.outputs.annotations }}
- name: Attest platform-specific images
- uses: actions/attest-build-provenance@v1
+ uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
if: github.event_name != 'pull_request'
with:
subject-name: ${{ env.GHCR_REPO }}
@@ -139,7 +140,7 @@ jobs:
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
@@ -159,35 +160,35 @@ jobs:
steps:
- name: Download digests
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Login to Docker Hub
- uses: docker/login-action@v3
+ uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GHCR
- uses: docker/login-action@v3
+ uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
+ uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
with:
driver-opts: |
image=ghcr.io/sysadminsmedia/buildkit:master
- name: Docker meta
id: meta
- uses: docker/metadata-action@v5
+ uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
with:
images: |
name=${{ env.DOCKERHUB_REPO }},enable=${{ github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') }}
@@ -218,7 +219,7 @@ jobs:
echo "digest=$digest" >> $GITHUB_OUTPUT
- name: Attest GHCR images
- uses: actions/attest-build-provenance@v1
+ uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
if: github.event_name != 'pull_request'
with:
subject-name: ${{ env.GHCR_REPO }}
@@ -242,7 +243,7 @@ jobs:
echo "digest=$digest" >> $GITHUB_OUTPUT
- name: Attest Dockerhub images
- uses: actions/attest-build-provenance@v1
+ uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
with:
subject-name: docker.io/${{ env.DOCKERHUB_REPO }}
diff --git a/.github/workflows/docker-publish.yaml b/.github/workflows/docker-publish.yaml
index 6fcc67d6..84d2712b 100644
--- a/.github/workflows/docker-publish.yaml
+++ b/.github/workflows/docker-publish.yaml
@@ -37,7 +37,7 @@ permissions:
jobs:
build:
- runs-on: ubuntu-latest
+ runs-on: ${{ matrix.runner }}
permissions:
contents: read # Allows access to repository contents (read-only)
packages: write # Allows pushing to GHCR
@@ -47,14 +47,15 @@ jobs:
strategy:
fail-fast: false
matrix:
- platform:
- - linux/amd64
- - linux/arm64
- - linux/arm/v7
+ include:
+ - platform: linux/amd64
+ runner: ubuntu-latest
+ - platform: linux/arm64
+ runner: ubuntu-24.04-arm
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
- name: Prepare
run: |
@@ -70,40 +71,40 @@ jobs:
- name: Docker meta
id: meta
- uses: docker/metadata-action@v5
+ uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
with:
images: |
name=${{ env.DOCKERHUB_REPO }},enable=${{ github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') }}
name=${{ env.GHCR_REPO }}
- name: Login to Docker Hub
- uses: docker/login-action@v3
+ uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GHCR
- uses: docker/login-action@v3
+ uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
- uses: docker/setup-qemu-action@v3
+ uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392
with:
image: ghcr.io/sysadminsmedia/binfmt:latest
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
+ uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
with:
driver-opts: |
image=ghcr.io/sysadminsmedia/buildkit:latest
- name: Build and push by digest
id: build
- uses: docker/build-push-action@v6
+ uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
with:
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
@@ -118,7 +119,7 @@ jobs:
annotations: ${{ steps.meta.outputs.annotations }}
- name: Attest platform-specific images
- uses: actions/attest-build-provenance@v1
+ uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
if: github.event_name != 'pull_request'
with:
subject-name: ${{ env.GHCR_REPO }}
@@ -132,7 +133,7 @@ jobs:
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
@@ -152,35 +153,35 @@ jobs:
steps:
- name: Download digests
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Login to Docker Hub
- uses: docker/login-action@v3
+ uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GHCR
- uses: docker/login-action@v3
+ uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
+ uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
with:
driver-opts: |
image=ghcr.io/sysadminsmedia/buildkit:master
- name: Docker meta
id: meta
- uses: docker/metadata-action@v5
+ uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
with:
images: |
name=${{ env.DOCKERHUB_REPO }},enable=${{ github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') }}
@@ -209,7 +210,7 @@ jobs:
echo "digest=$digest" >> $GITHUB_OUTPUT
- name: Attest GHCR images
- uses: actions/attest-build-provenance@v1
+ uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
if: github.event_name != 'pull_request'
with:
subject-name: ${{ env.GHCR_REPO }}
@@ -233,7 +234,7 @@ jobs:
echo "digest=$digest" >> $GITHUB_OUTPUT
- name: Attest Dockerhub images
- uses: actions/attest-build-provenance@v1
+ uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
with:
subject-name: docker.io/${{ env.DOCKERHUB_REPO }}
diff --git a/.github/workflows/e2e-partial.yaml b/.github/workflows/e2e-partial.yaml
index f6ee9213..8da98630 100644
--- a/.github/workflows/e2e-partial.yaml
+++ b/.github/workflows/e2e-partial.yaml
@@ -1,5 +1,11 @@
name: E2E (Playwright)
+permissions:
+ contents: read
+ actions: read
+ checks: write
+ pull-requests: write
+
on:
workflow_call:
@@ -15,28 +21,26 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
with:
fetch-depth: 0
- name: Install Task
- uses: arduino/setup-task@v1
+ uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Go
- uses: actions/setup-go@v5
+ uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c
with:
- go-version: "1.23"
+ go-version: "1.24"
cache-dependency-path: backend/go.mod
- - uses: actions/setup-node@v4
+ - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
with:
node-version: lts/*
- - uses: pnpm/action-setup@v3.0.0
- with:
- version: 9.12.2
+ - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
- name: Install dependencies
run: pnpm install
@@ -49,7 +53,7 @@ jobs:
- name: Run E2E Tests
run: task test:e2e -- --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
- - uses: actions/upload-artifact@v4
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
name: Upload partial Playwright report
if: ${{ !cancelled() }}
with:
@@ -64,20 +68,18 @@ jobs:
name: Merge Playwright Reports
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-node@v4
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
+ - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
with:
node-version: lts/*
- - uses: pnpm/action-setup@v3.0.0
- with:
- version: 9.12.2
+ - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
- name: Install dependencies
run: pnpm install
working-directory: frontend
- name: Download blob reports from GitHub Actions Artifacts
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
path: frontend/all-blob-reports
pattern: blob-report-*
@@ -88,7 +90,7 @@ jobs:
working-directory: frontend
- name: Upload HTML report
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: html-report--attempt-${{ github.run_attempt }}
path: frontend/playwright-report
diff --git a/.github/workflows/issue-gatekeeper.yml b/.github/workflows/issue-gatekeeper.yml
new file mode 100644
index 00000000..56717a31
--- /dev/null
+++ b/.github/workflows/issue-gatekeeper.yml
@@ -0,0 +1,50 @@
+name: Issue Gatekeeper
+
+permissions:
+ issues: write
+
+on:
+ issues:
+ types: [ opened ]
+
+jobs:
+ check-permissions:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Verify Internal Template Use
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
+ with:
+ script: |
+ const { owner, repo } = context.repo;
+ const issue_number = context.issue.number;
+ const actor = context.payload.sender.login;
+
+ // 1. Get user permission level
+ const { data: perms } = await github.rest.repos.getCollaboratorPermissionLevel({
+ owner,
+ repo,
+ username: actor
+ });
+
+ const isMember = ['admin', 'write'].includes(perms.permission);
+ const body = context.payload.issue.body || "";
+
+ // 2. Check if they used the internal template (or if the issue is blank)
+ // We detect this by checking for our specific template string or the 'internal' label
+ const usedInternal = context.payload.issue.labels.some(l => l.name === 'internal');
+
+ if (usedInternal && !isMember) {
+ await github.rest.issues.createComment({
+ owner,
+ repo,
+ issue_number,
+ body: `@${actor}, the "Internal" template is restricted to project members. Please use one of the standard bug or feature templates for this repository.`
+ });
+
+ await github.rest.issues.update({
+ owner,
+ repo,
+ issue_number,
+ state: 'closed'
+ });
+ }
\ No newline at end of file
diff --git a/.github/workflows/partial-backend.yaml b/.github/workflows/partial-backend.yaml
index 7c620b08..ac7efaf3 100644
--- a/.github/workflows/partial-backend.yaml
+++ b/.github/workflows/partial-backend.yaml
@@ -1,5 +1,11 @@
name: Go Build/Test
+permissions:
+ contents: read
+ actions: read
+ checks: write
+ pull-requests: write
+
on:
workflow_call:
@@ -7,21 +13,21 @@ jobs:
Go:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
- name: Set up Go
- uses: actions/setup-go@v5
+ uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c
with:
go-version: "1.24"
cache-dependency-path: backend/go.mod
- name: Install Task
- uses: arduino/setup-task@v1
+ uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: golangci-lint
- uses: golangci/golangci-lint-action@v7
+ uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20
with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
version: latest
diff --git a/.github/workflows/partial-frontend.yaml b/.github/workflows/partial-frontend.yaml
index b219fed3..0b2bac71 100644
--- a/.github/workflows/partial-frontend.yaml
+++ b/.github/workflows/partial-frontend.yaml
@@ -1,5 +1,11 @@
name: Frontend
+permissions:
+ contents: read
+ actions: read
+ checks: write
+ pull-requests: write
+
on:
workflow_call:
@@ -9,13 +15,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
with:
fetch-depth: 0
- - uses: pnpm/action-setup@v3.0.0
- with:
- version: 9.12.2
+ - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
- name: Install dependencies
run: pnpm install
@@ -48,28 +52,26 @@ jobs:
--health-retries 5
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
with:
fetch-depth: 0
- name: Install Task
- uses: arduino/setup-task@v1
+ uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Go
- uses: actions/setup-go@v5
+ uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c
with:
- go-version: "1.23"
+ go-version: "1.24"
cache-dependency-path: backend/go.mod
- - uses: actions/setup-node@v4
+ - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
with:
- node-version: 18
+ node-version: lts/*
- - uses: pnpm/action-setup@v3.0.0
- with:
- version: 9.12.2
+ - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
- name: Install dependencies
run: pnpm install
@@ -99,28 +101,26 @@ jobs:
- 5432:5432
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
with:
fetch-depth: 0
- name: Install Task
- uses: arduino/setup-task@v1
+ uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Go
- uses: actions/setup-go@v5
+ uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c
with:
- go-version: "1.23"
+ go-version: "1.24"
cache-dependency-path: backend/go.mod
- - uses: actions/setup-node@v4
+ - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
with:
node-version: lts/*
- - uses: pnpm/action-setup@v3.0.0
- with:
- version: 9.12.2
+ - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
- name: Install dependencies
run: pnpm install
diff --git a/.github/workflows/pull-requests.yaml b/.github/workflows/pull-requests.yaml
index fe4fe37b..bb8dff25 100644
--- a/.github/workflows/pull-requests.yaml
+++ b/.github/workflows/pull-requests.yaml
@@ -1,5 +1,11 @@
name: Pull Request CI
+permissions:
+ contents: read
+ actions: read
+ checks: write
+ pull-requests: write
+
on:
pull_request:
branches:
diff --git a/.github/workflows/update-currencies.yml b/.github/workflows/update-currencies.yml
index b5bc3965..0c56c48e 100644
--- a/.github/workflows/update-currencies.yml
+++ b/.github/workflows/update-currencies.yml
@@ -15,12 +15,12 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
with:
fetch-depth: 0
- name: Set up Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548
with:
python-version: '3.8'
cache: 'pip'
@@ -44,7 +44,7 @@ jobs:
- name: Create Pull Request
if: env.changed == 'true'
- uses: peter-evans/create-pull-request@v7
+ uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: update-currencies
diff --git a/.github/workflows/upgrade-test.yaml b/.github/workflows/upgrade-test.yaml
new file mode 100644
index 00000000..f0ce20ea
--- /dev/null
+++ b/.github/workflows/upgrade-test.yaml
@@ -0,0 +1,177 @@
+#name: HomeBox Upgrade Test
+
+# on:
+# schedule:
+ # Run daily at 2 AM UTC
+ # - cron: '0 2 * * *'
+# workflow_dispatch: # Allow manual trigger
+# push:
+# branches:
+# - main
+# paths:
+# - '.github/workflows/upgrade-test.yaml'
+# - '.github/scripts/upgrade-test/**'
+
+jobs:
+ upgrade-test:
+ name: Test Upgrade Path
+ runs-on: ubuntu-latest
+ timeout-minutes: 60
+ permissions:
+ contents: read # Read repository contents
+ packages: read # Pull Docker images from GHCR
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: lts/*
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v3.0.0
+ with:
+ version: 9.12.2
+
+ - name: Install Playwright
+ run: |
+ cd frontend
+ pnpm install
+ pnpm exec playwright install --with-deps chromium
+
+ - name: Create test data directory
+ run: |
+ mkdir -p /tmp/homebox-data-old
+ mkdir -p /tmp/homebox-data-new
+ chmod -R 777 /tmp/homebox-data-old
+ chmod -R 777 /tmp/homebox-data-new
+
+ # Step 1: Pull and deploy latest stable version
+ - name: Pull latest stable HomeBox image
+ run: |
+ docker pull ghcr.io/sysadminsmedia/homebox:latest
+
+ - name: Start HomeBox (stable version)
+ run: |
+ docker run -d \
+ --name homebox-old \
+ --restart unless-stopped \
+ -p 7745:7745 \
+ -e HBOX_LOG_LEVEL=debug \
+ -e HBOX_OPTIONS_ALLOW_REGISTRATION=true \
+ -e TZ=UTC \
+ -v /tmp/homebox-data-old:/data \
+ ghcr.io/sysadminsmedia/homebox:latest
+
+ # Wait for the service to be ready
+ timeout 60 bash -c 'until curl -f http://localhost:7745/api/v1/status; do sleep 2; done'
+ echo "HomeBox stable version is ready"
+
+ # Step 2: Create test data
+ - name: Create test data
+ run: |
+ chmod +x .github/scripts/upgrade-test/create-test-data.sh
+ .github/scripts/upgrade-test/create-test-data.sh
+ env:
+ HOMEBOX_URL: http://localhost:7745
+
+ - name: Verify initial data creation
+ run: |
+ echo "Verifying test data was created..."
+ # Check if database file exists and has content
+ if [ -f /tmp/homebox-data-old/homebox.db ]; then
+ ls -lh /tmp/homebox-data-old/homebox.db
+ echo "Database file exists"
+ else
+ echo "Database file not found!"
+ exit 1
+ fi
+
+ - name: Stop old HomeBox instance
+ run: |
+ docker stop homebox-old
+ docker rm homebox-old
+
+ # Step 3: Build latest version from main branch
+ - name: Build HomeBox from main branch
+ run: |
+ docker build \
+ --build-arg VERSION=main \
+ --build-arg COMMIT=${{ github.sha }} \
+ --build-arg BUILD_TIME="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
+ -t homebox:test \
+ -f Dockerfile \
+ .
+
+ # Step 4: Copy data and start new version
+ - name: Copy data to new location
+ run: |
+ cp -r /tmp/homebox-data-old/* /tmp/homebox-data-new/
+ chmod -R 777 /tmp/homebox-data-new
+
+ - name: Start HomeBox (new version)
+ run: |
+ docker run -d \
+ --name homebox-new \
+ --restart unless-stopped \
+ -p 7745:7745 \
+ -e HBOX_LOG_LEVEL=debug \
+ -e HBOX_OPTIONS_ALLOW_REGISTRATION=true \
+ -e TZ=UTC \
+ -v /tmp/homebox-data-new:/data \
+ homebox:test
+
+ # Wait for the service to be ready
+ timeout 60 bash -c 'until curl -f http://localhost:7745/api/v1/status; do sleep 2; done'
+ echo "HomeBox new version is ready"
+
+ # Step 5: Run verification tests with Playwright
+ - name: Run verification tests
+ run: |
+ cd frontend
+ TEST_DATA_FILE=/tmp/test-users.json \
+ E2E_BASE_URL=http://localhost:7745 \
+ pnpm exec playwright test \
+ -c ./test/playwright.config.ts \
+ --project=chromium \
+ test/upgrade/upgrade-verification.spec.ts
+ env:
+ HOMEBOX_URL: http://localhost:7745
+
+ - name: Upload Playwright report
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: playwright-report-upgrade-test
+ path: frontend/playwright-report/
+ retention-days: 30
+
+ - name: Upload test traces
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: playwright-traces
+ path: frontend/test-results/
+ retention-days: 7
+
+ - name: Collect logs on failure
+ if: failure()
+ run: |
+ echo "=== Docker logs for new version ==="
+ docker logs homebox-new || true
+ echo "=== Database content ==="
+ ls -la /tmp/homebox-data-new/ || true
+
+ - name: Cleanup
+ if: always()
+ run: |
+ docker stop homebox-new || true
+ docker rm homebox-new || true
+ docker rmi homebox:test || true
diff --git a/.scaffold/go.sum b/.scaffold/go.sum
index 8d91d370..d6c9e8a7 100644
--- a/.scaffold/go.sum
+++ b/.scaffold/go.sum
@@ -8,7 +8,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
-github.com/sysadminsmedia/homebox/backend v0.0.0-20251212183312-2d1d3d927bfd h1:QULUJSgHc4rSlTjb2qYT6FIgwDWFCqEpnYqc/ltsrkk=
-github.com/sysadminsmedia/homebox/backend v0.0.0-20251212183312-2d1d3d927bfd/go.mod h1:jB+tPmHtPDM1VnAjah0gvcRfP/s7c+rtQwpA8cvZD/U=
+github.com/sysadminsmedia/homebox/backend v0.0.0-20251228052820-4557df86eddb h1:nRu1qr3gceoIIDJolCRnd/Eo5VLAMoH9CYnyKCCVBuA=
+github.com/sysadminsmedia/homebox/backend v0.0.0-20251228052820-4557df86eddb/go.mod h1:9zHHw5TNttw5Kn4Wks+SxwXmJPz6PgGNbnB4BtF1Z4c=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/backend/.goreleaser.yaml b/backend/.goreleaser.yaml
index d6b77631..1a25fc51 100644
--- a/backend/.goreleaser.yaml
+++ b/backend/.goreleaser.yaml
@@ -17,8 +17,6 @@ builds:
- freebsd
goarch:
- amd64
- - "386"
- - arm
- arm64
- riscv64
flags:
@@ -28,20 +26,9 @@ builds:
- -X main.version={{.Version}}
- -X main.commit={{.Commit}}
- -X main.date={{.Date}}
- ignore:
- - goos: windows
- goarch: arm
- - goos: windows
- goarch: "386"
- - goos: freebsd
- goarch: arm
- - goos: freebsd
- goarch: "386"
tags:
- >-
{{- if eq .Arch "riscv64" }}nodynamic
- {{- else if eq .Arch "arm" }}nodynamic
- {{- else if eq .Arch "386" }}nodynamic
{{- else if eq .Os "freebsd" }}nodynamic
{{ end }}
@@ -62,7 +49,6 @@ archives:
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
- {{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
# use zip for windows archives
diff --git a/backend/app/api/handlers/v1/v1_ctrl_actions.go b/backend/app/api/handlers/v1/v1_ctrl_actions.go
index 9f53f496..9766ed96 100644
--- a/backend/app/api/handlers/v1/v1_ctrl_actions.go
+++ b/backend/app/api/handlers/v1/v1_ctrl_actions.go
@@ -2,6 +2,7 @@ package v1
import (
"context"
+ "errors"
"net/http"
"github.com/google/uuid"
@@ -94,3 +95,54 @@ func (ctrl *V1Controller) HandleSetPrimaryPhotos() errchain.HandlerFunc {
func (ctrl *V1Controller) HandleCreateMissingThumbnails() errchain.HandlerFunc {
return actionHandlerFactory("create missing thumbnails", ctrl.repo.Attachments.CreateMissingThumbnails)
}
+
+// WipeInventoryOptions represents the options for wiping inventory
+type WipeInventoryOptions struct {
+ WipeLabels bool `json:"wipeLabels"`
+ WipeLocations bool `json:"wipeLocations"`
+ WipeMaintenance bool `json:"wipeMaintenance"`
+}
+
+// HandleWipeInventory godoc
+//
+// @Summary Wipe Inventory
+// @Description Deletes all items in the inventory
+// @Tags Actions
+// @Produce json
+// @Param options body WipeInventoryOptions false "Wipe options"
+// @Success 200 {object} ActionAmountResult
+// @Router /v1/actions/wipe-inventory [Post]
+// @Security Bearer
+func (ctrl *V1Controller) HandleWipeInventory() errchain.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) error {
+ if ctrl.isDemo {
+ return validate.NewRequestError(errors.New("wipe inventory is not allowed in demo mode"), http.StatusForbidden)
+ }
+
+ ctx := services.NewContext(r.Context())
+
+ // Check if user is owner
+ if !ctx.User.IsOwner {
+ return validate.NewRequestError(errors.New("only group owners can wipe inventory"), http.StatusForbidden)
+ }
+
+ // Parse options from request body
+ var options WipeInventoryOptions
+ if err := server.Decode(r, &options); err != nil {
+ // If no body provided, use default (false for all)
+ options = WipeInventoryOptions{
+ WipeLabels: false,
+ WipeLocations: false,
+ WipeMaintenance: false,
+ }
+ }
+
+ totalCompleted, err := ctrl.repo.Items.WipeInventory(ctx, ctx.GID, options.WipeLabels, options.WipeLocations, options.WipeMaintenance)
+ if err != nil {
+ log.Err(err).Str("action_ref", "wipe inventory").Msg("failed to run action")
+ return validate.NewRequestError(err, http.StatusInternalServerError)
+ }
+
+ return server.JSON(w, http.StatusOK, ActionAmountResult{Completed: totalCompleted})
+ }
+}
diff --git a/backend/app/api/main.go b/backend/app/api/main.go
index c651f3c1..b3d8ccd0 100644
--- a/backend/app/api/main.go
+++ b/backend/app/api/main.go
@@ -108,7 +108,7 @@ func run(cfg *config.Config) error {
return err
}
- if strings.ToLower(cfg.Database.Driver) == "postgres" {
+ if strings.ToLower(cfg.Database.Driver) == config.DriverPostgres {
if !validatePostgresSSLMode(cfg.Database.SslMode) {
log.Error().Str("sslmode", cfg.Database.SslMode).Msg("invalid sslmode")
return fmt.Errorf("invalid sslmode: %s", cfg.Database.SslMode)
diff --git a/backend/app/api/routes.go b/backend/app/api/routes.go
index 57e7897e..c69707dc 100644
--- a/backend/app/api/routes.go
+++ b/backend/app/api/routes.go
@@ -109,6 +109,7 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
r.Post("/actions/ensure-import-refs", chain.ToHandlerFunc(v1Ctrl.HandleEnsureImportRefs(), userMW...))
r.Post("/actions/set-primary-photos", chain.ToHandlerFunc(v1Ctrl.HandleSetPrimaryPhotos(), userMW...))
r.Post("/actions/create-missing-thumbnails", chain.ToHandlerFunc(v1Ctrl.HandleCreateMissingThumbnails(), userMW...))
+ r.Post("/actions/wipe-inventory", chain.ToHandlerFunc(v1Ctrl.HandleWipeInventory(), userMW...))
r.Get("/locations", chain.ToHandlerFunc(v1Ctrl.HandleLocationGetAll(), userMW...))
r.Post("/locations", chain.ToHandlerFunc(v1Ctrl.HandleLocationCreate(), userMW...))
diff --git a/backend/app/api/setup.go b/backend/app/api/setup.go
index 78b374bd..3a38c327 100644
--- a/backend/app/api/setup.go
+++ b/backend/app/api/setup.go
@@ -41,7 +41,7 @@ func setupStorageDir(cfg *config.Config) error {
func setupDatabaseURL(cfg *config.Config) (string, error) {
databaseURL := ""
switch strings.ToLower(cfg.Database.Driver) {
- case "sqlite3":
+ case config.DriverSqlite3:
databaseURL = cfg.Database.SqlitePath
dbFilePath := strings.Split(cfg.Database.SqlitePath, "?")[0]
dbDir := filepath.Dir(dbFilePath)
@@ -49,7 +49,7 @@ func setupDatabaseURL(cfg *config.Config) (string, error) {
log.Error().Err(err).Str("path", dbDir).Msg("failed to create SQLite database directory")
return "", fmt.Errorf("failed to create SQLite database directory: %w", err)
}
- case "postgres":
+ case config.DriverPostgres:
databaseURL = fmt.Sprintf("host=%s port=%s dbname=%s sslmode=%s", cfg.Database.Host, cfg.Database.Port, cfg.Database.Database, cfg.Database.SslMode)
if cfg.Database.Username != "" {
databaseURL += fmt.Sprintf(" user=%s", cfg.Database.Username)
diff --git a/backend/go.sum b/backend/go.sum
index a2ed0ce4..0d766402 100644
--- a/backend/go.sum
+++ b/backend/go.sum
@@ -10,31 +10,22 @@ cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIi
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
-cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
-cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
-cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc=
-cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA=
cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY=
-cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
-cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
+cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw=
cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E=
-cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM=
-cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=
+cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
-cloud.google.com/go/pubsub v1.50.0 h1:hnYpOIxVlgVD1Z8LN7est4DQZK3K6tvZNurZjIVjUe0=
-cloud.google.com/go/pubsub v1.50.0/go.mod h1:Di2Y+nqXBpIS+dXUEJPQzLh8PbIQZMLE9IVUFhf2zmM=
cloud.google.com/go/pubsub v1.50.1 h1:fzbXpPyJnSGvWXF1jabhQeXyxdbCIkXTpjXHy7xviBM=
cloud.google.com/go/pubsub v1.50.1/go.mod h1:6YVJv3MzWJUVdvQXG081sFvS0dWQOdnV+oTo++q/xFk=
cloud.google.com/go/pubsub/v2 v2.2.1 h1:3brZcshL3fIiD1qOxAE2QW9wxsfjioy014x4yC9XuYI=
cloud.google.com/go/pubsub/v2 v2.2.1/go.mod h1:O5f0KHG9zDheZAd3z5rlCRhxt2JQtB+t/IYLKK3Bpvw=
cloud.google.com/go/storage v1.56.0 h1:iixmq2Fse2tqxMbWhLWC9HfBj1qdxqAmiK8/eqtsLxI=
cloud.google.com/go/storage v1.56.0/go.mod h1:Tpuj6t4NweCLzlNbw9Z9iwxEkrSem20AetIeH/shgVU=
-cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=
-cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
+cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
entgo.io/ent v0.14.5 h1:Rj2WOYJtCkWyFo6a+5wB3EfBRP0rnx1fMk6gGA0UUe4=
entgo.io/ent v0.14.5/go.mod h1:zTzLmWtPvGpmSwtkaayM2cm5m819NdM7z7tYPq3vN0U=
github.com/Azure/azure-amqp-common-go/v3 v3.2.3 h1:uDF62mbd9bypXWi19V1bN5NZEO84JqgmI5G73ibAmrk=
@@ -88,8 +79,6 @@ github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7l
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
-github.com/ardanlabs/conf/v3 v3.9.0 h1:aRBYHeD39/OkuaEXYIEoi4wvF3OnS7jUAPxXyLfEu20=
-github.com/ardanlabs/conf/v3 v3.9.0/go.mod h1:XlL9P0quWP4m1weOVFmlezabinbZLI05niDof/+Ochk=
github.com/ardanlabs/conf/v3 v3.10.0 h1:qIrJ/WBmH/hFQ/IX4xH9LX9LzwK44T9aEOy78M+4S+0=
github.com/ardanlabs/conf/v3 v3.10.0/go.mod h1:XlL9P0quWP4m1weOVFmlezabinbZLI05niDof/+Ochk=
github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk=
@@ -183,14 +172,10 @@ github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzP
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
-github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
-github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gen2brain/avif v0.4.4 h1:Ga/ss7qcWWQm2bxFpnjYjhJsNfZrWs5RsyklgFjKRSE=
github.com/gen2brain/avif v0.4.4/go.mod h1:/XCaJcjZraQwKVhpu9aEd9aLOssYOawLvhMBtmHVGqk=
-github.com/gen2brain/heic v0.4.6 h1:sNh3mfaEZLmDJnFc5WoLxCzh/wj5GwfJScPfvF5CNJE=
-github.com/gen2brain/heic v0.4.6/go.mod h1:ECnpqbqLu0qSje4KSNWUUDK47UPXPzl80T27GWGEL5I=
github.com/gen2brain/heic v0.4.7 h1:xw/e9R3HdIvb+uEhRDMRJdviYnB3ODe/VwL8SYLaMGc=
github.com/gen2brain/heic v0.4.7/go.mod h1:ECnpqbqLu0qSje4KSNWUUDK47UPXPzl80T27GWGEL5I=
github.com/gen2brain/jpegxl v0.4.5 h1:TWpVEn5xkIfsswzkjHBArd0Cc9AE0tbjBSoa0jDsrbo=
@@ -210,16 +195,10 @@ github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
-github.com/go-openapi/jsonpointer v0.22.3 h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYAsPotrkn8=
-github.com/go-openapi/jsonpointer v0.22.3/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
-github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
-github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
-github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k=
-github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA=
github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc=
github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
@@ -249,8 +228,6 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
-github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
@@ -290,8 +267,6 @@ github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
-github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
-github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
@@ -364,8 +339,6 @@ github.com/nats-io/jwt/v2 v2.5.0 h1:WQQ40AAlqqfx+f6ku+i0pOVm+ASirD4fUh+oQsiE9Ak=
github.com/nats-io/jwt/v2 v2.5.0/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI=
github.com/nats-io/nats-server/v2 v2.9.23 h1:6Wj6H6QpP9FMlpCyWUaNu2yeZ/qGj+mdRkZ1wbikExU=
github.com/nats-io/nats-server/v2 v2.9.23/go.mod h1:wEjrEy9vnqIGE4Pqz4/c75v9Pmaq7My2IgFmnykc4C0=
-github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
-github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
@@ -384,8 +357,6 @@ github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
-github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
-github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.23 h1:oJE7T90aYBGtFNrI8+KbETnPymobAhzRrR8Mu8n1yfU=
github.com/pierrec/lz4/v4 v4.1.23/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
@@ -445,8 +416,6 @@ github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSy
github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
-github.com/tetratelabs/wazero v1.10.1 h1:2DugeJf6VVk58KTPszlNfeeN8AhhpwcZqkJj2wwFuH8=
-github.com/tetratelabs/wazero v1.10.1/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU=
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
@@ -484,26 +453,16 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
-go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
-go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0 h1:6VjV6Et+1Hd2iLZEPtdV7vie80Yyqf7oikJLjQ/myi0=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0/go.mod h1:u8hcp8ji5gaM/RfcOo8z9NMnf1pVLfVY7lBY2VOGuUU=
-go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
-go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
-go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
-go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
-go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
-go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
-go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
-go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -524,21 +483,13 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
-golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
-golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
-golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
-golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
-golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
-golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
-golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -548,18 +499,12 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
-golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
-golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
-golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
-golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -575,8 +520,6 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
-golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -586,8 +529,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
-golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
@@ -595,8 +536,6 @@ golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
-golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -605,28 +544,16 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhS
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
-google.golang.org/api v0.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA=
-google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4=
google.golang.org/api v0.258.0 h1:IKo1j5FBlN74fe5isA2PVozN3Y5pwNKriEgAXPOkDAc=
google.golang.org/api v0.258.0/go.mod h1:qhOMTQEZ6lUps63ZNq9jhODswwjkjYYguA7fA3TBFww=
-google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 h1:Nt6z9UHqSlIdIGJdz6KhTIs2VRx/iOsA5iE8bmQNcxs=
-google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79/go.mod h1:kTmlBHMPqR5uCZPBvwa2B18mvubkjyY3CRLI0c6fj0s=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
-google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
-google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
-google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
-google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
-google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
-google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -648,8 +575,6 @@ modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
-modernc.org/libc v1.67.1 h1:bFaqOaa5/zbWYJo8aW0tXPX21hXsngG2M7mckCnFSVk=
-modernc.org/libc v1.67.1/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
modernc.org/libc v1.67.2 h1:ZbNmly1rcbjhot5jlOZG0q4p5VwFfjwWqZ5rY2xxOXo=
modernc.org/libc v1.67.2/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
@@ -660,8 +585,6 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
-modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY=
-modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
modernc.org/sqlite v1.41.0 h1:bJXddp4ZpsqMsNN1vS0jWo4IJTZzb8nWpcgvyCFG9Ck=
modernc.org/sqlite v1.41.0/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
diff --git a/backend/internal/data/ent/item_predicates.go b/backend/internal/data/ent/item_predicates.go
index 0dda2de5..c124a2af 100644
--- a/backend/internal/data/ent/item_predicates.go
+++ b/backend/internal/data/ent/item_predicates.go
@@ -4,6 +4,7 @@ import (
"entgo.io/ent/dialect/sql"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/item"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/predicate"
+ conf "github.com/sysadminsmedia/homebox/backend/internal/sys/config"
"github.com/sysadminsmedia/homebox/backend/pkgs/textutils"
)
@@ -24,7 +25,7 @@ func AccentInsensitiveContains(field string, searchValue string) predicate.Item
dialect := s.Dialect()
switch dialect {
- case "sqlite3":
+ case conf.DriverSqlite3:
// For SQLite, we'll create a custom normalization function using REPLACE
// to handle common accented characters
normalizeFunc := buildSQLiteNormalizeExpression(s.C(field))
@@ -32,7 +33,7 @@ func AccentInsensitiveContains(field string, searchValue string) predicate.Item
"LOWER("+normalizeFunc+") LIKE ?",
"%"+normalizedSearch+"%",
))
- case "postgres":
+ case conf.DriverPostgres:
// For PostgreSQL, use REPLACE-based normalization to avoid unaccent dependency
normalizeFunc := buildGenericNormalizeExpression(s.C(field))
// Use sql.P() for proper PostgreSQL parameter binding ($1, $2, etc.)
diff --git a/backend/internal/data/migrations/migrations.go b/backend/internal/data/migrations/migrations.go
index 838ba5eb..05bac552 100644
--- a/backend/internal/data/migrations/migrations.go
+++ b/backend/internal/data/migrations/migrations.go
@@ -6,6 +6,7 @@ import (
"fmt"
"github.com/rs/zerolog/log"
+ "github.com/sysadminsmedia/homebox/backend/internal/sys/config"
)
//go:embed all:postgres
@@ -21,9 +22,9 @@ var sqliteFiles embed.FS
// embedded file system containing the migration files for the specified dialect.
func Migrations(dialect string) (embed.FS, error) {
switch dialect {
- case "postgres":
+ case config.DriverPostgres:
return postgresFiles, nil
- case "sqlite3":
+ case config.DriverSqlite3:
return sqliteFiles, nil
default:
log.Error().Str("dialect", dialect).Msg("unknown sql dialect")
diff --git a/backend/internal/data/repo/repo_items.go b/backend/internal/data/repo/repo_items.go
index c2749b0a..49943dc1 100644
--- a/backend/internal/data/repo/repo_items.go
+++ b/backend/internal/data/repo/repo_items.go
@@ -809,6 +809,87 @@ func (e *ItemsRepository) DeleteByGroup(ctx context.Context, gid, id uuid.UUID)
return err
}
+func (e *ItemsRepository) WipeInventory(ctx context.Context, gid uuid.UUID, wipeLabels bool, wipeLocations bool, wipeMaintenance bool) (int, error) {
+ // Get all items for the group
+ items, err := e.db.Item.Query().
+ Where(item.HasGroupWith(group.ID(gid))).
+ WithAttachments().
+ All(ctx)
+ if err != nil {
+ return 0, err
+ }
+
+ deleted := 0
+ // Delete each item with its attachments
+ // Note: We manually delete attachments and items instead of calling DeleteByGroup
+ // to continue processing remaining items even if some deletions fail
+ for _, itm := range items {
+ // Delete all attachments first
+ for _, att := range itm.Edges.Attachments {
+ err := e.attachments.Delete(ctx, gid, itm.ID, att.ID)
+ if err != nil {
+ log.Err(err).Str("attachment_id", att.ID.String()).Msg("failed to delete attachment during wipe inventory")
+ // Continue with other attachments even if one fails
+ }
+ }
+
+ // Delete the item
+ _, err = e.db.Item.
+ Delete().
+ Where(
+ item.ID(itm.ID),
+ item.HasGroupWith(group.ID(gid)),
+ ).Exec(ctx)
+ if err != nil {
+ log.Err(err).Str("item_id", itm.ID.String()).Msg("failed to delete item during wipe inventory")
+ // Skip to next item without incrementing counter
+ continue
+ }
+
+ // Only increment counter if deletion succeeded
+ deleted++
+ }
+
+ // Wipe labels if requested
+ if wipeLabels {
+ labelCount, err := e.db.Label.Delete().Where(label.HasGroupWith(group.ID(gid))).Exec(ctx)
+ if err != nil {
+ log.Err(err).Msg("failed to delete labels during wipe inventory")
+ } else {
+ log.Info().Int("count", labelCount).Msg("deleted labels during wipe inventory")
+ deleted += labelCount
+ }
+ }
+
+ // Wipe locations if requested
+ if wipeLocations {
+ locationCount, err := e.db.Location.Delete().Where(location.HasGroupWith(group.ID(gid))).Exec(ctx)
+ if err != nil {
+ log.Err(err).Msg("failed to delete locations during wipe inventory")
+ } else {
+ log.Info().Int("count", locationCount).Msg("deleted locations during wipe inventory")
+ deleted += locationCount
+ }
+ }
+
+ // Wipe maintenance records if requested
+ if wipeMaintenance {
+ // Maintenance entries are linked to items, so we query by items in the group
+ maintenanceCount, err := e.db.MaintenanceEntry.Delete().
+ Where(maintenanceentry.HasItemWith(item.HasGroupWith(group.ID(gid)))).
+ Exec(ctx)
+ if err != nil {
+ log.Err(err).Msg("failed to delete maintenance entries during wipe inventory")
+ } else {
+ log.Info().Int("count", maintenanceCount).Msg("deleted maintenance entries during wipe inventory")
+ deleted += maintenanceCount
+ }
+ }
+
+ e.publishMutationEvent(gid)
+ return deleted, nil
+}
+
func (e *ItemsRepository) UpdateByGroup(ctx context.Context, gid uuid.UUID, data ItemUpdate) (ItemOut, error) {
q := e.db.Item.Update().Where(item.ID(data.ID), item.HasGroupWith(group.ID(gid))).
SetName(data.Name).
diff --git a/backend/internal/sys/config/conf_database.go b/backend/internal/sys/config/conf_database.go
index 22c2b244..91a0edd1 100644
--- a/backend/internal/sys/config/conf_database.go
+++ b/backend/internal/sys/config/conf_database.go
@@ -1,7 +1,8 @@
package config
const (
- DriverSqlite3 = "sqlite3"
+ DriverSqlite3 = "sqlite3"
+ DriverPostgres = "postgres"
)
type Storage struct {
diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
index ad457201..c71f7415 100644
--- a/docs/.vitepress/config.mts
+++ b/docs/.vitepress/config.mts
@@ -43,6 +43,7 @@ export default defineConfig({
nav: [
{ text: 'API Docs', link: '/en/api' },
{ text: 'Demo', link: 'https://demo.homebox.software' },
+ { text: 'Blog', link: 'https://sysadminsjournal.com/tag/homebox/' }
],
sidebar: {
diff --git a/frontend/components/WipeInventoryDialog.vue b/frontend/components/WipeInventoryDialog.vue
new file mode 100644
index 00000000..29f323b3
--- /dev/null
+++ b/frontend/components/WipeInventoryDialog.vue
@@ -0,0 +1,139 @@
+
+
+ {{ $t("tools.actions_set.wipe_inventory_note") }}
+