mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-25 14:59:21 +01:00
Compare commits
5 Commits
mk/keyless
...
homebox-co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b5d4074d3 | ||
|
|
4749ce791d | ||
|
|
1d941b148c | ||
|
|
c980ce679c | ||
|
|
84dc54be07 |
@@ -29,6 +29,6 @@
|
||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "node",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/go:1": "1.24"
|
||||
"ghcr.io/devcontainers/features/go:1": "1.21"
|
||||
}
|
||||
}
|
||||
|
||||
40
.github/AGENTS.md
vendored
40
.github/AGENTS.md
vendored
@@ -1,40 +0,0 @@
|
||||
This is a Go based repository with a VueJS client for the frontend built with Vite and Nuxt, with ShadCN.
|
||||
|
||||
To make life easier, the use of a Taskfile is included for the majority of development commands.
|
||||
|
||||
Please follow these guidelines when contributing:
|
||||
|
||||
## Required Before Each Commit
|
||||
- Generate Swagger Files: `task swag --force`
|
||||
- Generate JS API Client: `task typescript-types --force`
|
||||
- Lint Golang: `task go:lint`
|
||||
- Lint frontend: `task ui:fix`
|
||||
|
||||
## Repository Structure
|
||||
### Backend
|
||||
- `backend/`: Contains the backend folders
|
||||
- `backend/app`: Contains main app code including API endpoints
|
||||
- `backend/internal/core`: Contains basic services such as currencies
|
||||
- `backend/data`: Contains all information related to data, including `ent` schemas, repos, migrations, etc.
|
||||
- `backend/data/migrations`: Contains migration data, the `sqlite3` sub-folder contains sqlite migrations, `postgres` sub-folder the postgres migrations, BOTH are REQUIRED.
|
||||
- `backend/data/ent/schema`: Contains the actual `ent` data models.
|
||||
- `backend/data/repo`: Contains the data repositories
|
||||
- `backend/pkgs`: Contains general helper functions and services
|
||||
|
||||
### Frontend
|
||||
- `frontend/`: Contains initial frontend files
|
||||
- `frontend/components`: Contains the ShadCN components
|
||||
- `frontend/locales`: Contains the i18n JSON for languages
|
||||
- `frontend/pages`: Contains VueJS pages
|
||||
- `frontend/test`: Contains Playwright setup
|
||||
- `frontend/test/e2e`: Contains actual Playwright test files
|
||||
|
||||
### Docs
|
||||
- `docs/`: Contains VitePress based documentation
|
||||
|
||||
## Key Guidelines
|
||||
1. Follow best practices for the various programming languages
|
||||
2. Maintain existing code structure and organization when possible
|
||||
3. Use dependency injection when reasonable
|
||||
4. Write tests for new functionality and after fixing bugs to validate they're fixed
|
||||
5. Document changes to the `docs/` folder when appropriate
|
||||
21
.github/workflows/binaries-publish.yaml
vendored
21
.github/workflows/binaries-publish.yaml
vendored
@@ -1,7 +1,6 @@
|
||||
name: Publish Release Binaries
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags: [ 'v*.*.*' ]
|
||||
|
||||
@@ -9,10 +8,6 @@ jobs:
|
||||
goreleaser:
|
||||
name: goreleaser
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -42,7 +37,6 @@ jobs:
|
||||
go install github.com/sigstore/cosign/cmd/cosign@latest
|
||||
|
||||
- name: Run GoReleaser
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
workdir: "backend"
|
||||
@@ -51,18 +45,3 @@ jobs:
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COSIGN_PWD: ${{ secrets.COSIGN_PWD }}
|
||||
COSIGN_YES: "true"
|
||||
|
||||
- name: Run GoReleaser No Release
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
workdir: "backend"
|
||||
distribution: goreleaser
|
||||
version: "~> v2"
|
||||
args: release --clean --snapshot --skip=publish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COSIGN_PWD: ${{ secrets.COSIGN_PWD }}
|
||||
COSIGN_YES: "true"
|
||||
52
.github/workflows/copilot-setup-steps.yml
vendored
52
.github/workflows/copilot-setup-steps.yml
vendored
@@ -1,52 +0,0 @@
|
||||
name: "Copilot Setup Steps"
|
||||
|
||||
# Automatically run the setup steps when they are changed to allow for easy validation, and
|
||||
# allow manual testing through the repository's "Actions" tab
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- .github/workflows/copilot-setup-steps.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/copilot-setup-steps.yml
|
||||
|
||||
jobs:
|
||||
# The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot.
|
||||
copilot-setup-steps:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Set the permissions to the lowest permissions possible needed for your steps.
|
||||
# Copilot will be given its own token for its operations.
|
||||
permissions:
|
||||
# If you want to clone the repository as part of your setup steps, for example to install dependencies, you'll need the `contents: read` permission. If you don't clone the repository in your setup steps, Copilot will do this for you automatically after the steps complete.
|
||||
contents: read
|
||||
|
||||
# You can define any steps you want, and they will run before the agent starts.
|
||||
# If you do not check out your code, Copilot will do this for you.
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- uses: pnpm/action-setup@v3.0.0
|
||||
with:
|
||||
version: 9.12.2
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
cache-dependency-path: backend/go.mod
|
||||
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Perform setup
|
||||
run: task setup
|
||||
208
.github/workflows/docker-publish-hardened.yaml
vendored
208
.github/workflows/docker-publish-hardened.yaml
vendored
@@ -1,208 +0,0 @@
|
||||
name: Docker publish hardened
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '00 0 * * *'
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'frontend/**'
|
||||
- 'Dockerfile.hardened'
|
||||
- '.dockerignore'
|
||||
- '.github/workflows/docker-publish-hardened.yaml'
|
||||
tags: [ 'v*.*.*' ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'frontend/**'
|
||||
- 'Dockerfile.hardened'
|
||||
- '.dockerignore'
|
||||
- '.github/workflows/docker-publish-hardened.yaml'
|
||||
|
||||
permissions:
|
||||
contents: read # Access to repository contents
|
||||
packages: write # Write access for pushing to GHCR
|
||||
id-token: write # Required for OIDC authentication (if used)
|
||||
attestations: write # Required for signing and attestation (if needed)
|
||||
|
||||
env:
|
||||
DOCKERHUB_REPO: sysadminsmedia/homebox
|
||||
GHCR_REPO: ghcr.io/${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- linux/arm/v7
|
||||
|
||||
steps:
|
||||
- name: Enable Debug Logs
|
||||
run: echo "##[debug]Enabling debug logging"
|
||||
env:
|
||||
ACTIONS_RUNNER_DEBUG: true
|
||||
ACTIONS_STEP_DEBUG: true
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
echo "BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_ENV
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
branch=${{ github.event.pull_request.number || github.ref_name }}
|
||||
echo "BRANCH=${branch//\//-}" >> $GITHUB_ENV
|
||||
echo "DOCKERNAMES=${{ env.DOCKERHUB_REPO }},${{ env.GHCR_REPO }}" >> $GITHUB_ENV
|
||||
if [[ "${{ github.event_name }}" != "schedule" ]] || [[ "${{ github.ref }}" != refs/tags/* ]]; then
|
||||
echo "DOCKERNAMES=${{ env.GHCR_REPO }}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
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@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@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392
|
||||
with:
|
||||
image: ghcr.io/amitie10g/binfmt:latest
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
|
||||
with:
|
||||
driver-opts: |
|
||||
image=ghcr.io/amitie10g/buildkit:master
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
|
||||
with:
|
||||
context: . # Explicitly specify the build context
|
||||
file: ./Dockerfile.hardened # Explicitly specify the Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,"name=${{ env.DOCKERNAMES }}",push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
|
||||
cache-from: type=registry,ref=ghcr.io/sysadminsmedia/devcache:${{ env.PLATFORM_PAIR }}-${{ env.BRANCH }}-hardened
|
||||
cache-to: type=registry,ref=ghcr.io/sysadminsmedia/devcache:${{ env.PLATFORM_PAIR }}-${{ env.BRANCH }}-hardened,mode=max,ignore-error=true
|
||||
build-args: |
|
||||
VERSION=${{ github.ref_name }}
|
||||
COMMIT=${{ github.sha }}
|
||||
BUILD_TIME=${{ env.BUILD_TIME }}
|
||||
provenance: true
|
||||
sbom: true
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
needs:
|
||||
- build
|
||||
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
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@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
|
||||
with:
|
||||
driver-opts: |
|
||||
image=ghcr.io/amitie10g/buildkit:master
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
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 }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=schedule,pattern=nightly
|
||||
flavor: |
|
||||
suffix=-hardened,onlatest=true
|
||||
|
||||
- name: Create manifest list and push GHCR
|
||||
id: push-ghcr
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *)
|
||||
|
||||
- name: Create manifest list and push Dockerhub
|
||||
id: push-dockerhub
|
||||
working-directory: /tmp/digests
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.DOCKERHUB_REPO }}@sha256:%s ' *)
|
||||
12
.github/workflows/docker-publish-rootless.yaml
vendored
12
.github/workflows/docker-publish-rootless.yaml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'frontend/**'
|
||||
- 'Dockerfile.rootless'
|
||||
- 'Dockerfile'
|
||||
- '.dockerignore'
|
||||
- '.github/workflows/docker-publish-rootless.yaml'
|
||||
ignore:
|
||||
@@ -19,7 +19,7 @@ on:
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'frontend/**'
|
||||
- 'Dockerfile.rootless'
|
||||
- 'Dockerfile'
|
||||
- '.dockerignore'
|
||||
- '.github/workflows/docker-publish-rootless.yaml'
|
||||
ignore:
|
||||
@@ -33,7 +33,7 @@ permissions:
|
||||
|
||||
env:
|
||||
DOCKERHUB_REPO: sysadminsmedia/homebox
|
||||
GHCR_REPO: ghcr.io/${{ github.repository }}
|
||||
GHCR_REPO: ghcr.io/sysadminsmedia/homebox
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
@@ -159,7 +159,7 @@ jobs:
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
@@ -204,7 +204,7 @@ jobs:
|
||||
- name: Create manifest list and push Dockerhub
|
||||
id: push-dockerhub
|
||||
working-directory: /tmp/digests
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.DOCKERHUB_REPO }}@sha256:%s ' *)
|
||||
|
||||
7
.github/workflows/docker-publish.yaml
vendored
7
.github/workflows/docker-publish.yaml
vendored
@@ -27,7 +27,7 @@ on:
|
||||
|
||||
env:
|
||||
DOCKERHUB_REPO: sysadminsmedia/homebox
|
||||
GHCR_REPO: ghcr.io/${{ github.repository }}
|
||||
GHCR_REPO: ghcr.io/sysadminsmedia/homebox
|
||||
|
||||
permissions:
|
||||
contents: read # Access to repository contents
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
@@ -152,7 +152,6 @@ jobs:
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
@@ -195,7 +194,7 @@ jobs:
|
||||
- name: Create manifest list and push Dockerhub
|
||||
id: push-dockerhub
|
||||
working-directory: /tmp/digests
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.DOCKERHUB_REPO }}@sha256:%s ' *)
|
||||
|
||||
5
.github/workflows/pull-requests.yaml
vendored
5
.github/workflows/pull-requests.yaml
vendored
@@ -9,10 +9,7 @@ on:
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'frontend/**'
|
||||
- '.github/workflows/partial-backend.yaml'
|
||||
- '.github/workflows/partial-frontend.yaml'
|
||||
- '.github/workflows/e2e-partial.yaml'
|
||||
- '.github/workflows/pull-requests.yaml'
|
||||
- '.github/workflows/**'
|
||||
|
||||
jobs:
|
||||
backend-tests:
|
||||
|
||||
8
.vscode/launch.json
vendored
8
.vscode/launch.json
vendored
@@ -16,12 +16,14 @@
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "debug",
|
||||
"program": "${workspaceFolder}/backend/app/api/",
|
||||
"program": "${workspaceRoot}/backend/app/api/",
|
||||
"args": [],
|
||||
"env": {
|
||||
"HBOX_DEMO": "true",
|
||||
"HBOX_LOG_LEVEL": "debug",
|
||||
"HBOX_DEBUG_ENABLED": "true"
|
||||
"HBOX_DEBUG_ENABLED": "true",
|
||||
"HBOX_STORAGE_DATA": "${workspaceRoot}/backend/.data",
|
||||
"HBOX_STORAGE_SQLITE_URL": "${workspaceRoot}/backend/.data/homebox.db?_fk=1&_time_format=sqlite"
|
||||
},
|
||||
"console": "integratedTerminal",
|
||||
},
|
||||
@@ -44,4 +46,4 @@
|
||||
"console": "integratedTerminal",
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
# ---------------------------------------
|
||||
# Node dependencies stage
|
||||
# ---------------------------------------
|
||||
FROM public.ecr.aws/docker/library/node:lts-alpine AS frontend-dependencies
|
||||
WORKDIR /app
|
||||
|
||||
# Install pnpm globally (caching layer)
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Copy package.json and lockfile to leverage caching
|
||||
COPY frontend/package.json frontend/pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# ---------------------------------------
|
||||
# Build Nuxt (frontend) stage
|
||||
# ---------------------------------------
|
||||
FROM public.ecr.aws/docker/library/node:lts-alpine AS frontend-builder
|
||||
WORKDIR /app
|
||||
|
||||
# Install pnpm globally again (it can reuse the cache if not changed)
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Copy over source files and node_modules from dependencies stage
|
||||
COPY frontend .
|
||||
COPY --from=frontend-dependencies /app/node_modules ./node_modules
|
||||
RUN pnpm build
|
||||
|
||||
# ---------------------------------------
|
||||
# Go dependencies stage
|
||||
# ---------------------------------------
|
||||
FROM public.ecr.aws/docker/library/golang:alpine AS builder-dependencies
|
||||
WORKDIR /go/src/app
|
||||
|
||||
# Copy go.mod and go.sum for better caching
|
||||
COPY ./backend/go.mod ./backend/go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# ---------------------------------------
|
||||
# Build API + healthcheck stage
|
||||
# ---------------------------------------
|
||||
FROM public.ecr.aws/docker/library/golang:alpine AS builder
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG BUILD_TIME
|
||||
ARG COMMIT
|
||||
ARG VERSION
|
||||
|
||||
# Install necessary build tools
|
||||
RUN apk update && \
|
||||
apk upgrade && \
|
||||
apk add --no-cache git build-base gcc g++
|
||||
|
||||
WORKDIR /go/src/app
|
||||
|
||||
# Copy Go modules (from dependencies stage) and source code
|
||||
COPY --from=builder-dependencies /go/pkg/mod /go/pkg/mod
|
||||
COPY ./backend .
|
||||
|
||||
# Clear old public files and copy new ones from frontend build
|
||||
RUN rm -rf ./app/api/public
|
||||
COPY --from=frontend-builder /app/.output/public ./app/api/static/public
|
||||
|
||||
# Use cache for Go build artifacts to build Homebox API
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build \
|
||||
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
|
||||
-tags nodynamic -o /go/bin/api -v ./app/api/*.go
|
||||
|
||||
RUN chmod +x /go/bin/api
|
||||
RUN mkdir /app
|
||||
RUN mkdir /data
|
||||
|
||||
# ---------- Build static healthcheck helper ----------
|
||||
# A small Go program that GETs the status URL and exits 0 on 2xx.
|
||||
RUN cat > /tmp/healthcheck.go <<'EOF'
|
||||
package main
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
func main() {
|
||||
url := "http://127.0.0.1:7745/api/v1/status"
|
||||
if len(os.Args) > 1 { url = os.Args[1] }
|
||||
c := &http.Client{ Timeout: 3 * time.Second }
|
||||
resp, err := c.Get(url)
|
||||
if err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) }
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode/100 != 2 {
|
||||
fmt.Fprintln(os.Stderr, "unexpected status:", resp.StatusCode)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
|
||||
go build -ldflags "-s -w" -o /go/bin/hc /tmp/healthcheck.go
|
||||
|
||||
# ---------------------------------------
|
||||
# Production stage
|
||||
# ---------------------------------------
|
||||
FROM gcr.io/distroless/static:nonroot
|
||||
ENV HBOX_MODE=production
|
||||
ENV HBOX_STORAGE_CONN_STRING=file:///?no_tmp_dir=true
|
||||
ENV HBOX_STORAGE_PREFIX_PATH=data
|
||||
ENV HBOX_DATABASE_SQLITE_PATH=/data/homebox.db?_pragma=busy_timeout=2000&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite
|
||||
|
||||
# Create application directory and copy over built Go binary and assets
|
||||
COPY --from=builder --chown=65532:65532 /app /app
|
||||
COPY --from=builder --chown=65532:65532 --chmod=755 /go/bin/api /app
|
||||
COPY --from=builder --chown=65532:65532 /data /data
|
||||
|
||||
# Copy the healthcheck helper
|
||||
COPY --from=builder --chown=65532:65532 --chmod=755 /go/bin/hc /app/healthcheck
|
||||
|
||||
# Labels and configuration for the final image
|
||||
LABEL Name=homebox Version=0.0.1
|
||||
LABEL org.opencontainers.image.source="https://github.com/sysadminsmedia/homebox"
|
||||
|
||||
# Expose necessary ports for Homebox
|
||||
EXPOSE 7745
|
||||
WORKDIR /app
|
||||
|
||||
# Persist volume for data
|
||||
VOLUME [ "/data" ]
|
||||
|
||||
# Entrypoint and CMD
|
||||
USER 65532
|
||||
ENTRYPOINT [ "/app/api" ]
|
||||
CMD [ "/data/config.yml" ]
|
||||
|
||||
# JSON exec-form healthcheck (no shell, no wget)
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
||||
CMD ["/app/healthcheck", "http://127.0.0.1:7745/api/v1/status"]
|
||||
@@ -14,7 +14,6 @@ builds:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
- freebsd
|
||||
goarch:
|
||||
- amd64
|
||||
- "386"
|
||||
@@ -26,16 +25,11 @@ builds:
|
||||
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 }}
|
||||
|
||||
signs:
|
||||
@@ -43,7 +37,7 @@ signs:
|
||||
stdin: "{{ .Env.COSIGN_PWD }}"
|
||||
args:
|
||||
- "sign-blob"
|
||||
- "--output-certificate=${certificate}"
|
||||
- "--key=cosign.key"
|
||||
- "--output-signature=${signature}"
|
||||
- "${artifact}"
|
||||
- "--yes" # needed on cosign 2.0.0+
|
||||
|
||||
@@ -254,25 +254,6 @@ func (ctrl *V1Controller) HandleItemPatch() errchain.HandlerFunc {
|
||||
return adapters.ActionID("id", fn, http.StatusOK)
|
||||
}
|
||||
|
||||
// HandleItemDuplicate godocs
|
||||
//
|
||||
// @Summary Duplicate Item
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param payload body repo.DuplicateOptions true "Duplicate Options"
|
||||
// @Success 201 {object} repo.ItemOut
|
||||
// @Router /v1/items/{id}/duplicate [POST]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemDuplicate() errchain.HandlerFunc {
|
||||
fn := func(r *http.Request, ID uuid.UUID, options repo.DuplicateOptions) (repo.ItemOut, error) {
|
||||
ctx := services.NewContext(r.Context())
|
||||
return ctrl.svc.Items.Duplicate(ctx, ctx.GID, ID, options)
|
||||
}
|
||||
|
||||
return adapters.ActionID("id", fn, http.StatusCreated)
|
||||
}
|
||||
|
||||
// HandleGetAllCustomFieldNames godocs
|
||||
//
|
||||
// @Summary Get All Custom Field Names
|
||||
|
||||
@@ -205,7 +205,7 @@ func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r
|
||||
}(bucket)
|
||||
|
||||
// Set the Content-Disposition header for RFC6266 compliance
|
||||
disposition := "inline; filename*=UTF-8''" + url.QueryEscape(doc.Title)
|
||||
disposition := "attachment; filename*=UTF-8''" + url.QueryEscape(doc.Title)
|
||||
w.Header().Set("Content-Disposition", disposition)
|
||||
http.ServeContent(w, r, doc.Title, doc.CreatedAt, file)
|
||||
return nil
|
||||
|
||||
@@ -29,7 +29,7 @@ func generateOrPrint(ctrl *V1Controller, w http.ResponseWriter, r *http.Request,
|
||||
_, err = w.Write([]byte("Printed!"))
|
||||
return err
|
||||
} else {
|
||||
return labelmaker.GenerateLabel(w, ¶ms, ctrl.config)
|
||||
return labelmaker.GenerateLabel(w, ¶ms)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,332 +0,0 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hay-kot/httpkit/errchain"
|
||||
"github.com/hay-kot/httpkit/server"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/web/adapters"
|
||||
)
|
||||
|
||||
type UPCITEMDBResponse struct {
|
||||
Code string `json:"code"`
|
||||
Total int `json:"total"`
|
||||
Offset int `json:"offset"`
|
||||
Items []struct {
|
||||
Ean string `json:"ean"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Upc string `json:"upc"`
|
||||
Brand string `json:"brand"`
|
||||
Model string `json:"model"`
|
||||
Color string `json:"color"`
|
||||
Size string `json:"size"`
|
||||
Dimension string `json:"dimension"`
|
||||
Weight string `json:"weight"`
|
||||
Category string `json:"category"`
|
||||
LowestRecordedPrice float64 `json:"lowest_recorded_price"`
|
||||
HighestRecordedPrice float64 `json:"highest_recorded_price"`
|
||||
Images []string `json:"images"`
|
||||
Offers []struct {
|
||||
Merchant string `json:"merchant"`
|
||||
Domain string `json:"domain"`
|
||||
Title string `json:"title"`
|
||||
Currency string `json:"currency"`
|
||||
ListPrice string `json:"list_price"`
|
||||
Price float64 `json:"price"`
|
||||
Shipping string `json:"shipping"`
|
||||
Condition string `json:"condition"`
|
||||
Availability string `json:"availability"`
|
||||
Link string `json:"link"`
|
||||
UpdatedT int `json:"updated_t"`
|
||||
} `json:"offers"`
|
||||
Asin string `json:"asin"`
|
||||
Elid string `json:"elid"`
|
||||
} `json:"items"`
|
||||
}
|
||||
|
||||
type BARCODESPIDER_COMResponse struct {
|
||||
ItemResponse struct {
|
||||
Code int `json:"code"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
} `json:"item_response"`
|
||||
ItemAttributes struct {
|
||||
Title string `json:"title"`
|
||||
Upc string `json:"upc"`
|
||||
Ean string `json:"ean"`
|
||||
ParentCategory string `json:"parent_category"`
|
||||
Category string `json:"category"`
|
||||
Brand string `json:"brand"`
|
||||
Model string `json:"model"`
|
||||
Mpn string `json:"mpn"`
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
Publisher string `json:"publisher"`
|
||||
Asin string `json:"asin"`
|
||||
Color string `json:"color"`
|
||||
Size string `json:"size"`
|
||||
Weight string `json:"weight"`
|
||||
Image string `json:"image"`
|
||||
IsAdult string `json:"is_adult"`
|
||||
Description string `json:"description"`
|
||||
} `json:"item_attributes"`
|
||||
Stores []struct {
|
||||
StoreName string `json:"store_name"`
|
||||
Title string `json:"title"`
|
||||
Image string `json:"image"`
|
||||
Price string `json:"price"`
|
||||
Currency string `json:"currency"`
|
||||
Link string `json:"link"`
|
||||
Updated string `json:"updated"`
|
||||
} `json:"Stores"`
|
||||
}
|
||||
|
||||
// HandleGenerateQRCode godoc
|
||||
//
|
||||
// @Summary Search EAN from Barcode
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param data query string false "barcode to be searched"
|
||||
// @Success 200 {object} []repo.BarcodeProduct
|
||||
// @Router /v1/products/search-from-barcode [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleProductSearchFromBarcode(conf config.BarcodeAPIConf) errchain.HandlerFunc {
|
||||
type query struct {
|
||||
// 80 characters is the longest non-2D barcode length (GS1-128)
|
||||
EAN string `schema:"productEAN" validate:"required,max=80"`
|
||||
}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
q, err := adapters.DecodeQuery[query](r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
const TIMEOUT_SEC = 10
|
||||
|
||||
log.Info().Msg("Processing barcode lookup request on: " + q.EAN)
|
||||
|
||||
// Search on UPCITEMDB
|
||||
var products []repo.BarcodeProduct
|
||||
|
||||
// www.ean-search.org/: not free
|
||||
|
||||
// Example code: dewalt 5035048748428
|
||||
|
||||
upcitemdb := func(iEan string) ([]repo.BarcodeProduct, error) {
|
||||
client := &http.Client{Timeout: TIMEOUT_SEC * time.Second}
|
||||
resp, err := client.Get("https://api.upcitemdb.com/prod/trial/lookup?upc=" + iEan)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = errors.Join(err, resp.Body.Close())
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("API returned status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// We Read the response body on the line below.
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Uncomment the following string for debug
|
||||
// sb := string(body)
|
||||
// log.Debug().Msg("Response: " + sb)
|
||||
|
||||
var result UPCITEMDBResponse
|
||||
if err := json.Unmarshal(body, &result); err != nil { // Parse []byte to go struct pointer
|
||||
log.Error().Msg("Can not unmarshal JSON")
|
||||
}
|
||||
|
||||
var res []repo.BarcodeProduct
|
||||
|
||||
for _, it := range result.Items {
|
||||
var p repo.BarcodeProduct
|
||||
p.SearchEngineName = "upcitemdb.com"
|
||||
p.Barcode = iEan
|
||||
|
||||
p.Item.Description = it.Description
|
||||
p.Item.Name = it.Title
|
||||
p.Manufacturer = it.Brand
|
||||
p.ModelNumber = it.Model
|
||||
if len(it.Images) != 0 {
|
||||
p.ImageURL = it.Images[0]
|
||||
}
|
||||
|
||||
res = append(res, p)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
ps, err := upcitemdb(q.EAN)
|
||||
if err != nil {
|
||||
log.Error().Msg("Can not retrieve product from upcitemdb.com" + err.Error())
|
||||
}
|
||||
|
||||
// Barcode spider implementation
|
||||
barcodespider := func(tokenAPI string, iEan string) ([]repo.BarcodeProduct, error) {
|
||||
if len(tokenAPI) == 0 {
|
||||
return nil, errors.New("no api token configured for barcodespider. " +
|
||||
"Please define the api token in environment variable HBOX_BARCODE_TOKEN_BARCODESPIDER")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(
|
||||
"GET", "https://api.barcodespider.com/v1/lookup?upc="+iEan, nil)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add("token", tokenAPI)
|
||||
|
||||
client := &http.Client{Timeout: TIMEOUT_SEC * time.Second}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// defer the call to Body.Close(). We also check the error code, and merge
|
||||
// it with the other error in this code to avoid error overiding.
|
||||
defer func() {
|
||||
err = errors.Join(err, resp.Body.Close())
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("barcodespider API returned status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// We Read the response body on the line below.
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Uncomment the following string for debug
|
||||
// sb := string(body)
|
||||
// log.Debug().Msg("Response: " + sb)
|
||||
|
||||
var result BARCODESPIDER_COMResponse
|
||||
if err := json.Unmarshal(body, &result); err != nil { // Parse []byte to go struct pointer
|
||||
log.Error().Msg("Can not unmarshal JSON")
|
||||
}
|
||||
|
||||
// TODO: check 200 code on HTTP response.
|
||||
var p repo.BarcodeProduct
|
||||
p.Barcode = iEan
|
||||
p.SearchEngineName = "barcodespider.com"
|
||||
p.Item.Name = result.ItemAttributes.Title
|
||||
p.Item.Description = result.ItemAttributes.Description
|
||||
p.Manufacturer = result.ItemAttributes.Brand
|
||||
p.ModelNumber = result.ItemAttributes.Model
|
||||
p.ImageURL = result.ItemAttributes.Image
|
||||
|
||||
var res []repo.BarcodeProduct
|
||||
res = append(res, p)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
ps2, err := barcodespider(conf.TokenBarcodespider, q.EAN)
|
||||
if err != nil {
|
||||
log.Error().Msg("Can not retrieve product from barcodespider.com: " + err.Error())
|
||||
}
|
||||
|
||||
// Merge everything.
|
||||
products = append(products, ps...)
|
||||
|
||||
products = append(products, ps2...)
|
||||
|
||||
// Retrieve images if possible
|
||||
for i := range products {
|
||||
p := &products[i]
|
||||
|
||||
if len(p.ImageURL) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Validate URL is HTTPS
|
||||
u, err := url.Parse(p.ImageURL)
|
||||
if err != nil || u.Scheme != "https" {
|
||||
log.Warn().Msg("Skipping non-HTTPS image URL: " + p.ImageURL)
|
||||
continue
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: TIMEOUT_SEC * time.Second}
|
||||
res, err := client.Get(p.ImageURL)
|
||||
if err != nil {
|
||||
log.Warn().Msg("Cannot fetch image for URL: " + p.ImageURL + ": " + err.Error())
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = errors.Join(err, res.Body.Close())
|
||||
}()
|
||||
|
||||
// Validate response
|
||||
if res.StatusCode != http.StatusOK {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check content type
|
||||
contentType := res.Header.Get("Content-Type")
|
||||
if !strings.HasPrefix(contentType, "image/") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Limit image size to 8MB
|
||||
limitedReader := io.LimitReader(res.Body, 8*1024*1024)
|
||||
|
||||
// Read data of image
|
||||
bytes, err := io.ReadAll(limitedReader)
|
||||
if err != nil {
|
||||
log.Warn().Msg(err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
// Convert to Base64
|
||||
var base64Encoding string
|
||||
|
||||
// Determine the content type of the image file
|
||||
mimeType := http.DetectContentType(bytes)
|
||||
|
||||
// Prepend the appropriate URI scheme header depending
|
||||
// on the MIME type
|
||||
switch mimeType {
|
||||
case "image/jpeg":
|
||||
base64Encoding += "data:image/jpeg;base64,"
|
||||
case "image/png":
|
||||
base64Encoding += "data:image/png;base64,"
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
// Append the base64 encoded output
|
||||
base64Encoding += base64.StdEncoding.EncodeToString(bytes)
|
||||
|
||||
p.ImageBase64 = base64Encoding
|
||||
}
|
||||
|
||||
if len(products) != 0 {
|
||||
return server.JSON(w, http.StatusOK, products)
|
||||
}
|
||||
|
||||
return server.JSON(w, http.StatusNoContent, nil)
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sysadminsmedia/homebox/backend/pkgs/utils"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/pressly/goose/v3"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/analytics"
|
||||
|
||||
"github.com/hay-kot/httpkit/errchain"
|
||||
"github.com/hay-kot/httpkit/graceful"
|
||||
@@ -24,15 +28,16 @@ import (
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/migrations"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/analytics"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/web/mid"
|
||||
"go.balki.me/anyhttp"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
_ "github.com/sysadminsmedia/homebox/backend/internal/data/migrations/postgres"
|
||||
_ "github.com/sysadminsmedia/homebox/backend/internal/data/migrations/sqlite3"
|
||||
_ "github.com/sysadminsmedia/homebox/backend/pkgs/cgofreesqlite"
|
||||
|
||||
"gocloud.dev/pubsub"
|
||||
_ "gocloud.dev/pubsub/awssnssqs"
|
||||
_ "gocloud.dev/pubsub/azuresb"
|
||||
_ "gocloud.dev/pubsub/gcppubsub"
|
||||
@@ -97,56 +102,81 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func run(cfg *config.Config) error {
|
||||
app := new(cfg)
|
||||
app.setupLogger()
|
||||
|
||||
if cfg.Options.AllowAnalytics {
|
||||
analytics.Send(version, build())
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Initialize Database & Repos
|
||||
err := setupStorageDir(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
if strings.HasPrefix(cfg.Storage.ConnString, "file:///./") {
|
||||
raw := strings.TrimPrefix(cfg.Storage.ConnString, "file:///./")
|
||||
clean := filepath.Clean(raw)
|
||||
absBase, err := filepath.Abs(clean)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to get absolute path for storage connection string")
|
||||
}
|
||||
// Construct and validate the full storage path
|
||||
storageDir := filepath.Join(absBase, cfg.Storage.PrefixPath)
|
||||
// Set windows paths to use forward slashes required by go-cloud
|
||||
storageDir = strings.ReplaceAll(storageDir, "\\", "/")
|
||||
if !strings.HasPrefix(storageDir, absBase+"/") && storageDir != absBase {
|
||||
log.Fatal().
|
||||
Str("path", storageDir).
|
||||
Msg("invalid storage path: you tried to use a prefix that is not a subdirectory of the base path")
|
||||
}
|
||||
// Create with more restrictive permissions
|
||||
if err := os.MkdirAll(storageDir, 0o750); err != nil {
|
||||
log.Fatal().
|
||||
Err(err).
|
||||
Msg("failed to create data directory")
|
||||
}
|
||||
}
|
||||
|
||||
if strings.ToLower(cfg.Database.Driver) == "postgres" {
|
||||
if !validatePostgresSSLMode(cfg.Database.SslMode) {
|
||||
log.Error().Str("sslmode", cfg.Database.SslMode).Msg("invalid sslmode")
|
||||
return fmt.Errorf("invalid sslmode: %s", cfg.Database.SslMode)
|
||||
log.Fatal().Str("sslmode", cfg.Database.SslMode).Msg("invalid sslmode")
|
||||
}
|
||||
}
|
||||
|
||||
databaseURL, err := setupDatabaseURL(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
// Set up the database URL based on the driver because for some reason a common URL format is not used
|
||||
databaseURL := ""
|
||||
switch strings.ToLower(cfg.Database.Driver) {
|
||||
case "sqlite3":
|
||||
databaseURL = cfg.Database.SqlitePath
|
||||
|
||||
// Create directory for SQLite database if it doesn't exist
|
||||
dbFilePath := strings.Split(cfg.Database.SqlitePath, "?")[0] // Remove query parameters
|
||||
dbDir := filepath.Dir(dbFilePath)
|
||||
if err := os.MkdirAll(dbDir, 0o755); err != nil {
|
||||
log.Fatal().Err(err).Str("path", dbDir).Msg("failed to create SQLite database directory")
|
||||
}
|
||||
case "postgres":
|
||||
databaseURL = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", cfg.Database.Host, cfg.Database.Port, cfg.Database.Username, cfg.Database.Password, cfg.Database.Database, cfg.Database.SslMode)
|
||||
default:
|
||||
log.Fatal().Str("driver", cfg.Database.Driver).Msg("unsupported database driver")
|
||||
}
|
||||
|
||||
c, err := ent.Open(strings.ToLower(cfg.Database.Driver), databaseURL)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
log.Fatal().
|
||||
Err(err).
|
||||
Str("driver", strings.ToLower(cfg.Database.Driver)).
|
||||
Str("host", cfg.Database.Host).
|
||||
Str("port", cfg.Database.Port).
|
||||
Str("database", cfg.Database.Database).
|
||||
Msg("failed opening connection to {driver} database at {host}:{port}/{database}")
|
||||
return fmt.Errorf("failed opening connection to %s database at %s:%s/%s: %w",
|
||||
strings.ToLower(cfg.Database.Driver),
|
||||
cfg.Database.Host,
|
||||
cfg.Database.Port,
|
||||
cfg.Database.Database,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
migrationsFs, err := migrations.Migrations(strings.ToLower(cfg.Database.Driver))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get migrations for %s: %w", strings.ToLower(cfg.Database.Driver), err)
|
||||
}
|
||||
|
||||
goose.SetBaseFS(migrationsFs)
|
||||
goose.SetBaseFS(migrations.Migrations(strings.ToLower(cfg.Database.Driver)))
|
||||
err = goose.SetDialect(strings.ToLower(cfg.Database.Driver))
|
||||
if err != nil {
|
||||
log.Error().Str("driver", cfg.Database.Driver).Msg("unsupported database driver")
|
||||
log.Fatal().Str("driver", cfg.Database.Driver).Msg("unsupported database driver")
|
||||
return fmt.Errorf("unsupported database driver: %s", cfg.Database.Driver)
|
||||
}
|
||||
|
||||
@@ -156,9 +186,25 @@ func run(cfg *config.Config) error {
|
||||
return err
|
||||
}
|
||||
|
||||
collectFuncs, err := loadCurrencies(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
collectFuncs := []currencies.CollectorFunc{
|
||||
currencies.CollectDefaults(),
|
||||
}
|
||||
|
||||
if cfg.Options.CurrencyConfig != "" {
|
||||
log.Info().
|
||||
Str("path", cfg.Options.CurrencyConfig).
|
||||
Msg("loading currency config file")
|
||||
|
||||
content, err := os.ReadFile(cfg.Options.CurrencyConfig)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("path", cfg.Options.CurrencyConfig).
|
||||
Msg("failed to read currency config file")
|
||||
return err
|
||||
}
|
||||
|
||||
collectFuncs = append(collectFuncs, currencies.CollectJSON(bytes.NewReader(content)))
|
||||
}
|
||||
|
||||
currencies, err := currencies.CollectionCurrencies(collectFuncs...)
|
||||
@@ -212,52 +258,154 @@ func run(cfg *config.Config) error {
|
||||
_ = httpserver.Shutdown(context.Background())
|
||||
}()
|
||||
|
||||
listener, addrType, addrCfg, err := anyhttp.GetListener(cfg.Web.Host)
|
||||
if err == nil {
|
||||
switch addrType {
|
||||
case anyhttp.SystemdFD:
|
||||
sysdCfg := addrCfg.(*anyhttp.SysdConfig)
|
||||
if sysdCfg.IdleTimeout != nil {
|
||||
log.Error().Msg("idle timeout not yet supported. Please remove and try again")
|
||||
return errors.New("idle timeout not yet supported. Please remove and try again")
|
||||
}
|
||||
fallthrough
|
||||
case anyhttp.UnixSocket:
|
||||
log.Info().Msgf("Server is running on %s", cfg.Web.Host)
|
||||
return httpserver.Serve(listener)
|
||||
}
|
||||
} else {
|
||||
log.Debug().Msgf("anyhttp error: %v", err)
|
||||
}
|
||||
log.Info().Msgf("Server is running on %s:%s", cfg.Web.Host, cfg.Web.Port)
|
||||
return httpserver.ListenAndServe()
|
||||
})
|
||||
|
||||
// =========================================================================
|
||||
// Start Reoccurring Tasks
|
||||
registerRecurringTasks(app, cfg, runner)
|
||||
|
||||
// Send analytics if enabled at around midnight UTC
|
||||
if cfg.Options.AllowAnalytics {
|
||||
analyticsTime := time.Second
|
||||
runner.AddPlugin(NewTask("send-analytics", analyticsTime, func(ctx context.Context) {
|
||||
for {
|
||||
now := time.Now().UTC()
|
||||
nextMidnight := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, time.UTC)
|
||||
dur := time.Until(nextMidnight)
|
||||
analyticsTime = dur
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(dur):
|
||||
log.Debug().Msg("running send analytics")
|
||||
err := analytics.Send(version, build())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to send analytics")
|
||||
}
|
||||
runner.AddFunc("eventbus", app.bus.Run)
|
||||
|
||||
runner.AddFunc("seed_database", func(ctx context.Context) error {
|
||||
// TODO: Remove through external API that does setup
|
||||
if cfg.Demo {
|
||||
log.Info().Msg("Running in demo mode, creating demo data")
|
||||
err := app.SetupDemo()
|
||||
if err != nil {
|
||||
log.Fatal().Msg(err.Error())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
runner.AddPlugin(NewTask("purge-tokens", time.Duration(24)*time.Hour, func(ctx context.Context) {
|
||||
_, err := app.repos.AuthTokens.PurgeExpiredTokens(ctx)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Msg("failed to purge expired tokens")
|
||||
}
|
||||
}))
|
||||
|
||||
runner.AddPlugin(NewTask("purge-invitations", time.Duration(24)*time.Hour, func(ctx context.Context) {
|
||||
_, err := app.repos.Groups.InvitationPurge(ctx)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Msg("failed to purge expired invitations")
|
||||
}
|
||||
}))
|
||||
|
||||
runner.AddPlugin(NewTask("send-notifications", time.Duration(1)*time.Hour, func(ctx context.Context) {
|
||||
now := time.Now()
|
||||
|
||||
if now.Hour() == 8 {
|
||||
fmt.Println("run notifiers")
|
||||
err := app.services.BackgroundService.SendNotifiersToday(context.Background())
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Msg("failed to send notifiers")
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
go runner.AddFunc("create-thumbnails-subscription", func(ctx context.Context) error {
|
||||
pubsubString, err := utils.GenerateSubPubConn(cfg.Database.PubSubConnString, "thumbnails")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to generate pubsub connection string")
|
||||
return err
|
||||
}
|
||||
topic, err := pubsub.OpenTopic(ctx, pubsubString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(topic *pubsub.Topic, ctx context.Context) {
|
||||
err := topic.Shutdown(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("fail to shutdown pubsub topic")
|
||||
}
|
||||
}(topic, ctx)
|
||||
|
||||
subscription, err := pubsub.OpenSubscription(ctx, pubsubString)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to open pubsub topic")
|
||||
return err
|
||||
}
|
||||
defer func(topic *pubsub.Subscription, ctx context.Context) {
|
||||
err := topic.Shutdown(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("fail to shutdown pubsub topic")
|
||||
}
|
||||
}(subscription, ctx)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
msg, err := subscription.Receive(ctx)
|
||||
log.Debug().Msg("received thumbnail generation request from pubsub topic")
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to receive message from pubsub topic")
|
||||
}
|
||||
groupId, err := uuid.Parse(msg.Metadata["group_id"])
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("group_id", msg.Metadata["group_id"]).
|
||||
Msg("failed to parse group ID from message metadata")
|
||||
}
|
||||
attachmentId, err := uuid.Parse(msg.Metadata["attachment_id"])
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("attachment_id", msg.Metadata["attachment_id"]).
|
||||
Msg("failed to parse attachment ID from message metadata")
|
||||
}
|
||||
err = app.repos.Attachments.CreateThumbnail(ctx, groupId, attachmentId, msg.Metadata["title"], msg.Metadata["path"])
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to create thumbnail")
|
||||
}
|
||||
msg.Ack()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if cfg.Options.GithubReleaseCheck {
|
||||
runner.AddPlugin(NewTask("get-latest-github-release", time.Hour, func(ctx context.Context) {
|
||||
log.Debug().Msg("running get latest github release")
|
||||
err := app.services.BackgroundService.GetLatestGithubRelease(context.Background())
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Msg("failed to get latest github release")
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
if cfg.Debug.Enabled {
|
||||
runner.AddFunc("debug", func(ctx context.Context) error {
|
||||
debugserver := http.Server{
|
||||
Addr: fmt.Sprintf("%s:%s", cfg.Web.Host, cfg.Debug.Port),
|
||||
Handler: app.debugRouter(),
|
||||
ReadTimeout: cfg.Web.ReadTimeout,
|
||||
WriteTimeout: cfg.Web.WriteTimeout,
|
||||
IdleTimeout: cfg.Web.IdleTimeout,
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
_ = debugserver.Shutdown(context.Background())
|
||||
}()
|
||||
|
||||
log.Info().Msgf("Debug server is running on %s:%s", cfg.Web.Host, cfg.Debug.Port)
|
||||
return debugserver.ListenAndServe()
|
||||
})
|
||||
// Print the configuration to the console
|
||||
cfg.Print()
|
||||
}
|
||||
|
||||
return runner.Start(context.Background())
|
||||
}
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/httpkit/graceful"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
||||
"github.com/sysadminsmedia/homebox/backend/pkgs/utils"
|
||||
"gocloud.dev/pubsub"
|
||||
)
|
||||
|
||||
func registerRecurringTasks(app *app, cfg *config.Config, runner *graceful.Runner) {
|
||||
runner.AddFunc("eventbus", app.bus.Run)
|
||||
|
||||
runner.AddFunc("seed_database", func(ctx context.Context) error {
|
||||
if cfg.Demo {
|
||||
log.Info().Msg("Running in demo mode, creating demo data")
|
||||
err := app.SetupDemo()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to setup demo data")
|
||||
return fmt.Errorf("failed to setup demo data: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
runner.AddPlugin(NewTask("purge-tokens", 24*time.Hour, func(ctx context.Context) {
|
||||
_, err := app.repos.AuthTokens.PurgeExpiredTokens(ctx)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to purge expired tokens")
|
||||
}
|
||||
}))
|
||||
|
||||
runner.AddPlugin(NewTask("purge-invitations", 24*time.Hour, func(ctx context.Context) {
|
||||
_, err := app.repos.Groups.InvitationPurge(ctx)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to purge expired invitations")
|
||||
}
|
||||
}))
|
||||
|
||||
runner.AddPlugin(NewTask("send-notifications", time.Hour, func(ctx context.Context) {
|
||||
now := time.Now()
|
||||
if now.Hour() == 8 {
|
||||
fmt.Println("run notifiers")
|
||||
err := app.services.BackgroundService.SendNotifiersToday(context.Background())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to send notifiers")
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
if cfg.Thumbnail.Enabled {
|
||||
runner.AddFunc("create-thumbnails-subscription", func(ctx context.Context) error {
|
||||
pubsubString, err := utils.GenerateSubPubConn(cfg.Database.PubSubConnString, "thumbnails")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to generate pubsub connection string")
|
||||
return err
|
||||
}
|
||||
topic, err := pubsub.OpenTopic(ctx, pubsubString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(topic *pubsub.Topic, ctx context.Context) {
|
||||
err := topic.Shutdown(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("fail to shutdown pubsub topic")
|
||||
}
|
||||
}(topic, ctx)
|
||||
|
||||
subscription, err := pubsub.OpenSubscription(ctx, pubsubString)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to open pubsub topic")
|
||||
return err
|
||||
}
|
||||
defer func(topic *pubsub.Subscription, ctx context.Context) {
|
||||
err := topic.Shutdown(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("fail to shutdown pubsub topic")
|
||||
}
|
||||
}(subscription, ctx)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
msg, err := subscription.Receive(ctx)
|
||||
log.Debug().Msg("received thumbnail generation request from pubsub topic")
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to receive message from pubsub topic")
|
||||
continue
|
||||
}
|
||||
if msg == nil {
|
||||
log.Warn().Msg("received nil message from pubsub topic")
|
||||
continue
|
||||
}
|
||||
groupId, err := uuid.Parse(msg.Metadata["group_id"])
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("group_id", msg.Metadata["group_id"]).Msg("failed to parse group ID from message metadata")
|
||||
}
|
||||
attachmentId, err := uuid.Parse(msg.Metadata["attachment_id"])
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("attachment_id", msg.Metadata["attachment_id"]).Msg("failed to parse attachment ID from message metadata")
|
||||
}
|
||||
err = app.repos.Attachments.CreateThumbnail(ctx, groupId, attachmentId, msg.Metadata["title"], msg.Metadata["path"])
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to create thumbnail")
|
||||
}
|
||||
msg.Ack()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if cfg.Options.GithubReleaseCheck {
|
||||
runner.AddPlugin(NewTask("get-latest-github-release", time.Hour, func(ctx context.Context) {
|
||||
log.Debug().Msg("running get latest github release")
|
||||
err := app.services.BackgroundService.GetLatestGithubRelease(context.Background())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to get latest github release")
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
if cfg.Debug.Enabled {
|
||||
runner.AddFunc("debug", func(ctx context.Context) error {
|
||||
debugserver := http.Server{
|
||||
Addr: fmt.Sprintf("%s:%s", cfg.Web.Host, cfg.Debug.Port),
|
||||
Handler: app.debugRouter(),
|
||||
ReadTimeout: cfg.Web.ReadTimeout,
|
||||
WriteTimeout: cfg.Web.WriteTimeout,
|
||||
IdleTimeout: cfg.Web.IdleTimeout,
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
_ = debugserver.Shutdown(context.Background())
|
||||
}()
|
||||
|
||||
log.Info().Msgf("Debug server is running on %s:%s", cfg.Web.Host, cfg.Debug.Port)
|
||||
return debugserver.ListenAndServe()
|
||||
})
|
||||
// Print the configuration to the console
|
||||
cfg.Print()
|
||||
}
|
||||
}
|
||||
@@ -129,7 +129,6 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
|
||||
r.Put("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemUpdate(), userMW...))
|
||||
r.Patch("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemPatch(), userMW...))
|
||||
r.Delete("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemDelete(), userMW...))
|
||||
r.Post("/items/{id}/duplicate", chain.ToHandlerFunc(v1Ctrl.HandleItemDuplicate(), userMW...))
|
||||
|
||||
r.Post("/items/{id}/attachments", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentCreate(), userMW...))
|
||||
r.Put("/items/{id}/attachments/{attachment_id}", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentUpdate(), userMW...))
|
||||
@@ -158,8 +157,6 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
|
||||
a.mwRoles(RoleModeOr, authroles.RoleUser.String(), authroles.RoleAttachments.String()),
|
||||
}
|
||||
|
||||
r.Get("/products/search-from-barcode", chain.ToHandlerFunc(v1Ctrl.HandleProductSearchFromBarcode(a.conf.Barcode), userMW...))
|
||||
|
||||
r.Get("/qrcode", chain.ToHandlerFunc(v1Ctrl.HandleGenerateQRCode(), assetMW...))
|
||||
r.Get(
|
||||
"/items/{id}/attachments/{attachment_id}",
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/core/currencies"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
||||
)
|
||||
|
||||
// setupStorageDir handles the creation and validation of the storage directory.
|
||||
func setupStorageDir(cfg *config.Config) error {
|
||||
if strings.HasPrefix(cfg.Storage.ConnString, "file:///./") {
|
||||
raw := strings.TrimPrefix(cfg.Storage.ConnString, "file:///./")
|
||||
clean := filepath.Clean(raw)
|
||||
absBase, err := filepath.Abs(clean)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to get absolute path for storage connection string")
|
||||
return fmt.Errorf("failed to get absolute path for storage connection string: %w", err)
|
||||
}
|
||||
absBase = strings.ReplaceAll(absBase, "\\", "/")
|
||||
storageDir := filepath.Join(absBase, cfg.Storage.PrefixPath)
|
||||
storageDir = strings.ReplaceAll(storageDir, "\\", "/")
|
||||
if !strings.HasPrefix(storageDir, absBase+"/") && storageDir != absBase {
|
||||
log.Error().Str("path", storageDir).Msg("invalid storage path: you tried to use a prefix that is not a subdirectory of the base path")
|
||||
return fmt.Errorf("invalid storage path: you tried to use a prefix that is not a subdirectory of the base path")
|
||||
}
|
||||
if err := os.MkdirAll(storageDir, 0o750); err != nil {
|
||||
log.Error().Err(err).Msg("failed to create data directory")
|
||||
return fmt.Errorf("failed to create data directory: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupDatabaseURL returns the database URL and ensures any required directories exist.
|
||||
func setupDatabaseURL(cfg *config.Config) (string, error) {
|
||||
databaseURL := ""
|
||||
switch strings.ToLower(cfg.Database.Driver) {
|
||||
case "sqlite3":
|
||||
databaseURL = cfg.Database.SqlitePath
|
||||
dbFilePath := strings.Split(cfg.Database.SqlitePath, "?")[0]
|
||||
dbDir := filepath.Dir(dbFilePath)
|
||||
if err := os.MkdirAll(dbDir, 0o755); err != nil {
|
||||
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":
|
||||
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)
|
||||
}
|
||||
if cfg.Database.Password != "" {
|
||||
databaseURL += fmt.Sprintf(" password=%s", cfg.Database.Password)
|
||||
}
|
||||
if cfg.Database.SslRootCert != "" {
|
||||
if _, err := os.Stat(cfg.Database.SslRootCert); err != nil {
|
||||
log.Error().Err(err).Str("path", cfg.Database.SslRootCert).Msg("SSL root certificate file is not accessible")
|
||||
return "", fmt.Errorf("SSL root certificate file is not accessible: %w", err)
|
||||
}
|
||||
databaseURL += fmt.Sprintf(" sslrootcert=%s", cfg.Database.SslRootCert)
|
||||
}
|
||||
if cfg.Database.SslCert != "" {
|
||||
if _, err := os.Stat(cfg.Database.SslCert); err != nil {
|
||||
log.Error().Err(err).Str("path", cfg.Database.SslCert).Msg("SSL certificate file is not accessible")
|
||||
return "", fmt.Errorf("SSL certificate file is not accessible: %w", err)
|
||||
}
|
||||
databaseURL += fmt.Sprintf(" sslcert=%s", cfg.Database.SslCert)
|
||||
}
|
||||
if cfg.Database.SslKey != "" {
|
||||
if _, err := os.Stat(cfg.Database.SslKey); err != nil {
|
||||
log.Error().Err(err).Str("path", cfg.Database.SslKey).Msg("SSL key file is not accessible")
|
||||
return "", fmt.Errorf("SSL key file is not accessible: %w", err)
|
||||
}
|
||||
databaseURL += fmt.Sprintf(" sslkey=%s", cfg.Database.SslKey)
|
||||
}
|
||||
default:
|
||||
log.Error().Str("driver", cfg.Database.Driver).Msg("unsupported database driver")
|
||||
return "", fmt.Errorf("unsupported database driver: %s", cfg.Database.Driver)
|
||||
}
|
||||
return databaseURL, nil
|
||||
}
|
||||
|
||||
// loadCurrencies loads currency data from config if provided.
|
||||
func loadCurrencies(cfg *config.Config) ([]currencies.CollectorFunc, error) {
|
||||
collectFuncs := []currencies.CollectorFunc{
|
||||
currencies.CollectDefaults(),
|
||||
}
|
||||
if cfg.Options.CurrencyConfig != "" {
|
||||
log.Info().Str("path", cfg.Options.CurrencyConfig).Msg("loading currency config file")
|
||||
content, err := os.ReadFile(cfg.Options.CurrencyConfig)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("path", cfg.Options.CurrencyConfig).Msg("failed to read currency config file")
|
||||
return nil, err
|
||||
}
|
||||
collectFuncs = append(collectFuncs, currencies.CollectJSON(bytes.NewReader(content)))
|
||||
}
|
||||
return collectFuncs, nil
|
||||
}
|
||||
@@ -943,48 +943,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/items/{id}/duplicate": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Items"
|
||||
],
|
||||
"summary": "Duplicate Item",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Item ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Duplicate Options",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.DuplicateOptions"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.ItemOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/items/{id}/maintenance": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -1853,41 +1811,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/products/search-from-barcode": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Items"
|
||||
],
|
||||
"summary": "Search EAN from Barcode",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "barcode to be searched",
|
||||
"name": "data",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.BarcodeProduct"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/qrcode": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -3140,54 +3063,6 @@ const docTemplate = `{
|
||||
"TypeTime"
|
||||
]
|
||||
},
|
||||
"repo.BarcodeProduct": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"barcode": {
|
||||
"type": "string"
|
||||
},
|
||||
"imageBase64": {
|
||||
"type": "string"
|
||||
},
|
||||
"imageURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"item": {
|
||||
"$ref": "#/definitions/repo.ItemCreate"
|
||||
},
|
||||
"manufacturer": {
|
||||
"type": "string"
|
||||
},
|
||||
"modelNumber": {
|
||||
"description": "Identifications",
|
||||
"type": "string"
|
||||
},
|
||||
"notes": {
|
||||
"description": "Extras",
|
||||
"type": "string"
|
||||
},
|
||||
"search_engine_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.DuplicateOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"copyAttachments": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"copyCustomFields": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"copyMaintenance": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"copyPrefix": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.Group": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -3698,7 +3573,7 @@ const docTemplate = `{
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"maxLength": 1000
|
||||
"maxLength": 255
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
|
||||
@@ -941,48 +941,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/items/{id}/duplicate": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Items"
|
||||
],
|
||||
"summary": "Duplicate Item",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Item ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Duplicate Options",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.DuplicateOptions"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.ItemOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/items/{id}/maintenance": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -1851,41 +1809,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/products/search-from-barcode": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Items"
|
||||
],
|
||||
"summary": "Search EAN from Barcode",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "barcode to be searched",
|
||||
"name": "data",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.BarcodeProduct"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/qrcode": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -3138,54 +3061,6 @@
|
||||
"TypeTime"
|
||||
]
|
||||
},
|
||||
"repo.BarcodeProduct": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"barcode": {
|
||||
"type": "string"
|
||||
},
|
||||
"imageBase64": {
|
||||
"type": "string"
|
||||
},
|
||||
"imageURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"item": {
|
||||
"$ref": "#/definitions/repo.ItemCreate"
|
||||
},
|
||||
"manufacturer": {
|
||||
"type": "string"
|
||||
},
|
||||
"modelNumber": {
|
||||
"description": "Identifications",
|
||||
"type": "string"
|
||||
},
|
||||
"notes": {
|
||||
"description": "Extras",
|
||||
"type": "string"
|
||||
},
|
||||
"search_engine_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.DuplicateOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"copyAttachments": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"copyCustomFields": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"copyMaintenance": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"copyPrefix": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.Group": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -3696,7 +3571,7 @@
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"maxLength": 1000
|
||||
"maxLength": 255
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
|
||||
@@ -646,38 +646,6 @@ definitions:
|
||||
- TypeNumber
|
||||
- TypeBoolean
|
||||
- TypeTime
|
||||
repo.BarcodeProduct:
|
||||
properties:
|
||||
barcode:
|
||||
type: string
|
||||
imageBase64:
|
||||
type: string
|
||||
imageURL:
|
||||
type: string
|
||||
item:
|
||||
$ref: '#/definitions/repo.ItemCreate'
|
||||
manufacturer:
|
||||
type: string
|
||||
modelNumber:
|
||||
description: Identifications
|
||||
type: string
|
||||
notes:
|
||||
description: Extras
|
||||
type: string
|
||||
search_engine_name:
|
||||
type: string
|
||||
type: object
|
||||
repo.DuplicateOptions:
|
||||
properties:
|
||||
copyAttachments:
|
||||
type: boolean
|
||||
copyCustomFields:
|
||||
type: boolean
|
||||
copyMaintenance:
|
||||
type: boolean
|
||||
copyPrefix:
|
||||
type: string
|
||||
type: object
|
||||
repo.Group:
|
||||
properties:
|
||||
createdAt:
|
||||
@@ -1023,7 +991,7 @@ definitions:
|
||||
color:
|
||||
type: string
|
||||
description:
|
||||
maxLength: 1000
|
||||
maxLength: 255
|
||||
type: string
|
||||
name:
|
||||
maxLength: 255
|
||||
@@ -1979,32 +1947,6 @@ paths:
|
||||
summary: Update Item Attachment
|
||||
tags:
|
||||
- Items Attachments
|
||||
/v1/items/{id}/duplicate:
|
||||
post:
|
||||
parameters:
|
||||
- description: Item ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: Duplicate Options
|
||||
in: body
|
||||
name: payload
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/repo.DuplicateOptions'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"201":
|
||||
description: Created
|
||||
schema:
|
||||
$ref: '#/definitions/repo.ItemOut'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Duplicate Item
|
||||
tags:
|
||||
- Items
|
||||
/v1/items/{id}/maintenance:
|
||||
get:
|
||||
parameters:
|
||||
@@ -2601,27 +2543,6 @@ paths:
|
||||
summary: Test Notifier
|
||||
tags:
|
||||
- Notifiers
|
||||
/v1/products/search-from-barcode:
|
||||
get:
|
||||
parameters:
|
||||
- description: barcode to be searched
|
||||
in: query
|
||||
name: data
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/repo.BarcodeProduct'
|
||||
type: array
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Search EAN from Barcode
|
||||
tags:
|
||||
- Items
|
||||
/v1/qrcode:
|
||||
get:
|
||||
parameters:
|
||||
|
||||
216
backend/go.mod
216
backend/go.mod
@@ -1,11 +1,11 @@
|
||||
module github.com/sysadminsmedia/homebox/backend
|
||||
|
||||
go 1.24.0
|
||||
go 1.24
|
||||
|
||||
toolchain go1.24.3
|
||||
|
||||
require (
|
||||
entgo.io/ent v0.14.5
|
||||
entgo.io/ent v0.14.4
|
||||
github.com/ardanlabs/conf/v3 v3.8.0
|
||||
github.com/containrrr/shoutrrr v0.8.0
|
||||
github.com/evanoberholster/imagemeta v0.3.1
|
||||
@@ -14,91 +14,90 @@ require (
|
||||
github.com/gen2brain/jpegxl v0.4.5
|
||||
github.com/gen2brain/webp v0.5.5
|
||||
github.com/go-chi/chi/v5 v5.2.2
|
||||
github.com/go-playground/validator/v10 v10.27.0
|
||||
github.com/go-playground/validator/v10 v10.26.0
|
||||
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/schema v1.4.1
|
||||
github.com/hay-kot/httpkit v0.0.11
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/mattn/go-sqlite3 v1.14.32
|
||||
github.com/olahol/melody v1.3.0
|
||||
github.com/mattn/go-sqlite3 v1.14.28
|
||||
github.com/olahol/melody v1.2.1
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pressly/goose/v3 v3.24.3
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/shirou/gopsutil/v4 v4.25.7
|
||||
github.com/shirou/gopsutil/v4 v4.25.5
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/swaggo/http-swagger/v2 v2.0.2
|
||||
github.com/swaggo/swag v1.16.6
|
||||
github.com/swaggo/swag v1.16.4
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.5
|
||||
github.com/yeqown/go-qrcode/writer/standard v1.3.0
|
||||
github.com/zeebo/blake3 v0.2.4
|
||||
go.balki.me/anyhttp v0.5.2
|
||||
gocloud.dev v0.43.0
|
||||
gocloud.dev/pubsub/kafkapubsub v0.43.0
|
||||
gocloud.dev/pubsub/natspubsub v0.43.0
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.43.0
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/image v0.30.0
|
||||
golang.org/x/text v0.28.0
|
||||
modernc.org/sqlite v1.38.2
|
||||
gocloud.dev v0.41.0
|
||||
gocloud.dev/pubsub/kafkapubsub v0.41.0
|
||||
gocloud.dev/pubsub/natspubsub v0.41.0
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.41.0
|
||||
golang.org/x/crypto v0.39.0
|
||||
golang.org/x/image v0.28.0
|
||||
modernc.org/sqlite v1.37.1
|
||||
)
|
||||
|
||||
require (
|
||||
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9 // indirect
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
cloud.google.com/go v0.121.4 // indirect
|
||||
cloud.google.com/go/auth v0.16.4 // indirect
|
||||
ariga.io/atlas v0.31.1-0.20250212144724-069be8033e83 // indirect
|
||||
cel.dev/expr v0.22.1 // indirect
|
||||
cloud.google.com/go v0.120.0 // indirect
|
||||
cloud.google.com/go/auth v0.15.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.8.0 // indirect
|
||||
cloud.google.com/go/iam v1.5.2 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.2 // indirect
|
||||
cloud.google.com/go/pubsub v1.49.0 // indirect
|
||||
cloud.google.com/go/storage v1.55.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||
cloud.google.com/go/iam v1.4.2 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.1 // indirect
|
||||
cloud.google.com/go/pubsub v1.48.0 // indirect
|
||||
cloud.google.com/go/storage v1.51.0 // indirect
|
||||
github.com/Azure/azure-amqp-common-go/v3 v3.2.3 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.9.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.8.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 // indirect
|
||||
github.com/Azure/go-amqp v1.4.0 // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect
|
||||
github.com/IBM/sarama v1.45.2 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect
|
||||
github.com/IBM/sarama v1.45.1 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/agext/levenshtein v1.2.3 // indirect
|
||||
github.com/agext/levenshtein v1.2.1 // indirect
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/aws/aws-sdk-go v1.55.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.70 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect
|
||||
github.com/aws/aws-sdk-go v1.55.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.65 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.34.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect
|
||||
github.com/aws/smithy-go v1.22.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.34.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect
|
||||
github.com/aws/smithy-go v1.22.3 // indirect
|
||||
github.com/bmatcuk/doublestar v1.3.4 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/eapache/go-resiliency v1.7.0 // indirect
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect
|
||||
@@ -106,33 +105,33 @@ require (
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fatih/color v1.15.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fogleman/gg v1.3.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.1 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-openapi/inflect v0.19.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.2 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/spec v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.1 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.0 // indirect
|
||||
github.com/go-openapi/spec v0.20.6 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/wire v0.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.18.1 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.13.0 // indirect
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
|
||||
github.com/jcmturner/gofork v1.7.6 // indirect
|
||||
@@ -141,67 +140,68 @@ require (
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/nats-io/nats.go v1.44.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.11 // indirect
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
|
||||
github.com/nats-io/nats.go v1.40.1 // indirect
|
||||
github.com/nats-io/nkeys v0.4.10 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/philhofer/fwd v1.1.2 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/rabbitmq/amqp091-go v1.10.0 // indirect
|
||||
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
|
||||
github.com/swaggo/files/v2 v2.0.2 // indirect
|
||||
github.com/swaggo/files/v2 v2.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.9.0 // indirect
|
||||
github.com/tinylib/msgp v1.3.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/tinylib/msgp v1.1.8 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
github.com/zclconf/go-cty v1.14.4 // indirect
|
||||
github.com/zclconf/go-cty-yaml v1.1.0 // indirect
|
||||
github.com/zeebo/errs v1.4.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.37.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
|
||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/oauth2 v0.28.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
google.golang.org/api v0.247.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a // indirect
|
||||
google.golang.org/grpc v1.74.2 // indirect
|
||||
google.golang.org/protobuf v1.36.7 // indirect
|
||||
google.golang.org/api v0.228.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/grpc v1.71.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.66.7 // indirect
|
||||
modernc.org/libc v1.65.7 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
552
backend/go.sum
552
backend/go.sum
@@ -1,47 +1,48 @@
|
||||
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9 h1:E0wvcUXTkgyN4wy4LGtNzMNGMytJN8afmIWXJVMi4cc=
|
||||
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9/go.mod h1:Oe1xWPuu5q9LzyrWfbZmEZxFYeu4BHTyzfjeW2aZp/w=
|
||||
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
|
||||
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
||||
cloud.google.com/go v0.121.4 h1:cVvUiY0sX0xwyxPwdSU2KsF9knOVmtRyAMt8xou0iTs=
|
||||
cloud.google.com/go v0.121.4/go.mod h1:XEBchUiHFJbz4lKBZwYBDHV/rSyfFktk737TLDU089s=
|
||||
cloud.google.com/go/auth v0.16.4 h1:fXOAIQmkApVvcIn7Pc2+5J8QTMVbUGLscnSVNl11su8=
|
||||
cloud.google.com/go/auth v0.16.4/go.mod h1:j10ncYwjX/g3cdX7GpEzsdM+d+ZNsXAbb6qXA7p1Y5M=
|
||||
ariga.io/atlas v0.31.1-0.20250212144724-069be8033e83 h1:nX4HXncwIdvQ8/8sIUIf1nyCkK8qdBaHQ7EtzPpuiGE=
|
||||
ariga.io/atlas v0.31.1-0.20250212144724-069be8033e83/go.mod h1:Oe1xWPuu5q9LzyrWfbZmEZxFYeu4BHTyzfjeW2aZp/w=
|
||||
cel.dev/expr v0.22.1 h1:xoFEsNh972Yzey8N9TCPx2nDvMN7TMhQEzxLuj/iRrI=
|
||||
cel.dev/expr v0.22.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA=
|
||||
cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q=
|
||||
cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps=
|
||||
cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA=
|
||||
cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=
|
||||
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/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
|
||||
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
||||
cloud.google.com/go/iam v1.4.2 h1:4AckGYAYsowXeHzsn/LCKWIwSWLkdb0eGjH8wWkd27Q=
|
||||
cloud.google.com/go/iam v1.4.2/go.mod h1:REGlrt8vSlh4dfCJfSEcNjLGq75wW75c5aU3FLOYq34=
|
||||
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/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/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/pubsub v1.49.0 h1:5054IkbslnrMCgA2MAEPcsN3Ky+AyMpEZcii/DoySPo=
|
||||
cloud.google.com/go/pubsub v1.49.0/go.mod h1:K1FswTWP+C1tI/nfi3HQecoVeFvL4HUOB1tdaNXKhUY=
|
||||
cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2znf0=
|
||||
cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY=
|
||||
cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=
|
||||
cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
|
||||
entgo.io/ent v0.14.5 h1:Rj2WOYJtCkWyFo6a+5wB3EfBRP0rnx1fMk6gGA0UUe4=
|
||||
entgo.io/ent v0.14.5/go.mod h1:zTzLmWtPvGpmSwtkaayM2cm5m819NdM7z7tYPq3vN0U=
|
||||
cloud.google.com/go/longrunning v0.6.6 h1:XJNDo5MUfMM05xK3ewpbSdmt7R2Zw+aQEMbdQR65Rbw=
|
||||
cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw=
|
||||
cloud.google.com/go/monitoring v1.24.1 h1:vKiypZVFD/5a3BbQMvI4gZdl8445ITzXFh257XBgrS0=
|
||||
cloud.google.com/go/monitoring v1.24.1/go.mod h1:Z05d1/vn9NaujqY2voG6pVQXoJGbp+r3laV+LySt9K0=
|
||||
cloud.google.com/go/pubsub v1.48.0 h1:ntFpQVrr10Wj/GXSOpxGmexGynldv/bFp25H0jy8aOs=
|
||||
cloud.google.com/go/pubsub v1.48.0/go.mod h1:AAtyjyIT/+zaY1ERKFJbefOvkUxRDNp3nD6TdfdqUZk=
|
||||
cloud.google.com/go/storage v1.51.0 h1:ZVZ11zCiD7b3k+cH5lQs/qcNaoSz3U9I0jgwVzqDlCw=
|
||||
cloud.google.com/go/storage v1.51.0/go.mod h1:YEJfu/Ki3i5oHC/7jyTgsGZwdQ8P9hqMqvpi5kRKGgc=
|
||||
cloud.google.com/go/trace v1.11.5 h1:CALS1loyxJMnRiCwZSpdf8ac7iCsjreMxFD2WGxzzHU=
|
||||
cloud.google.com/go/trace v1.11.5/go.mod h1:TwblCcqNInriu5/qzaeYEIH7wzUcchSdeY2l5wL3Eec=
|
||||
entgo.io/ent v0.14.4 h1:/DhDraSLXIkBhyiVoJeSshr4ZYi7femzhj6/TckzZuI=
|
||||
entgo.io/ent v0.14.4/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM=
|
||||
github.com/Azure/azure-amqp-common-go/v3 v3.2.3 h1:uDF62mbd9bypXWi19V1bN5NZEO84JqgmI5G73ibAmrk=
|
||||
github.com/Azure/azure-amqp-common-go/v3 v3.2.3/go.mod h1:7rPmbSfszeovxGfc5fSAXE4ehlXQZHpMja2OtxC2Tas=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 h1:Wc1ml6QlJs2BHQ/9Bqu1jiyggbsSjramq2oUmp5WeIo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 h1:DSDNVxqkoXJiko6x8a90zidoYqnYYa6c1MTzDKzKkTo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1/go.mod h1:zGqV2R4Cr/k8Uye5w+dgQ06WJtEcbQG/8J7BB6hnCr4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.9.1 h1:CRZwf68N55u7ZZo3Xx2ynuqEA6k5GZfwsEUkU8qsAPk=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.9.1/go.mod h1:NydgUaroiShkgOcb+X6OUdS3RalWBrvDNtOyFHJtsZY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.0 h1:LR0kAX9ykz8G4YgLCaRDVJ3+n43R8MneB5dTy2konZo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.0/go.mod h1:DWAciXemNf++PQJLeXUB4HHH5OpsAh12HZnu2wXE1jA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 h1:lhZdRq7TIx0GJQvSyX2Si406vrYsov2FXGp/RnSEtcs=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1/go.mod h1:8cl44BDmi+effbARHMQjgOKA2AYvcohNm7KEt42mSV8=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.8.0 h1:JNgM3Tz592fUHU2vgwgvOgKxo5s9Ki0y2wicBeckn70=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.8.0/go.mod h1:6vUKmzY17h6dpn9ZLAhM4R/rcrltBeq52qZIkUR7Oro=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 h1:UXT0o77lXQrikd1kgwIPQOUect7EoR/+sbP4wQKdzxM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0/go.mod h1:cTvi54pg19DoT07ekoeMgE/taAwNtCShVeZqA+Iv2xI=
|
||||
github.com/Azure/go-amqp v0.17.0/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fwmw9Zlg=
|
||||
github.com/Azure/go-amqp v1.4.0 h1:Xj3caqi4comOF/L1Uc5iuBxR/pB6KumejC01YQOqOR4=
|
||||
github.com/Azure/go-amqp v1.4.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE=
|
||||
@@ -59,85 +60,91 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo=
|
||||
github.com/IBM/sarama v1.45.2 h1:8m8LcMCu3REcwpa7fCP6v2fuPuzVwXDAM2DOv3CBrKw=
|
||||
github.com/IBM/sarama v1.45.2/go.mod h1:ppaoTcVdGv186/z6MEKsMm70A5fwJfRTpstI37kVn3Y=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0=
|
||||
github.com/IBM/sarama v1.45.1 h1:nY30XqYpqyXOXSNoe2XCgjj9jklGM1Ye94ierUb1jQ0=
|
||||
github.com/IBM/sarama v1.45.1/go.mod h1:qifDhA3VWSrQ1TjSMyxDl3nYL3oX2C83u+G6L79sq4w=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
|
||||
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
|
||||
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
|
||||
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.8.0 h1:Mvv2wZJz8tIl705m5BU3ZRCP1V6TKY6qebA8i4sykrY=
|
||||
github.com/ardanlabs/conf/v3 v3.8.0/go.mod h1:XlL9P0quWP4m1weOVFmlezabinbZLI05niDof/+Ochk=
|
||||
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
|
||||
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.5 h1:0OF9RiEMEdDdZEMqF9MRjevyxAQcf6gY+E7vwBILFj0=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 h1:cTXRdLkpBanlDwISl+5chq5ui1d1YWg4PWMR9c3kXyw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84/go.mod h1:kwSy5X7tfIHN39uucmjQVs2LvDdXEjQucgQQEqCggEo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR6aqlgJaiaexz/aNvdCktW/kAM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8=
|
||||
github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk=
|
||||
github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.12 h1:Y/2a+jLPrPbHpFkpAAYkVEtJmxORlXoo5k2g1fa2sUo=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.12/go.mod h1:xse1YTjmORlb/6fhkWi8qJh3cvZi4JoVNhc+NbJt4kI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.65 h1:q+nV2yYegofO/SUXruT+pn4KxkxmaQ++1B/QedcKBFM=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.65/go.mod h1:4zyjAuGOdikpNYiSGpsGz8hLGmUzlY8pc8r9QQ/RXYQ=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69 h1:6VFPH/Zi9xYFMJKPQOX5URYkQoXRWeJ7V/7Y6ZDYoms=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69/go.mod h1:GJj8mmO6YT6EqgduWocwhMoxTLFitkhIrK+owzrYL2I=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36/go.mod h1:gDhdAV6wL3PmPqBhiPbnlS447GoWs8HTTOYef9/9Inw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 h1:nAP2GYbfh8dd2zGZqFRSMlq+/F6cMPBUuCsGAMkN074=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4/go.mod h1:LT10DsiGjLWh4GbjInf9LQejkYEhBgBCjLG5+lvk4EE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 h1:qcLWgdhq45sDM9na4cvXax9dyLitn8EYBRl8Ak4XtG4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17/go.mod h1:M+jkjBFZ2J6DJrjMv2+vkBbuht6kxJYtJiwoVgX4p4U=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 h1:0reDqfEN+tB+sozj2r92Bep8MEwBZgtAXTND1Kk9OXg=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.34.7 h1:OBuZE9Wt8h2imuRktu+WfjiTGrnYdCIJg8IX92aalHE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.34.7/go.mod h1:4WYoZAhHt+dWYpoOQUgkUKfuQbE6Gg/hW4oXE0pKS9U=
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8 h1:80dpSqWMwx2dAm30Ib7J6ucz1ZHfiv5OCRwN/EnCOXQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8/go.mod h1:IzNt/udsXlETCdvBOL0nmyMe2t9cGmXmZgsdoZGYYhI=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w=
|
||||
github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw=
|
||||
github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 h1:lguz0bmOoGzozP9XfRJR1QIayEYo+2vP/No3OfLF0pU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 h1:jIiopHEV22b4yQP2q36Y0OmwLbsxNWdWwfZRR5QRRO4=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.34.2 h1:PajtbJ/5bEo6iUAIGMYnK8ljqg2F1h4mMCGh1acjN30=
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.34.2/go.mod h1:PJtxxMdj747j8DeZENRTTYAz/lx/pADn/U0k7YNNiUY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.3 h1:j5BchjfDoS7K26vPdyJlyxBIIBGDflq3qjjJKBDlbcI=
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.3/go.mod h1:Bar4MrRxeqdn6XIh8JGfiXuFRmyrrsZNTJotxEJmWW0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.2 h1:pdgODsAhGo4dvzC3JAG5Ce0PX8kWXrTZGx+jxADD+5E=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.2/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0 h1:90uX0veLKcdHVfvxhkWUQSCi5VabtwMLFutYiRke4oo=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
|
||||
github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k=
|
||||
github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
|
||||
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k=
|
||||
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec=
|
||||
github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
@@ -151,18 +158,22 @@ github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
|
||||
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
|
||||
github.com/evanoberholster/imagemeta v0.3.1 h1:E4GUjXcvlVMjP9joN25+bBNf3Al3MTTfMqCrDOCW+LE=
|
||||
github.com/evanoberholster/imagemeta v0.3.1/go.mod h1:V0vtDJmjTqvwAYO8r+u33NRVIMXQb0qSqEfImoKEiXM=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
||||
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
|
||||
@@ -170,8 +181,8 @@ 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.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
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.5 h1:Cq3hPu6wwlTJNv2t48ro3oWje54h82Q5pALeCBNgaSk=
|
||||
@@ -182,33 +193,33 @@ github.com/gen2brain/webp v0.5.5 h1:MvQR75yIPU/9nSqYT5h13k4URaJK3gf9tgz/ksRbyEg=
|
||||
github.com/gen2brain/webp v0.5.5/go.mod h1:xOSMzp4aROt2KFW++9qcK/RBTOVC2S9tJG66ip/9Oc0=
|
||||
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
|
||||
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI=
|
||||
github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
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.21.2 h1:AqQaNADVwq/VnkCmQg6ogE+M3FOsKTytwges0JdwVuA=
|
||||
github.com/go-openapi/jsonpointer v0.21.2/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
|
||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
||||
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
|
||||
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
|
||||
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
|
||||
github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ=
|
||||
github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
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.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||
@@ -216,18 +227,37 @@ github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3a
|
||||
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ=
|
||||
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
@@ -242,20 +272,21 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI=
|
||||
github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/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.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
|
||||
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
|
||||
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
|
||||
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -264,8 +295,8 @@ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZYSo=
|
||||
github.com/hashicorp/hcl/v2 v2.18.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
|
||||
github.com/hashicorp/hcl/v2 v2.13.0 h1:0Apadu1w6M11dyGFxWnmhhcMjkbAiKCv7G1r/2QgCNc=
|
||||
github.com/hashicorp/hcl/v2 v2.13.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0=
|
||||
github.com/hay-kot/httpkit v0.0.11 h1:ZdB2uqsFBSDpfUoClGK5c5orjBjQkEVSXh7fZX5FKEk=
|
||||
github.com/hay-kot/httpkit v0.0.11/go.mod h1:0kZdk5/swzdfqfg2c6pBWimcgeJ9PTyO97EbHnYl2Sw=
|
||||
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
|
||||
@@ -290,14 +321,17 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
||||
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
||||
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs=
|
||||
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
@@ -308,44 +342,45 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
|
||||
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
||||
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
||||
github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
|
||||
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||
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.44.0 h1:ECKVrDLdh/kDPV1g0gAQ+2+m2KprqZK5O/eJAyAnH2M=
|
||||
github.com/nats-io/nats.go v1.44.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
|
||||
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
|
||||
github.com/nats-io/nats.go v1.40.1 h1:MLjDkdsbGUeCMKFyCFoLnNn/HDTqcgVa3EQm+pMNDPk=
|
||||
github.com/nats-io/nats.go v1.40.1/go.mod h1:wV73x0FSI/orHPSYoyMeJB+KajMDoWyXmFaRrrYaaTo=
|
||||
github.com/nats-io/nkeys v0.4.10 h1:glmRrpCmYLHByYcePvnTBEAwawwapjCPMjy2huw20wc=
|
||||
github.com/nats-io/nkeys v0.4.10/go.mod h1:OjRrnIKnWBFl+s4YK5ChQfvHP2fxqZexrKJoVVyWB3U=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/olahol/melody v1.3.0 h1:n7UlKiQnxVrgxKoM0d7usZiN+Z0y2lVENtYLgKtXS6s=
|
||||
github.com/olahol/melody v1.3.0/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4=
|
||||
github.com/olahol/melody v1.2.1 h1:xdwRkzHxf+B0w4TKbGpUSSkV516ZucQZJIWLztOWICQ=
|
||||
github.com/olahol/melody v1.2.1/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4=
|
||||
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
|
||||
github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts=
|
||||
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/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
|
||||
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
|
||||
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/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
@@ -354,19 +389,19 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/pressly/goose/v3 v3.24.3 h1:DSWWNwwggVUsYZ0X2VitiAa9sKuqtBfe+Jr9zFGwWlM=
|
||||
github.com/pressly/goose/v3 v3.24.3/go.mod h1:v9zYL4xdViLHCUUJh/mhjnm6JrK7Eul8AS93IxiZM4E=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
|
||||
github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
|
||||
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
@@ -374,16 +409,14 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
||||
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
||||
github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM=
|
||||
github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U=
|
||||
github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc=
|
||||
github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
|
||||
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -395,20 +428,20 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU=
|
||||
github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=
|
||||
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
|
||||
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
|
||||
github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg=
|
||||
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/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
|
||||
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
|
||||
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
||||
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
|
||||
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
|
||||
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
|
||||
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
||||
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
|
||||
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.5 h1:HCOe2bSjkhZyYoyyNaXNzh4DJZll6inVJQQw+8228Zk=
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.5/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw=
|
||||
github.com/yeqown/go-qrcode/writer/standard v1.3.0 h1:chdyhEfRtUPgQtuPeaWVGQ/TQx4rE1PqeoW3U+53t34=
|
||||
@@ -426,102 +459,121 @@ github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
||||
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
|
||||
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
|
||||
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
|
||||
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
|
||||
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||
go.balki.me/anyhttp v0.5.2 h1:et4tCDXLeXpWfMNvRKG7ojfrnlr3du7cEaG966MLSpA=
|
||||
go.balki.me/anyhttp v0.5.2/go.mod h1:JhfekOIjgVODoVqUCficjpIgmB3wwlB7jhN0eN2EZ/s=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.37.0 h1:B+WbN9RPsvobe6q4vP6KgM8/9plR/HNjgGBrfcOlweA=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.37.0/go.mod h1:K5zQ3TT7p2ru9Qkzk0bKtCql0RGkPj9pRjpXgZJZ+rU=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw=
|
||||
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.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
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.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
gocloud.dev v0.43.0 h1:aW3eq4RMyehbJ54PMsh4hsp7iX8cO/98ZRzJJOzN/5M=
|
||||
gocloud.dev v0.43.0/go.mod h1:eD8rkg7LhKUHrzkEdLTZ+Ty/vgPHPCd+yMQdfelQVu4=
|
||||
gocloud.dev/pubsub/kafkapubsub v0.43.0 h1:Kgwi0na69W3RgxEffEkdrMhox6A3Q0gajoJtjHGVr/s=
|
||||
gocloud.dev/pubsub/kafkapubsub v0.43.0/go.mod h1:uKI0CXuj7HJ/YnnOLQ3VkDnuUnkz+q/d+tRzmfhmOOU=
|
||||
gocloud.dev/pubsub/natspubsub v0.43.0 h1:k35tFoaorvD9Fa26zVEEzyXiMOEyXNHc0pBOmRYvQI0=
|
||||
gocloud.dev/pubsub/natspubsub v0.43.0/go.mod h1:xJn8TO8pGYieDn6AsRFsYfhQW8cnC+xGmG9APGNxkpQ=
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.43.0 h1:6nNZFSlJ1dk2GujL8PFltfLz3vC6IbrpjGS4FTduo1s=
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.43.0/go.mod h1:sEaueAGat+OASRoB3QDkghCtibKttgg7X6zsPTm1pl0=
|
||||
gocloud.dev v0.41.0 h1:qBKd9jZkBKEghYbP/uThpomhedK5s2Gy6Lz7h/zYYrM=
|
||||
gocloud.dev v0.41.0/go.mod h1:IetpBcWLUwroOOxKr90lhsZ8vWxeSkuszBnW62sbcf0=
|
||||
gocloud.dev/pubsub/kafkapubsub v0.41.0 h1:Ft6YB77ejqk++VjW51UP39RH/WDAMtv6ed3+PHMxBzg=
|
||||
gocloud.dev/pubsub/kafkapubsub v0.41.0/go.mod h1:kJf4c6b+4yJk6nXmv33yXKblbrgWmrYCzI5QEsr27G0=
|
||||
gocloud.dev/pubsub/natspubsub v0.41.0 h1:UxNb0DiAzdnyHut6jcCG7u6lsB/hzxTyZ/RHWeCUJ4Q=
|
||||
gocloud.dev/pubsub/natspubsub v0.41.0/go.mod h1:uCBKjwvIcuNuf3+ft4wUI9hPHHKQvroxq9ZPB/410ac=
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.41.0 h1:RutvHbacZxlFr0t3wlr+kz63j53UOfHY3PJR8NKN1EI=
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.41.0/go.mod h1:s7oQXOlQ2FOj8XmYMv5Ocgs1t+8hIXfsKaWGgECM9SQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
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.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
|
||||
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
|
||||
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
||||
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
@@ -529,39 +581,66 @@ golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
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.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
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.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
google.golang.org/api v0.247.0 h1:tSd/e0QrUlLsrwMKmkbQhYVa109qIintOls2Wh6bngc=
|
||||
google.golang.org/api v0.247.0/go.mod h1:r1qZOPmxXffXg6xS5uhx16Fa/UFY8QU/K4bfKrnvovM=
|
||||
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/googleapis/api v0.0.0-20250715232539-7130f93afb79 h1:iOye66xuaAK0WnkPuhQPUFy8eJcmwUXqGGP3om6IxX8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79/go.mod h1:HKJDgKsFUnv5VAGeQjz8kxcgDP0HoE0iZNp0OdZNlhE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a h1:tPE/Kp+x9dMSwUm/uM0JKK0IfdiJkwAbSMSeZBXXJXc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
|
||||
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
|
||||
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
|
||||
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
||||
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/api v0.228.0 h1:X2DJ/uoWGnY5obVjewbp8icSL5U4FzuCfy9OjbLSnLs=
|
||||
google.golang.org/api v0.228.0/go.mod h1:wNvRS1Pbe8r4+IfBIniV8fwCpGwTrYa+kMUDiC5z5a4=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20250324211829-b45e905df463 h1:qEFnJI6AnfZk0NNe8YTyXQh5i//Zxi4gBHwRgp76qpw=
|
||||
google.golang.org/genproto v0.0.0-20250324211829-b45e905df463/go.mod h1:SqIx1NV9hcvqdLHo7uNZDS5lrUJybQ3evo3+z/WBfA0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
||||
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@@ -569,20 +648,21 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.26.3 h1:yEN8dzrkRFnn4PUUKXLYIqVf2PJYAEjMTFjO3BDGc3I=
|
||||
modernc.org/cc/v4 v4.26.3/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
|
||||
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||
modernc.org/fileutil v1.3.15 h1:rJAXTP6ilMW/1+kzDiqmBlHLWszheUFXIyGQIAvjJpY=
|
||||
modernc.org/fileutil v1.3.15/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8=
|
||||
modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.66.7 h1:rjhZ8OSCybKWxS1CJr0hikpEi6Vg+944Ouyrd+bQsoY=
|
||||
modernc.org/libc v1.66.7/go.mod h1:ln6tbWX0NH+mzApEoDRvilBvAWFt1HX7AUA4VDdVDPM=
|
||||
modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00=
|
||||
modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
@@ -591,8 +671,8 @@ 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.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
||||
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||
modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs=
|
||||
modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -38,10 +38,6 @@ func (svc *ItemService) Create(ctx Context, item repo.ItemCreate) (repo.ItemOut,
|
||||
return svc.repo.Items.Create(ctx, ctx.GID, item)
|
||||
}
|
||||
|
||||
func (svc *ItemService) Duplicate(ctx Context, gid, id uuid.UUID, options repo.DuplicateOptions) (repo.ItemOut, error) {
|
||||
return svc.repo.Items.Duplicate(ctx, gid, id, options)
|
||||
}
|
||||
|
||||
func (svc *ItemService) EnsureAssetID(ctx context.Context, gid uuid.UUID) (int, error) {
|
||||
items, err := svc.repo.Items.GetAllZeroAssetID(ctx, gid)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
package ent
|
||||
|
||||
import (
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/item"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/predicate"
|
||||
"github.com/sysadminsmedia/homebox/backend/pkgs/textutils"
|
||||
)
|
||||
|
||||
// AccentInsensitiveContains creates a predicate that performs accent-insensitive text search.
|
||||
// It normalizes both the database field value and the search value for comparison.
|
||||
func AccentInsensitiveContains(field string, searchValue string) predicate.Item {
|
||||
if searchValue == "" {
|
||||
return predicate.Item(func(s *sql.Selector) {
|
||||
// Return a predicate that never matches if search is empty
|
||||
s.Where(sql.False())
|
||||
})
|
||||
}
|
||||
|
||||
// Normalize the search value
|
||||
normalizedSearch := textutils.NormalizeSearchQuery(searchValue)
|
||||
|
||||
return predicate.Item(func(s *sql.Selector) {
|
||||
dialect := s.Dialect()
|
||||
|
||||
switch dialect {
|
||||
case "sqlite3":
|
||||
// For SQLite, we'll create a custom normalization function using REPLACE
|
||||
// to handle common accented characters
|
||||
normalizeFunc := buildSQLiteNormalizeExpression(s.C(field))
|
||||
s.Where(sql.ExprP(
|
||||
"LOWER("+normalizeFunc+") LIKE ?",
|
||||
"%"+normalizedSearch+"%",
|
||||
))
|
||||
case "postgres":
|
||||
// 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.)
|
||||
s.Where(sql.P(func(b *sql.Builder) {
|
||||
b.WriteString("LOWER(")
|
||||
b.WriteString(normalizeFunc)
|
||||
b.WriteString(") LIKE ")
|
||||
b.Arg("%" + normalizedSearch + "%")
|
||||
}))
|
||||
default:
|
||||
// Default fallback using REPLACE for common accented characters
|
||||
normalizeFunc := buildGenericNormalizeExpression(s.C(field))
|
||||
s.Where(sql.ExprP(
|
||||
"LOWER("+normalizeFunc+") LIKE ?",
|
||||
"%"+normalizedSearch+"%",
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// buildSQLiteNormalizeExpression creates a SQLite expression to normalize accented characters
|
||||
func buildSQLiteNormalizeExpression(fieldExpr string) string {
|
||||
return buildGenericNormalizeExpression(fieldExpr)
|
||||
}
|
||||
|
||||
// buildGenericNormalizeExpression creates a database-agnostic expression to normalize common accented characters
|
||||
func buildGenericNormalizeExpression(fieldExpr string) string {
|
||||
// Chain REPLACE functions to handle the most common accented characters
|
||||
// Focused on the most frequently used accents in Spanish, French, and Portuguese
|
||||
// Ordered by frequency of use for better performance
|
||||
normalized := fieldExpr
|
||||
|
||||
// Most common accented characters ordered by frequency
|
||||
commonAccents := []struct {
|
||||
from, to string
|
||||
}{
|
||||
// Spanish - most common
|
||||
{"á", "a"}, {"é", "e"}, {"í", "i"}, {"ó", "o"}, {"ú", "u"}, {"ñ", "n"},
|
||||
{"Á", "A"}, {"É", "E"}, {"Í", "I"}, {"Ó", "O"}, {"Ú", "U"}, {"Ñ", "N"},
|
||||
|
||||
// French - most common
|
||||
{"è", "e"}, {"ê", "e"}, {"à", "a"}, {"ç", "c"},
|
||||
{"È", "E"}, {"Ê", "E"}, {"À", "A"}, {"Ç", "C"},
|
||||
|
||||
// German umlauts and Portuguese - common
|
||||
{"ä", "a"}, {"ö", "o"}, {"ü", "u"}, {"ã", "a"}, {"õ", "o"},
|
||||
{"Ä", "A"}, {"Ö", "O"}, {"Ü", "U"}, {"Ã", "A"}, {"Õ", "O"},
|
||||
}
|
||||
|
||||
for _, accent := range commonAccents {
|
||||
normalized = "REPLACE(" + normalized + ", '" + accent.from + "', '" + accent.to + "')"
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
// ItemNameAccentInsensitiveContains creates an accent-insensitive search predicate for the item name field.
|
||||
func ItemNameAccentInsensitiveContains(value string) predicate.Item {
|
||||
return AccentInsensitiveContains(item.FieldName, value)
|
||||
}
|
||||
|
||||
// ItemDescriptionAccentInsensitiveContains creates an accent-insensitive search predicate for the item description field.
|
||||
func ItemDescriptionAccentInsensitiveContains(value string) predicate.Item {
|
||||
return AccentInsensitiveContains(item.FieldDescription, value)
|
||||
}
|
||||
|
||||
// ItemSerialNumberAccentInsensitiveContains creates an accent-insensitive search predicate for the item serial number field.
|
||||
func ItemSerialNumberAccentInsensitiveContains(value string) predicate.Item {
|
||||
return AccentInsensitiveContains(item.FieldSerialNumber, value)
|
||||
}
|
||||
|
||||
// ItemModelNumberAccentInsensitiveContains creates an accent-insensitive search predicate for the item model number field.
|
||||
func ItemModelNumberAccentInsensitiveContains(value string) predicate.Item {
|
||||
return AccentInsensitiveContains(item.FieldModelNumber, value)
|
||||
}
|
||||
|
||||
// ItemManufacturerAccentInsensitiveContains creates an accent-insensitive search predicate for the item manufacturer field.
|
||||
func ItemManufacturerAccentInsensitiveContains(value string) predicate.Item {
|
||||
return AccentInsensitiveContains(item.FieldManufacturer, value)
|
||||
}
|
||||
|
||||
// ItemNotesAccentInsensitiveContains creates an accent-insensitive search predicate for the item notes field.
|
||||
func ItemNotesAccentInsensitiveContains(value string) predicate.Item {
|
||||
return AccentInsensitiveContains(item.FieldNotes, value)
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package ent
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBuildGenericNormalizeExpression(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
field string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Simple field name",
|
||||
field: "name",
|
||||
expected: "name", // Should be wrapped in many REPLACE functions
|
||||
},
|
||||
{
|
||||
name: "Complex field name",
|
||||
field: "description",
|
||||
expected: "description",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := buildGenericNormalizeExpression(tt.field)
|
||||
|
||||
// Should contain the original field
|
||||
assert.Contains(t, result, tt.field)
|
||||
|
||||
// Should contain REPLACE functions for accent normalization
|
||||
assert.Contains(t, result, "REPLACE(")
|
||||
|
||||
// Should handle common accented characters
|
||||
assert.Contains(t, result, "'á'", "Should handle Spanish á")
|
||||
assert.Contains(t, result, "'é'", "Should handle Spanish é")
|
||||
assert.Contains(t, result, "'ñ'", "Should handle Spanish ñ")
|
||||
assert.Contains(t, result, "'ü'", "Should handle German ü")
|
||||
|
||||
// Should handle uppercase accents too
|
||||
assert.Contains(t, result, "'Á'", "Should handle uppercase Spanish Á")
|
||||
assert.Contains(t, result, "'É'", "Should handle uppercase Spanish É")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLiteNormalizeExpression(t *testing.T) {
|
||||
result := buildSQLiteNormalizeExpression("test_field")
|
||||
|
||||
// Should contain the field name and REPLACE functions
|
||||
assert.Contains(t, result, "test_field")
|
||||
assert.Contains(t, result, "REPLACE(")
|
||||
// Check for some specific accent replacements (order doesn't matter)
|
||||
assert.Contains(t, result, "'á'", "Should handle Spanish á")
|
||||
assert.Contains(t, result, "'ó'", "Should handle Spanish ó")
|
||||
}
|
||||
|
||||
func TestAccentInsensitivePredicateCreation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
field string
|
||||
searchValue string
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "Normal search value",
|
||||
field: "name",
|
||||
searchValue: "electronica",
|
||||
description: "Should create predicate for normal search",
|
||||
},
|
||||
{
|
||||
name: "Accented search value",
|
||||
field: "description",
|
||||
searchValue: "electrónica",
|
||||
description: "Should create predicate for accented search",
|
||||
},
|
||||
{
|
||||
name: "Empty search value",
|
||||
field: "name",
|
||||
searchValue: "",
|
||||
description: "Should handle empty search gracefully",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
predicate := AccentInsensitiveContains(tt.field, tt.searchValue)
|
||||
assert.NotNil(t, predicate, tt.description)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecificItemPredicates(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
predicateFunc func(string) interface{}
|
||||
searchValue string
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "ItemNameAccentInsensitiveContains",
|
||||
predicateFunc: func(val string) interface{} { return ItemNameAccentInsensitiveContains(val) },
|
||||
searchValue: "electronica",
|
||||
description: "Should create accent-insensitive name search predicate",
|
||||
},
|
||||
{
|
||||
name: "ItemDescriptionAccentInsensitiveContains",
|
||||
predicateFunc: func(val string) interface{} { return ItemDescriptionAccentInsensitiveContains(val) },
|
||||
searchValue: "descripcion",
|
||||
description: "Should create accent-insensitive description search predicate",
|
||||
},
|
||||
{
|
||||
name: "ItemManufacturerAccentInsensitiveContains",
|
||||
predicateFunc: func(val string) interface{} { return ItemManufacturerAccentInsensitiveContains(val) },
|
||||
searchValue: "compañia",
|
||||
description: "Should create accent-insensitive manufacturer search predicate",
|
||||
},
|
||||
{
|
||||
name: "ItemSerialNumberAccentInsensitiveContains",
|
||||
predicateFunc: func(val string) interface{} { return ItemSerialNumberAccentInsensitiveContains(val) },
|
||||
searchValue: "sn123",
|
||||
description: "Should create accent-insensitive serial number search predicate",
|
||||
},
|
||||
{
|
||||
name: "ItemModelNumberAccentInsensitiveContains",
|
||||
predicateFunc: func(val string) interface{} { return ItemModelNumberAccentInsensitiveContains(val) },
|
||||
searchValue: "model456",
|
||||
description: "Should create accent-insensitive model number search predicate",
|
||||
},
|
||||
{
|
||||
name: "ItemNotesAccentInsensitiveContains",
|
||||
predicateFunc: func(val string) interface{} { return ItemNotesAccentInsensitiveContains(val) },
|
||||
searchValue: "notas importantes",
|
||||
description: "Should create accent-insensitive notes search predicate",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
predicate := tt.predicateFunc(tt.searchValue)
|
||||
assert.NotNil(t, predicate, tt.description)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,6 @@ package migrations
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -19,16 +17,15 @@ var sqliteFiles embed.FS
|
||||
// migration files in the binary at build time. The function takes a string
|
||||
// parameter "dialect" which specifies the SQL dialect to use. It returns an
|
||||
// embedded file system containing the migration files for the specified dialect.
|
||||
func Migrations(dialect string) (embed.FS, error) {
|
||||
func Migrations(dialect string) embed.FS {
|
||||
switch dialect {
|
||||
case "postgres":
|
||||
return postgresFiles, nil
|
||||
return postgresFiles
|
||||
case "sqlite3":
|
||||
return sqliteFiles, nil
|
||||
return sqliteFiles
|
||||
default:
|
||||
log.Error().Str("dialect", dialect).Msg("unknown sql dialect")
|
||||
return embed.FS{}, fmt.Errorf("unknown sql dialect: %s", dialect)
|
||||
log.Fatal().Str("dialect", dialect).Msg("unknown sql dialect")
|
||||
}
|
||||
// This should never get hit, but just in case
|
||||
return sqliteFiles, nil
|
||||
return sqliteFiles
|
||||
}
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
-- +goose Up
|
||||
-- GENERATED with 20250706190000_generate_migration.py
|
||||
-- Migrating auth_tokens/created_at
|
||||
update auth_tokens set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
|
||||
update auth_tokens set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
|
||||
|
||||
-- Migrating auth_tokens/updated_at
|
||||
update auth_tokens set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
|
||||
update auth_tokens set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
|
||||
|
||||
-- Migrating auth_tokens/expires_at
|
||||
update auth_tokens set expires_at = substr(expires_at,1, instr(expires_at, ' +')-1) || substr(expires_at, instr(expires_at, ' +')+1,3) || ':' || substr(expires_at, instr(expires_at, ' +')+4,2) where expires_at like '% +%';
|
||||
update auth_tokens set expires_at = substr(expires_at,1, instr(expires_at, ' -')-1) || substr(expires_at, instr(expires_at, ' -')+1,3) || ':' || substr(expires_at, instr(expires_at, ' -')+4,2) where expires_at like '% -%';
|
||||
|
||||
-- Migrating groups/created_at
|
||||
update groups set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
|
||||
update groups set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
|
||||
|
||||
-- Migrating groups/updated_at
|
||||
update groups set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
|
||||
update groups set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
|
||||
|
||||
-- Migrating group_invitation_tokens/created_at
|
||||
update group_invitation_tokens set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
|
||||
update group_invitation_tokens set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
|
||||
|
||||
-- Migrating group_invitation_tokens/updated_at
|
||||
update group_invitation_tokens set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
|
||||
update group_invitation_tokens set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
|
||||
|
||||
-- Migrating group_invitation_tokens/expires_at
|
||||
update group_invitation_tokens set expires_at = substr(expires_at,1, instr(expires_at, ' +')-1) || substr(expires_at, instr(expires_at, ' +')+1,3) || ':' || substr(expires_at, instr(expires_at, ' +')+4,2) where expires_at like '% +%';
|
||||
update group_invitation_tokens set expires_at = substr(expires_at,1, instr(expires_at, ' -')-1) || substr(expires_at, instr(expires_at, ' -')+1,3) || ':' || substr(expires_at, instr(expires_at, ' -')+4,2) where expires_at like '% -%';
|
||||
|
||||
-- Migrating item_fields/created_at
|
||||
update item_fields set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
|
||||
update item_fields set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
|
||||
|
||||
-- Migrating item_fields/updated_at
|
||||
update item_fields set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
|
||||
update item_fields set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
|
||||
|
||||
-- Migrating item_fields/time_value
|
||||
update item_fields set time_value = substr(time_value,1, instr(time_value, ' +')-1) || substr(time_value, instr(time_value, ' +')+1,3) || ':' || substr(time_value, instr(time_value, ' +')+4,2) where time_value like '% +%';
|
||||
update item_fields set time_value = substr(time_value,1, instr(time_value, ' -')-1) || substr(time_value, instr(time_value, ' -')+1,3) || ':' || substr(time_value, instr(time_value, ' -')+4,2) where time_value like '% -%';
|
||||
|
||||
-- Migrating labels/created_at
|
||||
update labels set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
|
||||
update labels set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
|
||||
|
||||
-- Migrating labels/updated_at
|
||||
update labels set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
|
||||
update labels set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
|
||||
|
||||
-- Migrating locations/created_at
|
||||
update locations set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
|
||||
update locations set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
|
||||
|
||||
-- Migrating locations/updated_at
|
||||
update locations set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
|
||||
update locations set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
|
||||
|
||||
-- Migrating maintenance_entries/created_at
|
||||
update maintenance_entries set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
|
||||
update maintenance_entries set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
|
||||
|
||||
-- Migrating maintenance_entries/updated_at
|
||||
update maintenance_entries set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
|
||||
update maintenance_entries set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
|
||||
|
||||
-- Migrating maintenance_entries/date
|
||||
update maintenance_entries set date = substr(date,1, instr(date, ' +')-1) || substr(date, instr(date, ' +')+1,3) || ':' || substr(date, instr(date, ' +')+4,2) where date like '% +%';
|
||||
update maintenance_entries set date = substr(date,1, instr(date, ' -')-1) || substr(date, instr(date, ' -')+1,3) || ':' || substr(date, instr(date, ' -')+4,2) where date like '% -%';
|
||||
|
||||
-- Migrating maintenance_entries/scheduled_date
|
||||
update maintenance_entries set scheduled_date = substr(scheduled_date,1, instr(scheduled_date, ' +')-1) || substr(scheduled_date, instr(scheduled_date, ' +')+1,3) || ':' || substr(scheduled_date, instr(scheduled_date, ' +')+4,2) where scheduled_date like '% +%';
|
||||
update maintenance_entries set scheduled_date = substr(scheduled_date,1, instr(scheduled_date, ' -')-1) || substr(scheduled_date, instr(scheduled_date, ' -')+1,3) || ':' || substr(scheduled_date, instr(scheduled_date, ' -')+4,2) where scheduled_date like '% -%';
|
||||
|
||||
-- Migrating notifiers/created_at
|
||||
update notifiers set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
|
||||
update notifiers set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
|
||||
|
||||
-- Migrating notifiers/updated_at
|
||||
update notifiers set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
|
||||
update notifiers set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
|
||||
|
||||
-- Migrating users/created_at
|
||||
update users set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
|
||||
update users set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
|
||||
|
||||
-- Migrating users/updated_at
|
||||
update users set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
|
||||
update users set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
|
||||
|
||||
-- Migrating users/activated_on
|
||||
update users set activated_on = substr(activated_on,1, instr(activated_on, ' +')-1) || substr(activated_on, instr(activated_on, ' +')+1,3) || ':' || substr(activated_on, instr(activated_on, ' +')+4,2) where activated_on like '% +%';
|
||||
update users set activated_on = substr(activated_on,1, instr(activated_on, ' -')-1) || substr(activated_on, instr(activated_on, ' -')+1,3) || ':' || substr(activated_on, instr(activated_on, ' -')+4,2) where activated_on like '% -%';
|
||||
|
||||
-- Migrating items/created_at
|
||||
update items set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
|
||||
update items set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
|
||||
|
||||
-- Migrating items/updated_at
|
||||
update items set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
|
||||
update items set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
|
||||
|
||||
-- Migrating items/warranty_expires
|
||||
update items set warranty_expires = substr(warranty_expires,1, instr(warranty_expires, ' +')-1) || substr(warranty_expires, instr(warranty_expires, ' +')+1,3) || ':' || substr(warranty_expires, instr(warranty_expires, ' +')+4,2) where warranty_expires like '% +%';
|
||||
update items set warranty_expires = substr(warranty_expires,1, instr(warranty_expires, ' -')-1) || substr(warranty_expires, instr(warranty_expires, ' -')+1,3) || ':' || substr(warranty_expires, instr(warranty_expires, ' -')+4,2) where warranty_expires like '% -%';
|
||||
|
||||
-- Migrating items/purchase_time
|
||||
update items set purchase_time = substr(purchase_time,1, instr(purchase_time, ' +')-1) || substr(purchase_time, instr(purchase_time, ' +')+1,3) || ':' || substr(purchase_time, instr(purchase_time, ' +')+4,2) where purchase_time like '% +%';
|
||||
update items set purchase_time = substr(purchase_time,1, instr(purchase_time, ' -')-1) || substr(purchase_time, instr(purchase_time, ' -')+1,3) || ':' || substr(purchase_time, instr(purchase_time, ' -')+4,2) where purchase_time like '% -%';
|
||||
|
||||
-- Migrating items/sold_time
|
||||
update items set sold_time = substr(sold_time,1, instr(sold_time, ' +')-1) || substr(sold_time, instr(sold_time, ' +')+1,3) || ':' || substr(sold_time, instr(sold_time, ' +')+4,2) where sold_time like '% +%';
|
||||
update items set sold_time = substr(sold_time,1, instr(sold_time, ' -')-1) || substr(sold_time, instr(sold_time, ' -')+1,3) || ':' || substr(sold_time, instr(sold_time, ' -')+4,2) where sold_time like '% -%';
|
||||
|
||||
-- Migrating attachments/created_at
|
||||
update attachments set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
|
||||
update attachments set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
|
||||
|
||||
-- Migrating attachments/updated_at
|
||||
update attachments set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
|
||||
update attachments set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
|
||||
# Extract fields with
|
||||
""" WITH tables AS (
|
||||
SELECT name AS table_name
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table'
|
||||
AND name NOT LIKE 'sqlite_%'
|
||||
)
|
||||
|
||||
SELECT
|
||||
'["' || t.table_name || '", "' || c.name || '"],' AS table_column
|
||||
FROM tables t
|
||||
JOIN pragma_table_info(t.table_name) c
|
||||
WHERE c.name like'%date%'; """
|
||||
|
||||
fields = [["auth_tokens", "created_at"],
|
||||
["auth_tokens", "updated_at"],
|
||||
["auth_tokens", "expires_at"],
|
||||
["groups", "created_at"],
|
||||
["groups", "updated_at"],
|
||||
["group_invitation_tokens", "created_at"],
|
||||
["group_invitation_tokens", "updated_at"],
|
||||
["group_invitation_tokens", "expires_at"],
|
||||
["item_fields", "created_at"],
|
||||
["item_fields", "updated_at"],
|
||||
["item_fields", "time_value"],
|
||||
["labels", "created_at"],
|
||||
["labels", "updated_at"],
|
||||
["locations", "created_at"],
|
||||
["locations", "updated_at"],
|
||||
["maintenance_entries", "created_at"],
|
||||
["maintenance_entries", "updated_at"],
|
||||
["maintenance_entries", "date"],
|
||||
["maintenance_entries", "scheduled_date"],
|
||||
["notifiers", "created_at"],
|
||||
["notifiers", "updated_at"],
|
||||
["users", "created_at"],
|
||||
["users", "updated_at"],
|
||||
["users", "activated_on"],
|
||||
["items", "created_at"],
|
||||
["items", "updated_at"],
|
||||
["items", "warranty_expires"],
|
||||
["items", "purchase_time"],
|
||||
["items", "sold_time"],
|
||||
["attachments", "created_at"],
|
||||
["attachments", "updated_at"]]
|
||||
|
||||
|
||||
def generate_migration(table_name, field_name):
|
||||
return f"""update {table_name} set {field_name} = substr({field_name},1, instr({field_name}, ' +')-1) || substr({field_name}, instr({field_name}, ' +')+1,3) || ':' || substr({field_name}, instr({field_name}, ' +')+4,2) where {field_name} like '% +%';\n""" + \
|
||||
f"""update {table_name} set {field_name} = substr({field_name},1, instr({field_name}, ' -')-1) || substr({field_name}, instr({field_name}, ' -')+1,3) || ':' || substr({field_name}, instr({field_name}, ' -')+4,2) where {field_name} like '% -%';"""
|
||||
|
||||
|
||||
print("-- +goose Up")
|
||||
print(f"-- GENERATED with {os.path.basename(__file__)}")
|
||||
for table, column in fields:
|
||||
print(f"-- Migrating {table}/{column}")
|
||||
print(generate_migration(table, column))
|
||||
print()
|
||||
@@ -5,15 +5,6 @@ import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/evanoberholster/imagemeta"
|
||||
"github.com/gen2brain/avif"
|
||||
"github.com/gen2brain/heic"
|
||||
@@ -25,6 +16,13 @@ import (
|
||||
"github.com/sysadminsmedia/homebox/backend/pkgs/utils"
|
||||
"github.com/zeebo/blake3"
|
||||
"golang.org/x/image/draw"
|
||||
"image"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent"
|
||||
@@ -102,30 +100,13 @@ func (r *AttachmentRepo) path(gid uuid.UUID, hash string) string {
|
||||
}
|
||||
|
||||
func (r *AttachmentRepo) GetConnString() string {
|
||||
// Handle the default case for file storage
|
||||
// which is file:///./ meaning relative to the current working directory
|
||||
if strings.HasPrefix(r.storage.ConnString, "file:///./") {
|
||||
dir, err := filepath.Abs(strings.TrimPrefix(r.storage.ConnString, "file:///./"))
|
||||
if runtime.GOOS == "windows" {
|
||||
dir = fmt.Sprintf("/%s", dir)
|
||||
}
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to get absolute path for attachment directory")
|
||||
return r.storage.ConnString
|
||||
}
|
||||
return strings.ReplaceAll(fmt.Sprintf("file://%s?no_tmp_dir=true", dir), "\\", "/")
|
||||
} else if strings.HasPrefix(r.storage.ConnString, "file://") {
|
||||
// Handle the case for file storage with an absolute path
|
||||
// Convert Windows paths to a format compatible with fileblob
|
||||
// e.g. file:///C:/path/to/file becomes file:///C/path
|
||||
dir := strings.TrimPrefix(strings.ReplaceAll(r.storage.ConnString, "\\", "/"), "file://")
|
||||
if runtime.GOOS == "windows" {
|
||||
// Remove the colon from the drive letter (in case the user adds it)
|
||||
dir = strings.ReplaceAll(dir, ":", "")
|
||||
// Ensure the path starts with a slash for Windows compatibility
|
||||
dir = fmt.Sprintf("/%s", dir)
|
||||
}
|
||||
return fmt.Sprintf("file://%s", dir)
|
||||
return fmt.Sprintf("file://%s?no_tmp_dir=true", dir)
|
||||
}
|
||||
return r.storage.ConnString
|
||||
}
|
||||
@@ -338,19 +319,16 @@ func (r *AttachmentRepo) Update(ctx context.Context, gid uuid.UUID, id uuid.UUID
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Only remove primary status from other photo attachments when setting a new photo as primary
|
||||
if typ == attachment.TypePhoto && data.Primary {
|
||||
err = r.db.Attachment.Update().
|
||||
Where(
|
||||
attachment.HasItemWith(item.ID(attachmentItem.ID)),
|
||||
attachment.IDNEQ(updatedAttachment.ID),
|
||||
attachment.TypeEQ(attachment.TypePhoto),
|
||||
).
|
||||
SetPrimary(false).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Ensure all other attachments are not primary
|
||||
err = r.db.Attachment.Update().
|
||||
Where(
|
||||
attachment.HasItemWith(item.ID(attachmentItem.ID)),
|
||||
attachment.IDNEQ(updatedAttachment.ID),
|
||||
).
|
||||
SetPrimary(false).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.Get(ctx, gid, updatedAttachment.ID)
|
||||
|
||||
@@ -152,132 +152,3 @@ func TestAttachmentRepo_EnsureSinglePrimaryAttachment(t *testing.T) {
|
||||
setAndVerifyPrimary(attachments[0].ID, attachments[1].ID)
|
||||
setAndVerifyPrimary(attachments[1].ID, attachments[0].ID)
|
||||
}
|
||||
|
||||
func TestAttachmentRepo_UpdateNonPhotoDoesNotAffectPrimaryPhoto(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
item := useItems(t, 1)[0]
|
||||
|
||||
// Create a photo attachment that will be primary
|
||||
photoAttachment, err := tRepos.Attachments.Create(ctx, item.ID, ItemCreateAttachment{Title: "Test Photo", Content: strings.NewReader("Photo content")}, attachment.TypePhoto, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a manual attachment (non-photo)
|
||||
manualAttachment, err := tRepos.Attachments.Create(ctx, item.ID, ItemCreateAttachment{Title: "Test Manual", Content: strings.NewReader("Manual content")}, attachment.TypeManual, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Cleanup
|
||||
t.Cleanup(func() {
|
||||
_ = tRepos.Attachments.Delete(ctx, tGroup.ID, item.ID, photoAttachment.ID)
|
||||
_ = tRepos.Attachments.Delete(ctx, tGroup.ID, item.ID, manualAttachment.ID)
|
||||
})
|
||||
|
||||
// Verify photo is primary initially
|
||||
photoAttachment, err = tRepos.Attachments.Get(ctx, tGroup.ID, photoAttachment.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, photoAttachment.Primary)
|
||||
|
||||
// Update the manual attachment (this should NOT affect the photo's primary status)
|
||||
_, err = tRepos.Attachments.Update(ctx, tGroup.ID, manualAttachment.ID, &ItemAttachmentUpdate{
|
||||
Type: attachment.TypeManual.String(),
|
||||
Title: "Updated Manual",
|
||||
Primary: false, // This should have no effect since it's not a photo
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify photo is still primary after updating the manual
|
||||
photoAttachment, err = tRepos.Attachments.Get(ctx, tGroup.ID, photoAttachment.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, photoAttachment.Primary, "Photo attachment should remain primary after updating non-photo attachment")
|
||||
|
||||
// Verify manual attachment is not primary
|
||||
manualAttachment, err = tRepos.Attachments.Get(ctx, tGroup.ID, manualAttachment.ID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, manualAttachment.Primary)
|
||||
}
|
||||
|
||||
func TestAttachmentRepo_AddingPDFAfterPhotoKeepsPhotoAsPrimary(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
item := useItems(t, 1)[0]
|
||||
|
||||
// Step 1: Upload a photo first (this should become primary since it's the first photo)
|
||||
photoAttachment, err := tRepos.Attachments.Create(ctx, item.ID, ItemCreateAttachment{Title: "Item Photo", Content: strings.NewReader("Photo content")}, attachment.TypePhoto, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Cleanup
|
||||
t.Cleanup(func() {
|
||||
_ = tRepos.Attachments.Delete(ctx, tGroup.ID, item.ID, photoAttachment.ID)
|
||||
})
|
||||
|
||||
// Verify photo becomes primary automatically (since it's the first photo)
|
||||
photoAttachment, err = tRepos.Attachments.Get(ctx, tGroup.ID, photoAttachment.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, photoAttachment.Primary, "First photo should automatically become primary")
|
||||
|
||||
// Step 2: Add a PDF receipt (this should NOT affect the photo's primary status)
|
||||
pdfAttachment, err := tRepos.Attachments.Create(ctx, item.ID, ItemCreateAttachment{Title: "Receipt PDF", Content: strings.NewReader("PDF content")}, attachment.TypeReceipt, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add to cleanup
|
||||
t.Cleanup(func() {
|
||||
_ = tRepos.Attachments.Delete(ctx, tGroup.ID, item.ID, pdfAttachment.ID)
|
||||
})
|
||||
|
||||
// Step 3: Verify photo is still primary after adding PDF
|
||||
photoAttachment, err = tRepos.Attachments.Get(ctx, tGroup.ID, photoAttachment.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, photoAttachment.Primary, "Photo should remain primary after adding PDF attachment")
|
||||
|
||||
// Verify PDF is not primary
|
||||
pdfAttachment, err = tRepos.Attachments.Get(ctx, tGroup.ID, pdfAttachment.ID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, pdfAttachment.Primary)
|
||||
|
||||
// Step 4: Test the actual item summary mapping (this is what determines the card display)
|
||||
updatedItem, err := tRepos.Items.GetOne(ctx, item.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The item should have the photo's ID as the imageId
|
||||
assert.NotNil(t, updatedItem.ImageID, "Item should have an imageId")
|
||||
assert.Equal(t, photoAttachment.ID, *updatedItem.ImageID, "Item's imageId should match the photo attachment ID")
|
||||
}
|
||||
|
||||
func TestAttachmentRepo_SettingPhotoPrimaryStillWorks(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
item := useItems(t, 1)[0]
|
||||
|
||||
// Create two photo attachments
|
||||
photo1, err := tRepos.Attachments.Create(ctx, item.ID, ItemCreateAttachment{Title: "Photo 1", Content: strings.NewReader("Photo 1 content")}, attachment.TypePhoto, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
photo2, err := tRepos.Attachments.Create(ctx, item.ID, ItemCreateAttachment{Title: "Photo 2", Content: strings.NewReader("Photo 2 content")}, attachment.TypePhoto, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Cleanup
|
||||
t.Cleanup(func() {
|
||||
_ = tRepos.Attachments.Delete(ctx, tGroup.ID, item.ID, photo1.ID)
|
||||
_ = tRepos.Attachments.Delete(ctx, tGroup.ID, item.ID, photo2.ID)
|
||||
})
|
||||
|
||||
// First photo should be primary (since it was created first)
|
||||
photo1, err = tRepos.Attachments.Get(ctx, tGroup.ID, photo1.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, photo1.Primary)
|
||||
|
||||
photo2, err = tRepos.Attachments.Get(ctx, tGroup.ID, photo2.ID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, photo2.Primary)
|
||||
|
||||
// Now set photo2 as primary (this should work and remove primary from photo1)
|
||||
photo2, err = tRepos.Attachments.Update(ctx, tGroup.ID, photo2.ID, &ItemAttachmentUpdate{
|
||||
Type: attachment.TypePhoto.String(),
|
||||
Title: "Photo 2",
|
||||
Primary: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, photo2.Primary)
|
||||
|
||||
// Verify photo1 is no longer primary
|
||||
photo1, err = tRepos.Attachments.Get(ctx, tGroup.ID, photo1.ID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, photo1.Primary, "Photo 1 should no longer be primary after setting Photo 2 as primary")
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/core/services/reporting/eventbus"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/attachment"
|
||||
@@ -15,7 +14,6 @@ import (
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/itemfield"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/label"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/location"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/maintenanceentry"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/predicate"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/types"
|
||||
)
|
||||
@@ -48,13 +46,6 @@ type (
|
||||
OrderBy string `json:"orderBy"`
|
||||
}
|
||||
|
||||
DuplicateOptions struct {
|
||||
CopyMaintenance bool `json:"copyMaintenance"`
|
||||
CopyAttachments bool `json:"copyAttachments"`
|
||||
CopyCustomFields bool `json:"copyCustomFields"`
|
||||
CopyPrefix string `json:"copyPrefix"`
|
||||
}
|
||||
|
||||
ItemField struct {
|
||||
ID uuid.UUID `json:"id,omitempty"`
|
||||
Type string `json:"type"`
|
||||
@@ -369,25 +360,14 @@ func (e *ItemsRepository) QueryByGroup(ctx context.Context, gid uuid.UUID, q Ite
|
||||
}
|
||||
|
||||
if q.Search != "" {
|
||||
// Use accent-insensitive search predicates that normalize both
|
||||
// the search query and database field values during comparison.
|
||||
// For queries without accents, the traditional search is more efficient.
|
||||
qb.Where(
|
||||
item.Or(
|
||||
// Regular case-insensitive search (fastest)
|
||||
item.NameContainsFold(q.Search),
|
||||
item.DescriptionContainsFold(q.Search),
|
||||
item.SerialNumberContainsFold(q.Search),
|
||||
item.ModelNumberContainsFold(q.Search),
|
||||
item.ManufacturerContainsFold(q.Search),
|
||||
item.NotesContainsFold(q.Search),
|
||||
// Accent-insensitive search using custom predicates
|
||||
ent.ItemNameAccentInsensitiveContains(q.Search),
|
||||
ent.ItemDescriptionAccentInsensitiveContains(q.Search),
|
||||
ent.ItemSerialNumberAccentInsensitiveContains(q.Search),
|
||||
ent.ItemModelNumberAccentInsensitiveContains(q.Search),
|
||||
ent.ItemManufacturerAccentInsensitiveContains(q.Search),
|
||||
ent.ItemNotesAccentInsensitiveContains(q.Search),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -1013,164 +993,3 @@ func (e *ItemsRepository) SetPrimaryPhotos(ctx context.Context, gid uuid.UUID) (
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
// Duplicate creates a copy of an item with configurable options for what data to copy.
|
||||
// The new item will have the next available asset ID and a customizable prefix in the name.
|
||||
func (e *ItemsRepository) Duplicate(ctx context.Context, gid, id uuid.UUID, options DuplicateOptions) (ItemOut, error) {
|
||||
tx, err := e.db.Tx(ctx)
|
||||
if err != nil {
|
||||
return ItemOut{}, err
|
||||
}
|
||||
committed := false
|
||||
defer func() {
|
||||
if !committed {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to rollback transaction during item duplication")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Get the original item with all its data
|
||||
originalItem, err := e.getOne(ctx, item.ID(id), item.HasGroupWith(group.ID(gid)))
|
||||
if err != nil {
|
||||
return ItemOut{}, err
|
||||
}
|
||||
|
||||
nextAssetID, err := e.GetHighestAssetID(ctx, gid)
|
||||
if err != nil {
|
||||
return ItemOut{}, err
|
||||
}
|
||||
nextAssetID++
|
||||
|
||||
// Set default copy prefix if not provided
|
||||
if options.CopyPrefix == "" {
|
||||
options.CopyPrefix = "Copy of "
|
||||
}
|
||||
|
||||
// Create the new item directly in the transaction
|
||||
newItemID := uuid.New()
|
||||
itemBuilder := tx.Item.Create().
|
||||
SetID(newItemID).
|
||||
SetName(options.CopyPrefix + originalItem.Name).
|
||||
SetDescription(originalItem.Description).
|
||||
SetQuantity(originalItem.Quantity).
|
||||
SetLocationID(originalItem.Location.ID).
|
||||
SetGroupID(gid).
|
||||
SetAssetID(int(nextAssetID)).
|
||||
SetSerialNumber(originalItem.SerialNumber).
|
||||
SetModelNumber(originalItem.ModelNumber).
|
||||
SetManufacturer(originalItem.Manufacturer).
|
||||
SetLifetimeWarranty(originalItem.LifetimeWarranty).
|
||||
SetWarrantyExpires(originalItem.WarrantyExpires.Time()).
|
||||
SetWarrantyDetails(originalItem.WarrantyDetails).
|
||||
SetPurchaseTime(originalItem.PurchaseTime.Time()).
|
||||
SetPurchaseFrom(originalItem.PurchaseFrom).
|
||||
SetPurchasePrice(originalItem.PurchasePrice).
|
||||
SetSoldTime(originalItem.SoldTime.Time()).
|
||||
SetSoldTo(originalItem.SoldTo).
|
||||
SetSoldPrice(originalItem.SoldPrice).
|
||||
SetSoldNotes(originalItem.SoldNotes).
|
||||
SetNotes(originalItem.Notes).
|
||||
SetInsured(originalItem.Insured).
|
||||
SetArchived(originalItem.Archived).
|
||||
SetSyncChildItemsLocations(originalItem.SyncChildItemsLocations)
|
||||
|
||||
if originalItem.Parent != nil {
|
||||
itemBuilder.SetParentID(originalItem.Parent.ID)
|
||||
}
|
||||
|
||||
// Add labels
|
||||
if len(originalItem.Labels) > 0 {
|
||||
labelIDs := make([]uuid.UUID, len(originalItem.Labels))
|
||||
for i, label := range originalItem.Labels {
|
||||
labelIDs[i] = label.ID
|
||||
}
|
||||
itemBuilder.AddLabelIDs(labelIDs...)
|
||||
}
|
||||
|
||||
_, err = itemBuilder.Save(ctx)
|
||||
if err != nil {
|
||||
return ItemOut{}, err
|
||||
}
|
||||
|
||||
// Copy custom fields if requested
|
||||
if options.CopyCustomFields {
|
||||
for _, field := range originalItem.Fields {
|
||||
_, err = tx.ItemField.Create().
|
||||
SetItemID(newItemID).
|
||||
SetType(itemfield.Type(field.Type)).
|
||||
SetName(field.Name).
|
||||
SetTextValue(field.TextValue).
|
||||
SetNumberValue(field.NumberValue).
|
||||
SetBooleanValue(field.BooleanValue).
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("field_name", field.Name).Msg("failed to copy custom field during duplication")
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy attachments if requested
|
||||
if options.CopyAttachments {
|
||||
for _, att := range originalItem.Attachments {
|
||||
// Get the original attachment file
|
||||
originalAttachment, err := tx.Attachment.Query().
|
||||
Where(attachment.ID(att.ID)).
|
||||
Only(ctx)
|
||||
if err != nil {
|
||||
// Log error but continue to copy other attachments
|
||||
log.Warn().Err(err).Str("attachment_id", att.ID.String()).Msg("failed to find attachment during duplication")
|
||||
continue
|
||||
}
|
||||
|
||||
// Create a copy of the attachment with the same file path
|
||||
// Since files are stored with hash-based paths, this is safe
|
||||
_, err = tx.Attachment.Create().
|
||||
SetItemID(newItemID).
|
||||
SetType(originalAttachment.Type).
|
||||
SetTitle(originalAttachment.Title).
|
||||
SetPath(originalAttachment.Path).
|
||||
SetMimeType(originalAttachment.MimeType).
|
||||
SetPrimary(originalAttachment.Primary).
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("original_attachment_id", att.ID.String()).Msg("failed to copy attachment during duplication")
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy maintenance entries if requested
|
||||
if options.CopyMaintenance {
|
||||
maintenanceEntries, err := tx.MaintenanceEntry.Query().
|
||||
Where(maintenanceentry.HasItemWith(item.ID(id))).
|
||||
All(ctx)
|
||||
if err == nil {
|
||||
for _, entry := range maintenanceEntries {
|
||||
_, err = tx.MaintenanceEntry.Create().
|
||||
SetItemID(newItemID).
|
||||
SetDate(entry.Date).
|
||||
SetScheduledDate(entry.ScheduledDate).
|
||||
SetName(entry.Name).
|
||||
SetDescription(entry.Description).
|
||||
SetCost(entry.Cost).
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("maintenance_entry_id", entry.ID.String()).Msg("failed to copy maintenance entry during duplication")
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return ItemOut{}, err
|
||||
}
|
||||
committed = true
|
||||
|
||||
e.publishMutationEvent(gid)
|
||||
|
||||
// Get the final item with all copied data
|
||||
return e.GetOne(ctx, newItemID)
|
||||
}
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sysadminsmedia/homebox/backend/pkgs/textutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestItemsRepository_AccentInsensitiveSearch(t *testing.T) {
|
||||
// Test cases for accent-insensitive search
|
||||
testCases := []struct {
|
||||
name string
|
||||
itemName string
|
||||
searchQuery string
|
||||
shouldMatch bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "Spanish accented item, search without accents",
|
||||
itemName: "electrónica",
|
||||
searchQuery: "electronica",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'electrónica' when searching for 'electronica'",
|
||||
},
|
||||
{
|
||||
name: "Spanish accented item, search with accents",
|
||||
itemName: "electrónica",
|
||||
searchQuery: "electrónica",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'electrónica' when searching for 'electrónica'",
|
||||
},
|
||||
{
|
||||
name: "Non-accented item, search with accents",
|
||||
itemName: "electronica",
|
||||
searchQuery: "electrónica",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'electronica' when searching for 'electrónica' (bidirectional search)",
|
||||
},
|
||||
{
|
||||
name: "Spanish item with tilde, search without accents",
|
||||
itemName: "café",
|
||||
searchQuery: "cafe",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'café' when searching for 'cafe'",
|
||||
},
|
||||
{
|
||||
name: "Spanish item without tilde, search with accents",
|
||||
itemName: "cafe",
|
||||
searchQuery: "café",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'cafe' when searching for 'café' (bidirectional)",
|
||||
},
|
||||
{
|
||||
name: "French accented item, search without accents",
|
||||
itemName: "pére",
|
||||
searchQuery: "pere",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'pére' when searching for 'pere'",
|
||||
},
|
||||
{
|
||||
name: "French: père without accent, search with accents",
|
||||
itemName: "pere",
|
||||
searchQuery: "père",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'pere' when searching for 'père' (bidirectional)",
|
||||
},
|
||||
{
|
||||
name: "Mixed case with accents",
|
||||
itemName: "Electrónica",
|
||||
searchQuery: "ELECTRONICA",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'Electrónica' when searching for 'ELECTRONICA' (case insensitive)",
|
||||
},
|
||||
{
|
||||
name: "Bidirectional: Non-accented item, search with different accents",
|
||||
itemName: "cafe",
|
||||
searchQuery: "café",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'cafe' when searching for 'café' (bidirectional)",
|
||||
},
|
||||
{
|
||||
name: "Bidirectional: Item with accent, search with different accent",
|
||||
itemName: "résumé",
|
||||
searchQuery: "resume",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'résumé' when searching for 'resume' (bidirectional)",
|
||||
},
|
||||
{
|
||||
name: "Bidirectional: Spanish ñ to n",
|
||||
itemName: "espanol",
|
||||
searchQuery: "español",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'espanol' when searching for 'español' (bidirectional ñ)",
|
||||
},
|
||||
{
|
||||
name: "French: français with accent, search without",
|
||||
itemName: "français",
|
||||
searchQuery: "francais",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'français' when searching for 'francais'",
|
||||
},
|
||||
{
|
||||
name: "French: français without accent, search with",
|
||||
itemName: "francais",
|
||||
searchQuery: "français",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'francais' when searching for 'français' (bidirectional)",
|
||||
},
|
||||
{
|
||||
name: "French: été with accent, search without",
|
||||
itemName: "été",
|
||||
searchQuery: "ete",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'été' when searching for 'ete'",
|
||||
},
|
||||
{
|
||||
name: "French: été without accent, search with",
|
||||
itemName: "ete",
|
||||
searchQuery: "été",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'ete' when searching for 'été' (bidirectional)",
|
||||
},
|
||||
{
|
||||
name: "French: hôtel with accent, search without",
|
||||
itemName: "hôtel",
|
||||
searchQuery: "hotel",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'hôtel' when searching for 'hotel'",
|
||||
},
|
||||
{
|
||||
name: "French: hôtel without accent, search with",
|
||||
itemName: "hotel",
|
||||
searchQuery: "hôtel",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'hotel' when searching for 'hôtel' (bidirectional)",
|
||||
},
|
||||
{
|
||||
name: "French: naïve with accent, search without",
|
||||
itemName: "naïve",
|
||||
searchQuery: "naive",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'naïve' when searching for 'naive'",
|
||||
},
|
||||
{
|
||||
name: "French: naïve without accent, search with",
|
||||
itemName: "naive",
|
||||
searchQuery: "naïve",
|
||||
shouldMatch: true,
|
||||
description: "Should find 'naive' when searching for 'naïve' (bidirectional)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Test the normalization logic used in the repository
|
||||
normalizedSearch := textutils.NormalizeSearchQuery(tc.searchQuery)
|
||||
|
||||
// This simulates what happens in the repository
|
||||
// The original search would find exact matches (case-insensitive)
|
||||
// The normalized search would find accent-insensitive matches
|
||||
|
||||
// Test that our normalization works as expected
|
||||
if tc.shouldMatch {
|
||||
// If it should match, then either the original query should match
|
||||
// or the normalized query should match when applied to the stored data
|
||||
assert.NotEqual(t, "", normalizedSearch, "Normalized search should not be empty")
|
||||
|
||||
// The key insight is that we're searching with both the original and normalized queries
|
||||
// So "electrónica" will be found when searching for "electronica" because:
|
||||
// 1. Original search: "electronica" doesn't match "electrónica"
|
||||
// 2. Normalized search: "electronica" matches the normalized version
|
||||
t.Logf("✓ %s: Item '%s' should be found with search '%s' (normalized: '%s')",
|
||||
tc.description, tc.itemName, tc.searchQuery, normalizedSearch)
|
||||
} else {
|
||||
t.Logf("✗ %s: Item '%s' should NOT be found with search '%s' (normalized: '%s')",
|
||||
tc.description, tc.itemName, tc.searchQuery, normalizedSearch)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeSearchQueryIntegration(t *testing.T) {
|
||||
// Test that the normalization function works correctly
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"electrónica", "electronica"},
|
||||
{"café", "cafe"},
|
||||
{"ELECTRÓNICA", "electronica"},
|
||||
{"Café París", "cafe paris"},
|
||||
{"hello world", "hello world"},
|
||||
// French accented words
|
||||
{"père", "pere"},
|
||||
{"français", "francais"},
|
||||
{"été", "ete"},
|
||||
{"hôtel", "hotel"},
|
||||
{"naïve", "naive"},
|
||||
{"PÈRE", "pere"},
|
||||
{"FRANÇAIS", "francais"},
|
||||
{"ÉTÉ", "ete"},
|
||||
{"HÔTEL", "hotel"},
|
||||
{"NAÏVE", "naive"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
result := textutils.NormalizeSearchQuery(tc.input)
|
||||
assert.Equal(t, tc.expected, result, "Normalization should work correctly")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -20,14 +20,14 @@ type LabelRepository struct {
|
||||
type (
|
||||
LabelCreate struct {
|
||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||
Description string `json:"description" validate:"max=1000"`
|
||||
Description string `json:"description" validate:"max=255"`
|
||||
Color string `json:"color"`
|
||||
}
|
||||
|
||||
LabelUpdate struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||
Description string `json:"description" validate:"max=1000"`
|
||||
Description string `json:"description" validate:"max=255"`
|
||||
Color string `json:"color"`
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package repo
|
||||
|
||||
type BarcodeProduct struct {
|
||||
SearchEngineName string `json:"search_engine_name"`
|
||||
|
||||
// Identifications
|
||||
ModelNumber string `json:"modelNumber"`
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
|
||||
// Extras
|
||||
Country string `json:"notes"`
|
||||
Barcode string `json:"barcode"`
|
||||
|
||||
ImageURL string `json:"imageURL"`
|
||||
ImageBase64 string `json:"imageBase64"`
|
||||
|
||||
Item ItemCreate `json:"item"`
|
||||
}
|
||||
@@ -11,8 +11,6 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var startTime = time.Now()
|
||||
|
||||
type Data struct {
|
||||
Domain string `json:"domain"`
|
||||
Name string `json:"name"`
|
||||
@@ -20,7 +18,7 @@ type Data struct {
|
||||
Props map[string]interface{} `json:"props"`
|
||||
}
|
||||
|
||||
func Send(version, buildInfo string) error {
|
||||
func Send(version, buildInfo string) {
|
||||
hostData, _ := host.Info()
|
||||
analytics := Data{
|
||||
Domain: "homebox.software",
|
||||
@@ -34,23 +32,22 @@ func Send(version, buildInfo string) error {
|
||||
"platform_version": hostData.PlatformVersion,
|
||||
"kernel_arch": hostData.KernelArch,
|
||||
"virt_type": hostData.VirtualizationSystem,
|
||||
"uptime_min": time.Since(startTime).Minutes(),
|
||||
},
|
||||
}
|
||||
jsonBody, err := json.Marshal(analytics)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to marshal analytics data")
|
||||
return err
|
||||
return
|
||||
}
|
||||
bodyReader := bytes.NewReader(jsonBody)
|
||||
req, err := http.NewRequest("POST", "https://a.sysadmins.zone/api/event", bodyReader)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to create analytics request")
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", "Homebox/"+version+"/(https://homebox.software)")
|
||||
req.Header.Set("User-Agent", "Homebox/"+version+"/"+buildInfo+" (https://homebox.software)")
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
@@ -59,7 +56,7 @@ func Send(version, buildInfo string) error {
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to send analytics request")
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
@@ -68,5 +65,4 @@ func Send(version, buildInfo string) error {
|
||||
log.Error().Err(err).Msg("failed to close response body")
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ type Config struct {
|
||||
Options Options `yaml:"options"`
|
||||
LabelMaker LabelMakerConf `yaml:"labelmaker"`
|
||||
Thumbnail Thumbnail `yaml:"thumbnail"`
|
||||
Barcode BarcodeAPIConf `yaml:"barcode"`
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
@@ -61,20 +60,14 @@ type WebConfig struct {
|
||||
}
|
||||
|
||||
type LabelMakerConf struct {
|
||||
Width int64 `yaml:"width" conf:"default:526"`
|
||||
Height int64 `yaml:"height" conf:"default:200"`
|
||||
Padding int64 `yaml:"padding" conf:"default:32"`
|
||||
Margin int64 `yaml:"margin" conf:"default:32"`
|
||||
FontSize float64 `yaml:"font_size" conf:"default:32.0"`
|
||||
PrintCommand *string `yaml:"string"`
|
||||
AdditionalInformation *string `yaml:"string"`
|
||||
DynamicLength bool `yaml:"bool" conf:"default:true"`
|
||||
LabelServiceUrl *string `yaml:"label_service_url"`
|
||||
LabelServiceTimeout *time.Duration `yaml:"label_service_timeout"`
|
||||
}
|
||||
|
||||
type BarcodeAPIConf struct {
|
||||
TokenBarcodespider string `yaml:"token_barcodespider"`
|
||||
Width int64 `yaml:"width" conf:"default:526"`
|
||||
Height int64 `yaml:"height" conf:"default:200"`
|
||||
Padding int64 `yaml:"padding" conf:"default:32"`
|
||||
Margin int64 `yaml:"margin" conf:"default:32"`
|
||||
FontSize float64 `yaml:"font_size" conf:"default:32.0"`
|
||||
PrintCommand *string `yaml:"string"`
|
||||
AdditionalInformation *string `yaml:"string"`
|
||||
DynamicLength bool `yaml:"bool" conf:"default:true"`
|
||||
}
|
||||
|
||||
// New parses the CLI/Config file and returns a Config struct. If the file argument is an empty string, the
|
||||
|
||||
@@ -17,10 +17,7 @@ type Database struct {
|
||||
Host string `yaml:"host"`
|
||||
Port string `yaml:"port"`
|
||||
Database string `yaml:"database"`
|
||||
SslMode string `yaml:"ssl_mode" conf:"default:prefer"`
|
||||
SslRootCert string `yaml:"ssl_rootcert"`
|
||||
SslCert string `yaml:"ssl_cert"`
|
||||
SslKey string `yaml:"ssl_key"`
|
||||
SslMode string `yaml:"ssl_mode"`
|
||||
SqlitePath string `yaml:"sqlite_path" conf:"default:./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite"`
|
||||
PubSubConnString string `yaml:"pubsub_conn_string" conf:"default:mem://{{ .Topic }}"`
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ import (
|
||||
"image/png"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -140,18 +138,11 @@ func wrapText(text string, face font.Face, maxWidth int, maxHeight int, lineHeig
|
||||
return wrappedLines, ""
|
||||
}
|
||||
|
||||
func GenerateLabel(w io.Writer, params *GenerateParameters, cfg *config.Config) error {
|
||||
func GenerateLabel(w io.Writer, params *GenerateParameters) error {
|
||||
if err := params.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If LabelServiceUrl is configured, fetch the label from the URL instead of generating it
|
||||
if cfg != nil && cfg.LabelMaker.LabelServiceUrl != nil && *cfg.LabelMaker.LabelServiceUrl != "" {
|
||||
log.Printf("LabelServiceUrl configured: %s", *cfg.LabelMaker.LabelServiceUrl)
|
||||
|
||||
return fetchLabelFromURL(w, *cfg.LabelMaker.LabelServiceUrl, params, cfg)
|
||||
}
|
||||
|
||||
bodyText := params.DescriptionText
|
||||
if params.AdditionalInformation != nil {
|
||||
bodyText = bodyText + "\n" + *params.AdditionalInformation
|
||||
@@ -227,7 +218,7 @@ func GenerateLabel(w io.Writer, params *GenerateParameters, cfg *config.Config)
|
||||
// Create the actual image with calculated height
|
||||
bounds := image.Rect(0, 0, params.Width, requiredHeight)
|
||||
img := image.NewRGBA(bounds)
|
||||
draw.Draw(img, bounds, &image.Uniform{C: color.White}, image.Point{}, draw.Src)
|
||||
draw.Draw(img, bounds, &image.Uniform{color.White}, image.Point{}, draw.Src)
|
||||
|
||||
// Draw QR code onto the image
|
||||
draw.Draw(img,
|
||||
@@ -288,98 +279,6 @@ func createContext(font *truetype.Font, size float64, img *image.RGBA, dpi float
|
||||
return c
|
||||
}
|
||||
|
||||
// fetchLabelFromURL fetches an image from the specified URL and writes it to the writer
|
||||
func fetchLabelFromURL(w io.Writer, serviceURL string, params *GenerateParameters, cfg *config.Config) error {
|
||||
// Parse the base URL
|
||||
baseURL, err := url.Parse(serviceURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse service URL %s: %w", serviceURL, err)
|
||||
}
|
||||
|
||||
// Build query parameters with the same attributes passed to print command
|
||||
query := url.Values{}
|
||||
query.Set("Width", fmt.Sprintf("%d", params.Width))
|
||||
query.Set("Height", fmt.Sprintf("%d", params.Height))
|
||||
query.Set("QrSize", fmt.Sprintf("%d", params.QrSize))
|
||||
query.Set("Margin", fmt.Sprintf("%d", params.Margin))
|
||||
query.Set("ComponentPadding", fmt.Sprintf("%d", params.ComponentPadding))
|
||||
query.Set("TitleText", params.TitleText)
|
||||
query.Set("TitleFontSize", fmt.Sprintf("%f", params.TitleFontSize))
|
||||
query.Set("DescriptionText", params.DescriptionText)
|
||||
query.Set("DescriptionFontSize", fmt.Sprintf("%f", params.DescriptionFontSize))
|
||||
query.Set("Dpi", fmt.Sprintf("%f", params.Dpi))
|
||||
query.Set("URL", params.URL)
|
||||
query.Set("DynamicLength", fmt.Sprintf("%t", params.DynamicLength))
|
||||
|
||||
// Add AdditionalInformation if it exists
|
||||
if params.AdditionalInformation != nil {
|
||||
query.Set("AdditionalInformation", *params.AdditionalInformation)
|
||||
}
|
||||
|
||||
// Set the query parameters
|
||||
baseURL.RawQuery = query.Encode()
|
||||
finalServiceURL := baseURL.String()
|
||||
|
||||
log.Printf("Fetching label from URL: %s", finalServiceURL)
|
||||
|
||||
// Use configured timeout or default to 30 seconds
|
||||
timeout := 30 * time.Second
|
||||
if cfg != nil && cfg.LabelMaker.LabelServiceTimeout != nil {
|
||||
timeout = *cfg.LabelMaker.LabelServiceTimeout
|
||||
}
|
||||
|
||||
// Create HTTP client with configurable timeout
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
// Create HTTP request with custom headers
|
||||
req, err := http.NewRequest("GET", finalServiceURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request for URL %s: %w", finalServiceURL, err)
|
||||
}
|
||||
|
||||
// Set custom headers
|
||||
req.Header.Set("User-Agent", "Homebox-LabelMaker/1.0")
|
||||
req.Header.Set("Accept", "image/*")
|
||||
|
||||
// Make HTTP request to the label service
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch label from URL %s: %w", finalServiceURL, err)
|
||||
}
|
||||
|
||||
// Check if the response status is OK
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("label service returned status %d for URL %s", resp.StatusCode, finalServiceURL)
|
||||
}
|
||||
|
||||
// Check if the response is an image
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if !strings.HasPrefix(contentType, "image/") {
|
||||
return fmt.Errorf("label service returned invalid content type %s, expected image/*", contentType)
|
||||
}
|
||||
|
||||
// Set default max response size (10MB)
|
||||
maxResponseSize := int64(10 << 20)
|
||||
if cfg != nil {
|
||||
maxResponseSize = cfg.Web.MaxUploadSize << 20
|
||||
}
|
||||
limitedReader := io.LimitReader(resp.Body, maxResponseSize)
|
||||
|
||||
// Copy the response body to the writer
|
||||
_, err = io.Copy(w, limitedReader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write fetched label data: %w", err)
|
||||
}
|
||||
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
log.Printf("failed to close response body: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func PrintLabel(cfg *config.Config, params *GenerateParameters) error {
|
||||
tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("label-%d.png", time.Now().UnixNano()))
|
||||
f, err := os.OpenFile(tmpFile, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
|
||||
@@ -393,7 +292,7 @@ func PrintLabel(cfg *config.Config, params *GenerateParameters) error {
|
||||
}
|
||||
}()
|
||||
|
||||
err = GenerateLabel(f, params, cfg)
|
||||
err = GenerateLabel(f, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -404,27 +303,8 @@ func PrintLabel(cfg *config.Config, params *GenerateParameters) error {
|
||||
|
||||
commandTemplate := template.Must(template.New("command").Parse(*cfg.LabelMaker.PrintCommand))
|
||||
builder := &strings.Builder{}
|
||||
additionalInformation := func() string {
|
||||
if params.AdditionalInformation != nil {
|
||||
return *params.AdditionalInformation
|
||||
}
|
||||
return ""
|
||||
}()
|
||||
if err := commandTemplate.Execute(builder, map[string]string{
|
||||
"FileName": f.Name(),
|
||||
"Width": fmt.Sprintf("%d", params.Width),
|
||||
"Height": fmt.Sprintf("%d", params.Height),
|
||||
"QrSize": fmt.Sprintf("%d", params.QrSize),
|
||||
"Margin": fmt.Sprintf("%d", params.Margin),
|
||||
"ComponentPadding": fmt.Sprintf("%d", params.ComponentPadding),
|
||||
"TitleText": params.TitleText,
|
||||
"TitleFontSize": fmt.Sprintf("%f", params.TitleFontSize),
|
||||
"DescriptionText": params.DescriptionText,
|
||||
"DescriptionFontSize": fmt.Sprintf("%f", params.DescriptionFontSize),
|
||||
"AdditionalInformation": additionalInformation,
|
||||
"Dpi": fmt.Sprintf("%f", params.Dpi),
|
||||
"URL": params.URL,
|
||||
"DynamicLength": fmt.Sprintf("%t", params.DynamicLength),
|
||||
"FileName": f.Name(),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
package textutils
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/text/runes"
|
||||
"golang.org/x/text/transform"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
// RemoveAccents removes accents from text by normalizing Unicode characters
|
||||
// and removing diacritical marks. This allows for accent-insensitive search.
|
||||
//
|
||||
// Example:
|
||||
// - "electrónica" becomes "electronica"
|
||||
// - "café" becomes "cafe"
|
||||
// - "père" becomes "pere"
|
||||
func RemoveAccents(text string) string {
|
||||
// Create a transformer that:
|
||||
// 1. Normalizes to NFD (canonical decomposition)
|
||||
// 2. Removes diacritical marks (combining characters)
|
||||
// 3. Normalizes back to NFC (canonical composition)
|
||||
t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
|
||||
|
||||
result, _, err := transform.String(t, text)
|
||||
if err != nil {
|
||||
// If transformation fails, return the original text
|
||||
return text
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// NormalizeSearchQuery normalizes a search query for accent-insensitive matching.
|
||||
// This function removes accents and converts to lowercase for consistent search behavior.
|
||||
func NormalizeSearchQuery(query string) string {
|
||||
normalized := RemoveAccents(query)
|
||||
return strings.ToLower(normalized)
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
package textutils
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRemoveAccents(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Spanish accented characters",
|
||||
input: "electrónica",
|
||||
expected: "electronica",
|
||||
},
|
||||
{
|
||||
name: "Spanish accented characters with tilde",
|
||||
input: "café",
|
||||
expected: "cafe",
|
||||
},
|
||||
{
|
||||
name: "French accented characters",
|
||||
input: "père",
|
||||
expected: "pere",
|
||||
},
|
||||
{
|
||||
name: "German umlauts",
|
||||
input: "Björk",
|
||||
expected: "Bjork",
|
||||
},
|
||||
{
|
||||
name: "Mixed accented characters",
|
||||
input: "résumé",
|
||||
expected: "resume",
|
||||
},
|
||||
{
|
||||
name: "Portuguese accented characters",
|
||||
input: "João",
|
||||
expected: "Joao",
|
||||
},
|
||||
{
|
||||
name: "No accents",
|
||||
input: "hello world",
|
||||
expected: "hello world",
|
||||
},
|
||||
{
|
||||
name: "Empty string",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "Numbers and symbols",
|
||||
input: "123!@#",
|
||||
expected: "123!@#",
|
||||
},
|
||||
{
|
||||
name: "Multiple accents in one word",
|
||||
input: "été",
|
||||
expected: "ete",
|
||||
},
|
||||
{
|
||||
name: "Complex Unicode characters",
|
||||
input: "français",
|
||||
expected: "francais",
|
||||
},
|
||||
{
|
||||
name: "Unicode diacritics",
|
||||
input: "naïve",
|
||||
expected: "naive",
|
||||
},
|
||||
{
|
||||
name: "Unicode combining characters",
|
||||
input: "e\u0301", // e with combining acute accent
|
||||
expected: "e",
|
||||
},
|
||||
{
|
||||
name: "Very long string with accents",
|
||||
input: strings.Repeat("café", 1000),
|
||||
expected: strings.Repeat("cafe", 1000),
|
||||
},
|
||||
{
|
||||
name: "All French accents",
|
||||
input: "àâäéèêëïîôöùûüÿç",
|
||||
expected: "aaaeeeeiioouuuyc",
|
||||
},
|
||||
{
|
||||
name: "All Spanish accents",
|
||||
input: "áéíóúñüÁÉÍÓÚÑÜ",
|
||||
expected: "aeiounuAEIOUNU",
|
||||
},
|
||||
{
|
||||
name: "All German umlauts",
|
||||
input: "äöüÄÖÜß",
|
||||
expected: "aouAOUß",
|
||||
},
|
||||
{
|
||||
name: "Mixed languages",
|
||||
input: "Français café España niño",
|
||||
expected: "Francais cafe Espana nino",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := RemoveAccents(tc.input)
|
||||
if result != tc.expected {
|
||||
t.Errorf("RemoveAccents(%q) = %q, expected %q", tc.input, result, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeSearchQuery(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Uppercase with accents",
|
||||
input: "ELECTRÓNICA",
|
||||
expected: "electronica",
|
||||
},
|
||||
{
|
||||
name: "Mixed case with accents",
|
||||
input: "Electrónica",
|
||||
expected: "electronica",
|
||||
},
|
||||
{
|
||||
name: "Multiple words with accents",
|
||||
input: "Café París",
|
||||
expected: "cafe paris",
|
||||
},
|
||||
{
|
||||
name: "No accents mixed case",
|
||||
input: "Hello World",
|
||||
expected: "hello world",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := NormalizeSearchQuery(tc.input)
|
||||
if result != tc.expected {
|
||||
t.Errorf("NormalizeSearchQuery(%q) = %q, expected %q", tc.input, result, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ services:
|
||||
image: homebox
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile.hardened
|
||||
dockerfile: ./Dockerfile.rootless
|
||||
args:
|
||||
- COMMIT=head
|
||||
- BUILD_TIME=0001-01-01T00:00:00Z
|
||||
|
||||
105
docs/.vitepress/components/BasicConfig.vue
Normal file
105
docs/.vitepress/components/BasicConfig.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div class="tab-content">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Basic Configuration</h2>
|
||||
<p class="card-description">Configure the basic settings for your Homebox instance.</p>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="form-row">
|
||||
<label for="rootless">Use Rootless Image</label>
|
||||
<div class="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="rootless"
|
||||
v-model="config.rootless"
|
||||
/>
|
||||
<label for="rootless"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="port">External Port</label>
|
||||
<input
|
||||
type="text"
|
||||
id="port"
|
||||
v-model="config.port"
|
||||
/>
|
||||
<p class="help-text">Only used if HTTPS is not enabled</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="maxFileUpload">Max File Upload (MB)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="maxFileUpload"
|
||||
v-model="config.maxFileUpload"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="allowAnalytics">Allow Analytics</label>
|
||||
<div class="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="allowAnalytics"
|
||||
v-model="config.allowAnalytics"
|
||||
/>
|
||||
<label for="allowAnalytics"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="allowRegistration">Allow Registration</label>
|
||||
<div class="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="allowRegistration"
|
||||
v-model="config.allowRegistration"
|
||||
/>
|
||||
<label for="allowRegistration"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="autoIncrementAssetId">Auto Increment Asset ID</label>
|
||||
<div class="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="autoIncrementAssetId"
|
||||
v-model="config.autoIncrementAssetId"
|
||||
/>
|
||||
<label for="autoIncrementAssetId"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="checkGithubRelease">Check GitHub Release</label>
|
||||
<div class="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="checkGithubRelease"
|
||||
v-model="config.checkGithubRelease"
|
||||
/>
|
||||
<label for="checkGithubRelease"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
config: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import 'common.css';
|
||||
</style>
|
||||
288
docs/.vitepress/components/ConfigEditor.vue
Normal file
288
docs/.vitepress/components/ConfigEditor.vue
Normal file
@@ -0,0 +1,288 @@
|
||||
<template>
|
||||
<div class="config-generator">
|
||||
<div class="config-layout">
|
||||
<div class="config-form">
|
||||
<div class="tabs">
|
||||
<div class="tab-list">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
class="tab-button"
|
||||
:class="{ active: activeTab === tab.value }"
|
||||
@click="activeTab = tab.value"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<BasicConfig
|
||||
v-show="activeTab === 'basic'"
|
||||
:config="config"
|
||||
/>
|
||||
|
||||
<DatabaseConfig
|
||||
v-show="activeTab === 'database'"
|
||||
:config="config"
|
||||
:show-password="showPassword"
|
||||
@toggle-password="showPassword = !showPassword"
|
||||
@regenerate-password="regeneratePassword"
|
||||
/>
|
||||
|
||||
<HttpsConfig
|
||||
v-show="activeTab === 'https'"
|
||||
:config="config"
|
||||
/>
|
||||
|
||||
<StorageConfig
|
||||
v-show="activeTab === 'storage'"
|
||||
:config="config"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfigPreview
|
||||
:config="generateDockerCompose(config)"
|
||||
@copy="copyToClipboard"
|
||||
@download="downloadConfig"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import BasicConfig from './BasicConfig.vue'
|
||||
import DatabaseConfig from './DatabaseConfig.vue'
|
||||
import HttpsConfig from './HttpsConfig.vue'
|
||||
import StorageConfig from './StorageConfig.vue'
|
||||
import ConfigPreview from './ConfigPreview.vue'
|
||||
import { generateDockerCompose } from './dockerComposeGenerator'
|
||||
|
||||
const showPassword = ref(false)
|
||||
const activeTab = ref('basic')
|
||||
|
||||
const tabs = [
|
||||
{ label: 'Basic', value: 'basic' },
|
||||
{ label: 'Database', value: 'database' },
|
||||
{ label: 'HTTPS', value: 'https' },
|
||||
{ label: 'Storage', value: 'storage' }
|
||||
]
|
||||
|
||||
function generateRandomPassword(length = 16) {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+"
|
||||
let password = ""
|
||||
for (let i = 0; i < length; i++) {
|
||||
const randomIndex = Math.floor(Math.random() * charset.length)
|
||||
password += charset[randomIndex]
|
||||
}
|
||||
return password
|
||||
}
|
||||
|
||||
const config = reactive({
|
||||
image: "ghcr.io/sysadminsmedia/homebox:latest",
|
||||
rootless: false,
|
||||
port: "3100",
|
||||
logLevel: "info",
|
||||
logFormat: "text",
|
||||
maxFileUpload: "10",
|
||||
allowAnalytics: false,
|
||||
|
||||
// HTTPS options
|
||||
httpsOption: "none", // none, traefik, nginx, caddy, cloudflared
|
||||
|
||||
// Traefik config
|
||||
traefikConfig: {
|
||||
domain: "homebox.example.com",
|
||||
email: "",
|
||||
},
|
||||
|
||||
// Nginx config
|
||||
nginxConfig: {
|
||||
domain: "homebox.example.com",
|
||||
port: "443",
|
||||
sslCertPath: "/etc/nginx/ssl/cert.pem",
|
||||
sslKeyPath: "/etc/nginx/ssl/key.pem",
|
||||
},
|
||||
|
||||
// Caddy config
|
||||
caddyConfig: {
|
||||
domain: "homebox.example.com",
|
||||
email: "",
|
||||
},
|
||||
|
||||
// Cloudflared config
|
||||
cloudflaredConfig: {
|
||||
tunnel: "homebox-tunnel",
|
||||
domain: "homebox.example.com",
|
||||
token: "",
|
||||
},
|
||||
|
||||
databaseType: "sqlite",
|
||||
postgresConfig: {
|
||||
host: "postgres",
|
||||
port: "5432",
|
||||
username: "homebox",
|
||||
password: generateRandomPassword(),
|
||||
database: "homebox",
|
||||
},
|
||||
allowRegistration: true,
|
||||
autoIncrementAssetId: true,
|
||||
checkGithubRelease: true,
|
||||
|
||||
// Storage Configuration
|
||||
storageType: "local", // local, s3, gcs, azure
|
||||
storageConfig: {
|
||||
// Local storage settings
|
||||
local: {
|
||||
type: "volume", // "volume" or "directory"
|
||||
directory: "./homebox-data",
|
||||
volumeName: "homebox-data",
|
||||
path: "/data", // Custom path for local storage
|
||||
},
|
||||
|
||||
// S3 storage settings
|
||||
s3: {
|
||||
bucket: "",
|
||||
region: "",
|
||||
endpoint: "", // For S3-compatible storage
|
||||
awsAccessKeyId: "",
|
||||
awsSecretAccessKey: "",
|
||||
awsSessionToken: "", // Optional for temporary credentials
|
||||
prefixPath: "", // Storage prefix path
|
||||
awsSdk: "v2", // AWS SDK version
|
||||
disableSSL: false,
|
||||
s3ForcePathStyle: false,
|
||||
sseType: "", // Server-side encryption type
|
||||
kmsKeyId: "", // KMS key ID for encryption
|
||||
fips: false,
|
||||
dualstack: false,
|
||||
accelerate: false,
|
||||
isCompatible: false, // Whether using S3-compatible storage
|
||||
compatibleService: "", // minio, cloudflare-r2, backblaze-b2, custom
|
||||
},
|
||||
|
||||
// Google Cloud Storage settings
|
||||
gcs: {
|
||||
bucket: "",
|
||||
projectId: "",
|
||||
credentialsPath: "/app/gcs-credentials.json", // Path to service account key
|
||||
prefixPath: "", // Storage prefix path
|
||||
},
|
||||
|
||||
// Azure Blob Storage settings
|
||||
azure: {
|
||||
container: "",
|
||||
storageAccount: "",
|
||||
storageKey: "",
|
||||
sasToken: "", // Optional SAS token
|
||||
useEmulator: false,
|
||||
emulatorEndpoint: "localhost:10001", // For local emulator
|
||||
prefixPath: "", // Storage prefix path
|
||||
},
|
||||
|
||||
// Container storage volumes (for non-local storage types)
|
||||
containerStorage: {
|
||||
postgresStorage: {
|
||||
type: "volume",
|
||||
directory: "./postgres-data",
|
||||
volumeName: "postgres-data",
|
||||
},
|
||||
traefikStorage: {
|
||||
type: "volume",
|
||||
directory: "./traefik-data",
|
||||
volumeName: "traefik-data",
|
||||
},
|
||||
nginxStorage: {
|
||||
type: "volume",
|
||||
directory: "./nginx-data",
|
||||
volumeName: "nginx-data",
|
||||
},
|
||||
caddyStorage: {
|
||||
type: "volume",
|
||||
directory: "./caddy-data",
|
||||
volumeName: "caddy-data",
|
||||
},
|
||||
cloudflaredStorage: {
|
||||
type: "volume",
|
||||
directory: "./cloudflared-data",
|
||||
volumeName: "cloudflared-data",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
function regeneratePassword() {
|
||||
config.postgresConfig.password = generateRandomPassword()
|
||||
alert('A new random password has been generated for the database.')
|
||||
}
|
||||
|
||||
function copyToClipboard() {
|
||||
navigator.clipboard.writeText(generateDockerCompose(config))
|
||||
alert('Docker Compose configuration has been copied to your clipboard.')
|
||||
}
|
||||
|
||||
function downloadConfig() {
|
||||
const element = document.createElement("a")
|
||||
const file = new Blob([generateDockerCompose(config)], { type: "text/plain" })
|
||||
element.href = URL.createObjectURL(file)
|
||||
element.download = "docker-compose.yml"
|
||||
document.body.appendChild(element)
|
||||
element.click()
|
||||
document.body.removeChild(element)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.config-generator {
|
||||
font-family: var(--vp-font-family-base);
|
||||
color: var(--vp-c-text-1);
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2rem;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.config-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tab-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
padding: 0.5rem;
|
||||
background-color: var(--vp-c-bg-mute);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
background-color: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.tab-button:hover:not(.active) {
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
}
|
||||
</style>
|
||||
81
docs/.vitepress/components/ConfigPreview.vue
Normal file
81
docs/.vitepress/components/ConfigPreview.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="config-preview">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title-with-actions">
|
||||
<h2 class="card-title">Docker Compose Configuration</h2>
|
||||
<div class="card-actions">
|
||||
<button class="icon-button" @click="$emit('copy')" title="Copy to clipboard">
|
||||
Copy
|
||||
</button>
|
||||
<button class="icon-button" @click="$emit('download')" title="Download as file">
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="card-description">This configuration will be saved as docker-compose.yml</p>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<textarea
|
||||
class="code-preview"
|
||||
readonly
|
||||
:value="config"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
config: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
})
|
||||
|
||||
defineEmits(['copy', 'download'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import './common.css';
|
||||
|
||||
.config-preview {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-title-with-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.code-preview {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
font-family: monospace;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
background-color: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
resize: none;
|
||||
white-space: pre;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
112
docs/.vitepress/components/DatabaseConfig.vue
Normal file
112
docs/.vitepress/components/DatabaseConfig.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="tab-content">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Database Configuration</h2>
|
||||
<p class="card-description">Configure the database for your Homebox instance.</p>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="form-group">
|
||||
<label for="databaseType">Database Type</label>
|
||||
<select id="databaseType" v-model="config.databaseType">
|
||||
<option value="sqlite">SQLite (Default)</option>
|
||||
<option value="postgres">PostgreSQL</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="config.databaseType === 'postgres'" class="nested-form">
|
||||
<div class="form-group">
|
||||
<label for="postgresHost">PostgreSQL Host</label>
|
||||
<input
|
||||
type="text"
|
||||
id="postgresHost"
|
||||
v-model="config.postgresConfig.host"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="postgresPort">PostgreSQL Port</label>
|
||||
<input
|
||||
type="text"
|
||||
id="postgresPort"
|
||||
v-model="config.postgresConfig.port"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="postgresUsername">PostgreSQL Username</label>
|
||||
<input
|
||||
type="text"
|
||||
id="postgresUsername"
|
||||
v-model="config.postgresConfig.username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="postgresPassword">PostgreSQL Password</label>
|
||||
<div class="password-input">
|
||||
<input
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
id="postgresPassword"
|
||||
v-model="config.postgresConfig.password"
|
||||
/>
|
||||
<button
|
||||
class="icon-button"
|
||||
@click="$emit('togglePassword')"
|
||||
type="button"
|
||||
>
|
||||
<span v-if="showPassword">Hide</span>
|
||||
<span v-else>Show</span>
|
||||
</button>
|
||||
<button
|
||||
class="icon-button"
|
||||
@click="$emit('regeneratePassword')"
|
||||
type="button"
|
||||
title="Generate new random password"
|
||||
>
|
||||
Regenerate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="postgresDatabase">PostgreSQL Database</label>
|
||||
<input
|
||||
type="text"
|
||||
id="postgresDatabase"
|
||||
v-model="config.postgresConfig.database"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
config: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
showPassword: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
defineEmits(['togglePassword', 'regeneratePassword'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import './common.css';
|
||||
|
||||
.password-input {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.password-input input {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
179
docs/.vitepress/components/HttpsConfig.vue
Normal file
179
docs/.vitepress/components/HttpsConfig.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<div class="tab-content">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">HTTPS Configuration</h2>
|
||||
<p class="card-description">Configure HTTPS for your Homebox instance.</p>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="form-group">
|
||||
<label for="httpsOption">HTTPS Option</label>
|
||||
<select id="httpsOption" v-model="config.httpsOption">
|
||||
<option value="none">None (HTTP only)</option>
|
||||
<option value="traefik">Traefik (Automatic HTTPS with Let's Encrypt)</option>
|
||||
<option value="nginx">Nginx (Custom SSL certificates)</option>
|
||||
<option value="caddy">Caddy (Automatic HTTPS with Let's Encrypt)</option>
|
||||
<option value="cloudflared">Cloudflare Tunnel</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Traefik Configuration -->
|
||||
<div v-if="config.httpsOption === 'traefik'" class="nested-form">
|
||||
<h3>Traefik Configuration</h3>
|
||||
<p class="help-text">Traefik automatically handles HTTPS certificates via Let's Encrypt</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="traefikDomain">Domain Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="traefikDomain"
|
||||
v-model="config.traefikConfig.domain"
|
||||
placeholder="homebox.example.com"
|
||||
/>
|
||||
<p class="help-text">The domain name must be pointed to your server's IP address</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="traefikEmail">Email Address (for Let's Encrypt)</label>
|
||||
<input
|
||||
type="email"
|
||||
id="traefikEmail"
|
||||
v-model="config.traefikConfig.email"
|
||||
placeholder="your-email@example.com"
|
||||
/>
|
||||
<p class="help-text">Required for Let's Encrypt certificate notifications</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nginx Configuration -->
|
||||
<div v-if="config.httpsOption === 'nginx'" class="nested-form">
|
||||
<h3>Nginx Configuration</h3>
|
||||
<p class="help-text">Nginx requires you to provide SSL certificates</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="nginxDomain">Domain Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="nginxDomain"
|
||||
v-model="config.nginxConfig.domain"
|
||||
placeholder="homebox.example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="nginxPort">HTTPS Port</label>
|
||||
<input
|
||||
type="text"
|
||||
id="nginxPort"
|
||||
v-model="config.nginxConfig.port"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="nginxSslCert">SSL Certificate Path</label>
|
||||
<input
|
||||
type="text"
|
||||
id="nginxSslCert"
|
||||
v-model="config.nginxConfig.sslCertPath"
|
||||
/>
|
||||
<p class="help-text">Path to SSL certificate file inside the Nginx container</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="nginxSslKey">SSL Key Path</label>
|
||||
<input
|
||||
type="text"
|
||||
id="nginxSslKey"
|
||||
v-model="config.nginxConfig.sslKeyPath"
|
||||
/>
|
||||
<p class="help-text">Path to SSL key file inside the Nginx container</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Caddy Configuration -->
|
||||
<div v-if="config.httpsOption === 'caddy'" class="nested-form">
|
||||
<h3>Caddy Configuration</h3>
|
||||
<p class="help-text">Caddy automatically handles HTTPS certificates via Let's Encrypt</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="caddyDomain">Domain Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="caddyDomain"
|
||||
v-model="config.caddyConfig.domain"
|
||||
placeholder="homebox.example.com"
|
||||
/>
|
||||
<p class="help-text">The domain name must be pointed to your server's IP address</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="caddyEmail">Email Address (for Let's Encrypt)</label>
|
||||
<input
|
||||
type="email"
|
||||
id="caddyEmail"
|
||||
v-model="config.caddyConfig.email"
|
||||
placeholder="your-email@example.com"
|
||||
/>
|
||||
<p class="help-text">Optional: Used for Let's Encrypt certificate notifications</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cloudflared Configuration -->
|
||||
<div v-if="config.httpsOption === 'cloudflared'" class="nested-form">
|
||||
<h3>Cloudflare Tunnel Configuration</h3>
|
||||
<p class="help-text">Cloudflare Tunnel provides secure access without exposing ports</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="cloudflaredTunnel">Tunnel Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="cloudflaredTunnel"
|
||||
v-model="config.cloudflaredConfig.tunnel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="cloudflaredDomain">Domain Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="cloudflaredDomain"
|
||||
v-model="config.cloudflaredConfig.domain"
|
||||
placeholder="homebox.example.com"
|
||||
/>
|
||||
<p class="help-text">The domain must be managed by Cloudflare</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="cloudflaredToken">Tunnel Token</label>
|
||||
<input
|
||||
type="password"
|
||||
id="cloudflaredToken"
|
||||
v-model="config.cloudflaredConfig.token"
|
||||
placeholder="Your Cloudflare Tunnel token"
|
||||
/>
|
||||
<p class="help-text">Create a tunnel in the Cloudflare Zero Trust dashboard to get a token</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
config: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import './common.css';
|
||||
|
||||
h3 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
</style>
|
||||
552
docs/.vitepress/components/StorageConfig.vue
Normal file
552
docs/.vitepress/components/StorageConfig.vue
Normal file
@@ -0,0 +1,552 @@
|
||||
<template>
|
||||
<div class="storage-config">
|
||||
<h3>Storage Configuration</h3>
|
||||
|
||||
<!-- Storage Type Selector -->
|
||||
<div class="form-group">
|
||||
<label for="storageType">Storage Type</label>
|
||||
<select id="storageType" v-model="config.storageType" class="form-input">
|
||||
<option value="local">Local Storage</option>
|
||||
<option value="s3">Amazon S3 / S3-Compatible</option>
|
||||
<option value="gcs">Google Cloud Storage</option>
|
||||
<option value="azure">Azure Blob Storage</option>
|
||||
</select>
|
||||
<p class="form-help">Choose where Homebox will store your data</p>
|
||||
</div>
|
||||
|
||||
<!-- Local Storage Configuration -->
|
||||
<div v-if="config.storageType === 'local'" class="storage-section">
|
||||
<h4>Local Storage Settings</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="localType">Storage Type</label>
|
||||
<select id="localType" v-model="config.storageConfig.local.type" class="form-input">
|
||||
<option value="volume">Docker Volume</option>
|
||||
<option value="directory">Host Directory</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="config.storageConfig.local.type === 'directory'" class="form-group">
|
||||
<label for="localDirectory">Host Directory Path</label>
|
||||
<input
|
||||
id="localDirectory"
|
||||
v-model="config.storageConfig.local.directory"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="./homebox-data"
|
||||
/>
|
||||
<p class="form-help">Path on the host system where data will be stored</p>
|
||||
</div>
|
||||
|
||||
<div v-if="config.storageConfig.local.type === 'volume'" class="form-group">
|
||||
<label for="localVolume">Volume Name</label>
|
||||
<input
|
||||
id="localVolume"
|
||||
v-model="config.storageConfig.local.volumeName"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="homebox-data"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="localPath">Custom Storage Path (Optional)</label>
|
||||
<input
|
||||
id="localPath"
|
||||
v-model="config.storageConfig.local.path"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="/data"
|
||||
/>
|
||||
<p class="form-help">Custom path inside the container. Leave as /data for default.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- S3 Storage Configuration -->
|
||||
<div v-if="config.storageType === 's3'" class="storage-section">
|
||||
<h4>S3 Storage Settings</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="config.storageConfig.s3.isCompatible"
|
||||
class="form-checkbox"
|
||||
/>
|
||||
Use S3-Compatible Storage (MinIO, Cloudflare R2, Backblaze B2, etc.)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="config.storageConfig.s3.isCompatible" class="form-group">
|
||||
<label for="s3Service">S3-Compatible Service</label>
|
||||
<select id="s3Service" v-model="config.storageConfig.s3.compatibleService" class="form-input">
|
||||
<option value="">Custom/Other</option>
|
||||
<option value="minio">MinIO</option>
|
||||
<option value="cloudflare-r2">Cloudflare R2</option>
|
||||
<option value="backblaze-b2">Backblaze B2</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="s3Bucket">Bucket Name</label>
|
||||
<input
|
||||
id="s3Bucket"
|
||||
v-model="config.storageConfig.s3.bucket"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="my-homebox-bucket"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="!config.storageConfig.s3.isCompatible" class="form-group">
|
||||
<label for="s3Region">AWS Region</label>
|
||||
<input
|
||||
id="s3Region"
|
||||
v-model="config.storageConfig.s3.region"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="us-east-1"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="config.storageConfig.s3.isCompatible" class="form-group">
|
||||
<label for="s3Endpoint">Endpoint URL</label>
|
||||
<input
|
||||
id="s3Endpoint"
|
||||
v-model="config.storageConfig.s3.endpoint"
|
||||
type="text"
|
||||
class="form-input"
|
||||
:placeholder="getS3EndpointPlaceholder()"
|
||||
/>
|
||||
<p class="form-help">The endpoint URL for your S3-compatible service</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="s3AccessKey">AWS Access Key ID</label>
|
||||
<input
|
||||
id="s3AccessKey"
|
||||
v-model="config.storageConfig.s3.awsAccessKeyId"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="AKIAIOSFODNN7EXAMPLE"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="s3SecretKey">AWS Secret Access Key</label>
|
||||
<input
|
||||
id="s3SecretKey"
|
||||
v-model="config.storageConfig.s3.awsSecretAccessKey"
|
||||
type="password"
|
||||
class="form-input"
|
||||
placeholder="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="s3SessionToken">AWS Session Token (Optional)</label>
|
||||
<input
|
||||
id="s3SessionToken"
|
||||
v-model="config.storageConfig.s3.awsSessionToken"
|
||||
type="password"
|
||||
class="form-input"
|
||||
placeholder="For temporary credentials"
|
||||
/>
|
||||
<p class="form-help">Only needed for temporary AWS credentials</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="s3PrefixPath">Storage Prefix Path (Optional)</label>
|
||||
<input
|
||||
id="s3PrefixPath"
|
||||
v-model="config.storageConfig.s3.prefixPath"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="homebox/"
|
||||
/>
|
||||
<p class="form-help">Prefix for all stored objects in the bucket</p>
|
||||
</div>
|
||||
|
||||
<!-- Advanced S3 Settings -->
|
||||
<details class="advanced-settings">
|
||||
<summary>Advanced S3 Settings</summary>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="s3AwsSdk">AWS SDK Version</label>
|
||||
<select id="s3AwsSdk" v-model="config.storageConfig.s3.awsSdk" class="form-input">
|
||||
<option value="v2">v2 (Recommended)</option>
|
||||
<option value="v1">v1</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="config.storageConfig.s3.disableSSL"
|
||||
class="form-checkbox"
|
||||
/>
|
||||
Disable SSL
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="config.storageConfig.s3.s3ForcePathStyle"
|
||||
class="form-checkbox"
|
||||
/>
|
||||
Force Path Style Access
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="s3SseType">Server-Side Encryption</label>
|
||||
<select id="s3SseType" v-model="config.storageConfig.s3.sseType" class="form-input">
|
||||
<option value="">None</option>
|
||||
<option value="AES256">AES256</option>
|
||||
<option value="aws:kms">AWS KMS</option>
|
||||
<option value="aws:kms:dsse">AWS KMS DSSE</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="config.storageConfig.s3.sseType.includes('kms')" class="form-group">
|
||||
<label for="s3KmsKey">KMS Key ID</label>
|
||||
<input
|
||||
id="s3KmsKey"
|
||||
v-model="config.storageConfig.s3.kmsKeyId"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="config.storageConfig.s3.fips"
|
||||
class="form-checkbox"
|
||||
/>
|
||||
Use FIPS Endpoints
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="config.storageConfig.s3.dualstack"
|
||||
class="form-checkbox"
|
||||
/>
|
||||
Use Dual-Stack Endpoints
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="config.storageConfig.s3.accelerate"
|
||||
class="form-checkbox"
|
||||
/>
|
||||
Use S3 Transfer Acceleration
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- Google Cloud Storage Configuration -->
|
||||
<div v-if="config.storageType === 'gcs'" class="storage-section">
|
||||
<h4>Google Cloud Storage Settings</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="gcsBucket">Bucket Name</label>
|
||||
<input
|
||||
id="gcsBucket"
|
||||
v-model="config.storageConfig.gcs.bucket"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="my-homebox-bucket"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="gcsProject">Project ID</label>
|
||||
<input
|
||||
id="gcsProject"
|
||||
v-model="config.storageConfig.gcs.projectId"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="my-gcp-project"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="gcsCredentialsPath">Service Account Key Path</label>
|
||||
<input
|
||||
id="gcsCredentialsPath"
|
||||
v-model="config.storageConfig.gcs.credentialsPath"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="/app/gcs-credentials.json"
|
||||
/>
|
||||
<p class="form-help">Path to the service account JSON key file inside the container</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="gcsPrefixPath">Storage Prefix Path (Optional)</label>
|
||||
<input
|
||||
id="gcsPrefixPath"
|
||||
v-model="config.storageConfig.gcs.prefixPath"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="homebox/"
|
||||
/>
|
||||
<p class="form-help">Prefix for all stored objects in the bucket</p>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<h5>📋 Setup Instructions:</h5>
|
||||
<ol>
|
||||
<li>Create a service account in your GCP project</li>
|
||||
<li>Grant Storage Admin permissions to the service account</li>
|
||||
<li>Download the JSON key file</li>
|
||||
<li>Mount the key file as a read-only volume in your container</li>
|
||||
<li>Set GOOGLE_APPLICATION_CREDENTIALS environment variable</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Azure Blob Storage Configuration -->
|
||||
<div v-if="config.storageType === 'azure'" class="storage-section">
|
||||
<h4>Azure Blob Storage Settings</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="config.storageConfig.azure.useEmulator"
|
||||
class="form-checkbox"
|
||||
/>
|
||||
Use Azure Storage Emulator (for development)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="azureContainer">Container Name</label>
|
||||
<input
|
||||
id="azureContainer"
|
||||
v-model="config.storageConfig.azure.container"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="homebox-container"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="!config.storageConfig.azure.useEmulator" class="form-group">
|
||||
<label for="azureAccount">Storage Account Name</label>
|
||||
<input
|
||||
id="azureAccount"
|
||||
v-model="config.storageConfig.azure.storageAccount"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="mystorageaccount"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="!config.storageConfig.azure.useEmulator" class="form-group">
|
||||
<label for="azureKey">Storage Account Key</label>
|
||||
<input
|
||||
id="azureKey"
|
||||
v-model="config.storageConfig.azure.storageKey"
|
||||
type="password"
|
||||
class="form-input"
|
||||
placeholder="Your Azure storage account key"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="!config.storageConfig.azure.useEmulator" class="form-group">
|
||||
<label for="azureSas">SAS Token (Optional)</label>
|
||||
<input
|
||||
id="azureSas"
|
||||
v-model="config.storageConfig.azure.sasToken"
|
||||
type="password"
|
||||
class="form-input"
|
||||
placeholder="?sv=2021-06-08&ss=b&srt=sco&sp=rwdlacupx&se=..."
|
||||
/>
|
||||
<p class="form-help">Use SAS token instead of storage account key</p>
|
||||
</div>
|
||||
|
||||
<div v-if="config.storageConfig.azure.useEmulator" class="form-group">
|
||||
<label for="azureEmulatorEndpoint">Emulator Endpoint</label>
|
||||
<input
|
||||
id="azureEmulatorEndpoint"
|
||||
v-model="config.storageConfig.azure.emulatorEndpoint"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="localhost:10001"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="azurePrefixPath">Storage Prefix Path (Optional)</label>
|
||||
<input
|
||||
id="azurePrefixPath"
|
||||
v-model="config.storageConfig.azure.prefixPath"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="homebox/"
|
||||
/>
|
||||
<p class="form-help">Prefix for all stored objects in the container</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
function getS3EndpointPlaceholder() {
|
||||
const service = props.config.storageConfig.s3.compatibleService
|
||||
switch (service) {
|
||||
case 'minio':
|
||||
return 'http://minio:9000'
|
||||
case 'cloudflare-r2':
|
||||
return 'https://<account-id>.r2.cloudflarestorage.com'
|
||||
case 'backblaze-b2':
|
||||
return 'https://s3.us-west-004.backblazeb2.com'
|
||||
default:
|
||||
return 'https://your-s3-compatible-endpoint.com'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.storage-config {
|
||||
padding: 1.5rem;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.storage-section {
|
||||
margin-top: 1.5rem;
|
||||
padding: 1rem;
|
||||
background-color: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
background-color: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--vp-c-brand);
|
||||
box-shadow: 0 0 0 2px var(--vp-c-brand-light);
|
||||
}
|
||||
|
||||
.form-checkbox {
|
||||
width: auto;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.form-help {
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.advanced-settings {
|
||||
margin-top: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.advanced-settings summary {
|
||||
padding: 0.75rem;
|
||||
background-color: var(--vp-c-bg-mute);
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.advanced-settings[open] summary {
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.advanced-settings .form-group {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
border-left: 4px solid var(--vp-c-brand);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.info-box h5 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.info-box ol {
|
||||
margin: 0;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.info-box li {
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 1.5rem 0;
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
95
docs/.vitepress/components/StorageTypeSelector.vue
Normal file
95
docs/.vitepress/components/StorageTypeSelector.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="storage-selector">
|
||||
<h3>{{ label }}</h3>
|
||||
<p class="help-text">{{ description }}</p>
|
||||
|
||||
<div class="radio-group">
|
||||
<div class="radio-option">
|
||||
<input
|
||||
type="radio"
|
||||
:id="`${storageKey}-volume`"
|
||||
value="volume"
|
||||
v-model="config.storageConfig[storageKey].type"
|
||||
/>
|
||||
<label :for="`${storageKey}-volume`">Docker Volume</label>
|
||||
</div>
|
||||
<div class="radio-option">
|
||||
<input
|
||||
type="radio"
|
||||
:id="`${storageKey}-directory`"
|
||||
value="directory"
|
||||
v-model="config.storageConfig[storageKey].type"
|
||||
/>
|
||||
<label :for="`${storageKey}-directory`">Host Directory</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="config.storageConfig[storageKey].type === 'volume'" class="form-group">
|
||||
<label :for="`${storageKey}-volume-name`">Volume Name</label>
|
||||
<input
|
||||
type="text"
|
||||
:id="`${storageKey}-volume-name`"
|
||||
v-model="config.storageConfig[storageKey].volumeName"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="form-group">
|
||||
<label :for="`${storageKey}-directory-path`">Directory Path</label>
|
||||
<input
|
||||
type="text"
|
||||
:id="`${storageKey}-directory-path`"
|
||||
v-model="config.storageConfig[storageKey].directory"
|
||||
/>
|
||||
<p class="help-text">Absolute path recommended (e.g., /home/user/data)</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
storageKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
config: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import './common.css';
|
||||
|
||||
.storage-selector {
|
||||
margin-bottom: 2rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.storage-selector h3 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.radio-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
150
docs/.vitepress/components/common.css
Normal file
150
docs/.vitepress/components/common.css
Normal file
@@ -0,0 +1,150 @@
|
||||
.card {
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.5rem;
|
||||
|
||||
border-top: 0px;
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px soli var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
background-color: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--vp-c-brand);
|
||||
box-shadow: 0 0 0 2px rgba(var(--vp-c-brand-rgb), 0.1);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.toggle-switch label {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--vp-c-divider);
|
||||
transition: .4s;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.toggle-switch label:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.toggle-switch input:checked + label {
|
||||
background-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.toggle-switch input:checked + label:before {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.help-text {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.separator {
|
||||
height: 1px;
|
||||
background-color: var(--vp-c-divider);
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.nested-form {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
padding: 0.5rem;
|
||||
background-color: var(--vp-c-bg-mute);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
}
|
||||
440
docs/.vitepress/components/dockerComposeGenerator.ts
Normal file
440
docs/.vitepress/components/dockerComposeGenerator.ts
Normal file
@@ -0,0 +1,440 @@
|
||||
export function generateDockerCompose(config: any): string {
|
||||
const services: any = {}
|
||||
const volumes: any = {}
|
||||
const networks: any = {
|
||||
homebox: {
|
||||
driver: 'bridge'
|
||||
}
|
||||
}
|
||||
|
||||
// Generate Homebox service
|
||||
services.homebox = generateHomeboxService(config)
|
||||
|
||||
// Add database service if PostgreSQL is selected
|
||||
if (config.databaseType === 'postgres') {
|
||||
services.postgres = generatePostgresService(config)
|
||||
if (config.storageConfig.containerStorage.postgresStorage.type === 'volume') {
|
||||
volumes[config.storageConfig.containerStorage.postgresStorage.volumeName] = null
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure homebox-data volume exists if SQLite is selected
|
||||
if (config.databaseType === 'sqlite') {
|
||||
volumes['homebox-data'] = null
|
||||
}
|
||||
|
||||
// Add reverse proxy services based on HTTPS option
|
||||
switch (config.httpsOption) {
|
||||
case 'traefik':
|
||||
services.traefik = generateTraefikService(config)
|
||||
if (config.storageConfig.containerStorage.traefikStorage.type === 'volume') {
|
||||
volumes[config.storageConfig.containerStorage.traefikStorage.volumeName] = null
|
||||
}
|
||||
break
|
||||
case 'nginx':
|
||||
services.nginx = generateNginxService(config)
|
||||
if (config.storageConfig.containerStorage.nginxStorage.type === 'volume') {
|
||||
volumes[config.storageConfig.containerStorage.nginxStorage.volumeName] = null
|
||||
}
|
||||
break
|
||||
case 'caddy':
|
||||
services.caddy = generateCaddyService(config)
|
||||
if (config.storageConfig.containerStorage.caddyStorage.type === 'volume') {
|
||||
volumes[config.storageConfig.containerStorage.caddyStorage.volumeName] = null
|
||||
}
|
||||
break
|
||||
case 'cloudflared':
|
||||
services.cloudflared = generateCloudflaredService(config)
|
||||
if (config.storageConfig.containerStorage.cloudflaredStorage.type === 'volume') {
|
||||
volumes[config.storageConfig.containerStorage.cloudflaredStorage.volumeName] = null
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Add Homebox storage volume only for local storage
|
||||
if (config.storageType === 'local' && config.storageConfig.local.type === 'volume') {
|
||||
volumes[config.storageConfig.local.volumeName] = null
|
||||
}
|
||||
|
||||
const compose = {
|
||||
version: '3.8',
|
||||
services,
|
||||
...(Object.keys(volumes).length > 0 && {volumes}),
|
||||
networks
|
||||
}
|
||||
|
||||
return `# Generated Homebox Docker Compose Config Generator 1.0 Beta
|
||||
# Storage Type: ${config.storageType.toUpperCase()}
|
||||
# Generated on: ${new Date().toISOString()}
|
||||
${yaml.stringify(compose)}`
|
||||
}
|
||||
|
||||
function generateHomeboxService(config: any): any {
|
||||
const service: any = {
|
||||
image: config.rootless ? config.image.replace(':latest', ':latest-rootless') : config.image,
|
||||
container_name: 'homebox',
|
||||
restart: 'unless-stopped',
|
||||
environment: generateEnvironmentVariables(config),
|
||||
networks: ['homebox']
|
||||
}
|
||||
|
||||
// Add ports for direct access (when no reverse proxy is used)
|
||||
if (config.httpsOption === 'none') {
|
||||
service.ports = [`${config.port}:7745`]
|
||||
}
|
||||
|
||||
// Configure storage based on storage type
|
||||
if (config.storageType === 'local') {
|
||||
service.volumes = generateLocalStorageVolumes(config)
|
||||
} else {
|
||||
// For cloud storage, we might still need some local volumes for certain files
|
||||
service.volumes = generateCloudStorageVolumes(config)
|
||||
}
|
||||
|
||||
// Always mount homebox-data at /data if SQLite is used
|
||||
if (config.databaseType === 'sqlite') {
|
||||
if (!service.volumes) service.volumes = []
|
||||
// Only add if not already present
|
||||
if (!service.volumes.some(v => v.startsWith('homebox-data:'))) {
|
||||
service.volumes.push('homebox-data:/data')
|
||||
}
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
function generateEnvironmentVariables(config: any): string[] {
|
||||
const env: string[] = [
|
||||
`HBOX_LOG_LEVEL=${config.logLevel}`,
|
||||
`HBOX_LOG_FORMAT=${config.logFormat}`,
|
||||
`HBOX_MAX_UPLOAD_SIZE=${config.maxFileUpload}`,
|
||||
`HBOX_AUTO_INCREMENT_ASSET_ID=${config.autoIncrementAssetId}`,
|
||||
`HBOX_WEB_PORT=7745`
|
||||
]
|
||||
|
||||
// Database configuration
|
||||
if (config.databaseType === 'postgres') {
|
||||
env.push(
|
||||
`HBOX_DATABASE_DRIVER=postgres`,
|
||||
`HBOX_DATABASE_HOST=${config.postgresConfig.host}`,
|
||||
`HBOX_DATABASE_PORT=${config.postgresConfig.port}`,
|
||||
`HBOX_DATABASE_NAME=${config.postgresConfig.database}`,
|
||||
`HBOX_DATABASE_USER=${config.postgresConfig.username}`,
|
||||
`HBOX_DATABASE_PASS=${config.postgresConfig.password}`
|
||||
)
|
||||
}
|
||||
|
||||
// Registration settings
|
||||
if (!config.allowRegistration) {
|
||||
env.push('HBOX_OPTIONS_ALLOW_REGISTRATION=false')
|
||||
}
|
||||
|
||||
// Analytics settings
|
||||
if (!config.allowAnalytics) {
|
||||
env.push('HBOX_OPTIONS_ALLOW_ANALYTICS=false')
|
||||
}
|
||||
|
||||
// GitHub release check
|
||||
if (!config.checkGithubRelease) {
|
||||
env.push('HBOX_OPTIONS_CHECK_GITHUB_RELEASE=false')
|
||||
}
|
||||
|
||||
// Storage configuration
|
||||
env.push(...generateStorageEnvironmentVariables(config))
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
function generateStorageEnvironmentVariables(config: any): string[] {
|
||||
const env: string[] = []
|
||||
|
||||
switch (config.storageType) {
|
||||
case 'local':
|
||||
const storagePath = config.storageConfig.local.path || '/data'
|
||||
env.push(`HBOX_STORAGE_CONN_STRING=file://${storagePath}`)
|
||||
if (config.storageConfig.local.prefixPath) {
|
||||
env.push(`HBOX_STORAGE_PREFIX_PATH=${config.storageConfig.local.prefixPath}`)
|
||||
}
|
||||
break
|
||||
|
||||
case 's3':
|
||||
const s3Config = config.storageConfig.s3
|
||||
let connectionString = `s3://${s3Config.bucket}?awssdk=${s3Config.awsSdk}`
|
||||
|
||||
if (s3Config.region && !s3Config.isCompatible) {
|
||||
connectionString += `®ion=${s3Config.region}`
|
||||
}
|
||||
|
||||
if (s3Config.endpoint) {
|
||||
connectionString += `&endpoint=${s3Config.endpoint}`
|
||||
}
|
||||
|
||||
if (s3Config.disableSSL) {
|
||||
connectionString += '&disableSSL=true'
|
||||
}
|
||||
|
||||
if (s3Config.s3ForcePathStyle) {
|
||||
connectionString += '&s3ForcePathStyle=true'
|
||||
}
|
||||
|
||||
if (s3Config.sseType) {
|
||||
connectionString += `&sseType=${s3Config.sseType}`
|
||||
}
|
||||
|
||||
if (s3Config.kmsKeyId) {
|
||||
connectionString += `&kmskeyid=${s3Config.kmsKeyId}`
|
||||
}
|
||||
|
||||
if (s3Config.fips) {
|
||||
connectionString += '&fips=true'
|
||||
}
|
||||
|
||||
if (s3Config.dualstack) {
|
||||
connectionString += '&dualstack=true'
|
||||
}
|
||||
|
||||
if (s3Config.accelerate) {
|
||||
connectionString += '&accelerate=true'
|
||||
}
|
||||
|
||||
env.push(`HBOX_STORAGE_CONN_STRING=${connectionString}`)
|
||||
|
||||
if (s3Config.prefixPath) {
|
||||
env.push(`HBOX_STORAGE_PREFIX_PATH=${s3Config.prefixPath}`)
|
||||
}
|
||||
|
||||
// AWS credentials
|
||||
env.push(`AWS_ACCESS_KEY_ID=${s3Config.awsAccessKeyId}`)
|
||||
env.push(`AWS_SECRET_ACCESS_KEY=${s3Config.awsSecretAccessKey}`)
|
||||
|
||||
if (s3Config.awsSessionToken) {
|
||||
env.push(`AWS_SESSION_TOKEN=${s3Config.awsSessionToken}`)
|
||||
}
|
||||
break
|
||||
|
||||
case 'gcs':
|
||||
const gcsConfig = config.storageConfig.gcs
|
||||
env.push(`HBOX_STORAGE_CONN_STRING=gcs://${gcsConfig.bucket}`)
|
||||
|
||||
if (gcsConfig.prefixPath) {
|
||||
env.push(`HBOX_STORAGE_PREFIX_PATH=${gcsConfig.prefixPath}`)
|
||||
}
|
||||
|
||||
env.push(`GOOGLE_APPLICATION_CREDENTIALS=${gcsConfig.credentialsPath}`)
|
||||
break
|
||||
|
||||
case 'azure':
|
||||
const azureConfig = config.storageConfig.azure
|
||||
let azureConnectionString = `azblob://${azureConfig.container}`
|
||||
|
||||
if (azureConfig.useEmulator) {
|
||||
azureConnectionString += `?protocol=http&domain=${azureConfig.emulatorEndpoint}`
|
||||
}
|
||||
|
||||
env.push(`HBOX_STORAGE_CONN_STRING=${azureConnectionString}`)
|
||||
|
||||
if (azureConfig.prefixPath) {
|
||||
env.push(`HBOX_STORAGE_PREFIX_PATH=${azureConfig.prefixPath}`)
|
||||
}
|
||||
|
||||
if (!azureConfig.useEmulator) {
|
||||
env.push(`AZURE_STORAGE_ACCOUNT=${azureConfig.storageAccount}`)
|
||||
|
||||
if (azureConfig.sasToken) {
|
||||
env.push(`AZURE_STORAGE_SAS_TOKEN=${azureConfig.sasToken}`)
|
||||
} else {
|
||||
env.push(`AZURE_STORAGE_KEY=${azureConfig.storageKey}`)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
function generateLocalStorageVolumes(config: any): string[] {
|
||||
const volumes: string[] = []
|
||||
|
||||
if (config.storageConfig.local.type === 'volume') {
|
||||
const mountPath = config.storageConfig.local.path || '/data'
|
||||
volumes.push(`${config.storageConfig.local.volumeName}:${mountPath}`)
|
||||
} else {
|
||||
const mountPath = config.storageConfig.local.path || '/data'
|
||||
volumes.push(`${config.storageConfig.local.directory}:${mountPath}`)
|
||||
}
|
||||
|
||||
return volumes
|
||||
}
|
||||
|
||||
function generateCloudStorageVolumes(config: any): string[] {
|
||||
const volumes: string[] = []
|
||||
|
||||
// For cloud storage, we might still need local volumes for certain files like GCS credentials
|
||||
if (config.storageType === 'gcs') {
|
||||
volumes.push('/path/to/gcs-credentials.json:/app/gcs-credentials.json:ro')
|
||||
}
|
||||
|
||||
return volumes
|
||||
}
|
||||
|
||||
function generatePostgresService(config: any): any {
|
||||
const service: any = {
|
||||
image: 'postgres:17-alpine',
|
||||
container_name: 'homebox_postgres',
|
||||
restart: 'unless-stopped',
|
||||
environment: [
|
||||
`POSTGRES_USER=${config.postgresConfig.username}`,
|
||||
`POSTGRES_PASSWORD=${config.postgresConfig.password}`,
|
||||
`POSTGRES_DB=${config.postgresConfig.database}`
|
||||
],
|
||||
networks: ['homebox']
|
||||
}
|
||||
|
||||
if (config.storageConfig.containerStorage.postgresStorage.type === 'volume') {
|
||||
service.volumes = [`${config.storageConfig.containerStorage.postgresStorage.volumeName}:/var/lib/postgresql/data`]
|
||||
} else {
|
||||
service.volumes = [`${config.storageConfig.containerStorage.postgresStorage.directory}:/var/lib/postgresql/data`]
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
function generateTraefikService(config: any): any {
|
||||
const service: any = {
|
||||
image: 'traefik:v3.0',
|
||||
container_name: 'traefik',
|
||||
restart: 'unless-stopped',
|
||||
command: [
|
||||
'--api.dashboard=true',
|
||||
'--providers.docker=true',
|
||||
'--providers.docker.exposedbydefault=false',
|
||||
'--entrypoints.web.address=:80',
|
||||
'--entrypoints.websecure.address=:443',
|
||||
'--certificatesresolvers.letsencrypt.acme.tlschallenge=true',
|
||||
`--certificatesresolvers.letsencrypt.acme.email=${config.traefikConfig.email}`,
|
||||
'--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json'
|
||||
],
|
||||
ports: ['80:80', '443:443'],
|
||||
networks: ['homebox'],
|
||||
labels: [
|
||||
'traefik.enable=true',
|
||||
'traefik.http.routers.traefik.rule=Host(`traefik.${config.traefikConfig.domain}`)',
|
||||
'traefik.http.routers.traefik.entrypoints=websecure',
|
||||
'traefik.http.routers.traefik.tls.certresolver=letsencrypt',
|
||||
'traefik.http.routers.traefik.service=api@internal'
|
||||
]
|
||||
}
|
||||
|
||||
if (config.storageConfig.containerStorage.traefikStorage.type === 'volume') {
|
||||
service.volumes = [
|
||||
'/var/run/docker.sock:/var/run/docker.sock:ro',
|
||||
`${config.storageConfig.containerStorage.traefikStorage.volumeName}:/letsencrypt`
|
||||
]
|
||||
} else {
|
||||
service.volumes = [
|
||||
'/var/run/docker.sock:/var/run/docker.sock:ro',
|
||||
`${config.storageConfig.containerStorage.traefikStorage.directory}:/letsencrypt`
|
||||
]
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
function generateNginxService(config: any): any {
|
||||
// This would generate an Nginx service with SSL configuration
|
||||
// Implementation would depend on specific Nginx configuration needs
|
||||
return {
|
||||
image: 'nginx:alpine',
|
||||
container_name: 'nginx',
|
||||
restart: 'unless-stopped',
|
||||
ports: [`${config.nginxConfig.port}:443`, '80:80'],
|
||||
networks: ['homebox']
|
||||
}
|
||||
}
|
||||
|
||||
function generateCaddyService(config: any): any {
|
||||
return {
|
||||
image: 'caddy:alpine',
|
||||
container_name: 'caddy',
|
||||
restart: 'unless-stopped',
|
||||
ports: ['80:80', '443:443'],
|
||||
networks: ['homebox']
|
||||
}
|
||||
}
|
||||
|
||||
function generateCloudflaredService(config: any): any {
|
||||
return {
|
||||
image: 'cloudflare/cloudflared:latest',
|
||||
container_name: 'cloudflared',
|
||||
restart: 'unless-stopped',
|
||||
command: `tunnel --no-autoupdate run --token ${config.cloudflaredConfig.token}`,
|
||||
networks: ['homebox']
|
||||
}
|
||||
}
|
||||
|
||||
// Simple YAML stringifier (basic implementation
|
||||
|
||||
const yaml = {
|
||||
stringify(obj: any, indent = 0, parentKey = "", isTopLevel = true): string {
|
||||
const spaces = ' '.repeat(indent)
|
||||
const nextSpaces = ' '.repeat(indent + 1)
|
||||
if (obj === null || obj === undefined) {
|
||||
return 'null'
|
||||
}
|
||||
if (typeof obj === 'string') {
|
||||
if (parentKey === 'environment') {
|
||||
// Should not be used, handled by stringifyEnv
|
||||
return obj
|
||||
}
|
||||
if (obj.includes(':') || obj.includes('#') || obj.includes('\n') || /^[0-9]/.test(obj) || obj.includes('${')) {
|
||||
return `"${obj.replace(/"/g, '\\"')}"`
|
||||
}
|
||||
return obj
|
||||
}
|
||||
if (typeof obj === 'number' || typeof obj === 'boolean') {
|
||||
return String(obj)
|
||||
}
|
||||
if (Array.isArray(obj)) {
|
||||
if (obj.length === 0) return '[]'
|
||||
if (parentKey === 'environment') {
|
||||
return yaml.stringifyEnv(obj, indent)
|
||||
}
|
||||
// For arrays under object keys, indent dashes at the same level as the parent key's value (spaces)
|
||||
return '\n' + obj.map(item => `${spaces}- ${this.stringify(item, indent + 1, '', false).replace(/^\s+/, '')}`).join('\n')
|
||||
}
|
||||
if (typeof obj === 'object') {
|
||||
const keys = Object.keys(obj)
|
||||
if (keys.length === 0) return '{}'
|
||||
return (isTopLevel ? '' : '\n') + keys.map(key => {
|
||||
const value = this.stringify(obj[key], indent + 1, key, false)
|
||||
// If value is an array, ensure correct indentation
|
||||
if (Array.isArray(obj[key])) {
|
||||
// Place key at current indent, then array items at next indent
|
||||
return `${isTopLevel ? '' : spaces}${key}:${value}`
|
||||
}
|
||||
if (value.startsWith('\n')) {
|
||||
return `${isTopLevel ? '' : spaces}${key}:${value}`
|
||||
}
|
||||
return `${isTopLevel ? '' : spaces}${key}: ${value}`
|
||||
}).join('\n')
|
||||
}
|
||||
return String(obj)
|
||||
},
|
||||
|
||||
stringifyEnv(envArr: string[], indent = 0): string {
|
||||
const spaces = ' '.repeat(indent)
|
||||
return '\n' + envArr.map(env => {
|
||||
const eqIdx = env.indexOf('=')
|
||||
if (eqIdx !== -1) {
|
||||
const key = env.slice(0, eqIdx + 1)
|
||||
let value = env.slice(eqIdx + 1)
|
||||
// Only quote the value if it contains special YAML characters
|
||||
if (value.match(/[:#\n]|^\d|\${/)) {
|
||||
value = `"${value.replace(/"/g, '\\"')}"`
|
||||
}
|
||||
return `${spaces}- ${key}${value}`
|
||||
}
|
||||
return `${spaces}- ${env}`
|
||||
}).join('\n')
|
||||
}
|
||||
}
|
||||
90
docs/.vitepress/components/types.ts
Normal file
90
docs/.vitepress/components/types.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
// types.ts
|
||||
|
||||
export type StorageType = "volume" | "directory"
|
||||
export type HttpsOption = "none" | "traefik" | "nginx" | "caddy" | "cloudflared"
|
||||
export type DatabaseType = "sqlite" | "postgres"
|
||||
|
||||
export interface StorageDetail {
|
||||
type: StorageType
|
||||
directory: string
|
||||
volumeName: string
|
||||
}
|
||||
|
||||
export interface StorageConfig {
|
||||
homeboxStorage: StorageDetail
|
||||
postgresStorage: StorageDetail
|
||||
traefikStorage: StorageDetail
|
||||
nginxStorage: StorageDetail
|
||||
caddyStorage: StorageDetail
|
||||
cloudflaredStorage: StorageDetail
|
||||
}
|
||||
|
||||
export interface PostgresConfig {
|
||||
host: string
|
||||
port: string
|
||||
username: string
|
||||
password: string
|
||||
database: string
|
||||
}
|
||||
|
||||
export interface TraefikConfig {
|
||||
domain: string
|
||||
email: string
|
||||
}
|
||||
|
||||
export interface NginxConfig {
|
||||
domain: string
|
||||
port: string
|
||||
sslCertPath: string
|
||||
sslKeyPath: string
|
||||
}
|
||||
|
||||
export interface CaddyConfig {
|
||||
domain: string
|
||||
email: string
|
||||
}
|
||||
|
||||
export interface CloudflaredConfig {
|
||||
tunnel: string // Note: This wasn't used in the generator function, but kept for completeness
|
||||
domain: string
|
||||
token: string
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
image: string // Not directly used in generator, but part of the config
|
||||
rootless: boolean
|
||||
port: string
|
||||
logLevel: string
|
||||
logFormat: string
|
||||
maxFileUpload: string
|
||||
allowAnalytics: boolean
|
||||
httpsOption: HttpsOption
|
||||
traefikConfig: TraefikConfig
|
||||
nginxConfig: NginxConfig
|
||||
caddyConfig: CaddyConfig
|
||||
cloudflaredConfig: CloudflaredConfig
|
||||
databaseType: DatabaseType
|
||||
postgresConfig: PostgresConfig
|
||||
allowRegistration: boolean
|
||||
autoIncrementAssetId: boolean
|
||||
checkGithubRelease: boolean
|
||||
storageConfig: StorageConfig
|
||||
}
|
||||
|
||||
// Types for the generated Docker Compose structure
|
||||
export interface DockerService {
|
||||
image: string
|
||||
container_name: string
|
||||
restart: string
|
||||
environment?: string[]
|
||||
volumes: string[]
|
||||
ports?: string[]
|
||||
expose?: string[]
|
||||
labels?: string[]
|
||||
command?: string[]
|
||||
depends_on?: string[]
|
||||
}
|
||||
|
||||
export interface DockerServices {
|
||||
[key: string]: DockerService
|
||||
}
|
||||
@@ -941,48 +941,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/items/{id}/duplicate": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Items"
|
||||
],
|
||||
"summary": "Duplicate Item",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Item ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Duplicate Options",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.DuplicateOptions"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/repo.ItemOut"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/items/{id}/maintenance": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -1851,41 +1809,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/products/search-from-barcode": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Items"
|
||||
],
|
||||
"summary": "Search EAN from Barcode",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "barcode to be searched",
|
||||
"name": "data",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/repo.BarcodeProduct"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/qrcode": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -3138,54 +3061,6 @@
|
||||
"TypeTime"
|
||||
]
|
||||
},
|
||||
"repo.BarcodeProduct": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"barcode": {
|
||||
"type": "string"
|
||||
},
|
||||
"imageBase64": {
|
||||
"type": "string"
|
||||
},
|
||||
"imageURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"item": {
|
||||
"$ref": "#/definitions/repo.ItemCreate"
|
||||
},
|
||||
"manufacturer": {
|
||||
"type": "string"
|
||||
},
|
||||
"modelNumber": {
|
||||
"description": "Identifications",
|
||||
"type": "string"
|
||||
},
|
||||
"notes": {
|
||||
"description": "Extras",
|
||||
"type": "string"
|
||||
},
|
||||
"search_engine_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.DuplicateOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"copyAttachments": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"copyCustomFields": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"copyMaintenance": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"copyPrefix": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repo.Group": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -3696,7 +3571,7 @@
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"maxLength": 1000
|
||||
"maxLength": 255
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
|
||||
@@ -646,38 +646,6 @@ definitions:
|
||||
- TypeNumber
|
||||
- TypeBoolean
|
||||
- TypeTime
|
||||
repo.BarcodeProduct:
|
||||
properties:
|
||||
barcode:
|
||||
type: string
|
||||
imageBase64:
|
||||
type: string
|
||||
imageURL:
|
||||
type: string
|
||||
item:
|
||||
$ref: '#/definitions/repo.ItemCreate'
|
||||
manufacturer:
|
||||
type: string
|
||||
modelNumber:
|
||||
description: Identifications
|
||||
type: string
|
||||
notes:
|
||||
description: Extras
|
||||
type: string
|
||||
search_engine_name:
|
||||
type: string
|
||||
type: object
|
||||
repo.DuplicateOptions:
|
||||
properties:
|
||||
copyAttachments:
|
||||
type: boolean
|
||||
copyCustomFields:
|
||||
type: boolean
|
||||
copyMaintenance:
|
||||
type: boolean
|
||||
copyPrefix:
|
||||
type: string
|
||||
type: object
|
||||
repo.Group:
|
||||
properties:
|
||||
createdAt:
|
||||
@@ -1023,7 +991,7 @@ definitions:
|
||||
color:
|
||||
type: string
|
||||
description:
|
||||
maxLength: 1000
|
||||
maxLength: 255
|
||||
type: string
|
||||
name:
|
||||
maxLength: 255
|
||||
@@ -1979,32 +1947,6 @@ paths:
|
||||
summary: Update Item Attachment
|
||||
tags:
|
||||
- Items Attachments
|
||||
/v1/items/{id}/duplicate:
|
||||
post:
|
||||
parameters:
|
||||
- description: Item ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: Duplicate Options
|
||||
in: body
|
||||
name: payload
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/repo.DuplicateOptions'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"201":
|
||||
description: Created
|
||||
schema:
|
||||
$ref: '#/definitions/repo.ItemOut'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Duplicate Item
|
||||
tags:
|
||||
- Items
|
||||
/v1/items/{id}/maintenance:
|
||||
get:
|
||||
parameters:
|
||||
@@ -2601,27 +2543,6 @@ paths:
|
||||
summary: Test Notifier
|
||||
tags:
|
||||
- Notifiers
|
||||
/v1/products/search-from-barcode:
|
||||
get:
|
||||
parameters:
|
||||
- description: barcode to be searched
|
||||
in: query
|
||||
name: data
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/repo.BarcodeProduct'
|
||||
type: array
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Search EAN from Barcode
|
||||
tags:
|
||||
- Items
|
||||
/v1/qrcode:
|
||||
get:
|
||||
parameters:
|
||||
|
||||
@@ -11,7 +11,7 @@ aside: false
|
||||
|-----------------------------------------|----------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| HBOX_MODE | `production` | application mode used for runtime behavior can be one of: `development`, `production` |
|
||||
| HBOX_WEB_PORT | 7745 | port to run the web server on, if you're using docker do not change this |
|
||||
| HBOX_WEB_HOST | | host to run the web server on, if you're using docker do not change this. see below for examples |
|
||||
| HBOX_WEB_HOST | | host to run the web server on, if you're using docker do not change this |
|
||||
| HBOX_OPTIONS_ALLOW_REGISTRATION | true | allow users to register themselves |
|
||||
| HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID | true | auto-increments the asset_id field for new items |
|
||||
| HBOX_OPTIONS_CURRENCY_CONFIG | | json configuration file containing additional currencie |
|
||||
@@ -35,13 +35,10 @@ aside: false
|
||||
| HBOX_DATABASE_SQLITE_PATH | ./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1 | sets the directory path for Sqlite |
|
||||
| HBOX_DATABASE_HOST | | sets the hostname for a postgres database |
|
||||
| HBOX_DATABASE_PORT | | sets the port for a postgres database |
|
||||
| HBOX_DATABASE_USERNAME | | sets the username for a postgres connection (optional if using cert auth) |
|
||||
| HBOX_DATABASE_PASSWORD | | sets the password for a postgres connection (optional if using cert auth) |
|
||||
| HBOX_DATABASE_USERNAME | | sets the username for a postgres connection |
|
||||
| HBOX_DATABASE_PASSWORD | | sets the password for a postgres connection |
|
||||
| HBOX_DATABASE_DATABASE | | sets the database for a postgres connection |
|
||||
| HBOX_DATABASE_SSL_MODE | | sets the sslmode for a postgres connection |
|
||||
| HBOX_DATABASE_SSL_CERT | | sets the sslcert for a postgres connection (should be a path) |
|
||||
| HBOX_DATABASE_SSL_KEY | | sets the sslkey for a postgres connection (should be a path) |
|
||||
| HBOX_DATABASE_SSL_ROOTCERT | | sets the sslrootcert for a postgres connection (should be a path) |
|
||||
| HBOX_OPTIONS_CHECK_GITHUB_RELEASE | true | check for new github releases |
|
||||
| HBOX_LABEL_MAKER_WIDTH | 526 | width for generated labels in pixels |
|
||||
| HBOX_LABEL_MAKER_HEIGHT | 200 | height for generated labels in pixels |
|
||||
@@ -54,84 +51,6 @@ aside: false
|
||||
| HBOX_THUMBNAIL_WIDTH | 500 | width for generated thumbnails in pixels |
|
||||
| HBOX_THUMBNAIL_HEIGHT | 500 | height for generated thumbnails in pixels |
|
||||
|
||||
### HBOX_WEB_HOST examples
|
||||
|
||||
| Value | Notes |
|
||||
|-----------------------------|------------------------------------------------------------|
|
||||
| 0.0.0.0 | Visible all interfaces (default behaviour) |
|
||||
| 127.0.0.1 | Only visible on same host |
|
||||
| 100.64.0.1 | Only visible on a specific interface (e.g., VPN in a VPS). |
|
||||
| unix?path=/run/homebox.sock | Listen on unix socket at specified path |
|
||||
| sysd?name=homebox.socket | Listen on systemd socket |
|
||||
|
||||
For unix and systemd socket address syntax and available options, see the [anyhttp address-syntax documentation](https://pkg.go.dev/go.balki.me/anyhttp#readme-address-syntax).
|
||||
|
||||
#### Private network example
|
||||
|
||||
Below example starts homebox in an isolated network. The process cannot make
|
||||
any external requests (including check for newer release) and thus more secure.
|
||||
|
||||
```bash
|
||||
❯ sudo systemd-run --property=PrivateNetwork=yes --uid $UID --pty --same-dir --wait --collect homebox --web-host "unix?path=/run/user/$UID/homebox.sock"
|
||||
Running as unit: run-p74482-i74483.service
|
||||
Press ^] three times within 1s to disconnect TTY.
|
||||
2025/07/11 22:33:29 goose: no migrations to run. current version: 20250706190000
|
||||
10:33PM INF ../../../go/src/app/app/api/handlers/v1/v1_ctrl_auth.go:98 > registering auth provider name=local
|
||||
10:33PM INF ../../../go/src/app/app/api/main.go:275 > Server is running on unix?path=/run/user/1000/homebox.sock
|
||||
10:33PM ERR ../../../go/src/app/app/api/main.go:403 > failed to get latest github release error="failed to make latest version request: Get \"https://api.github.com/repos/sysadminsmedia/homebox/releases/l
|
||||
atest\": dial tcp: lookup api.github.com on [::1]:53: read udp [::1]:50951->[::1]:53: read: connection refused"
|
||||
10:33PM INF ../../../go/src/app/internal/web/mid/logger.go:36 > request received method=GET path=/ rid=hname/PoXyRgt6ol-000001
|
||||
10:33PM INF ../../../go/src/app/internal/web/mid/logger.go:41 > request finished method=GET path=/ rid=hname/PoXyRgt6ol-000001 status=0
|
||||
```
|
||||
|
||||
#### Systemd socket example
|
||||
|
||||
In the example below, Homebox listens on a systemd socket securely so that only
|
||||
the webserver (Caddy) can access it. Other processes/containers on the host
|
||||
cannot connect to Homebox directly, bypassing the webserver.
|
||||
|
||||
File: homebox.socket
|
||||
```systemd
|
||||
# /usr/local/lib/systemd/system/homebox.socket
|
||||
[Unit]
|
||||
Description=Homebox socket
|
||||
|
||||
[Socket]
|
||||
ListenStream=/run/homebox.sock
|
||||
SocketGroup=caddy
|
||||
SocketMode=0660
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
```
|
||||
|
||||
File: homebox.service
|
||||
```systemd
|
||||
# /usr/local/lib/systemd/system/homebox.service
|
||||
[Unit]
|
||||
Description=Homebox
|
||||
After=network.target
|
||||
Documentation=https://homebox.software
|
||||
|
||||
[Service]
|
||||
DynamicUser=yes
|
||||
StateDirectory=homebox
|
||||
Environment=HBOX_WEB_HOST=sysd?name=homebox.socket
|
||||
WorkingDirectory=/var/lib/homebox
|
||||
|
||||
ExecStart=/usr/local/bin/homebox
|
||||
|
||||
NoNewPrivileges=yes
|
||||
CapabilityBoundingSet=
|
||||
RestrictNamespaces=true
|
||||
SystemCallFilter=@system-service
|
||||
```
|
||||
Usage:
|
||||
|
||||
```bash
|
||||
systemctl start homebox.socket
|
||||
```
|
||||
|
||||
::: warning Security Considerations
|
||||
For postgreSQL in production:
|
||||
|
||||
|
||||
@@ -42,28 +42,7 @@ $ docker run -d \
|
||||
|
||||
1. Create a `docker-compose.yml` file.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
homebox:
|
||||
image: ghcr.io/sysadminsmedia/homebox:latest
|
||||
# image: ghcr.io/sysadminsmedia/homebox:latest-rootless
|
||||
container_name: homebox
|
||||
restart: always
|
||||
environment:
|
||||
- HBOX_LOG_LEVEL=info
|
||||
- HBOX_LOG_FORMAT=text
|
||||
- HBOX_WEB_MAX_FILE_UPLOAD=10
|
||||
# Please consider allowing analytics to help us improve Homebox (basic computer information, no personal data)
|
||||
- HBOX_OPTIONS_ALLOW_ANALYTICS=false
|
||||
volumes:
|
||||
- homebox-data:/data/
|
||||
ports:
|
||||
- 3100:7745
|
||||
|
||||
volumes:
|
||||
homebox-data:
|
||||
driver: local
|
||||
```
|
||||
<ConfigEditor />
|
||||
|
||||
::: info
|
||||
If you use the `rootless` image, and instead of using named volumes you would prefer using a hostMount directly (e.g., `volumes: [ /path/to/data/folder:/data ]`) you need to `chown` the chosen directory in advance to the `65532` user (as shown in the Docker example above).
|
||||
@@ -103,3 +82,7 @@ You can learn more about Docker by [reading the official Docker documentation.](
|
||||
2. Extract the archive.
|
||||
3. Run the `homebox` executable.
|
||||
4. The web interface will be accessible on port 7745 by default. Access the page by navigating to `http://local.ip.address:7745/` (replace with the right ip address)
|
||||
|
||||
<script setup>
|
||||
import ConfigEditor from '../.vitepress/components/ConfigEditor.vue'
|
||||
</script>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
/*
|
||||
X-Frame-Options: DENY
|
||||
X-Content-Type-Options: nosniff
|
||||
Content-Security-Policy: default-src 'self'; script-src 'report-sample' 'unsafe-inline' 'self' https://a.sysadmins.zone/js/embed.host.js https://static.cloudflareinsights.com/beacon.min.js/vcd15cbe7772f49c399c6a5babf22c1241717689176015 https://unpkg.com/@stoplight/elements/web-components.min.js; style-src 'report-sample' 'unsafe-inline' 'self' https://unpkg.com; object-src 'none'; base-uri 'self'; connect-src 'self' https://raw.githubusercontent.com; font-src 'self'; frame-src 'self' https://a.sysadmins.zone; img-src 'self' data: http://translate.sysadminsmedia.com; manifest-src 'self'; media-src 'self'; worker-src 'none';
|
||||
@@ -1,7 +0,0 @@
|
||||
name = "homebox-docs"
|
||||
compatibility_date = "2025-07-12"
|
||||
preview_urls = true
|
||||
|
||||
[assets]
|
||||
directory = ".vitepress/dist"
|
||||
not_found_handling = "single-page-application"
|
||||
@@ -2,10 +2,7 @@
|
||||
<Dialog v-if="isDesktop" :dialog-id="dialogId">
|
||||
<DialogScrollContent>
|
||||
<DialogHeader>
|
||||
<div class="mr-4 flex place-items-center justify-between">
|
||||
<DialogTitle>{{ title }}</DialogTitle>
|
||||
<slot name="header-actions" />
|
||||
</div>
|
||||
<DialogTitle>{{ title }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<slot />
|
||||
@@ -32,9 +29,6 @@
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>{{ title }}</DrawerTitle>
|
||||
</DrawerHeader>
|
||||
<div class="flex justify-center">
|
||||
<slot name="header-actions" />
|
||||
</div>
|
||||
|
||||
<div class="m-2 overflow-y-auto p-2">
|
||||
<slot />
|
||||
@@ -45,14 +39,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMediaQuery } from "@vueuse/core";
|
||||
import type { DialogID } from "@/components/ui/dialog-provider/utils";
|
||||
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from "@/components/ui/drawer";
|
||||
import { Dialog, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
|
||||
const isDesktop = useMediaQuery("(min-width: 768px)");
|
||||
|
||||
defineProps<{
|
||||
dialogId: DialogID;
|
||||
dialogId: string;
|
||||
title: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Dialog :dialog-id="DialogID.Import">
|
||||
<Dialog dialog-id="import">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ $t("components.app.import_dialog.title") }}</DialogTitle>
|
||||
@@ -38,7 +38,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { DialogID } from "@/components/ui/dialog-provider/utils";
|
||||
import { toast } from "@/components/ui/sonner";
|
||||
import {
|
||||
Dialog,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { DialogID, type NoParamDialogIDs, type OptionalDialogIDs } from "@/components/ui/dialog-provider/utils";
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
@@ -15,7 +14,7 @@
|
||||
|
||||
export type QuickMenuAction =
|
||||
| { text: string; href: string; type: "navigate" }
|
||||
| { text: string; dialogId: NoParamDialogIDs | OptionalDialogIDs; shortcut: string; type: "create" };
|
||||
| { text: string; dialogId: string; shortcut: string; type: "create" };
|
||||
|
||||
const props = defineProps({
|
||||
actions: {
|
||||
@@ -28,11 +27,11 @@
|
||||
const { t } = useI18n();
|
||||
const { closeDialog, openDialog } = useDialog();
|
||||
|
||||
useDialogHotkey(DialogID.QuickMenu, { code: "Backquote", ctrl: true });
|
||||
useDialogHotkey("quick-menu", { code: "Backquote", ctrl: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CommandDialog :dialog-id="DialogID.QuickMenu">
|
||||
<CommandDialog dialog-id="quick-menu">
|
||||
<CommandInput
|
||||
:placeholder="t('components.quick_menu.shortcut_hint')"
|
||||
@keydown="
|
||||
@@ -40,12 +39,12 @@
|
||||
const item = props.actions.filter(item => 'shortcut' in item).find(item => item.shortcut === e.key);
|
||||
if (item) {
|
||||
e.preventDefault();
|
||||
openDialog(item.dialogId as NoParamDialogIDs);
|
||||
openDialog(item.dialogId);
|
||||
}
|
||||
// if esc is pressed, close the dialog
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
closeDialog(DialogID.QuickMenu);
|
||||
closeDialog('quick-menu');
|
||||
}
|
||||
}
|
||||
"
|
||||
@@ -61,7 +60,7 @@
|
||||
@select="
|
||||
e => {
|
||||
e.preventDefault();
|
||||
openDialog(create.dialogId as NoParamDialogIDs);
|
||||
openDialog(create.dialogId);
|
||||
}
|
||||
"
|
||||
>
|
||||
@@ -77,7 +76,7 @@
|
||||
:value="`global.navigate_${i + 1}`"
|
||||
@select="
|
||||
() => {
|
||||
closeDialog(DialogID.QuickMenu);
|
||||
closeDialog('quick-menu');
|
||||
navigateTo(navigate.href);
|
||||
}
|
||||
"
|
||||
@@ -88,8 +87,8 @@
|
||||
value="scanner"
|
||||
@select="
|
||||
() => {
|
||||
closeDialog(DialogID.QuickMenu);
|
||||
openDialog(DialogID.Scanner);
|
||||
closeDialog('quick-menu');
|
||||
openDialog('scanner');
|
||||
}
|
||||
"
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Dialog :dialog-id="DialogID.Scanner">
|
||||
<Dialog dialog-id="scanner">
|
||||
<DialogScrollContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("scanner.title") }}</DialogTitle>
|
||||
@@ -13,25 +13,6 @@
|
||||
<MdiAlertCircleOutline class="text-destructive" />
|
||||
<span class="text-sm font-medium">{{ errorMessage }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="detectedBarcode"
|
||||
class="mb-5 flex flex-col items-center gap-2 rounded-md border border-accent-foreground bg-accent p-4 text-accent-foreground"
|
||||
role="alert"
|
||||
>
|
||||
<div class="flex">
|
||||
<MdiBarcode class="mr-2" />
|
||||
<span class="flex-1 text-center text-sm font-medium">
|
||||
{{ detectedBarcodeType }} {{ $t("scanner.barcode_detected_message") }}:
|
||||
<strong>{{ detectedBarcode }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ButtonGroup>
|
||||
<Button :disabled="loading" type="submit" @click="handleButtonClick">
|
||||
{{ $t("scanner.barcode_fetch_data") }}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<!-- eslint-disable-next-line tailwindcss/no-custom-classname -->
|
||||
<video ref="video" class="aspect-video w-full rounded-lg bg-muted shadow" poster="data:image/gif,AAAA"></video>
|
||||
<div class="mt-4">
|
||||
@@ -53,19 +34,16 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from "vue";
|
||||
import { BrowserMultiFormatReader, NotFoundException, BarcodeFormat } from "@zxing/library";
|
||||
import { BrowserMultiFormatReader, NotFoundException } from "@zxing/library";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { DialogID } from "@/components/ui/dialog-provider/utils";
|
||||
import { Dialog, DialogHeader, DialogTitle, DialogScrollContent } from "@/components/ui/dialog";
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import MdiBarcode from "~icons/mdi/barcode";
|
||||
import MdiAlertCircleOutline from "~icons/mdi/alert-circle-outline";
|
||||
import { useDialog } from "@/components/ui/dialog-provider";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { activeDialog, openDialog, closeDialog } = useDialog();
|
||||
const open = computed(() => activeDialog && activeDialog.value === DialogID.Scanner);
|
||||
const { activeDialog } = useDialog();
|
||||
const open = computed(() => activeDialog.value === "scanner");
|
||||
|
||||
const sources = ref<MediaDeviceInfo[]>([]);
|
||||
const selectedSource = ref<string | null>(null);
|
||||
@@ -73,8 +51,6 @@
|
||||
const video = ref<HTMLVideoElement>();
|
||||
const codeReader = new BrowserMultiFormatReader();
|
||||
const errorMessage = ref<string | null>(null);
|
||||
const detectedBarcode = ref<string>("");
|
||||
const detectedBarcodeType = ref<string>("");
|
||||
|
||||
const handleError = (error: unknown) => {
|
||||
console.error("Scanner error:", error);
|
||||
@@ -92,10 +68,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
const handleButtonClick = () => {
|
||||
openDialog(DialogID.ProductImport, { params: { barcode: detectedBarcode.value } });
|
||||
};
|
||||
|
||||
const startScanner = async () => {
|
||||
errorMessage.value = null;
|
||||
if (!(navigator && navigator.mediaDevices && "enumerateDevices" in navigator.mediaDevices)) {
|
||||
@@ -137,7 +109,6 @@
|
||||
|
||||
watch(open, async isOpen => {
|
||||
if (isOpen) {
|
||||
detectedBarcode.value = "";
|
||||
await startScanner();
|
||||
} else {
|
||||
stopScanner();
|
||||
@@ -158,27 +129,10 @@
|
||||
throw new Error(t("scanner.invalid_url"));
|
||||
}
|
||||
const sanitizedPath = url.pathname.replace(/[^a-zA-Z0-9-_/]/g, "");
|
||||
closeDialog(DialogID.Scanner);
|
||||
navigateTo(sanitizedPath);
|
||||
} catch (err) {
|
||||
// Check if it's a barcode for a new element
|
||||
const bcfmt = result.getBarcodeFormat();
|
||||
|
||||
switch (bcfmt) {
|
||||
case BarcodeFormat.EAN_13:
|
||||
case BarcodeFormat.UPC_A:
|
||||
case BarcodeFormat.UPC_E:
|
||||
case BarcodeFormat.UPC_EAN_EXTENSION:
|
||||
console.info("Barcode detected");
|
||||
detectedBarcode.value = result.getText();
|
||||
detectedBarcodeType.value = BarcodeFormat[bcfmt].replaceAll("_", "-");
|
||||
break;
|
||||
|
||||
default:
|
||||
handleError(err);
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
handleError(err);
|
||||
}
|
||||
}
|
||||
if (err && !(err instanceof NotFoundException)) {
|
||||
@@ -195,3 +149,9 @@
|
||||
stopScanner();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
video {
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { themes } from "~~/lib/data/themes";
|
||||
import { useTheme } from "~/composables/use-theme";
|
||||
|
||||
const { setTheme } = useTheme();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="homebox grid grid-cols-1 gap-4 font-sans sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
|
||||
<div
|
||||
v-for="theme in themes"
|
||||
:key="theme.value"
|
||||
:class="'theme-' + theme.value"
|
||||
class="overflow-hidden rounded-lg border outline-2 outline-offset-2"
|
||||
:data-theme="theme.value"
|
||||
:data-set-theme="theme.value"
|
||||
data-act-class="outline"
|
||||
@click="setTheme(theme.value)"
|
||||
>
|
||||
<div :data-theme="theme.value" class="w-full cursor-pointer bg-background-accent text-foreground">
|
||||
<div class="grid grid-cols-5 grid-rows-3">
|
||||
<div class="col-start-1 row-start-1 bg-background"></div>
|
||||
<div class="col-start-1 row-start-2 bg-sidebar"></div>
|
||||
<div class="col-start-1 row-start-3 bg-background-accent"></div>
|
||||
<div class="col-span-4 col-start-2 row-span-3 row-start-1 flex flex-col gap-1 bg-background p-2">
|
||||
<div class="font-bold">{{ theme.label }}</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<div class="flex size-5 items-center justify-center rounded bg-primary lg:size-6">
|
||||
<div class="text-sm font-bold text-primary-foreground">A</div>
|
||||
</div>
|
||||
<div class="flex size-5 items-center justify-center rounded bg-secondary lg:size-6">
|
||||
<div class="text-sm font-bold text-secondary-foreground">A</div>
|
||||
</div>
|
||||
<div class="flex size-5 items-center justify-center rounded bg-accent lg:size-6">
|
||||
<div class="text-sm font-bold text-accent-foreground">A</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -15,7 +15,6 @@
|
||||
import "@vuepic/vue-datepicker/dist/main.css";
|
||||
import * as datelib from "~/lib/datelib/datelib";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { darkThemes } from "~/lib/data/themes";
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "update:text"]);
|
||||
|
||||
@@ -35,7 +34,7 @@
|
||||
},
|
||||
});
|
||||
|
||||
const isDark = useIsThemeInList(darkThemes);
|
||||
const isDark = useIsDark();
|
||||
|
||||
const formatDate = (date: Date | string | number) => fmtDate(date, "human", "date");
|
||||
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
<template>
|
||||
<Dialog :dialog-id="DialogID.ProductImport">
|
||||
<DialogContent :class="'w-full md:max-w-xl lg:max-w-4xl'">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ $t("components.item.product_import.title") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div
|
||||
v-if="errorMessage"
|
||||
class="flex items-center gap-2 rounded-md border border-destructive bg-destructive/10 p-4 text-destructive"
|
||||
role="alert"
|
||||
>
|
||||
<MdiAlertCircleOutline class="text-destructive" />
|
||||
<span class="text-sm font-medium">{{ errorMessage }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<FormTextField
|
||||
v-model="barcode"
|
||||
:disabled="searching"
|
||||
class="w-[30%]"
|
||||
:label="$t('components.item.product_import.barcode')"
|
||||
@keyup.enter="retrieveProductInfo(barcode)"
|
||||
/>
|
||||
<Button
|
||||
:variant="searching ? 'destructive' : 'default'"
|
||||
class="mt-auto h-10"
|
||||
@click="retrieveProductInfo(barcode)"
|
||||
>
|
||||
<MdiLoading v-if="searching" class="animate-spin" />
|
||||
<div v-if="!searching" class="relative mx-2">
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<MdiBarcode class="size-5 group-hover:hidden" />
|
||||
</div>
|
||||
</div>
|
||||
{{ searching ? $t("global.cancel") : $t("components.item.product_import.search_item") }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<BaseCard>
|
||||
<Table class="w-full">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead
|
||||
v-for="h in headers"
|
||||
:key="h.value"
|
||||
class="text-no-transform bg-secondary text-sm text-secondary-foreground hover:bg-secondary/90"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-1"
|
||||
:class="{
|
||||
'justify-center': h.align === 'center',
|
||||
}"
|
||||
>
|
||||
<template v-if="typeof h === 'string'">{{ h }}</template>
|
||||
<template v-else>{{ $t(h.text) }}</template>
|
||||
</div>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
<TableRow
|
||||
v-for="(p, index) in products"
|
||||
:key="index"
|
||||
class="cursor-pointer"
|
||||
:class="{ selected: selectedRow === index }"
|
||||
@click="selectProduct(index)"
|
||||
>
|
||||
<TableCell
|
||||
v-for="h in headers"
|
||||
:key="h.value"
|
||||
:class="{
|
||||
'text-center': h.align === 'center',
|
||||
}"
|
||||
>
|
||||
<template v-if="h.type === 'name'">
|
||||
<div class="flex items-center space-x-4">
|
||||
<img :src="p.imageBase64" class="w-16 rounded object-fill shadow-sm" alt="Product's photo" />
|
||||
<span class="text-sm font-medium">
|
||||
{{ p.item.name }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="h.type === 'url'">
|
||||
<NuxtLink class="underline" :to="'https://' + extractValue(p, h.value)" target="_blank">{{
|
||||
extractValue(p, h.value)
|
||||
}}</NuxtLink>
|
||||
</template>
|
||||
|
||||
<slot v-else :name="cell(h)">
|
||||
{{ extractValue(p, h.value) }}
|
||||
</slot>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</BaseCard>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="import" :disabled="selectedRow === -1" @click="createItem"> Import selected </Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { DialogID } from "@/components/ui/dialog-provider/utils";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import type { BarcodeProduct } from "~~/lib/api/types/data-contracts";
|
||||
import { useDialog } from "~/components/ui/dialog-provider";
|
||||
import MdiAlertCircleOutline from "~icons/mdi/alert-circle-outline";
|
||||
import MdiBarcode from "~icons/mdi/barcode";
|
||||
import MdiLoading from "~icons/mdi/loading";
|
||||
import type { TableData } from "~/components/Item/View/Table.types";
|
||||
|
||||
const { openDialog, registerOpenDialogCallback } = useDialog();
|
||||
const { t } = useI18n();
|
||||
|
||||
const searching = ref(false);
|
||||
const barcode = ref<string>("");
|
||||
const products = ref<BarcodeProduct[] | null>(null);
|
||||
const selectedRow = ref(-1);
|
||||
const errorMessage = ref<string | null>(null);
|
||||
|
||||
type BarcodeTableHeader = {
|
||||
text: string;
|
||||
value: string;
|
||||
align?: "left" | "center" | "right";
|
||||
type?: "name" | "url";
|
||||
};
|
||||
|
||||
const defaultHeaders = [
|
||||
{
|
||||
text: "items.name",
|
||||
value: "name",
|
||||
align: "center",
|
||||
type: "name",
|
||||
},
|
||||
{ text: "items.manufacturer", value: "manufacturer", align: "center" },
|
||||
{ text: "items.model_number", value: "modelNumber", align: "center" },
|
||||
{ text: "components.item.product_import.db_source", value: "search_engine_name", align: "center", type: "url" },
|
||||
] satisfies BarcodeTableHeader[];
|
||||
|
||||
// Need for later filtering
|
||||
const headers = defaultHeaders;
|
||||
|
||||
onMounted(() => {
|
||||
const cleanup = registerOpenDialogCallback(DialogID.ProductImport, params => {
|
||||
selectedRow.value = -1;
|
||||
searching.value = false;
|
||||
errorMessage.value = null;
|
||||
|
||||
if (params?.barcode) {
|
||||
// Reset if the barcode is different
|
||||
if (params.barcode !== barcode.value) {
|
||||
barcode.value = params.barcode;
|
||||
|
||||
retrieveProductInfo(barcode.value).then(() => {
|
||||
console.log("Processing finished");
|
||||
});
|
||||
}
|
||||
} else {
|
||||
barcode.value = "";
|
||||
products.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(cleanup);
|
||||
});
|
||||
|
||||
const api = useUserApi();
|
||||
|
||||
function createItem() {
|
||||
if (
|
||||
products.value !== null &&
|
||||
products.value.length > 0 &&
|
||||
selectedRow.value >= 0 &&
|
||||
selectedRow.value < products.value.length
|
||||
) {
|
||||
const p = products.value![selectedRow.value];
|
||||
openDialog(DialogID.CreateItem, {
|
||||
params: { product: p },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function retrieveProductInfo(barcode: string) {
|
||||
errorMessage.value = null;
|
||||
|
||||
if (!barcode || barcode.trim().length === 0 || !/^[0-9]+$/.test(barcode)) {
|
||||
errorMessage.value = t("components.item.product_import.error_invalid_barcode");
|
||||
console.error(errorMessage.value);
|
||||
return;
|
||||
}
|
||||
|
||||
products.value = null;
|
||||
searching.value = true;
|
||||
|
||||
try {
|
||||
const result = await api.products.searchFromBarcode(barcode.trim());
|
||||
if (result.error) {
|
||||
errorMessage.value = t("errors.api_failure") + result.error;
|
||||
console.error(errorMessage.value);
|
||||
} else {
|
||||
if (result.data === undefined || result.data.length === undefined || result.data.length === 0) {
|
||||
errorMessage.value = t("components.item.product_import.error_not_found");
|
||||
}
|
||||
|
||||
products.value = result.data;
|
||||
}
|
||||
} catch (error) {
|
||||
errorMessage.value = t("components.item.product_import.error_exception") + error;
|
||||
console.error(errorMessage.value);
|
||||
} finally {
|
||||
searching.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function extractValue(data: TableData, value: string) {
|
||||
const parts = value.split(".");
|
||||
let current = data;
|
||||
for (const part of parts) {
|
||||
current = current[part];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function cell(h: BarcodeTableHeader) {
|
||||
return `cell-${h.value.replace(".", "_")}`;
|
||||
}
|
||||
|
||||
function selectProduct(index: number) {
|
||||
// Unselect if already selected
|
||||
if (selectedRow.value === index) {
|
||||
selectedRow.value = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
selectedRow.value = index;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
tr.selected {
|
||||
background-color: hsl(var(--primary));
|
||||
color: hsl(var(--background));
|
||||
}
|
||||
|
||||
tr:hover.selected {
|
||||
background-color: hsl(var(--primary));
|
||||
}
|
||||
</style>
|
||||
@@ -1,34 +1,5 @@
|
||||
<template>
|
||||
<BaseModal :dialog-id="DialogID.CreateItem" :title="$t('components.item.create_modal.title')">
|
||||
<template #header-actions>
|
||||
<div class="flex">
|
||||
<TooltipProvider :delay-duration="0">
|
||||
<ButtonGroup>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button variant="outline" :disabled="loading" size="icon" data-pos="start" @click="openQrScannerPage()">
|
||||
<MdiBarcodeScan class="size-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{{ $t("components.item.create_modal.product_tooltip_scan_barcode") }}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button variant="outline" :disabled="loading" size="icon" data-pos="end" @click="openBarcodeDialog()">
|
||||
<MdiBarcode class="size-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{{ $t("components.item.create_modal.product_tooltip_input_barcode") }}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<BaseModal dialog-id="create-item" :title="$t('components.item.create_modal.title')">
|
||||
<form class="flex flex-col gap-2" @submit.prevent="create()">
|
||||
<LocationSelector v-model="form.location" />
|
||||
<ItemSelector
|
||||
@@ -169,7 +140,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { DialogID } from "@/components/ui/dialog-provider/utils";
|
||||
import { toast } from "@/components/ui/sonner";
|
||||
import { Button, ButtonGroup } from "~/components/ui/button";
|
||||
import BaseModal from "@/components/App/CreateModal.vue";
|
||||
@@ -178,8 +148,6 @@
|
||||
import type { ItemCreate, LocationOut } from "~~/lib/api/types/data-contracts";
|
||||
import { useLabelStore } from "~~/stores/labels";
|
||||
import { useLocationStore } from "~~/stores/locations";
|
||||
import MdiBarcode from "~icons/mdi/barcode";
|
||||
import MdiBarcodeScan from "~icons/mdi/barcode-scan";
|
||||
import MdiPackageVariant from "~icons/mdi/package-variant";
|
||||
import MdiPackageVariantClosed from "~icons/mdi/package-variant-closed";
|
||||
import MdiDelete from "~icons/mdi/delete";
|
||||
@@ -199,9 +167,9 @@
|
||||
}
|
||||
|
||||
const { t } = useI18n();
|
||||
const { openDialog, closeDialog, registerOpenDialogCallback } = useDialog();
|
||||
const { activeDialog, closeDialog } = useDialog();
|
||||
|
||||
useDialogHotkey(DialogID.CreateItem, { code: "Digit1", shift: true });
|
||||
useDialogHotkey("create-item", { code: "Digit1", shift: true });
|
||||
|
||||
const api = useUserApi();
|
||||
|
||||
@@ -299,69 +267,55 @@
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const cleanup = registerOpenDialogCallback(DialogID.CreateItem, async params => {
|
||||
// needed since URL will be cleared in the next step => ParentId Selection should stay though
|
||||
subItemCreate.value = subItemCreateParam.value === "y";
|
||||
let parentItemLocationId = null;
|
||||
watch(
|
||||
() => activeDialog.value,
|
||||
async active => {
|
||||
if (active === "create-item") {
|
||||
// needed since URL will be cleared in the next step => ParentId Selection should stay though
|
||||
subItemCreate.value = subItemCreateParam.value === "y";
|
||||
let parentItemLocationId = null;
|
||||
|
||||
if (subItemCreate.value && itemId.value) {
|
||||
const itemIdRead = typeof itemId.value === "string" ? (itemId.value as string) : itemId.value[0];
|
||||
const { data, error } = await api.items.get(itemIdRead);
|
||||
if (error || !data) {
|
||||
toast.error(t("components.item.create_modal.toast.failed_load_parent"));
|
||||
console.error("Parent item fetch error:", error);
|
||||
if (subItemCreate.value && itemId.value) {
|
||||
const itemIdRead = typeof itemId.value === "string" ? (itemId.value as string) : itemId.value[0];
|
||||
const { data, error } = await api.items.get(itemIdRead);
|
||||
if (error || !data) {
|
||||
toast.error(t("components.item.create_modal.toast.failed_load_parent"));
|
||||
console.error("Parent item fetch error:", error);
|
||||
}
|
||||
|
||||
if (data) {
|
||||
parent.value = data;
|
||||
}
|
||||
|
||||
if (data.location) {
|
||||
const { location } = data;
|
||||
parentItemLocationId = location.id;
|
||||
}
|
||||
|
||||
// clear URL Parameter (subItemCreate) since intention was communicated and received
|
||||
const currentQuery = { ...route.query };
|
||||
delete currentQuery.subItemCreate;
|
||||
await router.push({ query: currentQuery });
|
||||
} else {
|
||||
// since Input is hidden in this case, make sure no accidental parent information is sent out
|
||||
parent.value = {};
|
||||
form.parentId = null;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
parent.value = data;
|
||||
const locId = locationId.value ? locationId.value : parentItemLocationId;
|
||||
|
||||
if (locId) {
|
||||
const found = locations.value.find(l => l.id === locId);
|
||||
if (found) {
|
||||
form.location = found;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.location) {
|
||||
const { location } = data;
|
||||
parentItemLocationId = location.id;
|
||||
}
|
||||
|
||||
// clear URL Parameter (subItemCreate) since intention was communicated and received
|
||||
const currentQuery = { ...route.query };
|
||||
delete currentQuery.subItemCreate;
|
||||
await router.push({ query: currentQuery });
|
||||
} else {
|
||||
// since Input is hidden in this case, make sure no accidental parent information is sent out
|
||||
parent.value = {};
|
||||
form.parentId = null;
|
||||
}
|
||||
|
||||
const locId = locationId.value ? locationId.value : parentItemLocationId;
|
||||
|
||||
if (locId) {
|
||||
const found = locations.value.find(l => l.id === locId);
|
||||
if (found) {
|
||||
form.location = found;
|
||||
if (labelId.value) {
|
||||
form.labels = labels.value.filter(l => l.id === labelId.value).map(l => l.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (params?.product) {
|
||||
form.name = params.product.item.name;
|
||||
form.description = params.product.item.description;
|
||||
|
||||
if (params.product.imageURL) {
|
||||
form.photos.push({
|
||||
photoName: "product_view.jpg",
|
||||
fileBase64: params.product.imageBase64,
|
||||
primary: form.photos.length === 0,
|
||||
file: dataURLtoFile(params.product.imageBase64, "product_view.jpg"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (labelId.value) {
|
||||
form.labels = labels.value.filter(l => l.id === labelId.value).map(l => l.id);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(cleanup);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
async function create(close = true) {
|
||||
if (!form.location?.id) {
|
||||
@@ -432,7 +386,7 @@
|
||||
loading.value = false;
|
||||
|
||||
if (close) {
|
||||
closeDialog(DialogID.CreateItem);
|
||||
closeDialog("create-item");
|
||||
navigateTo(`/item/${data.id}`);
|
||||
}
|
||||
}
|
||||
@@ -511,12 +465,4 @@
|
||||
offScreenCanvas.height = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function openQrScannerPage() {
|
||||
openDialog(DialogID.Scanner);
|
||||
}
|
||||
|
||||
function openBarcodeDialog() {
|
||||
openDialog(DialogID.ProductImport);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import type { DuplicateSettings } from "~/composables/use-preferences";
|
||||
|
||||
type Props = {
|
||||
modelValue: DuplicateSettings;
|
||||
};
|
||||
|
||||
type Emits = {
|
||||
(e: "update:modelValue", value: DuplicateSettings): void;
|
||||
};
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const enableCustomPrefix = ref(props.modelValue.copyPrefixOverride !== null);
|
||||
const prefix = ref(props.modelValue.copyPrefixOverride ?? "");
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const settings = computed({
|
||||
get: () => props.modelValue,
|
||||
set: value => emit("update:modelValue", value),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch id="copy-maintenance" v-model="settings.copyMaintenance" />
|
||||
<Label for="copy-maintenance">
|
||||
{{ $t("items.duplicate.copy_maintenance") }}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch id="copy-attachments" v-model="settings.copyAttachments" />
|
||||
<Label for="copy-attachments">
|
||||
{{ $t("items.duplicate.copy_attachments") }}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch id="copy-custom-fields" v-model="settings.copyCustomFields" />
|
||||
<Label for="copy-custom-fields">
|
||||
{{ $t("items.duplicate.copy_custom_fields") }}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch
|
||||
id="copy-prefix"
|
||||
v-model="enableCustomPrefix"
|
||||
@update:model-value="
|
||||
v => {
|
||||
settings.copyPrefixOverride = v ? prefix : null;
|
||||
}
|
||||
"
|
||||
/>
|
||||
<Label for="copy-prefix">{{ $t("items.duplicate.enable_custom_prefix") }}</Label>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label for="copy-prefix" :class="{ 'opacity-50': !enableCustomPrefix }">
|
||||
{{ $t("items.duplicate.custom_prefix") }}
|
||||
</Label>
|
||||
<Input
|
||||
id="copy-prefix"
|
||||
v-model="prefix"
|
||||
:disabled="!enableCustomPrefix"
|
||||
:placeholder="$t('items.duplicate.prefix')"
|
||||
class="w-full"
|
||||
@input="settings.copyPrefixOverride = prefix"
|
||||
/>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ $t("items.duplicate.prefix_instructions") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,99 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||
import { buttonVariants, Button } from "@/components/ui/button";
|
||||
import { useDialog } from "@/components/ui/dialog-provider";
|
||||
import { DialogID } from "~/components/ui/dialog-provider/utils";
|
||||
import { useConfirm } from "@/composables/use-confirm";
|
||||
import { toast } from "@/components/ui/sonner";
|
||||
import MdiClose from "~icons/mdi/close";
|
||||
import MdiDownload from "~icons/mdi/download";
|
||||
import MdiDelete from "~icons/mdi/delete";
|
||||
|
||||
const { t } = useI18n();
|
||||
const confirm = useConfirm();
|
||||
|
||||
const { closeDialog, registerOpenDialogCallback } = useDialog();
|
||||
|
||||
const api = useUserApi();
|
||||
|
||||
const image = reactive<{
|
||||
attachmentId: string;
|
||||
itemId: string;
|
||||
originalSrc: string;
|
||||
originalType?: string;
|
||||
thumbnailSrc?: string;
|
||||
}>({
|
||||
attachmentId: "",
|
||||
itemId: "",
|
||||
originalSrc: "",
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
const cleanup = registerOpenDialogCallback(DialogID.ItemImage, params => {
|
||||
image.attachmentId = params.attachmentId;
|
||||
image.itemId = params.itemId;
|
||||
if (params.type === "preloaded") {
|
||||
image.originalSrc = params.originalSrc;
|
||||
image.originalType = params.originalType;
|
||||
image.thumbnailSrc = params.thumbnailSrc;
|
||||
} else if (params.type === "attachment") {
|
||||
image.originalSrc = api.authURL(`/items/${params.itemId}/attachments/${params.attachmentId}`);
|
||||
image.originalType = params.mimeType;
|
||||
image.thumbnailSrc = params.thumbnailId
|
||||
? api.authURL(`/items/${params.itemId}/attachments/${params.thumbnailId}`)
|
||||
: image.originalSrc;
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(cleanup);
|
||||
});
|
||||
|
||||
async function deleteAttachment() {
|
||||
const confirmed = await confirm.open(t("items.delete_attachment_confirm"));
|
||||
|
||||
if (confirmed.isCanceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = await api.items.attachments.delete(image.itemId, image.attachmentId);
|
||||
|
||||
if (error) {
|
||||
toast.error(t("items.toast.failed_delete_attachment"));
|
||||
return;
|
||||
}
|
||||
|
||||
closeDialog(DialogID.ItemImage, {
|
||||
action: "delete",
|
||||
id: image.attachmentId,
|
||||
});
|
||||
toast.success(t("items.toast.attachment_deleted"));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog :dialog-id="DialogID.ItemImage">
|
||||
<DialogContent class="w-auto border-transparent bg-transparent p-0" disable-close>
|
||||
<picture>
|
||||
<source :srcset="image.originalSrc" :type="image.originalType" />
|
||||
<img :src="image.thumbnailSrc" alt="attachment image" />
|
||||
</picture>
|
||||
<Button variant="destructive" size="icon" class="absolute right-[84px] top-1" @click="deleteAttachment">
|
||||
<MdiDelete />
|
||||
</Button>
|
||||
<a :class="buttonVariants({ size: 'icon' })" :href="image.originalSrc" download class="absolute right-11 top-1">
|
||||
<MdiDownload />
|
||||
</a>
|
||||
<Button
|
||||
size="icon"
|
||||
class="absolute right-1 top-1"
|
||||
@click="
|
||||
closeDialog(DialogID.ItemImage);
|
||||
image.originalSrc = '';
|
||||
"
|
||||
>
|
||||
<MdiClose />
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Dialog :dialog-id="DialogID.ItemTableSettings">
|
||||
<Dialog dialog-id="item-table-settings">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ $t("components.item.view.table.table_settings") }}</DialogTitle>
|
||||
@@ -41,7 +41,7 @@
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button @click="closeDialog(DialogID.ItemTableSettings)"> {{ $t("global.save") }} </Button>
|
||||
<Button @click="closeDialog('item-table-settings')"> {{ $t("global.save") }} </Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -123,7 +123,7 @@
|
||||
hidden: disableControls,
|
||||
}"
|
||||
>
|
||||
<Button class="size-10 p-0" variant="outline" @click="openDialog(DialogID.ItemTableSettings)">
|
||||
<Button class="size-10 p-0" variant="outline" @click="openDialog('item-table-settings')">
|
||||
<MdiTableCog />
|
||||
</Button>
|
||||
<Pagination
|
||||
@@ -174,7 +174,6 @@
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { useDialog } from "@/components/ui/dialog-provider";
|
||||
import { DialogID } from "~/components/ui/dialog-provider/utils";
|
||||
|
||||
const { openDialog, closeDialog } = useDialog();
|
||||
|
||||
|
||||
@@ -42,6 +42,6 @@
|
||||
<MdiArrowUp class="hidden group-hover/label-chip:block" />
|
||||
</div>
|
||||
</div>
|
||||
{{ label.name }}
|
||||
{{ label.name.length > 20 ? `${label.name.substring(0, 20)}...` : label.name }}
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<BaseModal :dialog-id="DialogID.CreateLabel" :title="$t('components.label.create_modal.title')">
|
||||
<BaseModal dialog-id="create-label" :title="$t('components.label.create_modal.title')">
|
||||
<form class="flex flex-col gap-2" @submit.prevent="create()">
|
||||
<FormTextField
|
||||
v-model="form.name"
|
||||
@@ -12,7 +12,7 @@
|
||||
<FormTextArea
|
||||
v-model="form.description"
|
||||
:label="$t('components.label.create_modal.label_description')"
|
||||
:max-length="1000"
|
||||
:max-length="255"
|
||||
/>
|
||||
<ColorSelector v-model="form.color" :label="$t('components.label.create_modal.label_color')" :show-hex="true" />
|
||||
<div class="mt-4 flex flex-row-reverse">
|
||||
@@ -29,7 +29,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { DialogID } from "@/components/ui/dialog-provider/utils";
|
||||
import { toast } from "@/components/ui/sonner";
|
||||
import BaseModal from "@/components/App/CreateModal.vue";
|
||||
import { useDialog, useDialogHotkey } from "~/components/ui/dialog-provider";
|
||||
@@ -39,7 +38,7 @@
|
||||
|
||||
const { closeDialog } = useDialog();
|
||||
|
||||
useDialogHotkey(DialogID.CreateLabel, { code: "Digit2", shift: true });
|
||||
useDialogHotkey("create-label", { code: "Digit2", shift: true });
|
||||
|
||||
const loading = ref(false);
|
||||
const focused = ref(false);
|
||||
@@ -86,7 +85,7 @@
|
||||
reset();
|
||||
|
||||
if (close) {
|
||||
closeDialog(DialogID.CreateLabel);
|
||||
closeDialog("create-label");
|
||||
navigateTo(`/label/${data.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,16 +7,16 @@
|
||||
<TagsInput
|
||||
v-model="modelValue"
|
||||
class="w-full gap-0 px-0"
|
||||
:display-value="v => props.labels.find(l => l.id === v)?.name ?? 'Loading...'"
|
||||
:display-value="v => shortenedLabels.find(l => l.id === v)?.name ?? 'Loading...'"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-2 overflow-hidden px-3">
|
||||
<TagsInputItem v-for="item in modelValue" :key="item" :value="item" class="h-auto overflow-hidden text-wrap">
|
||||
<div class="flex flex-wrap items-center gap-2 px-3">
|
||||
<TagsInputItem v-for="item in modelValue" :key="item" :value="item">
|
||||
<span
|
||||
v-if="props.labels.find(l => l.id === item)?.color"
|
||||
class="ml-2 size-4 shrink-0 rounded-full"
|
||||
:style="{ backgroundColor: props.labels.find(l => l.id === item)?.color }"
|
||||
v-if="shortenedLabels.find(l => l.id === item)?.color"
|
||||
class="ml-2 inline-block size-4 rounded-full"
|
||||
:style="{ backgroundColor: shortenedLabels.find(l => l.id === item)?.color }"
|
||||
/>
|
||||
<TagsInputItemText class="py-0.5" />
|
||||
<TagsInputItemText />
|
||||
<TagsInputItemDelete />
|
||||
</TagsInputItem>
|
||||
</div>
|
||||
@@ -61,9 +61,9 @@
|
||||
"
|
||||
>
|
||||
<span
|
||||
class="mr-2 size-4 shrink-0 rounded-full align-middle"
|
||||
:class="{ border: props.labels.find(l => l.id === label.value)?.color }"
|
||||
:style="{ backgroundColor: props.labels.find(l => l.id === label.value)?.color }"
|
||||
class="mr-2 inline-block size-4 rounded-full align-middle"
|
||||
:class="{ border: shortenedLabels.find(l => l.id === label.value)?.color }"
|
||||
:style="{ backgroundColor: shortenedLabels.find(l => l.id === label.value)?.color }"
|
||||
/>
|
||||
{{ label.label }}
|
||||
</CommandItem>
|
||||
@@ -114,23 +114,24 @@
|
||||
const open = ref(false);
|
||||
const searchTerm = ref("");
|
||||
|
||||
const shortenedLabels = computed(() => {
|
||||
return props.labels.map(l => ({
|
||||
...l,
|
||||
name: l.name.length > 20 ? `${l.name.substring(0, 20)}...` : l.name,
|
||||
}));
|
||||
});
|
||||
|
||||
const filteredLabels = computed(() => {
|
||||
const filtered = fuzzysort
|
||||
.go(searchTerm.value, props.labels, { key: "name", all: true })
|
||||
.go(searchTerm.value, shortenedLabels.value, { key: "name", all: true })
|
||||
.map(l => ({
|
||||
value: l.obj.id,
|
||||
label: l.obj.name,
|
||||
}))
|
||||
.filter(i => !modelValue.value.includes(i.value));
|
||||
|
||||
// Only show "Create" option if search term is not empty and no exact match exists
|
||||
if (searchTerm.value.trim() !== "") {
|
||||
const trimmedSearchTerm = searchTerm.value.trim();
|
||||
const hasExactMatch = props.labels.some(label => label.name.toLowerCase() === trimmedSearchTerm.toLowerCase());
|
||||
|
||||
if (!hasExactMatch) {
|
||||
filtered.push({ value: "create-item", label: `${t("global.create")} ${searchTerm.value}` });
|
||||
}
|
||||
filtered.push({ value: "create-item", label: `${t("global.create")} ${searchTerm.value}` });
|
||||
}
|
||||
|
||||
return filtered;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<BaseModal :dialog-id="DialogID.CreateLocation" :title="$t('components.location.create_modal.title')">
|
||||
<BaseModal dialog-id="create-location" :title="$t('components.location.create_modal.title')">
|
||||
<form class="flex flex-col gap-2" @submit.prevent="create()">
|
||||
<LocationSelector v-model="form.parent" />
|
||||
<FormTextField
|
||||
@@ -31,7 +31,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { DialogID } from "@/components/ui/dialog-provider/utils";
|
||||
import { toast } from "@/components/ui/sonner";
|
||||
import { Button, ButtonGroup } from "~/components/ui/button";
|
||||
import BaseModal from "@/components/App/CreateModal.vue";
|
||||
@@ -42,7 +41,7 @@
|
||||
|
||||
const { activeDialog, closeDialog } = useDialog();
|
||||
|
||||
useDialogHotkey(DialogID.CreateLocation, { code: "Digit3", shift: true });
|
||||
useDialogHotkey("create-location", { code: "Digit3", shift: true });
|
||||
|
||||
const loading = ref(false);
|
||||
const focused = ref(false);
|
||||
@@ -55,11 +54,19 @@
|
||||
watch(
|
||||
() => activeDialog.value,
|
||||
active => {
|
||||
if (active && active === DialogID.CreateLocation) {
|
||||
if (active === "create-location") {
|
||||
// useTimeoutFn(() => {
|
||||
// focused.value = true;
|
||||
// }, 50);
|
||||
|
||||
if (locationId.value) {
|
||||
const found = locations.value.find(l => l.id === locationId.value);
|
||||
form.parent = found || null;
|
||||
if (found) {
|
||||
form.parent = found;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// focused.value = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -67,6 +74,7 @@
|
||||
function reset() {
|
||||
form.name = "";
|
||||
form.description = "";
|
||||
form.parent = null;
|
||||
focused.value = false;
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -110,11 +118,10 @@
|
||||
if (data) {
|
||||
toast.success(t("components.location.create_modal.toast.create_success"));
|
||||
}
|
||||
|
||||
reset();
|
||||
|
||||
if (close) {
|
||||
closeDialog(DialogID.CreateLocation);
|
||||
closeDialog("create-location");
|
||||
navigateTo(`/location/${data.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Dialog :dialog-id="DialogID.EditMaintenance">
|
||||
<Dialog dialog-id="edit-maintenance">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
@@ -27,7 +27,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { DialogID } from "@/components/ui/dialog-provider/utils";
|
||||
import { toast } from "@/components/ui/sonner";
|
||||
import type { MaintenanceEntry, MaintenanceEntryWithDetails } from "~~/lib/api/types/data-contracts";
|
||||
import MdiPost from "~icons/mdi/post";
|
||||
@@ -78,7 +77,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
closeDialog(DialogID.EditMaintenance);
|
||||
closeDialog("edit-maintenance");
|
||||
emit("changed");
|
||||
}
|
||||
|
||||
@@ -100,7 +99,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
closeDialog(DialogID.EditMaintenance);
|
||||
closeDialog("edit-maintenance");
|
||||
emit("changed");
|
||||
}
|
||||
|
||||
@@ -112,7 +111,7 @@
|
||||
entry.description = "";
|
||||
entry.cost = "";
|
||||
entry.itemId = itemId;
|
||||
openDialog(DialogID.EditMaintenance);
|
||||
openDialog("edit-maintenance");
|
||||
};
|
||||
|
||||
const openUpdateModal = (maintenanceEntry: MaintenanceEntry | MaintenanceEntryWithDetails) => {
|
||||
@@ -123,7 +122,7 @@
|
||||
entry.description = maintenanceEntry.description;
|
||||
entry.cost = maintenanceEntry.cost;
|
||||
entry.itemId = null;
|
||||
openDialog(DialogID.EditMaintenance);
|
||||
openDialog("edit-maintenance");
|
||||
};
|
||||
|
||||
const confirm = useConfirm();
|
||||
@@ -165,7 +164,7 @@
|
||||
entry.description = maintenanceEntry.description;
|
||||
entry.cost = maintenanceEntry.cost;
|
||||
entry.itemId = itemId;
|
||||
openDialog(DialogID.EditMaintenance);
|
||||
openDialog("edit-maintenance");
|
||||
}
|
||||
|
||||
defineExpose({ openCreateModal, openUpdateModal, deleteEntry, complete, duplicate });
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { route } from "../../lib/api/base";
|
||||
import PageQRCode from "./PageQRCode.vue";
|
||||
import { DialogID } from "@/components/ui/dialog-provider/utils";
|
||||
import { toast } from "@/components/ui/sonner";
|
||||
import MdiLoading from "~icons/mdi/loading";
|
||||
import MdiPrinterPos from "~icons/mdi/printer-pos";
|
||||
@@ -64,7 +63,7 @@
|
||||
}
|
||||
|
||||
toast.success(t("components.global.label_maker.toast.print_success"));
|
||||
closeDialog(DialogID.PrintLabel);
|
||||
closeDialog("print-label");
|
||||
serverPrinting.value = false;
|
||||
}
|
||||
|
||||
@@ -94,7 +93,7 @@
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Dialog :dialog-id="DialogID.PrintLabel">
|
||||
<Dialog dialog-id="print-label">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
@@ -138,7 +137,7 @@
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button size="icon" @click="openDialog(DialogID.PrintLabel)">
|
||||
<Button size="icon" @click="openDialog('print-label')">
|
||||
<MdiPrinterPos name="mdi-printer-pos" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import MarkdownIt from "markdown-it";
|
||||
import { imgSize } from "@mdit/plugin-img-size";
|
||||
import DOMPurify from "dompurify";
|
||||
|
||||
type Props = {
|
||||
@@ -15,7 +14,7 @@
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
}).use(imgSize);
|
||||
});
|
||||
|
||||
const raw = computed(() => {
|
||||
const html = md.render(props.source || "").replace(/\n$/, ""); // remove trailing newline
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogID } from "@/components/ui/dialog-provider/utils";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { route } from "~/lib/api/base";
|
||||
import MdiQrcode from "~icons/mdi/qrcode";
|
||||
@@ -17,7 +16,7 @@
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog :dialog-id="DialogID.PageQRCode">
|
||||
<Dialog dialog-id="page-qr-code">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
@@ -30,7 +29,7 @@
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button size="icon" @click="openDialog(DialogID.PageQRCode)">
|
||||
<Button size="icon" @click="openDialog('page-qr-code')">
|
||||
<MdiQrcode name="mdi-qrcode" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -25,13 +25,13 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
<template>
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay
|
||||
class="fixed inset-0 z-[60] bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
||||
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
||||
/>
|
||||
<AlertDialogContent
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'fixed left-1/2 top-1/2 z-[60] grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
|
||||
@@ -3,9 +3,8 @@ import type { DialogRootEmits, DialogRootProps } from 'reka-ui'
|
||||
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
||||
import { useForwardPropsEmits } from 'reka-ui'
|
||||
import Command from './Command.vue'
|
||||
import type { DialogID } from '@/components/ui/dialog-provider/utils';
|
||||
|
||||
const props = defineProps<DialogRootProps & { dialogId: DialogID }>();
|
||||
const props = defineProps<DialogRootProps & { dialogId: string }>();
|
||||
const emits = defineEmits<DialogRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
|
||||
@@ -1,59 +1,25 @@
|
||||
<!-- DialogProvider.vue -->
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import {
|
||||
provideDialogContext,
|
||||
type DialogID,
|
||||
type DialogParamsMap,
|
||||
} from './utils';
|
||||
import { ref, reactive, computed } from "vue";
|
||||
import { provideDialogContext } from "./utils";
|
||||
|
||||
const activeDialog = ref<DialogID | null>(null);
|
||||
const activeDialog = ref<string | null>(null);
|
||||
const activeAlerts = reactive<string[]>([]);
|
||||
const openDialogCallbacks = new Map<DialogID, (params: any) => void>();
|
||||
|
||||
// onClose for the currently-open dialog (only one dialog can be active)
|
||||
let activeOnCloseCallback: ((result?: any) => void) | undefined;
|
||||
|
||||
const registerOpenDialogCallback = <T extends DialogID>(
|
||||
dialogId: T,
|
||||
callback: (params?: T extends keyof DialogParamsMap ? DialogParamsMap[T] : undefined) => void
|
||||
) => {
|
||||
openDialogCallbacks.set(dialogId, callback as (params: any) => void);
|
||||
return () => {
|
||||
openDialogCallbacks.delete(dialogId);
|
||||
};
|
||||
};
|
||||
|
||||
const openDialog = <T extends DialogID>(dialogId: T, options?: any) => {
|
||||
const openDialog = (dialogId: string) => {
|
||||
if (activeAlerts.length > 0) return;
|
||||
|
||||
activeDialog.value = dialogId;
|
||||
activeOnCloseCallback = options?.onClose;
|
||||
|
||||
const openCallback = openDialogCallbacks.get(dialogId);
|
||||
if (openCallback) {
|
||||
openCallback(options?.params);
|
||||
}
|
||||
};
|
||||
|
||||
function closeDialog(dialogId?: DialogID, result?: any) {
|
||||
// No dialogId passed -> close current active dialog without result
|
||||
if (!dialogId) {
|
||||
if (activeDialog.value) {
|
||||
// call onClose (if any) with no result
|
||||
activeOnCloseCallback?.(undefined);
|
||||
activeOnCloseCallback = undefined;
|
||||
const closeDialog = (dialogId?: string) => {
|
||||
if (dialogId) {
|
||||
if (activeDialog.value === dialogId) {
|
||||
activeDialog.value = null;
|
||||
}
|
||||
activeDialog.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// dialogId passed -> if it's the active dialog, call onClose with result
|
||||
if (activeDialog.value && activeDialog.value === dialogId) {
|
||||
activeOnCloseCallback?.(result);
|
||||
activeOnCloseCallback = undefined;
|
||||
} else {
|
||||
activeDialog.value = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const addAlert = (alertId: string) => {
|
||||
activeAlerts.push(alertId);
|
||||
@@ -61,13 +27,14 @@
|
||||
|
||||
const removeAlert = (alertId: string) => {
|
||||
const index = activeAlerts.indexOf(alertId);
|
||||
if (index !== -1) activeAlerts.splice(index, 1);
|
||||
if (index !== -1) {
|
||||
activeAlerts.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
// Provide context to child components
|
||||
provideDialogContext({
|
||||
activeDialog: computed(() => activeDialog.value),
|
||||
registerOpenDialogCallback,
|
||||
openDialog,
|
||||
closeDialog,
|
||||
activeAlerts: computed(() => activeAlerts),
|
||||
|
||||
@@ -1,188 +1,56 @@
|
||||
import { computed, type ComputedRef } from 'vue';
|
||||
import { createContext } from 'reka-ui';
|
||||
import { useMagicKeys, useActiveElement } from '@vueuse/core';
|
||||
import type { BarcodeProduct } from '~~/lib/api/types/data-contracts';
|
||||
|
||||
export enum DialogID {
|
||||
AttachmentEdit = 'attachment-edit',
|
||||
ChangePassword = 'changePassword',
|
||||
CreateItem = 'create-item',
|
||||
CreateLocation = 'create-location',
|
||||
CreateLabel = 'create-label',
|
||||
CreateNotifier = 'create-notifier',
|
||||
DuplicateSettings = 'duplicate-settings',
|
||||
DuplicateTemporarySettings = 'duplicate-temporary-settings',
|
||||
EditMaintenance = 'edit-maintenance',
|
||||
Import = 'import',
|
||||
ItemImage = 'item-image',
|
||||
ItemTableSettings = 'item-table-settings',
|
||||
PrintLabel = 'print-label',
|
||||
ProductImport = 'product-import',
|
||||
QuickMenu = 'quick-menu',
|
||||
Scanner = 'scanner',
|
||||
PageQRCode = 'page-qr-code',
|
||||
UpdateLabel = 'update-label',
|
||||
UpdateLocation = 'update-location',
|
||||
}
|
||||
|
||||
/**
|
||||
* - Keys present without ? => params required
|
||||
* - Keys present with ? => params optional
|
||||
* - Keys not present => no params allowed
|
||||
*/
|
||||
export type DialogParamsMap = {
|
||||
[DialogID.ItemImage]:
|
||||
| ({
|
||||
type: 'preloaded';
|
||||
originalSrc: string;
|
||||
originalType?: string;
|
||||
thumbnailSrc?: string;
|
||||
}
|
||||
| {
|
||||
type: 'attachment';
|
||||
mimeType: string;
|
||||
thumbnailId?: string;
|
||||
}) & {
|
||||
itemId: string;
|
||||
attachmentId: string;
|
||||
};
|
||||
[DialogID.CreateItem]?: { product?: BarcodeProduct };
|
||||
[DialogID.ProductImport]?: { barcode?: string };
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines the payload type for a dialog's onClose callback.
|
||||
*/
|
||||
export type DialogResultMap = {
|
||||
[DialogID.ItemImage]?: { action: 'delete', id: string };
|
||||
};
|
||||
|
||||
/** Helpers to split IDs by requirement */
|
||||
type OptionalKeys<T> = {
|
||||
[K in keyof T]-?: {} extends Pick<T, K> ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
type RequiredKeys<T> = Exclude<keyof T, OptionalKeys<T>>;
|
||||
|
||||
type SpecifiedDialogIDs = keyof DialogParamsMap;
|
||||
export type NoParamDialogIDs = Exclude<DialogID, SpecifiedDialogIDs>;
|
||||
export type RequiredDialogIDs = RequiredKeys<DialogParamsMap>;
|
||||
export type OptionalDialogIDs = OptionalKeys<DialogParamsMap>;
|
||||
|
||||
type ParamsOf<T extends DialogID> = T extends SpecifiedDialogIDs
|
||||
? DialogParamsMap[T]
|
||||
: never;
|
||||
|
||||
type ResultOf<T extends DialogID> = T extends keyof DialogResultMap
|
||||
? DialogResultMap[T]
|
||||
: void;
|
||||
|
||||
type OpenDialog = {
|
||||
// Dialogs with no parameters
|
||||
<T extends NoParamDialogIDs>(
|
||||
dialogId: T,
|
||||
options?: { onClose?: (result?: ResultOf<T>) => void; params?: never }
|
||||
): void;
|
||||
// Dialogs with required parameters
|
||||
<T extends RequiredDialogIDs>(
|
||||
dialogId: T,
|
||||
options: { params: ParamsOf<T>; onClose?: (result?: ResultOf<T>) => void }
|
||||
): void;
|
||||
// Dialogs with optional parameters
|
||||
<T extends OptionalDialogIDs>(
|
||||
dialogId: T,
|
||||
options?: { params?: ParamsOf<T>; onClose?: (result?: ResultOf<T>) => void }
|
||||
): void;
|
||||
};
|
||||
|
||||
type CloseDialog = {
|
||||
// Close the currently active dialog, no ID specified. No result payload.
|
||||
(): void;
|
||||
// Close a specific dialog that has a defined result type.
|
||||
<T extends keyof DialogResultMap>(dialogId: T, result?: ResultOf<T>): void;
|
||||
// Close a specific dialog that has NO defined result type.
|
||||
<T extends Exclude<DialogID, keyof DialogResultMap>>(
|
||||
dialogId: T,
|
||||
result?: never
|
||||
): void;
|
||||
};
|
||||
|
||||
type OpenCallback = {
|
||||
<T extends NoParamDialogIDs>(dialogId: T, cb: () => void): () => void;
|
||||
<T extends RequiredDialogIDs>(
|
||||
dialogId: T,
|
||||
cb: (params: ParamsOf<T>) => void
|
||||
): () => void;
|
||||
<T extends OptionalDialogIDs>(
|
||||
dialogId: T,
|
||||
cb: (params?: ParamsOf<T>) => void
|
||||
): () => void;
|
||||
};
|
||||
import type { ComputedRef } from "vue";
|
||||
import { createContext } from "reka-ui";
|
||||
import { useMagicKeys, useActiveElement } from "@vueuse/core";
|
||||
|
||||
export const [useDialog, provideDialogContext] = createContext<{
|
||||
activeDialog: ComputedRef<DialogID | null>;
|
||||
activeDialog: ComputedRef<string | null>;
|
||||
activeAlerts: ComputedRef<string[]>;
|
||||
registerOpenDialogCallback: OpenCallback;
|
||||
openDialog: OpenDialog;
|
||||
closeDialog: CloseDialog;
|
||||
openDialog: (dialogId: string) => void;
|
||||
closeDialog: (dialogId?: string) => void;
|
||||
addAlert: (alertId: string) => void;
|
||||
removeAlert: (alertId: string) => void;
|
||||
}>('DialogProvider');
|
||||
}>("DialogProvider");
|
||||
|
||||
/**
|
||||
* Hotkey helper:
|
||||
* - No/optional params: pass dialogId + key
|
||||
* - Required params: pass dialogId + key + getParams()
|
||||
*/
|
||||
type HotkeyKey = {
|
||||
shift?: boolean;
|
||||
ctrl?: boolean;
|
||||
code: string;
|
||||
};
|
||||
|
||||
export function useDialogHotkey<T extends NoParamDialogIDs | OptionalDialogIDs>(
|
||||
dialogId: T,
|
||||
key: HotkeyKey
|
||||
): void;
|
||||
export function useDialogHotkey<T extends RequiredDialogIDs>(
|
||||
dialogId: T,
|
||||
key: HotkeyKey,
|
||||
getParams: () => ParamsOf<T>
|
||||
): void;
|
||||
export function useDialogHotkey(
|
||||
dialogId: DialogID,
|
||||
key: HotkeyKey,
|
||||
getParams?: () => unknown
|
||||
) {
|
||||
export const useDialogHotkey = (
|
||||
dialogId: string,
|
||||
key: {
|
||||
shift?: boolean;
|
||||
ctrl?: boolean;
|
||||
code: string;
|
||||
}
|
||||
) => {
|
||||
const { openDialog } = useDialog();
|
||||
|
||||
const activeElement = useActiveElement();
|
||||
|
||||
const notUsingInput = computed(
|
||||
() =>
|
||||
activeElement.value?.tagName !== 'INPUT' &&
|
||||
activeElement.value?.tagName !== 'TEXTAREA'
|
||||
() => activeElement.value?.tagName !== "INPUT" && activeElement.value?.tagName !== "TEXTAREA"
|
||||
);
|
||||
|
||||
useMagicKeys({
|
||||
passive: false,
|
||||
onEventFired: (event) => {
|
||||
onEventFired: event => {
|
||||
// console.log({
|
||||
// event,
|
||||
// notUsingInput: notUsingInput.value,
|
||||
// eventType: event.type,
|
||||
// keyCode: event.code,
|
||||
// matchingKeyCode: key.code === event.code,
|
||||
// shift: event.shiftKey,
|
||||
// matchingShift: key.shift === undefined || event.shiftKey === key.shift,
|
||||
// ctrl: event.ctrlKey,
|
||||
// matchingCtrl: key.ctrl === undefined || event.ctrlKey === key.ctrl,
|
||||
// });
|
||||
if (
|
||||
notUsingInput.value &&
|
||||
event.type === 'keydown' &&
|
||||
event.type === "keydown" &&
|
||||
event.code === key.code &&
|
||||
(key.shift === undefined || event.shiftKey === key.shift) &&
|
||||
(key.ctrl === undefined || event.ctrlKey === key.ctrl)
|
||||
) {
|
||||
if (getParams) {
|
||||
openDialog(dialogId as RequiredDialogIDs, {
|
||||
params: getParams() as never,
|
||||
});
|
||||
} else {
|
||||
openDialog(dialogId as NoParamDialogIDs);
|
||||
}
|
||||
openDialog(dialogId);
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogRoot, type DialogRootEmits, type DialogRootProps, useForwardPropsEmits } from "reka-ui";
|
||||
import { useDialog, type DialogID } from "@/components/ui/dialog-provider/utils";
|
||||
import { useDialog } from "../dialog-provider/utils";
|
||||
|
||||
const props = defineProps<DialogRootProps & { dialogId: DialogID }>();
|
||||
const props = defineProps<DialogRootProps & { dialogId: string }>();
|
||||
const emits = defineEmits<DialogRootEmits>();
|
||||
|
||||
const { closeDialog, activeDialog } = useDialog();
|
||||
|
||||
const isOpen = computed(() => (activeDialog.value && activeDialog.value === props.dialogId));
|
||||
const isOpen = computed(() => activeDialog.value === props.dialogId);
|
||||
const onOpenChange = (open: boolean) => {
|
||||
if (!open) closeDialog(props.dialogId as any);
|
||||
if (!open) closeDialog(props.dialogId);
|
||||
};
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
import type { DrawerRootEmits, DrawerRootProps } from "vaul-vue";
|
||||
import { useForwardPropsEmits } from "reka-ui";
|
||||
import { DrawerRoot } from "vaul-vue";
|
||||
import { DialogID, useDialog } from "@/components/ui/dialog-provider/utils";
|
||||
import { useDialog } from "../dialog-provider/utils";
|
||||
|
||||
const props = withDefaults(defineProps<DrawerRootProps & { dialogId: string }>(), {
|
||||
shouldScaleBackground: true,
|
||||
}) as DrawerRootProps & { dialogId: DialogID };
|
||||
}) as DrawerRootProps & { dialogId: string };
|
||||
|
||||
const emits = defineEmits<DrawerRootEmits>();
|
||||
|
||||
const { closeDialog, activeDialog } = useDialog();
|
||||
|
||||
const isOpen = computed(() => activeDialog.value !== null && activeDialog.value === props.dialogId);
|
||||
const isOpen = computed(() => activeDialog.value === props.dialogId);
|
||||
const onOpenChange = (open: boolean) => {
|
||||
if (!open) closeDialog(props.dialogId as any);
|
||||
if (!open) closeDialog(props.dialogId);
|
||||
};
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
|
||||
@@ -4,13 +4,6 @@ import type { DaisyTheme } from "~~/lib/data/themes";
|
||||
|
||||
export type ViewType = "table" | "card" | "tree";
|
||||
|
||||
export type DuplicateSettings = {
|
||||
copyMaintenance: boolean;
|
||||
copyAttachments: boolean;
|
||||
copyCustomFields: boolean;
|
||||
copyPrefixOverride: string | null;
|
||||
};
|
||||
|
||||
export type LocationViewPreferences = {
|
||||
showDetails: boolean;
|
||||
showEmpty: boolean;
|
||||
@@ -22,7 +15,6 @@ export type LocationViewPreferences = {
|
||||
displayLegacyHeader: boolean;
|
||||
language?: string;
|
||||
overrideFormatLocale?: string;
|
||||
duplicateSettings: DuplicateSettings;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -42,12 +34,6 @@ export function useViewPreferences(): Ref<LocationViewPreferences> {
|
||||
displayLegacyHeader: false,
|
||||
language: null,
|
||||
overrideFormatLocale: null,
|
||||
duplicateSettings: {
|
||||
copyMaintenance: false,
|
||||
copyAttachments: true,
|
||||
copyCustomFields: true,
|
||||
copyPrefixOverride: null,
|
||||
},
|
||||
},
|
||||
{ mergeDefaults: true }
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ComputedRef } from "vue";
|
||||
import { type DaisyTheme } from "~~/lib/data/themes";
|
||||
import type { DaisyTheme } from "~~/lib/data/themes";
|
||||
|
||||
export interface UseTheme {
|
||||
theme: ComputedRef<DaisyTheme>;
|
||||
@@ -42,11 +42,27 @@ export function useTheme(): UseTheme {
|
||||
return { theme, setTheme };
|
||||
}
|
||||
|
||||
export function useIsThemeInList(list: DaisyTheme[]) {
|
||||
export function useIsDark() {
|
||||
const theme = useTheme();
|
||||
|
||||
const darkthemes = [
|
||||
"synthwave",
|
||||
"retro",
|
||||
"cyberpunk",
|
||||
"valentine",
|
||||
"halloween",
|
||||
"forest",
|
||||
"aqua",
|
||||
"black",
|
||||
"luxury",
|
||||
"dracula",
|
||||
"business",
|
||||
"night",
|
||||
"coffee",
|
||||
];
|
||||
|
||||
return computed(() => {
|
||||
return list.includes(theme.theme.value);
|
||||
return darkthemes.includes(theme.theme.value);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
<ItemCreateModal />
|
||||
<LabelCreateModal />
|
||||
<LocationCreateModal />
|
||||
<ItemBarcodeModal />
|
||||
<AppQuickMenuModal :actions="quickMenuActions" />
|
||||
<AppScannerModal />
|
||||
<SidebarProvider :default-open="sidebarState">
|
||||
@@ -42,7 +41,7 @@
|
||||
v-for="btn in dropdown"
|
||||
:key="btn.id"
|
||||
class="group cursor-pointer text-lg"
|
||||
@click="openDialog(btn.dialogId as NoParamDialogIDs)"
|
||||
@click="openDialog(btn.dialogId)"
|
||||
>
|
||||
{{ btn.name.value }}
|
||||
<Shortcut
|
||||
@@ -79,7 +78,7 @@
|
||||
'text-nowrap': typeof locale === 'string' && locale.startsWith('zh-'),
|
||||
}"
|
||||
:tooltip="$t('menu.scanner')"
|
||||
@click.prevent="openDialog(DialogID.Scanner)"
|
||||
@click.prevent="openDialog('scanner')"
|
||||
>
|
||||
<MdiQrcodeScan />
|
||||
<span>{{ $t("menu.scanner") }}</span>
|
||||
@@ -210,7 +209,6 @@
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { toast } from "@/components/ui/sonner";
|
||||
import { DialogID, type NoParamDialogIDs, type OptionalDialogIDs } from "~/components/ui/dialog-provider/utils";
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const username = computed(() => authCtx.user?.name || "User");
|
||||
@@ -251,7 +249,7 @@
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({ video: true })
|
||||
.then(() => {
|
||||
openDialog(DialogID.Scanner);
|
||||
openDialog("scanner");
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
@@ -265,31 +263,24 @@
|
||||
// Preload currency format
|
||||
useFormatCurrency();
|
||||
|
||||
type DropdownItem = {
|
||||
id: number;
|
||||
name: ComputedRef<string>;
|
||||
shortcut: string;
|
||||
dialogId: NoParamDialogIDs | OptionalDialogIDs;
|
||||
};
|
||||
|
||||
const dropdown: DropdownItem[] = [
|
||||
const dropdown = [
|
||||
{
|
||||
id: 0,
|
||||
name: computed(() => t("menu.create_item")),
|
||||
shortcut: "Shift+1",
|
||||
dialogId: DialogID.CreateItem,
|
||||
dialogId: "create-item",
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: computed(() => t("menu.create_location")),
|
||||
shortcut: "Shift+3",
|
||||
dialogId: DialogID.CreateLocation,
|
||||
dialogId: "create-location",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: computed(() => t("menu.create_label")),
|
||||
shortcut: "Shift+2",
|
||||
dialogId: DialogID.CreateLabel,
|
||||
dialogId: "create-label",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -343,7 +334,7 @@
|
||||
const quickMenuActions = reactive([
|
||||
...dropdown.map(v => ({
|
||||
text: computed(() => v.name.value),
|
||||
dialogId: v.dialogId as NoParamDialogIDs,
|
||||
dialogId: v.dialogId,
|
||||
shortcut: v.shortcut.split("+")[1],
|
||||
type: "create" as const,
|
||||
})),
|
||||
|
||||
@@ -153,26 +153,6 @@ export class ItemsApi extends BaseAPI {
|
||||
return resp;
|
||||
}
|
||||
|
||||
duplicate(
|
||||
id: string,
|
||||
options: {
|
||||
copyMaintenance?: boolean;
|
||||
copyAttachments?: boolean;
|
||||
copyCustomFields?: boolean;
|
||||
copyPrefix?: string;
|
||||
} = {}
|
||||
) {
|
||||
return this.http.post<typeof options, ItemOut>({
|
||||
url: route(`/items/${id}/duplicate`),
|
||||
body: {
|
||||
copyMaintenance: options.copyMaintenance,
|
||||
copyAttachments: options.copyAttachments,
|
||||
copyCustomFields: options.copyCustomFields,
|
||||
copyPrefix: options.copyPrefix,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
import(file: File | Blob) {
|
||||
const formData = new FormData();
|
||||
formData.append("csv", file);
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { BaseAPI, route } from "../base";
|
||||
import type { BarcodeProduct } from "../types/data-contracts";
|
||||
|
||||
export class ProductAPI extends BaseAPI {
|
||||
searchFromBarcode(productEAN: string) {
|
||||
return this.http.get<BarcodeProduct[]>({ url: route(`/products/search-from-barcode`, { productEAN }) });
|
||||
}
|
||||
}
|
||||
@@ -451,19 +451,6 @@ export interface EntUserEdges {
|
||||
notifiers: EntNotifier[];
|
||||
}
|
||||
|
||||
export interface BarcodeProduct {
|
||||
barcode: string;
|
||||
imageBase64: string;
|
||||
imageURL: string;
|
||||
item: ItemCreate;
|
||||
manufacturer: string;
|
||||
/** Identifications */
|
||||
modelNumber: string;
|
||||
/** Extras */
|
||||
notes: string;
|
||||
search_engine_name: string;
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
createdAt: Date | string;
|
||||
currency: string;
|
||||
@@ -644,7 +631,7 @@ export interface ItemUpdate {
|
||||
|
||||
export interface LabelCreate {
|
||||
color: string;
|
||||
/** @maxLength 1000 */
|
||||
/** @maxLength 255 */
|
||||
description: string;
|
||||
/**
|
||||
* @minLength 1
|
||||
|
||||
@@ -10,7 +10,6 @@ import { AssetsApi } from "./classes/assets";
|
||||
import { ReportsAPI } from "./classes/reports";
|
||||
import { NotifiersAPI } from "./classes/notifiers";
|
||||
import { MaintenanceAPI } from "./classes/maintenance";
|
||||
import { ProductAPI } from "./classes/product";
|
||||
import type { Requests } from "~~/lib/requests";
|
||||
|
||||
export class UserClient extends BaseAPI {
|
||||
@@ -25,7 +24,6 @@ export class UserClient extends BaseAPI {
|
||||
assets: AssetsApi;
|
||||
reports: ReportsAPI;
|
||||
notifiers: NotifiersAPI;
|
||||
products: ProductAPI;
|
||||
|
||||
constructor(requests: Requests, attachmentToken: string) {
|
||||
super(requests, attachmentToken);
|
||||
@@ -41,7 +39,6 @@ export class UserClient extends BaseAPI {
|
||||
this.assets = new AssetsApi(requests);
|
||||
this.reports = new ReportsAPI(requests);
|
||||
this.notifiers = new NotifiersAPI(requests);
|
||||
this.products = new ProductAPI(requests);
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
@@ -153,19 +153,3 @@ export const themes: ThemeOption[] = [
|
||||
value: "winter",
|
||||
},
|
||||
];
|
||||
|
||||
export const darkThemes: DaisyTheme[] = [
|
||||
"synthwave",
|
||||
"retro",
|
||||
"cyberpunk",
|
||||
"valentine",
|
||||
"halloween",
|
||||
"forest",
|
||||
"aqua",
|
||||
"black",
|
||||
"luxury",
|
||||
"dracula",
|
||||
"business",
|
||||
"night",
|
||||
"coffee",
|
||||
];
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
"parent_location": "Ubicació pare"
|
||||
},
|
||||
"tree": {
|
||||
"no_locations": "No hi ha ubicacions disponibles. Afegiu ubicacions amb el botó\n '<span class=\"link-primary\">'Crea'</span>' a la barra de navegació."
|
||||
"no_locations": "No hi ha ubicacions disponibles. Afegiu ubicacions amb el botó\n `<`span class=\"link-primary\"`>`Crea`<`/span`>` a la barra de navegació."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -355,6 +355,6 @@
|
||||
"bill_of_materials": "Llista de materials",
|
||||
"bill_of_materials_button": "Genera llista de materials"
|
||||
},
|
||||
"reports_sub": "Genera informes per a l'inventari."
|
||||
"reports_sub": "Genera informes per a l'inventari"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,8 +100,6 @@
|
||||
"item_photo": "Fotografie položky 📷",
|
||||
"item_quantity": "Množství položek",
|
||||
"parent_item": "Nadřazená položka",
|
||||
"product_tooltip_input_barcode": "Automatické vyplnění ručně zadaným čárovým kódem",
|
||||
"product_tooltip_scan_barcode": "Automatické vyplnění čárovým kódem z 📷",
|
||||
"rotate_photo": "Otočit fotku",
|
||||
"set_as_primary_photo": "Nastavit jako { isPrimary, select, true {non-} false {} other {}}primární fotku",
|
||||
"title": "Vytvořit položku",
|
||||
@@ -122,15 +120,6 @@
|
||||
"upload_photos": "Nahrát fotografie",
|
||||
"uploaded": "Fotka byla nahrána"
|
||||
},
|
||||
"product_import": {
|
||||
"barcode": "Čárový kód produktu",
|
||||
"db_source": "Zdroj DB",
|
||||
"error_exception": "Při načítání čárového kódu položky došlo k výjimce: ",
|
||||
"error_invalid_barcode": "Byl zadán neplatný čárový kód",
|
||||
"error_not_found": "Žádný produkt s daným čárovým kódem nebyl nalezen.",
|
||||
"search_item": "Vyhledat produkt",
|
||||
"title": "Importovat produkt"
|
||||
},
|
||||
"selector": {
|
||||
"no_results": "Nebyly nalezeny žádné výsledky",
|
||||
"placeholder": "Vyberte…",
|
||||
@@ -195,9 +184,6 @@
|
||||
"shortcut_hint": "Pomocí číselných tlačítek rychle vyberte akci."
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"api_failure": "Volání backendového API selhalo: "
|
||||
},
|
||||
"global": {
|
||||
"add": "Přidat",
|
||||
"archived": "Archivované",
|
||||
@@ -573,8 +559,6 @@
|
||||
}
|
||||
},
|
||||
"scanner": {
|
||||
"barcode_detected_message": "byl zjištěn čárový kód produktu",
|
||||
"barcode_fetch_data": "Načíst produktová data",
|
||||
"error": "Při skenování došlo k chybě",
|
||||
"invalid_url": "Neplatná adresa URL čárového kódu",
|
||||
"no_sources": "Nejsou k dispozici žádné zdroje videa",
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"create_modal": {
|
||||
"createAndAddAnother": "Brug {shiftKey} + {enterKey} til at oprette og tilføje en ny.",
|
||||
"enter": "Indtast",
|
||||
"shift": "Flytte"
|
||||
},
|
||||
"import_dialog": {
|
||||
"change_warning": "Adfærd for imports med eksisterende import_refs har ændret sig. Hvis en import_ref er tilstede i CSV filen,\nvil genstanden blive opdateret med værdierne fra CSV filen.",
|
||||
"description": "Importer en CSV fil som indeholder dine genstande, etiketter, og lokationer. Se dokumentation for mere information vedrørende\nden korrekte format.",
|
||||
"title": "Importer CSV Fil",
|
||||
"toast": {
|
||||
"import_failed": "Importen mislykkedes. Prøv igen senere.",
|
||||
"import_success": "Importen er gennemført!",
|
||||
"please_select_file": "Vælg venligst en fil, der skal importeres."
|
||||
}
|
||||
"title": "Importer CSV Fil"
|
||||
},
|
||||
"outdated": {
|
||||
"current_version": "Nuværende version",
|
||||
@@ -24,18 +14,6 @@
|
||||
"new_version_available_link": "Klik her for at læse udgivelsesnoterne"
|
||||
}
|
||||
},
|
||||
"color_selector": {
|
||||
"clear": "Reset farve",
|
||||
"color": "Farve",
|
||||
"no_color": "Ingen farve",
|
||||
"no_color_selected": "Ingen farve valgt",
|
||||
"randomize": "Tilfældig farve"
|
||||
},
|
||||
"form": {
|
||||
"password": {
|
||||
"toggle_show": "Slå adgangskode til/fra Vis"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"copy_text": {
|
||||
"documentation": "Dokumentation",
|
||||
@@ -73,12 +51,7 @@
|
||||
"download": "Hent label",
|
||||
"print": "Print label",
|
||||
"server_print": "Print på Server",
|
||||
"titles": "Labels",
|
||||
"toast": {
|
||||
"load_status_failed": "Status kunne ikke indlæses",
|
||||
"print_failed": "Kunne ikke udskrive etiketten",
|
||||
"print_success": "Etiket udskrevet"
|
||||
}
|
||||
"titles": "Labels"
|
||||
},
|
||||
"page_qr_code": {
|
||||
"page_url": "Side URL",
|
||||
@@ -89,50 +62,12 @@
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"attachments_list": {
|
||||
"download": "Download",
|
||||
"open_new_tab": "Åbn i ny fane"
|
||||
},
|
||||
"create_modal": {
|
||||
"delete_photo": "Slet billede",
|
||||
"item_description": "Genstandsbeskrivelse",
|
||||
"item_name": "Genstandsnavn",
|
||||
"item_photo": "Vare Foto 📷",
|
||||
"item_quantity": "Vare Antal",
|
||||
"parent_item": "Overordnet element",
|
||||
"product_tooltip_scan_barcode": "Fyld automatisk med stregkode fra 📷",
|
||||
"rotate_photo": "Roter foto",
|
||||
"set_as_primary_photo": "Sæt som { isPrimary, select, true {non-} false {} other {}}primært foto",
|
||||
"title": "Opret genstand",
|
||||
"toast": {
|
||||
"already_creating": "Opretter allerede et element",
|
||||
"create_failed": "Kunne ikke oprette elementet",
|
||||
"create_success": "Element oprettet",
|
||||
"failed_load_parent": "Kunne ikke indlæse overordnet element - vælg venligst manuelt",
|
||||
"no_canvas_support": "Din browser understøtter ikke canvas-handlinger",
|
||||
"please_select_location": "Vælg venligst en placering.",
|
||||
"rotate_failed": "Kunne ikke rotere billedet: { error }",
|
||||
"rotate_process_failed": "Kunne ikke behandle roteret billede",
|
||||
"some_photos_failed": "{count, plural, =0 {Ingen billeder at uploade.} =1 {1 billede kunne ikke uploades.} other {Nogle billeder kunne ikke uploades.}}",
|
||||
"upload_failed": "Kunne ikke uploade billede: { photoName }",
|
||||
"upload_success": "{count, plural, =0 {Ingen billeder uploadet.} =1 {Foto uploadet.} other {Alle billeder uploadet.}}",
|
||||
"uploading_photos": "{count, plural, =0 {Ingen billeder at uploade} =1 {Uploader 1 billede…} other {Uploader {count} billeder…}}"
|
||||
},
|
||||
"upload_photos": "Upload Billeder",
|
||||
"uploaded": "Uploadet billede"
|
||||
},
|
||||
"product_import": {
|
||||
"barcode": "Produkts stregkode",
|
||||
"db_source": "DB kilde",
|
||||
"error_invalid_barcode": "Ugyldig stregkode angivet",
|
||||
"error_not_found": "Intet produkt fundet med angivet stregkode.",
|
||||
"search_item": "Søg produkt",
|
||||
"title": "Importér produkt"
|
||||
},
|
||||
"selector": {
|
||||
"no_results": "Ingen resultater fundet",
|
||||
"placeholder": "Vælg…",
|
||||
"search_placeholder": "Skriv for at søge…"
|
||||
"upload_photos": "Upload Billeder"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
@@ -142,26 +77,17 @@
|
||||
"table": "Tabel"
|
||||
},
|
||||
"table": {
|
||||
"headers": "Overskrifter",
|
||||
"page": "Side",
|
||||
"rows_per_page": "Rækker per side",
|
||||
"table_settings": "Tabel Indstillinger",
|
||||
"view_item": "Se vare"
|
||||
"table_settings": "Tabel Indstillinger"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"label_color": "Etiketfarve",
|
||||
"label_description": "Etiketbeskrivelse",
|
||||
"label_name": "Etiketnavn",
|
||||
"title": "Opret label",
|
||||
"toast": {
|
||||
"already_creating": "Allerede oprettet en etiket",
|
||||
"create_failed": "Kunne ikke oprette etiket",
|
||||
"create_success": "Etiket oprettet",
|
||||
"label_name_too_long": "Etiketnavnet må ikke være længere end 50 tegn"
|
||||
}
|
||||
"title": "Opret label"
|
||||
},
|
||||
"selector": {
|
||||
"select_labels": "Vælg Etiketter"
|
||||
@@ -171,74 +97,47 @@
|
||||
"create_modal": {
|
||||
"location_description": "Lokationsbeskrivelse",
|
||||
"location_name": "Lokationsnavn",
|
||||
"title": "Opret lokation",
|
||||
"toast": {
|
||||
"already_creating": "Allerede oprettet en lokation",
|
||||
"create_failed": "Kunne ikke oprette placering",
|
||||
"create_success": "Placering oprettet"
|
||||
}
|
||||
"title": "Opret lokation"
|
||||
},
|
||||
"selector": {
|
||||
"no_location_found": "Ingen placering fundet",
|
||||
"parent_location": "Forældrelokation",
|
||||
"search_location": "Søg efter placeringer",
|
||||
"select_location": "Vælg en placering"
|
||||
"parent_location": "Forældrelokation"
|
||||
},
|
||||
"tree": {
|
||||
"no_locations": "Ingen tilgængelige placeringer. Tilføj nye placeringer via knappen \n'<span class=\"link-primary\">'Opret'</span>' på navigationslinjen."
|
||||
"no_locations": "Ingen tilgængelige lokationer. Opret nye lokationer gennem\n`<`span class=\"link-primary\">`Opret`<`/span`>` knappen i navigationslinjen."
|
||||
}
|
||||
},
|
||||
"quick_menu": {
|
||||
"no_results": "Ingen resultater fundet.",
|
||||
"shortcut_hint": "Brug de numeriske taster til hurtigt at vælge en handling."
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"api_failure": "Backend API kald fejlede: "
|
||||
},
|
||||
"global": {
|
||||
"add": "Tilføj",
|
||||
"archived": "Arkiveret",
|
||||
"build": "Build: { build }",
|
||||
"cancel": "Ophæv",
|
||||
"confirm": "Bekræft",
|
||||
"create": "Opret",
|
||||
"create_and_add": "Opret og tilføj ny",
|
||||
"create_subitem": "Opret underelement",
|
||||
"created": "Oprettet",
|
||||
"delete": "Slet",
|
||||
"delete_confirm": "Er du sikker på, at du vil slette dette element? ",
|
||||
"demo_instance": "Dette er en demo-instans",
|
||||
"details": "Detaljer",
|
||||
"duplicate": "Dupliker",
|
||||
"edit": "Rediger",
|
||||
"email": "Email",
|
||||
"follow_dev": "Følg udvikleren",
|
||||
"footer": {
|
||||
"api_link": "'<a href=\"https://homebox.software/en/api/\" target=\"_blank\">'API'</a>'",
|
||||
"version_link": "'<'a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/{ version }\" target=\"_blank\"'>' Version: { version } Build: { build } '</a>'"
|
||||
},
|
||||
"github": "GitHub projekt",
|
||||
"insured": "Forsikret",
|
||||
"items": "Genstande",
|
||||
"join_discord": "Deltag i vores Discord",
|
||||
"labels": "Etiketter",
|
||||
"loading": "Indlæser…",
|
||||
"locations": "Lokationer",
|
||||
"maintenance": "Opretholdelse",
|
||||
"name": "Navn",
|
||||
"navigate": "Naviger",
|
||||
"password": "Adgangskode",
|
||||
"quantity": "Mængde",
|
||||
"read_docs": "Læs Docs",
|
||||
"return_home": "Vend hjem",
|
||||
"save": "Gem",
|
||||
"search": "Søg",
|
||||
"sign_out": "Log ud",
|
||||
"submit": "Indsend",
|
||||
"unknown": "Ukendt",
|
||||
"update": "Opdater",
|
||||
"updating": "Opdaterer",
|
||||
"value": "Værdi",
|
||||
"version": "Version: { version }",
|
||||
"welcome": "Velkommen, { username }"
|
||||
@@ -263,49 +162,27 @@
|
||||
"set_email": "Hvad er din E-Mail?",
|
||||
"set_name": "Hvad hedder du?",
|
||||
"set_password": "Opret din adgangskode",
|
||||
"tagline": "Følg, Organiser, og Håndter dine Ting.",
|
||||
"title": "Organiser og Tag dine ting",
|
||||
"toast": {
|
||||
"invalid_email": "Ugyldig e-mailadresse",
|
||||
"invalid_email_password": "Ugyldig e-mail eller adgangskode",
|
||||
"login_success": "Logget ind",
|
||||
"problem_registering": "Problem med at registrere bruger",
|
||||
"user_registered": "Bruger registreret"
|
||||
}
|
||||
"tagline": "Følg, Organiser, og Håndter dine Ting."
|
||||
},
|
||||
"items": {
|
||||
"add": "Tilføj",
|
||||
"advanced": "Avanceret",
|
||||
"archived": "Arkiveret",
|
||||
"asset_id": "Aktiv-id",
|
||||
"associated_with_multiple": "Dette aktiv-id er knyttet til flere varer",
|
||||
"attachment": "Vedhæftning",
|
||||
"attachments": "Vedhæftninger",
|
||||
"changes_persisted_immediately": "Ændringer af vedhæftede filer gemmes med det samme",
|
||||
"created_at": "Oprettet den",
|
||||
"custom_fields": "Brugerdefinerede felter",
|
||||
"delete_attachment_confirm": "Er du sikker på, at du vil slette denne vedhæftede fil?",
|
||||
"delete_item_confirm": "Er du sikker på, at du vil slette dette element?",
|
||||
"description": "Beskrivelse",
|
||||
"details": "Detaljer",
|
||||
"drag_and_drop": "Træk og slip filer her, eller klik for at vælge filer",
|
||||
"edit": {
|
||||
"edit_attachment_dialog": {
|
||||
"attachment_title": "Titel på vedhæftet fil",
|
||||
"attachment_type": "Vedhæftningstype",
|
||||
"primary_photo": "Primært foto",
|
||||
"primary_photo_sub": "Denne mulighed er kun tilgængelig for fotos. Kun ét foto kan være primært. Hvis du vælger denne mulighed, vil det aktuelle primære foto, hvis der er et, blive fravalgt.",
|
||||
"select_type": "Vælg en type",
|
||||
"title": "Rediger vedhæftet fil"
|
||||
}
|
||||
},
|
||||
"edit_details": "Rediger detaljer",
|
||||
"field_selector": "Feltvælger",
|
||||
"field_value": "Feltværdi",
|
||||
"first": "Første",
|
||||
"include_archive": "Medtag arkiverede elementer",
|
||||
"insured": "Forsikret",
|
||||
"invalid_asset_id": "Ugyldigt aktiv-ID",
|
||||
"last": "Sidst",
|
||||
"lifetime_warranty": "livstidsgaranti",
|
||||
"location": "Lokalitet",
|
||||
@@ -316,7 +193,6 @@
|
||||
"name": "Navn",
|
||||
"negate_labels": "Ophæv valgte etiketter",
|
||||
"next_page": "Næste side",
|
||||
"no_attachments": "Ingen vedhæftede filer fundet",
|
||||
"no_results": "Ingen elementer fundet",
|
||||
"notes": "Noter",
|
||||
"only_with_photo": "Kun elementer med foto",
|
||||
@@ -338,77 +214,35 @@
|
||||
"receipts": "Kvitteringer",
|
||||
"reset_search": "Nulstil Søgning",
|
||||
"results": "{ total } Wyniki",
|
||||
"select_field": "Vælg et felt",
|
||||
"serial_number": "Serienummer",
|
||||
"show_advanced_view_options": "vis avancerede indstillinger",
|
||||
"sold_at": "Solgt D.",
|
||||
"sold_details": "Salgs detaljer",
|
||||
"sold_price": "Solgt pris",
|
||||
"sold_to": "Sold til",
|
||||
"sync_child_locations": "Synkroniser placeringer af underordnede elementer",
|
||||
"tip_1": "Placerings- og etiketfiltre bruger betjeningen 'ELLER'. Hvis mere end én er valgt, kræves der kun én\n til et match.",
|
||||
"tip_2": "Søgninger med præfikset '#'' vil forespørge efter et aktiv-id (eksempel '#000-001')",
|
||||
"tip_3": "Feltfiltre bruger handlingen 'ELLER'. Hvis mere end én er valgt, kræves der kun én til en\n kamp.",
|
||||
"tips": "Tips",
|
||||
"tips_sub": "Søgetips",
|
||||
"toast": {
|
||||
"asset_not_found": "Aktivet blev ikke fundet",
|
||||
"attachment_deleted": "Vedhæftet fil slettet",
|
||||
"attachment_updated": "Vedhæftet fil opdateret",
|
||||
"attachment_uploaded": "Vedhæftet fil uploadet",
|
||||
"child_items_location_no_longer_synced": "Placeringen af underelementer vil ikke længere blive synkroniseret med dette element.",
|
||||
"child_items_location_synced": "Placeringerne af underordnede elementer er blevet synkroniseret med dette element",
|
||||
"child_location_desync": "Ændring af placering vil afsynkronisere den fra forælderens placering",
|
||||
"error_loading_parent_data": "Noget gik galt under indlæsning af overordnede data",
|
||||
"failed_adjust_quantity": "Kunne ikke justere mængden",
|
||||
"failed_delete_attachment": "Kunne ikke slette vedhæftet fil",
|
||||
"failed_delete_item": "Kunne ikke slette elementet",
|
||||
"failed_duplicate_item": "Kunne ikke duplikere elementet",
|
||||
"failed_load_asset": "Kunne ikke indlæse aktiv",
|
||||
"failed_load_item": "Kunne ikke indlæse elementet",
|
||||
"failed_load_items": "Kunne ikke indlæse elementer",
|
||||
"failed_save": "Kunne ikke gemme elementet",
|
||||
"failed_save_no_location": "Kunne ikke gemme elementet: ingen placering valgt",
|
||||
"failed_search_items": "Kunne ikke søge efter elementer",
|
||||
"failed_update_attachment": "Kunne ikke opdatere vedhæftet fil",
|
||||
"failed_upload_attachment": "Kunne ikke uploade vedhæftet fil",
|
||||
"item_deleted": "Element slettet",
|
||||
"item_saved": "Element gemt",
|
||||
"quantity_cannot_negative": "Mængden må ikke være negativ",
|
||||
"sync_child_location": "Den valgte forælder synkroniserer sine børns placeringer med sine egne. Placeringen er blevet opdateret."
|
||||
},
|
||||
"updated_at": "Opdateret d.",
|
||||
"warranty": "Garanti",
|
||||
"warranty_details": "Oplysninger om garanti",
|
||||
"warranty_expires": "Garantien udløber"
|
||||
},
|
||||
"labels": {
|
||||
"label_delete_confirm": "Er du sikker på, at du vil slette denne etiket? Denne handling kan ikke fortrydes.",
|
||||
"no_results": "Ingen etiketter fundet",
|
||||
"toast": {
|
||||
"failed_delete_label": "Etiketten kunne ikke slettes",
|
||||
"failed_load_label": "Etiketten kunne ikke indlæses",
|
||||
"failed_update_label": "Etiketten kunne ikke opdateres",
|
||||
"label_deleted": "Etiket slettet",
|
||||
"label_updated": "Etiket opdateret"
|
||||
},
|
||||
"update_label": "Opdater etiket"
|
||||
},
|
||||
"languages": {
|
||||
"ca": "Catalansk",
|
||||
"cs-CZ": "Tjekkisk",
|
||||
"de": "Tysk",
|
||||
"en": "Engelsk",
|
||||
"es": "Spansk",
|
||||
"fi-FI": "Finsk",
|
||||
"fr": "Fransk",
|
||||
"hu": "Ungarsk",
|
||||
"id-ID": "Indonesisk",
|
||||
"it": "Italiensk",
|
||||
"ja-JP": "Japansk",
|
||||
"ko-KR": "Koreansk",
|
||||
"lb-LU": "Luxembourgsk (Luxembourg)",
|
||||
"lt-LT": "Litauisk (Litauen)",
|
||||
"nb-NO": "Norsk",
|
||||
"nl": "Hollandsk",
|
||||
"pl": "Polsk",
|
||||
@@ -416,7 +250,6 @@
|
||||
"pt-PT": "Portugisisk (Portugal)",
|
||||
"ru": "Russisk",
|
||||
"sl": "Slovensk",
|
||||
"sq-AL": "Albansk",
|
||||
"sv": "Svensk",
|
||||
"ta-IN": "Tamilsk",
|
||||
"th-TH": "Thailandsk",
|
||||
@@ -433,16 +266,7 @@
|
||||
"locations": {
|
||||
"child_locations": "Underordnede placeringer",
|
||||
"collapse_tree": "Kollaps træ",
|
||||
"expand_tree": "Udvid træ",
|
||||
"location_items_delete_confirm": "Er du sikker på, at du vil slette denne placering og alle dens elementer? Denne handling kan ikke fortrydes.",
|
||||
"no_results": "Ingen placeringer fundet",
|
||||
"toast": {
|
||||
"failed_delete_location": "Kunne ikke slette placeringen",
|
||||
"failed_load_location": "Placeringen kunne ikke indlæses",
|
||||
"failed_update_location": "Kunne ikke opdatere placeringen",
|
||||
"location_deleted": "Placering slettet",
|
||||
"location_updated": "Placering opdateret"
|
||||
},
|
||||
"update_location": "Opdatér sted"
|
||||
},
|
||||
"maintenance": {
|
||||
@@ -498,10 +322,7 @@
|
||||
"currency_format": "Valuta format",
|
||||
"current_password": "Aktuel adgangskode",
|
||||
"delete_account": "Slet Konto",
|
||||
"delete_account_confirm": "Er du sikker på, at du vil slette din konto? Hvis du er det sidste medlem i din gruppe, vil alle dine data blive slettet. Denne handling kan ikke fortrydes.",
|
||||
"delete_account_sub": "Slet din konto og alle dens tilknyttede data. Dette kan ikke laves om.",
|
||||
"delete_notifier_confirm": "Er du sikker på, at du vil slette denne underretter?",
|
||||
"display_legacy_header": "{ currentValue, select, true {Deaktiver Legacy Header} false {Aktiver Legacy Header} other {Ikke ramt}}",
|
||||
"enabled": "Aktiveret",
|
||||
"example": "Eksempel",
|
||||
"gen_invite": "Generer invitationslink",
|
||||
@@ -511,97 +332,39 @@
|
||||
"language": "Sprog",
|
||||
"new_password": "Ny Adgangskode",
|
||||
"no_notifiers": "Ingen notifikationer konfiguret",
|
||||
"no_override": "Ingen tilsidesættelse",
|
||||
"notifier_modal": "{ type, select, true {Rediger} false {Opret} other {Andet}} Meddeler",
|
||||
"notifiers": "Meddelere",
|
||||
"notifiers_sub": "Få notifikationer om kommende vedligeholdelsespåmindelser",
|
||||
"override_locale": "Tilsidesæt dato og valutasprog",
|
||||
"test": "Test",
|
||||
"theme_settings": "Temaindstillinger",
|
||||
"theme_settings_sub": "Temaindstillinger gemmes i din browsers lokale lager. Du kan til enhver tid ændre temaet. Hvis du har\n problemer med at indstille dit tema, kan du prøve at opdatere din browser.",
|
||||
"toast": {
|
||||
"account_deleted": "Din konto er blevet slettet.",
|
||||
"failed_change_password": "Kunne ikke ændre adgangskode.",
|
||||
"failed_create_notifier": "Kunne ikke oprette underretteren.",
|
||||
"failed_delete_account": "Kunne ikke slette din konto.",
|
||||
"failed_delete_notifier": "Kunne ikke slette underretteren.",
|
||||
"failed_get_currencies": "Kunne ikke hente valutaer",
|
||||
"failed_test_notifier": "Kunne ikke teste underretteren.",
|
||||
"failed_update_group": "Gruppen kunne ikke opdateres",
|
||||
"failed_update_notifier": "Kunne ikke opdatere underretteren.",
|
||||
"group_updated": "Gruppen er opdateret",
|
||||
"notifier_test_success": "Underretter-testen er gennemført.",
|
||||
"password_changed": "Adgangskoden er ændret."
|
||||
},
|
||||
"update_group": "Opdatér Gruppe",
|
||||
"update_language": "Opdater sprogfil",
|
||||
"url": "URL",
|
||||
"user_profile": "Brugerprofil",
|
||||
"user_profile_sub": "Inviter brugere, og administrer din konto."
|
||||
},
|
||||
"reports": {
|
||||
"label_generator": {
|
||||
"asset_end": "Aktivets slut",
|
||||
"asset_start": "Aktivets start",
|
||||
"base_url": "Base URL",
|
||||
"bordered_labels": "Etiketter med kant",
|
||||
"generate_page": "Generer side",
|
||||
"input_placeholder": "Skriv her",
|
||||
"instruction_1": "Homebox Label Generator er et værktøj, der hjælper dig med at udskrive etiketter til dit Homebox-lager. Disse er beregnet til\nat være etiketter, der kan udskrives på forhånd, så du kan udskrive mange etiketter og have dem klar til påsætning.",
|
||||
"instruction_2": "Disse etiketter fungerer derfor ved at udskrive en URL, QR-kode og AssetID-oplysninger på en etiket. Hvis du har deaktiveret\nAssetID'er i dine Homebox-indstillinger, kan du stadig bruge dette værktøj, men AssetID'erne vil ikke referere til nogen varer.",
|
||||
"instruction_3": "Denne funktion er i de tidlige udviklingsfaser og kan ændres i fremtidige udgivelser. Hvis du har feedback, bedes\ndu give den i '<a href=\"https://github.com/sysadminsmedia/homebox/discussions/53\">'GitHub-diskussionen'</a>'",
|
||||
"label_height": "Etikethøjde",
|
||||
"label_width": "Etiketbredde",
|
||||
"measure_type": "Målingstype",
|
||||
"page_bottom_padding": "Sidebundspolstring",
|
||||
"page_height": "Sidehøjde",
|
||||
"page_left_padding": "Venstre sidepolstring",
|
||||
"page_right_padding": "Højre sidepolstring",
|
||||
"page_top_padding": "Sidetoppolstring",
|
||||
"page_width": "Sidebredde",
|
||||
"qr_code_example": "Eksempel på QR-kode",
|
||||
"tip_1": "Standardindstillingerne her er konfigureret for\n'<a href=\"https://www.avery.com/templates/5260\">'Avery 5260 etiketark'</a>'. Hvis du bruger et andet ark,\nskal du justere indstillingerne, så de passer til dit ark.",
|
||||
"tip_2": "Hvis du tilpasser dit ark, er dimensionerne i tommer. Da jeg byggede 5260-arket, opdagede jeg, at de\ndimensioner, der blev brugt i deres skabelon, ikke matchede det, der var nødvendigt for at udskrive i felterne.\n'<b>'Vær forberedt på nogle forsøg og fejl.'</b>'",
|
||||
"tip_3": "Ved printning sørg for at:\n'<ol><li>'Indstil margenerne til 0 eller Ingen'</li><li>'Indstil skaleringen til 100%'</li><li>'Deaktiver dobbeltsidet udskrivning'</li><li>'Udskriv en testside, før du udskriver flere sider'</li></ol>'",
|
||||
"tips": "Tips",
|
||||
"title": "Etiketgenerator",
|
||||
"toast": {
|
||||
"page_too_small_card": "Sidestørrelsen er for lille til kortstørrelsen"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner": {
|
||||
"barcode_detected_message": "produkt stregkode opdaget",
|
||||
"barcode_fetch_data": "Hent produktdata",
|
||||
"error": "Der skete en fejl under skanningen",
|
||||
"invalid_url": "Ugyldig stregkode-URL",
|
||||
"no_sources": "Ingen videokilder er tilgængelig",
|
||||
"permission_denied": "Kameratilladelse nægtet, Tillad venligst adgang til kameraet i dine browserindstillinger",
|
||||
"select_video_source": "Vælg en videokilde",
|
||||
"title": "Skanner",
|
||||
"unsupported": "Media Stream API understøttes ikke uden HTTPS"
|
||||
},
|
||||
"tools": {
|
||||
"actions": "Handlinger på lagerbeholdning",
|
||||
"actions_set": {
|
||||
"create_missing_thumbnails": "Opret manglende miniaturebilleder",
|
||||
"create_missing_thumbnails_button": "Opret miniaturebilleder",
|
||||
"create_missing_thumbnails_confirm": "Er du sikker på, at du vil oprette manglende miniaturebilleder? Dette kan tage et stykke tid og kan ikke sættes på pause.",
|
||||
"create_missing_thumbnails_sub": "Opretter miniaturebilleder for alle vedhæftede filer, der understøttes af den aktuelle konfiguration. Dette er nyttigt for vedhæftede filer, der blev uploadet før v0.20.0-udgivelsen af Homebox. Dette overskriver ikke eksisterende miniaturebilleder, men opretter kun nye for vedhæftede filer, der ikke har et miniaturebillede. Bemærk, at miniaturebillederne oprettes i baggrunden og kan tage et stykke tid at fuldføre.",
|
||||
"ensure_ids": "Sørg for aktiv-id'er",
|
||||
"ensure_ids_button": "Sørg for aktiv-id'er",
|
||||
"ensure_ids_confirm": "Er du sikker på, at du vil sikre dig, at alle aktiver har et ID? Dette kan tage et stykke tid og kan ikke fortrydes.",
|
||||
"ensure_ids_sub": "Sikrer, at alle varer på lageret har et gyldigt asset_id felt. Dette gøres ved at finde det højeste aktuelle aktiv_id felt i databasen og anvende den næste værdi på hvert element, der har et ikke sat aktiv_id felt. Dette gøres i rækkefølge efter feltet opret_den.",
|
||||
"ensure_import_refs": "Sørg for importreferencer",
|
||||
"ensure_import_refs_button": "Sørg for importreferencer",
|
||||
"ensure_import_refs_sub": "Sikrer, at alle varer på lageret har et gyldigt import_ref felt. Dette gøres ved tilfældigt at generere en streng på 8 tegn for hvert element, der har et uindstillet import_ref felt.",
|
||||
"set_primary_photo": "Indstil primært foto",
|
||||
"set_primary_photo_button": "Indstil primært foto",
|
||||
"set_primary_photo_confirm": "Er du sikker på, at du vil indstille primære billeder? Dette kan tage et stykke tid og kan ikke fortrydes.",
|
||||
"set_primary_photo_sub": "I version v0.10.0 af Homebox blev det primære billedfelt tilføjet til vedhæftede filer af typen foto. Denne handling indstiller det primære billedfelt til det første billede i matrixen for vedhæftede filer i databasen, hvis det ikke allerede er angivet. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/pull/576\">'Se GitHub PR #576'</a>'",
|
||||
"zero_datetimes": "Nul Vare Dato Tider",
|
||||
"zero_datetimes_button": "Nul Varedato Tider",
|
||||
"zero_datetimes_confirm": "Er du sikker på, at du vil nulstille alle dato- og klokkeslætsværdier? Dette kan tage et stykke tid og kan ikke fortrydes.",
|
||||
"zero_datetimes_sub": "Nulstiller klokkeslætsværdien for alle dato- og klokkeslætsfelter i lageret til begyndelsen af datoen. Dette er for at rette en fejl, der blev introduceret tidligt i udviklingen af webstedet, der forårsagede, at tidsværdien blev gemt med tiden, hvilket forårsagede problemer med datofelter, der viste nøjagtige værdier. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/issues/236\" target=\"_blank\">'Se Github-udgave #236 for flere detaljer.'</a>'"
|
||||
},
|
||||
"actions_sub": "Anvend flere handlinger på din beholdning på én gang. Det er uigenkaldelige handlinger. '<b>'Vær forsigtig.'</b>'",
|
||||
@@ -612,7 +375,6 @@
|
||||
"export_sub": "Eksporterer standard CSV-formatet til Homebox. Dette vil eksportere alle varer i dit lager.",
|
||||
"import": "Importeret beholdning",
|
||||
"import_button": "Importer beholdning",
|
||||
"import_ref_confirm": "Er du sikker på, at du vil sikre dig, at alle aktiver har en import_ref? Dette kan tage et stykke tid og kan ikke fortrydes.",
|
||||
"import_sub": "Importerer standard CSV-formatet til Homebox. Uden en '<code>'HB.import_ref'</code>'-kolonne vil dette '<b>'ikke'</b>' overskrive eksisterende genstande i dit lager, kun tilføje nye genstande. Rækker med kolonnen \"<code>HB.import_ref\"</code> flettes ind i eksisterende elementer med samme import_ref, hvis der findes en."
|
||||
},
|
||||
"import_export_sub": "Importér og eksporter din lagerbeholdning til og fra en CSV-fil. Dette er nyttigt til at migrere dit lager til en ny forekomst af Homebox.",
|
||||
@@ -625,14 +387,6 @@
|
||||
"bill_of_materials_button": "Generer stykliste",
|
||||
"bill_of_materials_sub": "Genererer en CSV-fil (kommaseparerede værdier), der kan importeres til et regnearksprogram. Dette er en oversigt over din beholdning med grundlæggende vare- og prisoplysninger."
|
||||
},
|
||||
"reports_sub": "Generer forskellige rapporter for dit lager.",
|
||||
"toast": {
|
||||
"asset_success": "Aktiverne i { results } er blevet opdateret.",
|
||||
"failed_create_missing_thumbnails": "Kunne ikke oprette manglende miniaturebilleder.",
|
||||
"failed_ensure_ids": "Kunne ikke sikre aktiv-ID'er.",
|
||||
"failed_ensure_import_refs": "Kunne ikke sikre importreferencer.",
|
||||
"failed_set_primary_photos": "Kunne ikke indstille primære billeder.",
|
||||
"failed_zero_datetimes": "Dato- og klokkeslætsværdier kunne ikke nulstilles."
|
||||
}
|
||||
"reports_sub": "Generer forskellige rapporter for dit lager."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"components": {
|
||||
"app": {
|
||||
"create_modal": {
|
||||
"createAndAddAnother": "Verwenden Sie {shiftKey} + {enterKey}, um eine weitere zu erstellen und hinzuzufügen.",
|
||||
"createAndAddAnother": "Verwenden Sie {Umschalttaste} + {Eingabetaste}, um eine weitere zu erstellen und hinzuzufügen.",
|
||||
"enter": "Eingabe",
|
||||
"shift": "Shift"
|
||||
},
|
||||
@@ -28,8 +28,7 @@
|
||||
"clear": "Farbe löschen",
|
||||
"color": "Farbe",
|
||||
"no_color": "Keine Farbe",
|
||||
"no_color_selected": "Keine Farbe ausgewählt",
|
||||
"randomize": "Zufällige Farbe"
|
||||
"no_color_selected": "Keine Farbe ausgewählt"
|
||||
},
|
||||
"form": {
|
||||
"password": {
|
||||
@@ -100,8 +99,6 @@
|
||||
"item_photo": "Artikel Bild",
|
||||
"item_quantity": "Anzahl der Artikel",
|
||||
"parent_item": "Übergeordneter Gegenstand",
|
||||
"product_tooltip_input_barcode": "Automatisch ausfüllen mit einem manuell bereitgestellten Barcode",
|
||||
"product_tooltip_scan_barcode": "Automatisch ausfüllen mit einem Barcode von 📷",
|
||||
"rotate_photo": "Photo drehen",
|
||||
"set_as_primary_photo": "Festlegen als { isPrimary, select, true {non-} false {} other {}}primäres Foto",
|
||||
"title": "Gegenstand erstellen",
|
||||
@@ -114,27 +111,18 @@
|
||||
"please_select_location": "Bitte einen Ort auswählen.",
|
||||
"rotate_failed": "Drehen des Bildes fehlgeschlagen: {error}",
|
||||
"rotate_process_failed": "Das gedrehte Bild konnte nicht verarbeitet werden",
|
||||
"some_photos_failed": "{count, plural, =0 {Keine Fotos zum Hochladen.} =1 {1 Foto konnte nicht hochgeladen werden.} other {Einige Fotos konnten nicht hochgeladen werden.}}",
|
||||
"some_photos_failed": "{Anzahl, Plural, =0 {Keine Fotos zum Hochladen.} =1 {1 Foto konnte nicht hochgeladen werden.} andere {Einige Fotos konnten nicht hochgeladen werden.}}",
|
||||
"upload_failed": "Hochladen des Bildes Fehlgeschlagen: { photoName }",
|
||||
"upload_success": "{count, plural, =0 {Keine Fotos hochgeladen.} =1 {Foto erfolgreich hochgeladen.} other {Alle Fotos erfolgreich hochgeladen.}}",
|
||||
"uploading_photos": "{count, plural, =0 {Keine Fotos zum Hochladen} =1 {1 Foto wird hochgeladen...} other {{count} Fotos werden hochgeladen...}}"
|
||||
"upload_success": "{Anzahl, plural, =0 {Keine Fotos hochgeladen.} =1 {Foto erfolgreich hochgeladen.} other {Alle Fotos erfolgreich hochgeladen.}}",
|
||||
"uploading_photos": "{Anzahl, plural, =0 {Keine Fotos zum Hochladen} =1 {1 Foto wird hochgeladen...} other {{Anzahl} Fotos werden hochgeladen...}}"
|
||||
},
|
||||
"upload_photos": "Upload Bilder",
|
||||
"uploaded": "Bild hochgeladen"
|
||||
},
|
||||
"product_import": {
|
||||
"barcode": "Produkt-Strichcode",
|
||||
"db_source": "DB-Quelle",
|
||||
"error_exception": "Beim Abrufen des Artikel-Barcodes ist ein Problem aufgetreten: ",
|
||||
"error_invalid_barcode": "Ungültiger Barcode angegeben",
|
||||
"error_not_found": "Kein Produkt mit dem angegebenen Barcode gefunden.",
|
||||
"search_item": "Produkt suchen",
|
||||
"title": "Produkt importieren"
|
||||
},
|
||||
"selector": {
|
||||
"no_results": "Keine Ergebnisse gefunden",
|
||||
"placeholder": "Auswählen…",
|
||||
"search_placeholder": "Für Suche tippen…"
|
||||
"placeholder": "Auswählen...",
|
||||
"search_placeholder": "Für Suche tippen..."
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
@@ -154,7 +142,6 @@
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"label_color": "Label-Farbe",
|
||||
"label_description": "Label-Beschreibung",
|
||||
"label_name": "Label-Name",
|
||||
"title": "Label erstellen",
|
||||
@@ -187,7 +174,7 @@
|
||||
"select_location": "Standort wählen"
|
||||
},
|
||||
"tree": {
|
||||
"no_locations": "Keine Standorte verfügbar. Fügen Sie neue Standorte über die Schaltfläche\n `<span class=\"link-primary\">`Erstellen`</span>` in der Navigationsleiste hinzu."
|
||||
"no_locations": "Keine Standorte verfügbar. Fügen Sie neue Standorte über die Schaltfläche\n `<`span class=\"link-primary\"`>`Erstellen`<`/span`>` in der Navigationsleiste hinzu."
|
||||
}
|
||||
},
|
||||
"quick_menu": {
|
||||
@@ -195,9 +182,6 @@
|
||||
"shortcut_hint": "Verwenden Sie die Zifferntasten, um schnell eine Aktion auszuwählen."
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"api_failure": "Backend-API-Aufruf fehlgeschlagen: "
|
||||
},
|
||||
"global": {
|
||||
"add": "Hinzufügen",
|
||||
"archived": "Archiviert",
|
||||
@@ -225,7 +209,7 @@
|
||||
"items": "Gegenstände",
|
||||
"join_discord": "Discord beitreten",
|
||||
"labels": "Labels",
|
||||
"loading": "Wird geladen…",
|
||||
"loading": "Wird geladen …",
|
||||
"locations": "Lagerorte",
|
||||
"maintenance": "Wartung",
|
||||
"name": "Name",
|
||||
@@ -397,7 +381,6 @@
|
||||
"update_label": "Label aktualisieren"
|
||||
},
|
||||
"languages": {
|
||||
"bs-BA": "Bosnisch (Bosnien und Herzegowina)",
|
||||
"ca": "Katalanisch",
|
||||
"cs-CZ": "Tschechisch",
|
||||
"de": "Deutsch",
|
||||
@@ -424,10 +407,9 @@
|
||||
"th-TH": "Thailändisch",
|
||||
"tr": "Türkisch",
|
||||
"uk-UA": "Ukrainisch",
|
||||
"vi-VN": "Vietnamesisch",
|
||||
"zh-CN": "Chinesisch (vereinfacht)",
|
||||
"zh-CN": "Chinesisch (einfach)",
|
||||
"zh-HK": "Chinesisch (Hong Kong)",
|
||||
"zh-MO": "Chinesisch (Macau)",
|
||||
"zh-MO": "Chinesisch (Macao)",
|
||||
"zh-TW": "Chinesisch (traditionell)"
|
||||
},
|
||||
"languages.da-DK": "Dänisch",
|
||||
@@ -439,7 +421,7 @@
|
||||
"collapse_tree": "Baum einklappen",
|
||||
"expand_tree": "Baum ausklappen",
|
||||
"location_items_delete_confirm": "Möchten Sie diesen Standort und alle darin enthaltenen Elemente wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"no_results": "Keine Standorte gefunden",
|
||||
"no_results": "Keine Orte gefunden",
|
||||
"toast": {
|
||||
"failed_delete_location": "Standort konnte nicht gelöscht werden",
|
||||
"failed_load_location": "Standort konnte nicht geladen werden",
|
||||
@@ -565,7 +547,7 @@
|
||||
"page_width": "Seitenbreite",
|
||||
"qr_code_example": "QR-Code Beispiel",
|
||||
"tip_1": "Die Standardeinstellungen hier sind für die\n'<a href=\"https://www.avery.com/templates/5260\">'Avery 5260 Etikettenbögen'</a>'. Wenn Sie einen anderen Bogen verwenden,\nmüssen Sie die Einstellungen an Ihr Blatt anpassen.",
|
||||
"tip_2": "Wenn Sie Ihr Blatt anpassen, werden die Abmessungen in Zoll angegeben. Beim Erstellen des 5260-Blattes habe ich festgestellt, dass die\nin deren Vorlage verwendeten Abmessungen nicht mit den für den Druck in den Feldern erforderlichen Abmessungen übereinstimmen.\n'<b>'Seien Sie auf einige Versuche und Irrtümer gefasst.'</b>'",
|
||||
"tip_2": "Wenn Sie Ihr Blatt anpassen, werden die Abmessungen in Zoll angegeben. Beim Erstellen des 5260-Blattes habe ich festgestellt, dass die\nin deren Vorlage verwendeten Abmessungen nicht mit den für den Druck in den Feldern erforderlichen Abmessungen übereinstimmen.\n„<b>Seien Sie auf einige Versuche und Irrtümer gefasst.“</b>",
|
||||
"tip_3": "Achten Sie beim Drucken auf Folgendes:\n'<ol><li>'Setzen Sie die Ränder auf 0 oder Keine'</li><li>'Setzen Sie die Skalierung auf 100 %'</li><li>'Deaktivieren Sie den beidseitigen Druck'</li><li>'Drucken Sie eine Testseite, bevor Sie mehrere Seiten drucken'</li></ol>'",
|
||||
"tips": "Tipps",
|
||||
"title": "Etikettengenerator",
|
||||
@@ -575,8 +557,6 @@
|
||||
}
|
||||
},
|
||||
"scanner": {
|
||||
"barcode_detected_message": "Produkt-Barcode erkannt",
|
||||
"barcode_fetch_data": "Produktdaten abrufen",
|
||||
"error": "Beim Scannen ist ein Fehler aufgetreten",
|
||||
"invalid_url": "Ungültige Barcode-URL",
|
||||
"no_sources": "Keine Videoquellen verfügbar",
|
||||
|
||||
@@ -100,8 +100,6 @@
|
||||
"item_photo": "Item Photo 📷",
|
||||
"item_quantity": "Item Quantity",
|
||||
"parent_item": "Parent Item",
|
||||
"product_tooltip_input_barcode": "Autofill with a manually provided barcode",
|
||||
"product_tooltip_scan_barcode": "Autofill with a barcode from 📷",
|
||||
"rotate_photo": "Rotate photo",
|
||||
"set_as_primary_photo": "Set as { isPrimary, select, true {non-} false {} other {}}primary photo",
|
||||
"title": "Create Item",
|
||||
@@ -122,15 +120,6 @@
|
||||
"upload_photos": "Upload Photos",
|
||||
"uploaded": "Uploaded Photo"
|
||||
},
|
||||
"product_import": {
|
||||
"barcode": "Product's barcode",
|
||||
"db_source": "DB source",
|
||||
"error_exception": "Exception occured while retrieving item barcode: ",
|
||||
"error_invalid_barcode": "Invalid barcode provided",
|
||||
"error_not_found": "No product found with given barcode.",
|
||||
"search_item": "Search product",
|
||||
"title": "Import product"
|
||||
},
|
||||
"selector": {
|
||||
"no_results": "No Results Found",
|
||||
"placeholder": "Select…",
|
||||
@@ -195,9 +184,6 @@
|
||||
"shortcut_hint": "Use the number keys to quickly select an action."
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"api_failure": "Backend API call failed: "
|
||||
},
|
||||
"global": {
|
||||
"add": "Add",
|
||||
"archived": "Archived",
|
||||
@@ -289,18 +275,6 @@
|
||||
"delete_attachment_confirm": "Are you sure you want to delete this attachment?",
|
||||
"delete_item_confirm": "Are you sure you want to delete this item?",
|
||||
"description": "Description",
|
||||
"duplicate": {
|
||||
"prefix": "Copy of ",
|
||||
"copy_maintenance": "Copy Maintenance",
|
||||
"copy_attachments": "Copy Attachments",
|
||||
"copy_custom_fields": "Copy Custom Fields",
|
||||
"custom_prefix": "Copy Prefix",
|
||||
"enable_custom_prefix": "Enable Custom Prefix",
|
||||
"prefix_instructions": "This prefix will be added to the beginning of the duplicated item's name. Include a space at the end of the prefix to add a space between the prefix and the item name.",
|
||||
"temporary_title": "Temporary Settings",
|
||||
"title": "Duplicate Settings",
|
||||
"override_instructions": "Hold shift when clicking the duplicate button to override these settings."
|
||||
},
|
||||
"details": "Details",
|
||||
"drag_and_drop": "Drag and drop files here or click to select files",
|
||||
"edit": {
|
||||
@@ -311,8 +285,7 @@
|
||||
"primary_photo_sub": "This option is only available for photos. Only one photo can be primary. If you select this option, the current primary photo, if any will be unselected.",
|
||||
"select_type": "Select a type",
|
||||
"title": "Attachment Edit"
|
||||
},
|
||||
"view_image": "View Image"
|
||||
}
|
||||
},
|
||||
"edit_details": "Edit Details",
|
||||
"field_selector": "Field Selector",
|
||||
@@ -410,7 +383,6 @@
|
||||
"update_label": "Update Label"
|
||||
},
|
||||
"languages": {
|
||||
"bs-BA": "Bosnian (Bosnia and Herzegovina)",
|
||||
"ca": "Catalan",
|
||||
"cs-CZ": "Czech",
|
||||
"da-DK": "Danish",
|
||||
@@ -441,7 +413,6 @@
|
||||
"th-TH": "Thai",
|
||||
"tr": "Turkish",
|
||||
"uk-UA": "Ukrainian",
|
||||
"vi-VN": "Vietnamese",
|
||||
"zh-CN": "Chinese (Simplified)",
|
||||
"zh-HK": "Chinese (Hong Kong)",
|
||||
"zh-MO": "Chinese (Macau)",
|
||||
@@ -588,8 +559,6 @@
|
||||
}
|
||||
},
|
||||
"scanner": {
|
||||
"barcode_detected_message": "product barcode detected",
|
||||
"barcode_fetch_data": "Fetch product data",
|
||||
"error": "An error occurred while scanning",
|
||||
"invalid_url": "Invalid barcode URL",
|
||||
"no_sources": "No video sources available",
|
||||
|
||||
@@ -100,8 +100,6 @@
|
||||
"item_photo": "Foto del artículo 📷",
|
||||
"item_quantity": "Cantidad de Elementos",
|
||||
"parent_item": "Elemento Padre",
|
||||
"product_tooltip_input_barcode": "Autocompletar con un código de barras proporcionado manualmente",
|
||||
"product_tooltip_scan_barcode": "Autocompletar con un código de barras desde 📷",
|
||||
"rotate_photo": "Girar foto",
|
||||
"set_as_primary_photo": "Establecer como { isPrimary, select, true {non-} false {} other {}} foto principal",
|
||||
"title": "Crear Elemento",
|
||||
@@ -122,15 +120,6 @@
|
||||
"upload_photos": "Fotos Subidas",
|
||||
"uploaded": "Foto Subida"
|
||||
},
|
||||
"product_import": {
|
||||
"barcode": "Código de barras del producto",
|
||||
"db_source": "Fuente de la base de datos",
|
||||
"error_exception": "Se ha producido una excepción al recuperar el código de barras del artículo: ",
|
||||
"error_invalid_barcode": "Código de barras proporcionado no válido",
|
||||
"error_not_found": "No se ha encontrado ningún producto con código de barras.",
|
||||
"search_item": "Buscar producto",
|
||||
"title": "Importar producto"
|
||||
},
|
||||
"selector": {
|
||||
"no_results": "Resultados No Encontrados",
|
||||
"placeholder": "Seleccionar…",
|
||||
@@ -195,9 +184,6 @@
|
||||
"shortcut_hint": "Usa las teclas numéricas para seleccionar rápidamente una acción."
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"api_failure": "Error en la llamada a la API del backend: "
|
||||
},
|
||||
"global": {
|
||||
"add": "Añadir",
|
||||
"archived": "Archivado",
|
||||
@@ -551,7 +537,7 @@
|
||||
"input_placeholder": "Escribe aquí",
|
||||
"instruction_1": "El Generador de Etiquetas Homebox es una herramienta que para ayudarte a imprimir etiquetas para tu inventario Homebox. Están pensadas para\n ser etiquetas de impresión anticipada para que puedas imprimir muchas etiquetas y tenerlas listas para usarlas",
|
||||
"instruction_2": "Como tal, estas etiquetas funcionan imprimiendo un código QR de URL e información ID de Activo en una etiqueta. Si has desactivadod\n ID de Activo en la configuración de tu Homebox, puedes seguir utilizando esta herramienta, pero los IDs de Activo no harán referencia a ningún elemento",
|
||||
"instruction_3": "Esta función se encuentra en las primeras etapas de desarrollo y puede cambiar en futuras versiones. Si tienes algún comentario, indícalo\n en la '<a href=\"https://github.com/sysadminsmedia/homebox/discussions/53\">'Discusión de GitHub'</a>'",
|
||||
"instruction_3": "Esta función se encuentra en las primeras etapas de desarrollo y puede cambiar en futuras versiones. Si tienes algún comentario, indícalo\n en la<a href=\"https://github.com/sysadminsmedia/homebox/discussions/53\"> «Discusión de GitHub»</a>",
|
||||
"label_height": "Altura de la Etiqueta",
|
||||
"label_width": "Ancho de la Etiqueta",
|
||||
"measure_type": "Tipo de Medida",
|
||||
@@ -573,8 +559,6 @@
|
||||
}
|
||||
},
|
||||
"scanner": {
|
||||
"barcode_detected_message": "código de barras del producto detectado",
|
||||
"barcode_fetch_data": "Obtener datos del producto",
|
||||
"error": "Se ha producido un error mientras se escaneaba",
|
||||
"invalid_url": "URL de código de barras inválido",
|
||||
"no_sources": "No hay fuentes de vídeo disponibles",
|
||||
@@ -601,12 +585,12 @@
|
||||
"set_primary_photo_button": "Establecer Foto Principal",
|
||||
"set_primary_photo_confirm": "¿Estás seguro de que quieres configurar las fotos principales? Esto puede tardar un tiempo y no se puede deshacer.",
|
||||
"set_primary_photo_sub": "En la versión v0.10.0 de Homebox, se añadió el indicador de imagen principal a los ficheros adjuntos de tipo foto. Esta acción establecerá la primera imagen de cada artículo como su imagen principal, si no hay una imagen principal ya definida. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/pull/576\">'Ver PR #576 en GitHub'</a>'",
|
||||
"zero_datetimes": "Poner a cero las horas de los artículos",
|
||||
"zero_datetimes_button": "Poner a cero las horas de los artículos",
|
||||
"zero_datetimes": "Cero Horas Elementos",
|
||||
"zero_datetimes_button": "Cero Horas Elementos",
|
||||
"zero_datetimes_confirm": "¿Estás seguro de que deseas restablecer todos los valores de fecha y hora? Esto puede tardar un tiempo y no se puede deshacer.",
|
||||
"zero_datetimes_sub": "Restablece el valor de la hora para todos los campos de fecha/hora en tu inventario al principio de esa fecha. Esto se hace para corregir un error que se introdujo al principio del desarrollo de la aplicación, que causó que el valor de la hora se almacenase con la fecha, lo cual produjo problemas al mostrar valores precisos del campo. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/issues/236\" target=\"_blank\">'Ver el issue #236 de GitHub para más detalles.'</a>'"
|
||||
},
|
||||
"actions_sub": "Aplica Acciones a tu inventario de forma masiva. Estas acciones son irreversibles. '<b>'Ten Cuidado.'</b>'",
|
||||
"actions_sub": "Aplica Acciones a tu inventario de forma masiva. Estas son acciones irreversibles. '<b>'Ten Cuidado.'</b>'",
|
||||
"import_export": "Importar/Exportar",
|
||||
"import_export_set": {
|
||||
"export": "Exportar Inventario",
|
||||
@@ -614,7 +598,7 @@
|
||||
"export_sub": "Exporta el formato CSV estándar para Homebox. Esto exportará todos los elementos de tu inventario.",
|
||||
"import": "Importar Inventario",
|
||||
"import_button": "Importar Inventario",
|
||||
"import_ref_confirm": "¿Estás seguro de que deseas asegurarte de que todos los activos tengan un import_ref? Esto puede tardar un tiempo y no se puede deshacer.",
|
||||
"import_ref_confirm": "¿Estás seguro de que deseas asegurarse de que todos los activos tengan un import_ref? Esto puede tardar un tiempo y no se puede deshacer.",
|
||||
"import_sub": "Importa el formato CSV estándar para Homebox. Sin una columna '<code>'HB.import_ref'</code>', esto '<b>'no'</b>' sobrescribirá cualquier elemento existente en tu inventario, sólo añadirá nuevos artículos. Las filas con una columna '<code>'HB.import_ref'</code>' se fusionan con los artículos existentes con la misma import_ref, si existe."
|
||||
},
|
||||
"import_export_sub": "Importa y exporta tu inventario a y desde un archivo CSV. Esto es útil para migrar tu inventario a una nueva instancia de Homebox.",
|
||||
@@ -624,7 +608,7 @@
|
||||
"asset_labels_button": "Generador de Etiquetas",
|
||||
"asset_labels_sub": "Genera un PDF para impresión de etiquetas para un rango de IDs de Activos. Estas etiquetas no son específicas para tu inventario, por lo que puedes imprimirlas con antelación y aplicarlas a tu inventario cuando las recibas.",
|
||||
"bill_of_materials": "Lista de Materiales",
|
||||
"bill_of_materials_button": "Generar Lista de Materiales",
|
||||
"bill_of_materials_button": "Generar lista de materiales",
|
||||
"bill_of_materials_sub": "Genera un archivo CSV (Valores Separados por Comas) que puede importarse a un programa de hojas de cálculo. Es un resumen de tu inventario con información básica sobre artículos y precios."
|
||||
},
|
||||
"reports_sub": "Genera diferentes informes para tu inventario.",
|
||||
|
||||
@@ -24,11 +24,6 @@
|
||||
"new_version_available_link": "Cliquez ici pour consulter les notes de version"
|
||||
}
|
||||
},
|
||||
"color_selector": {
|
||||
"color": "Couleur",
|
||||
"no_color_selected": "Aucune couleur sélectionnée",
|
||||
"randomize": "Couleur aléatoire"
|
||||
},
|
||||
"form": {
|
||||
"password": {
|
||||
"toggle_show": "Activer/désactiver l'affichage du mot de passe"
|
||||
@@ -120,8 +115,8 @@
|
||||
},
|
||||
"selector": {
|
||||
"no_results": "Aucun résultat trouvé",
|
||||
"placeholder": "Sélectionner…",
|
||||
"search_placeholder": "Tapez pour rechercher…"
|
||||
"placeholder": "Sélectionner...",
|
||||
"search_placeholder": "Tapez pour rechercher..."
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
@@ -134,8 +129,7 @@
|
||||
"headers": "En-têtes",
|
||||
"page": "Page",
|
||||
"rows_per_page": "Lignes par page",
|
||||
"table_settings": "Paramètres du Tableau",
|
||||
"view_item": "Afficher l'élément"
|
||||
"table_settings": "Paramètres du Tableau"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -173,7 +167,7 @@
|
||||
"select_location": "Choisir un emplacement"
|
||||
},
|
||||
"tree": {
|
||||
"no_locations": "Aucun emplacement disponible. Créez votre premier emplacement avec\nle bouton '<span class=\"link-primary\">'Créer'</span>' dans la barre de navigation."
|
||||
"no_locations": "Aucun emplacement disponible. Créez votre premier emplacement avec\nle bouton `<`span class=\"link-primary\"`>`Créer`<`/span`>` dans la barre de navigation."
|
||||
}
|
||||
},
|
||||
"quick_menu": {
|
||||
@@ -208,7 +202,7 @@
|
||||
"items": "Articles",
|
||||
"join_discord": "Rejoindre le Discord",
|
||||
"labels": "Étiquettes",
|
||||
"loading": "Chargement…",
|
||||
"loading": "Chargement...",
|
||||
"locations": "Emplacements",
|
||||
"maintenance": "Maintenance",
|
||||
"name": "Nom",
|
||||
@@ -406,7 +400,6 @@
|
||||
"th-TH": "Thaï",
|
||||
"tr": "Turc",
|
||||
"uk-UA": "Ukrainien",
|
||||
"vi-VN": "Vietnamien",
|
||||
"zh-CN": "Chinois (simplifié)",
|
||||
"zh-HK": "Chinois (Hong Kong)",
|
||||
"zh-MO": "Chinois (Macao)",
|
||||
@@ -568,10 +561,6 @@
|
||||
"tools": {
|
||||
"actions": "Actions d’inventaire",
|
||||
"actions_set": {
|
||||
"create_missing_thumbnails": "Crée les miniatures manquantes",
|
||||
"create_missing_thumbnails_button": "Crée les miniatures",
|
||||
"create_missing_thumbnails_confirm": "Êtes-vous sûr de vouloir créer les vignettes manquantes ? Cette opération peut prendre un certain temps et ne peut pas être interrompue.",
|
||||
"create_missing_thumbnails_sub": "Crée des miniatures pour toutes les pièces jointes prises en charge par la configuration actuelle. Ceci est utile pour les pièces jointes importées avant la version 0.20.0 de Homebox. Cette opération n'écrase pas les miniatures existantes, mais crée de nouvelles miniatures pour les pièces jointes sans miniature. Veuillez noter que la création des miniatures s'effectue en arrière-plan et peut prendre un certain temps.",
|
||||
"ensure_ids": "Vérifier les ID de ressources",
|
||||
"ensure_ids_button": "Vérifier les ID de ressources",
|
||||
"ensure_ids_confirm": "Êtes-vous certain de vous assurer que toutes les ressources ont une ID ? Cela peut prendre du temps et est irréversible.",
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"create_modal": {
|
||||
"createAndAddAnother": "Nyomj {shiftKey} + {enterKey}t, hogy létrehozd ezt és hozzáadj egy újat.",
|
||||
"enter": "Enter",
|
||||
"shift": "Shift"
|
||||
},
|
||||
"import_dialog": {
|
||||
"change_warning": "A meglévő import_ref-fel rendelkező tételek importálásának menete megváltozott. Ha a CSV fájlban van import_ref, \nakkor a tételt felülírják a CSV fájlban található értékek.",
|
||||
"description": "Importálj egy CSV fájlt, amely tartalmazza a tételeidet, címkéidet és helyeidet. A szükséges formátumról bővebben \na dokumentációban olvashatsz.",
|
||||
"title": "Importálás CSV-fájlból",
|
||||
"toast": {
|
||||
"import_failed": "Sikertelen importálás. Kérlek próbáld újra később.",
|
||||
"import_success": "Sikeres importálás!",
|
||||
"please_select_file": "Kérlek válassz egy fájlt az importáláshoz."
|
||||
}
|
||||
"title": "Importálás CSV-fájlból"
|
||||
},
|
||||
"outdated": {
|
||||
"current_version": "Jelenlegi verzió",
|
||||
@@ -24,18 +14,6 @@
|
||||
"new_version_available_link": "Kattints ide az újdonságok megtekintéséhez"
|
||||
}
|
||||
},
|
||||
"color_selector": {
|
||||
"clear": "Szín törlése",
|
||||
"color": "Szín",
|
||||
"no_color": "Nincs szín",
|
||||
"no_color_selected": "Nincs kiválasztott szín",
|
||||
"randomize": "Véletlenszerű színezés"
|
||||
},
|
||||
"form": {
|
||||
"password": {
|
||||
"toggle_show": "Jelszó megjelenítése"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"copy_text": {
|
||||
"documentation": "dokumentáció",
|
||||
@@ -73,12 +51,7 @@
|
||||
"download": "Címke letöltése",
|
||||
"print": "Címke nyomtatása",
|
||||
"server_print": "Nyomtatás a szerveren",
|
||||
"titles": "Címkék",
|
||||
"toast": {
|
||||
"load_status_failed": "Sikertelen állapot betöltés",
|
||||
"print_failed": "Sikertelen címke nyomtatás",
|
||||
"print_success": "Címke kinyomtatva"
|
||||
}
|
||||
"titles": "Címkék"
|
||||
},
|
||||
"page_qr_code": {
|
||||
"page_url": "Oldal URL-je",
|
||||
@@ -89,52 +62,14 @@
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"attachments_list": {
|
||||
"download": "Letöltés",
|
||||
"open_new_tab": "Megnyitás új lapon"
|
||||
},
|
||||
"create_modal": {
|
||||
"delete_photo": "Kép törlése",
|
||||
"item_description": "Tétel leírása",
|
||||
"item_name": "Tétel neve",
|
||||
"item_photo": "Tétel fényképe 📷",
|
||||
"item_quantity": "Tételek mennyisége",
|
||||
"parent_item": "Szülő tétel",
|
||||
"product_tooltip_input_barcode": "Automatikus feltöltés kézzel megadott vonalkóddal",
|
||||
"product_tooltip_scan_barcode": "Automatikus kitöltés vonalkóddal innen: 📷",
|
||||
"rotate_photo": "Kép forgatása",
|
||||
"set_as_primary_photo": "Beállítás {isPrimary , select, true {nem } false {} other {}}elsődleges fényképként",
|
||||
"title": "Új elem létrehozása",
|
||||
"toast": {
|
||||
"already_creating": "Elem létrehozása már folyamatban",
|
||||
"create_failed": "Sikertelen elem létrehozás",
|
||||
"create_success": "Elem létrehozva",
|
||||
"failed_load_parent": "Szülő elem betöltése sikertelen - válaszd ki manuálisan",
|
||||
"no_canvas_support": "A böngésződ nem támogatja a canvas műveleteket",
|
||||
"please_select_location": "Válassz egy helyet.",
|
||||
"rotate_failed": "Sikertelen képforgatás: { error }",
|
||||
"rotate_process_failed": "Elforgatott kép feldolgozása sikertelen",
|
||||
"some_photos_failed": "{count, plural, =0 {Nincs feltölthető fénykép.} =1 {1 fénykép feltöltése nem sikerült.} other {Néhány fénykép feltöltése nem sikerült.}}",
|
||||
"upload_failed": "Kép feltöltése sikertelen: { photoName }",
|
||||
"upload_success": "{count, plural, =0 {Nincsenek feltöltött fényképek.} =1 {A fénykép feltöltése sikeres.} other {Minden fénykép feltöltése sikeres.}}",
|
||||
"uploading_photos": "{count, plural, =0 {Nincs feltöltendő fénykép} =1 {1 fénykép feltöltése…} other {{count} fénykép feltöltése…}}"
|
||||
},
|
||||
"upload_photos": "Fotók feltöltése",
|
||||
"uploaded": "Feltöltött fénykép"
|
||||
},
|
||||
"product_import": {
|
||||
"barcode": "Termék vonalkódja",
|
||||
"db_source": "Adatbázis forrás",
|
||||
"error_exception": "Kivétel történt a tétel vonalkódjának lekérése során: ",
|
||||
"error_invalid_barcode": "Érvénytelen vonalkód",
|
||||
"error_not_found": "Nem található termék a megadott vonalkóddal.",
|
||||
"search_item": "Termék keresése",
|
||||
"title": "Termék importálása"
|
||||
},
|
||||
"selector": {
|
||||
"no_results": "Nincs Találat",
|
||||
"placeholder": "Válassz…",
|
||||
"search_placeholder": "Kezdj gépelni a kereséshez…"
|
||||
"upload_photos": "Fotók feltöltése"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
@@ -147,23 +82,15 @@
|
||||
"headers": "Fejlécek",
|
||||
"page": "Oldal",
|
||||
"rows_per_page": "Sorok oldalanként",
|
||||
"table_settings": "Táblázatbeállítások",
|
||||
"view_item": "Elem megtekintése"
|
||||
"table_settings": "Táblázatbeállítások"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"label_color": "Címke színe",
|
||||
"label_description": "Címke leírása",
|
||||
"label_name": "Címke neve",
|
||||
"title": "Címke létrehozása",
|
||||
"toast": {
|
||||
"already_creating": "Címke létrehozása már folyamatban",
|
||||
"create_failed": "Sikertelen címke létrehozás",
|
||||
"create_success": "Címke létrehozva",
|
||||
"label_name_too_long": "A címke neve nem lehet hosszabb 50 karakternél"
|
||||
}
|
||||
"title": "Címke létrehozása"
|
||||
},
|
||||
"selector": {
|
||||
"select_labels": "Címkék kiválasztása"
|
||||
@@ -173,12 +100,7 @@
|
||||
"create_modal": {
|
||||
"location_description": "Hely leírása",
|
||||
"location_name": "Hely neve",
|
||||
"title": "Új hely létrehozása",
|
||||
"toast": {
|
||||
"already_creating": "Hely létrehozása már folyamatban",
|
||||
"create_failed": "Sikertelen hely létrehozás",
|
||||
"create_success": "Hely létrehozva"
|
||||
}
|
||||
"title": "Új hely létrehozása"
|
||||
},
|
||||
"selector": {
|
||||
"no_location_found": "Nem található hely",
|
||||
@@ -187,7 +109,7 @@
|
||||
"select_location": "Válassz egy helyet"
|
||||
},
|
||||
"tree": {
|
||||
"no_locations": "Nincs elérhető hely. Adj hozzá új helyet a\n '<span class=\"link-primary\">'Létrehozás'</span>' gombbal a navigációs sávon."
|
||||
"no_locations": "Nincs elérhető hely. Adj hozzá új helyet a\n `<`span class=\"link-primary\"`>`Létrehozás`<`/span`>` gombbal a navigációs sávon."
|
||||
}
|
||||
},
|
||||
"quick_menu": {
|
||||
@@ -195,9 +117,6 @@
|
||||
"shortcut_hint": "Használd a számgombokat egy művelet gyors kiválasztásához."
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"api_failure": "Backend API hívás sikertelen: "
|
||||
},
|
||||
"global": {
|
||||
"add": "Hozzáadás",
|
||||
"archived": "Archivált",
|
||||
@@ -209,8 +128,6 @@
|
||||
"create_subitem": "Alelem létrehozása",
|
||||
"created": "Létrehozva",
|
||||
"delete": "Törlés",
|
||||
"delete_confirm": "Biztosan törlöd ezt az elemet? ",
|
||||
"demo_instance": "Ez egy demó példány",
|
||||
"details": "Részletek",
|
||||
"duplicate": "Másolás",
|
||||
"edit": "Szerkesztés",
|
||||
@@ -225,7 +142,6 @@
|
||||
"items": "Tételek",
|
||||
"join_discord": "Csatlakozz a Discordhoz",
|
||||
"labels": "Címkék",
|
||||
"loading": "Betöltés…",
|
||||
"locations": "Helyek",
|
||||
"maintenance": "Karbantartás",
|
||||
"name": "Név",
|
||||
@@ -233,14 +149,11 @@
|
||||
"password": "Jelszó",
|
||||
"quantity": "Mennyiség",
|
||||
"read_docs": "Olvasd el a dokumentációt",
|
||||
"return_home": "Vissza a kezdőlapra",
|
||||
"save": "Mentés",
|
||||
"search": "Keresés",
|
||||
"sign_out": "Kijelentkezés",
|
||||
"submit": "Elküldés",
|
||||
"unknown": "Ismeretlen",
|
||||
"update": "Módosítás",
|
||||
"updating": "Frissítés",
|
||||
"value": "Érték",
|
||||
"version": "Verzió: { version }",
|
||||
"welcome": "Üdv, { username }"
|
||||
@@ -265,49 +178,27 @@
|
||||
"set_email": "Mi az email címed?",
|
||||
"set_name": "Mi a neved?",
|
||||
"set_password": "Állíts be egy jelszót!",
|
||||
"tagline": "Kövesd nyomon, rendszerezd és kezeld a dolgaidat.",
|
||||
"title": "Rendezd és címkézd a dolgaidat",
|
||||
"toast": {
|
||||
"invalid_email": "E-mail cím érvénytelen",
|
||||
"invalid_email_password": "Az E-mail vagy jelszó érvénytelen",
|
||||
"login_success": "Sikeres bejelentkezés",
|
||||
"problem_registering": "Hiba a felhasználó regisztrálásakor",
|
||||
"user_registered": "Felhasználó regisztrálva"
|
||||
}
|
||||
"tagline": "Kövesd nyomon, rendszerezd és kezeld a dolgaidat."
|
||||
},
|
||||
"items": {
|
||||
"add": "Hozzáadás",
|
||||
"advanced": "Haladó",
|
||||
"archived": "Archivált",
|
||||
"asset_id": "Eszközazonosító",
|
||||
"associated_with_multiple": "Ez az eszköz Id több elemhez van hozzárendelve",
|
||||
"attachment": "Melléklet",
|
||||
"attachments": "Mellékletek",
|
||||
"changes_persisted_immediately": "A mellékletek módosításai azonnal mentésre kerülnek",
|
||||
"created_at": "Létrehozás dátuma",
|
||||
"custom_fields": "Egyedi mezők",
|
||||
"delete_attachment_confirm": "Biztos törlöd ezt a mellékletet?",
|
||||
"delete_item_confirm": "Biztosan törlöd ezt az elemet?",
|
||||
"description": "Leírás",
|
||||
"details": "Részletek",
|
||||
"drag_and_drop": "Húzd ide a fájlokat, vagy kattints a fájlok kiválasztásához",
|
||||
"edit": {
|
||||
"edit_attachment_dialog": {
|
||||
"attachment_title": "Melléklet Cím",
|
||||
"attachment_type": "Melléklet típusa",
|
||||
"primary_photo": "Elsődleges fénykép",
|
||||
"primary_photo_sub": "Ez a lehetőség csak fényképeknél érhető el. Csak egy fotó lehet elsődleges. Ha kiválasztod ezt a műveletet, a jelenlegi elsődleges fotó elveszti ezt a jellegét.",
|
||||
"select_type": "Válassz típust",
|
||||
"title": "Melléklet szerkesztése"
|
||||
}
|
||||
},
|
||||
"edit_details": "Részletek szerkesztése",
|
||||
"field_selector": "Mezőválasztó",
|
||||
"field_value": "Mező értéke",
|
||||
"first": "Első",
|
||||
"include_archive": "Archivált elemek belefoglalása",
|
||||
"insured": "Biztosítva",
|
||||
"invalid_asset_id": "Érvénytelen eszközazonosító",
|
||||
"last": "Utolsó",
|
||||
"lifetime_warranty": "Élettartam garancia",
|
||||
"location": "Hely",
|
||||
@@ -318,7 +209,6 @@
|
||||
"name": "Név",
|
||||
"negate_labels": "Címkeválasztás negálása",
|
||||
"next_page": "Következő oldal",
|
||||
"no_attachments": "Nem található melléklet",
|
||||
"no_results": "Egy elem sem található",
|
||||
"notes": "Megjegyzések",
|
||||
"only_with_photo": "Csak fényképes tételek",
|
||||
@@ -340,60 +230,24 @@
|
||||
"receipts": "Számlák",
|
||||
"reset_search": "Alaphelyzet",
|
||||
"results": "{total} találat",
|
||||
"select_field": "Válaszd ki a mezőt",
|
||||
"serial_number": "Sorozatszám",
|
||||
"show_advanced_view_options": "További beállítások megjelenítése",
|
||||
"sold_at": "Eladás dátuma",
|
||||
"sold_details": "Eladás részletei",
|
||||
"sold_price": "Eladási ár",
|
||||
"sold_to": "Vevő",
|
||||
"sync_child_locations": "Gyermekelemek helyeinek szinkronizálása",
|
||||
"tip_1": "A hely- és címkeszűrők a „vagy” műveletet használják. Ha egynél többet választasz ki,\n bármelyik egyezése esetén megjelenik a tétel.",
|
||||
"tip_2": "A '#' előtaggal ellátott keresések egy eszközazonosítót fognak lekérdezni (például '#000-001')",
|
||||
"tip_3": "A mezőszűrők a „vagy” műveletet használják. Ha egynél többet választasz ki,\n bármelyik egyezése esetén megjelenik a tétel.",
|
||||
"tips": "Tippek",
|
||||
"tips_sub": "Tippek a kereséshez",
|
||||
"toast": {
|
||||
"asset_not_found": "Az eszköz nem található",
|
||||
"attachment_deleted": "Melléklet törölve",
|
||||
"attachment_updated": "Melléklet frissítve",
|
||||
"attachment_uploaded": "Melléklet feltöltve",
|
||||
"child_items_location_no_longer_synced": "A gyermekelemek helye a továbbiakban nem lesz szinkronizálva ezzel a tétellel.",
|
||||
"child_items_location_synced": "A gyermekelemek helye szinkronizálva lett ezzel a tétellel",
|
||||
"child_location_desync": "A hely módosítása de-szinkronizálja a szülő helyéről",
|
||||
"error_loading_parent_data": "Hiba történt a szülőadatok betöltése során",
|
||||
"failed_adjust_quantity": "Mennyiség beállítása sikertelen",
|
||||
"failed_delete_attachment": "Melléklet törlése sikertelen",
|
||||
"failed_delete_item": "Tétel törlése sikertelen",
|
||||
"failed_duplicate_item": "Tétel másolása sikertelen",
|
||||
"failed_load_asset": "Eszköz betöltése sikertelen",
|
||||
"failed_load_item": "Tétel betöltése sikertelen",
|
||||
"failed_load_items": "Tételek betöltése sikertelen",
|
||||
"failed_save": "Tétel mentése sikertelen",
|
||||
"failed_save_no_location": "Tétel mentése sikertelen: nincs kiválasztott hely",
|
||||
"failed_search_items": "Tételek keresése sikertelen",
|
||||
"failed_update_attachment": "Melléklet frissítése sikertelen",
|
||||
"failed_upload_attachment": "Melléklet feltöltése sikertelen",
|
||||
"item_deleted": "Tétel törölve",
|
||||
"item_saved": "Tétel mentve",
|
||||
"quantity_cannot_negative": "A mennyiség nem lehet negatív",
|
||||
"sync_child_location": "A kiválasztott szülő szinkronizálja gyermekei tartózkodási helyét a sajátjával. Hely frissítve."
|
||||
},
|
||||
"updated_at": "Változtatás dátuma",
|
||||
"warranty": "Garancia",
|
||||
"warranty_details": "Garancia részletei",
|
||||
"warranty_expires": "Garancia vége"
|
||||
},
|
||||
"labels": {
|
||||
"label_delete_confirm": "Biztos vagy benne, hogy törölni szeretnéd ezt a címkét? A művelet nem visszafordítható.",
|
||||
"no_results": "Nem található címke",
|
||||
"toast": {
|
||||
"failed_delete_label": "Címke törlése sikertelen",
|
||||
"failed_load_label": "Címke betöltése sikertelen",
|
||||
"failed_update_label": "Címke frissítése sikertelen",
|
||||
"label_deleted": "Címke törölve",
|
||||
"label_updated": "Címke frissítve"
|
||||
},
|
||||
"update_label": "Címke módosítása"
|
||||
},
|
||||
"languages": {
|
||||
@@ -436,15 +290,7 @@
|
||||
"child_locations": "Tartalmazott helyek",
|
||||
"collapse_tree": "Fanézet becsukása",
|
||||
"expand_tree": "Fa kibontása",
|
||||
"location_items_delete_confirm": "Biztosan törlöd ezt a helyet és az összes elemét? Ez a művelet nem visszavonható.",
|
||||
"no_results": "Nem található hely",
|
||||
"toast": {
|
||||
"failed_delete_location": "Hely törlése sikertelen",
|
||||
"failed_load_location": "Hely betöltése sikertelen",
|
||||
"failed_update_location": "Hely frissítése sikertelen",
|
||||
"location_deleted": "Hely törölve",
|
||||
"location_updated": "Hely frissítve"
|
||||
},
|
||||
"update_location": "Hely módosítása"
|
||||
},
|
||||
"maintenance": {
|
||||
@@ -500,10 +346,7 @@
|
||||
"currency_format": "Pénz formátum",
|
||||
"current_password": "Jelenlegi jelszó",
|
||||
"delete_account": "Fiók törlése",
|
||||
"delete_account_confirm": "Biztosan törlöd a fiókodat? Ha te vagy az utolsó tag a csoportodban, minden adatod törlődik. Ez a művelet nem visszafordítható.",
|
||||
"delete_account_sub": "Törlöd a fiókodat és az összes kapcsolódó adatot. Ezt a műveletet nem lehet visszavonni.",
|
||||
"delete_notifier_confirm": "Biztos, hogy törölni akarod ezt az értesítőt?",
|
||||
"display_legacy_header": "{ currentValue, select, true {Legacy fejléc letiltása} false {Legacy fejléc engedélyezése} other {Nincs találat}}",
|
||||
"enabled": "Engedélyezve",
|
||||
"example": "Példa",
|
||||
"gen_invite": "Meghívó link létrehozása",
|
||||
@@ -521,64 +364,16 @@
|
||||
"test": "Teszt",
|
||||
"theme_settings": "Téma Beállítások",
|
||||
"theme_settings_sub": "A témabeállítások a böngésző helyi tárhelyén tárolódnak. Bármikor megváltoztathatod a témát. Ha problémába\n ütközöl a téma beállításakor, próbáld meg frissíteni az oldalt a böngésződben.",
|
||||
"toast": {
|
||||
"account_deleted": "Fiókodat sikeresen töröltük.",
|
||||
"failed_change_password": "Jelszó megváltoztatása sikertelen.",
|
||||
"failed_create_notifier": "Értesítő létrehozása sikertelen.",
|
||||
"failed_delete_account": "Nem sikerült törölni a fiókodat.",
|
||||
"failed_delete_notifier": "Értesítő törlése sikertelen.",
|
||||
"failed_get_currencies": "Pénznemek lekérése sikertelen",
|
||||
"failed_test_notifier": "Nem sikerült tesztelni az értesítőt.",
|
||||
"failed_update_group": "Csoport frissítése sikertelen",
|
||||
"failed_update_notifier": "Nem sikerült frissíteni az értesítőt.",
|
||||
"group_updated": "Csoport frissítve",
|
||||
"notifier_test_success": "Az értesítő tesztje sikeres volt.",
|
||||
"password_changed": "A jelszóváltoztatás sikeres volt."
|
||||
},
|
||||
"update_group": "Csoport módosítása",
|
||||
"update_language": "Nyelv átállítása",
|
||||
"url": "URL",
|
||||
"user_profile": "Felhasználói profil",
|
||||
"user_profile_sub": "Hívj meg felhasználókat, és kezeld a fiókodat."
|
||||
},
|
||||
"reports": {
|
||||
"label_generator": {
|
||||
"asset_end": "Utolsó eszköz",
|
||||
"asset_start": "Első eszköz",
|
||||
"base_url": "Alap URL",
|
||||
"bordered_labels": "Keretes címkék",
|
||||
"generate_page": "Oldal létrehozása",
|
||||
"input_placeholder": "Írj ide",
|
||||
"instruction_1": "A Homebox Label Generator egy olyan eszköz, amely segít a Homebox-leltár címkéinek nyomtatásában. Ezeket előre\n kinyomtathatod, hogy bármikor felragaszthass egy új, még használatlan címkét",
|
||||
"instruction_2": "Ezek a címkék ezért úgy működnek, hogy egy URL QR-kódot és eszközazonosítót nyomtatnak egy címkére. Ha kikapcsoltad\n az eszközazonosítókat a Homebox beállításokban, akkor is használhatod ezt az eszközt, de nem mutat majd az eszközazonosító semmilyen tételre",
|
||||
"instruction_3": "Ez a funkció korai fejlesztési szakaszban van, és a jövőbeli kiadásokban változhat, ha visszajelzésed van, kérlek\nírd meg nekünk a '<a href=\"https://github.com/sysadminsmedia/homebox/discussions/53\">'GitHub vitafórumon'</a>'",
|
||||
"label_height": "Címke magassága",
|
||||
"label_width": "Címke szélessége",
|
||||
"measure_type": "Mértékegység",
|
||||
"page_bottom_padding": "Oldal alsó margója",
|
||||
"page_height": "Oldal magassága",
|
||||
"page_left_padding": "Oldal bal margója",
|
||||
"page_right_padding": "Oldal jobb margója",
|
||||
"page_top_padding": "Oldal felső margója",
|
||||
"page_width": "Oldalszélesség",
|
||||
"qr_code_example": "QR-kód példa",
|
||||
"tip_1": "Az alapértelmezett beállítások\n'<a href=\"https://www.avery.com/templates/5260\">'Avery 5260 címkeívek'</a>'nek felelnek meg. Ha más íveket használsz,\n módosítanod kell a beállításokat, hogy a laphoz igazodjanak.",
|
||||
"tip_2": "Ha a lapot testre szabod, a méretek hüvelykben vannak megadva. A 5260 ívek létrehozásakor feltűnt,\n hogy a hivatalosan megadott méretek nem azonosak a megfelelő szövegdoboz méretekkel.\n'<b>'Szükséged lehet néhány próbálkozásra a megfelelő méretek megtalálásához.'</b>'",
|
||||
"tip_3": "Nyomtatáskor ügyelj a következőkre:\n '<ol><li>'Állítsd a margókat 0-ra vagy Nincsre'</li><li>'Állítsd a skálázást 100% -ra'</li><li>'Tiltsd le a kétoldalas nyomtatást'</li><li>'Több oldal nyomtatása előtt nyomtass tesztoldalt'</li></ol>'",
|
||||
"tips": "Tippek",
|
||||
"title": "Címkegenerátor",
|
||||
"toast": {
|
||||
"page_too_small_card": "Az oldal mérete túl kicsi a kártya méretéhez képest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner": {
|
||||
"barcode_detected_message": "termék vonalkód észlelve",
|
||||
"barcode_fetch_data": "Termékadatok lekérése",
|
||||
"error": "Hiba történt a szkennelés közben",
|
||||
"invalid_url": "Érvénytelen vonalkód URL",
|
||||
"no_sources": "Nincs elérhető videóforrás",
|
||||
"permission_denied": "A kamera engedélye megtagadva, engedélyezd a kamerához való hozzáférést a böngésző beállításaiban",
|
||||
"select_video_source": "Videóforrás kiválasztása",
|
||||
"title": "Szkenner",
|
||||
"unsupported": "A Media Stream API nem támogatott HTTPS nélkül"
|
||||
@@ -586,24 +381,17 @@
|
||||
"tools": {
|
||||
"actions": "Készletműveletek",
|
||||
"actions_set": {
|
||||
"create_missing_thumbnails": "Hiányzó bélyegképek létrehozása",
|
||||
"create_missing_thumbnails_button": "Bélyegképek létrehozása",
|
||||
"create_missing_thumbnails_confirm": "Biztosan létrehozod a hiányzó bélyegképeket? Ez eltarthat egy ideig, és nem lehet szüneteltetni.",
|
||||
"create_missing_thumbnails_sub": "Bélyegképeket hoz létre minden olyan melléklethez, melyet a jelenlegi konfiguráció támogat. Ez abban az esetben lehet hasznos, ha a mellékletek a Homebox v0.20.0 verziója előtt lettek feltöltve. A folyamat nem ír felül már létező bélyegképeket, csak újakat készít, ha a melléklet még nem rendelkezik ilyennel. Figyelem, a bélyegképeket egy háttérfolyamat generálja, és egy ideig eltarthat, amíg mind elkészülnek.",
|
||||
"ensure_ids": "Eszközazonosítók meglétének biztosítása",
|
||||
"ensure_ids_button": "Eszközazonosítók generálása",
|
||||
"ensure_ids_confirm": "Biztos elindítod az eszközazonosítók generálását? Ez eltarthat egy ideig, és nem lehet visszavonni.",
|
||||
"ensure_ids_sub": "Biztosítja, hogy a készletben lévő összes tétel rendelkezzen érvényes asset_id (eszközazonosító) mezővel. Ehhez megkeresi a legmagasabb asset_id mezőértéket az adatbázisban és minden olyan tételhez, amelynek nem beállított az asset_id mezője, rendre eggyel növelt értéket állít be. Ezt a created_at mezők értékének (a tétel létrehozásának dátuma) sorrendjében teszi.",
|
||||
"ensure_import_refs": "Importálási hivatkozások meglétének biztosítása",
|
||||
"ensure_import_refs_button": "Hivatkozások generálása",
|
||||
"ensure_import_refs_sub": "Biztosítja, hogy a készletben lévő összes tétel rendelkezzen érvényes import_ref mezővel. Véletlenszerűen generál egy 8 hosszúságú karakterláncot minden olyan tételhez, amelynél az import_ref mező üres.",
|
||||
"set_primary_photo": "Elsődleges fénykép hozzárendelése",
|
||||
"set_primary_photo_button": "Hozzárendelés",
|
||||
"set_primary_photo_confirm": "Biztosan elindítod az elsődleges fényképek hozzárendelését? Ez eltarthat egy ideig, és nem lehet visszavonni.",
|
||||
"set_primary_photo_sub": "A Homebox v0.10.0 verziójában hozzáadtuk a fénykép típusú mellékletekhez az elsődleges fényképként történő megjelölés lehetőségét. Ezzel a művelettel a mellékletekben található első fényképet állítod be elsődleges fényképnek, ha ilyen a tételhez még nincs kiválasztva. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/pull/576\">'Lásd az #576 GitHub PR-t'</a>'",
|
||||
"zero_datetimes": "Idő törlése a tételek dátummezőiből",
|
||||
"zero_datetimes_button": "Dátummezők javítása",
|
||||
"zero_datetimes_confirm": "Biztosan elindítod az összes dátummező időértékének törlését? Ez eltarthat egy ideig, és nem lehet visszavonni.",
|
||||
"zero_datetimes_sub": "Visszaállítja a dátumot és időt tartalmazó mezők értékét a dátum kezdetére a teljes készletben. Ezzel javíthatsz egy olyan bugot, mely során az oldal fejlesztésének korai szakaszában az időértékek mentése a dátumok pontos megjelenítésében hibát okozott. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/issues/236\" target=\"_blank\">'Lásd a #236 Github Issue-t további részletekért.'</a>'"
|
||||
},
|
||||
"actions_sub": "Műveletek tömeges alkalmazása a készletre. Ezeket a vissza nem vonható műveleteket csak '<b>'kellő körültekintés mellett használd'</b>'.",
|
||||
@@ -614,7 +402,6 @@
|
||||
"export_sub": "Exportálja a Homebox szabványos CSV formátumát. Ez minden készletedben található tételt exportál.",
|
||||
"import": "Készlet importálása",
|
||||
"import_button": "Készlet importálása",
|
||||
"import_ref_confirm": "Biztos elindítod az importálási hivatkozások meglétének biztosítását? Ez eltarthat egy ideig, és nem lehet visszavonni.",
|
||||
"import_sub": "Importálja a Homebox szabványos CSV formátumát. Amennyiben nem található '<code>'HB.import_ref'</code>' oszlop a fájlban, ez '<b>'nem'</b>' ír felül létező tételeket a készletedben, csak újakat ad hozzá. Azon sorok, melyeknél a '<code>'HB.import_ref'</code>' oszlop értéke megegyezik egy létező tétel import_ref mezőjének értékével, a sor tartalma beolvad a létező tételbe."
|
||||
},
|
||||
"import_export_sub": "Készlet importálása és exportálása CSV-fájlba és CSV-fájlból. Ez hasznos lehet a készleted átmozgatásához a Homebox egy új példányába.",
|
||||
@@ -627,14 +414,6 @@
|
||||
"bill_of_materials_button": "Jegyzék létrehozása",
|
||||
"bill_of_materials_sub": "Létrehoz egy CSV (vesszővel elválasztott értékek) fájlt, amely importálható egy táblázatkezelő programba. Ez a készleted összesítése a tételek alap és árra vonatkozó információival."
|
||||
},
|
||||
"reports_sub": "Hozz létre különböző jelentéseket a készletedhez.",
|
||||
"toast": {
|
||||
"asset_success": "{ results } eszköz frissítve.",
|
||||
"failed_create_missing_thumbnails": "Hiányzó bélyegképek létrehozása sikertelen.",
|
||||
"failed_ensure_ids": "Nem sikerült biztosítani az eszközazonosítók létezését.",
|
||||
"failed_ensure_import_refs": "Importálási hivatkozások meglétének biztosítása sikertelen.",
|
||||
"failed_set_primary_photos": "Nem sikerült beállítani az elsődleges fényképeket.",
|
||||
"failed_zero_datetimes": "Nem sikerült visszaállítani a dátum- és időértékeket."
|
||||
}
|
||||
"reports_sub": "Hozz létre különböző jelentéseket a készletedhez."
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user