mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-30 17:47:24 +01:00
Compare commits
91 Commits
v0.22.0-rc
...
copilot/au
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bc6b4519c | ||
|
|
6f77eae638 | ||
|
|
6926aabd62 | ||
|
|
b735ad12fd | ||
|
|
f3e817e139 | ||
|
|
153ecd1094 | ||
|
|
0be54da9cf | ||
|
|
e4aa38b264 | ||
|
|
e60f005990 | ||
|
|
7dfaa0298b | ||
|
|
fbe7382acd | ||
|
|
1003223b47 | ||
|
|
3c532896f5 | ||
|
|
4ba1a263c8 | ||
|
|
94f0123d9c | ||
|
|
1f6782f8be | ||
|
|
ec8703114f | ||
|
|
5cd7792701 | ||
|
|
d82c52df26 | ||
|
|
033c17552b | ||
|
|
2355438962 | ||
|
|
2a6773d1d6 | ||
|
|
c8c07e2878 | ||
|
|
a3c05c3497 | ||
|
|
ab0647fe68 | ||
|
|
0b616225a6 | ||
|
|
dc9c7b76f2 | ||
|
|
b99102e093 | ||
|
|
3077602f93 | ||
|
|
2bd6ff580a | ||
|
|
35941583c8 | ||
|
|
d576c89c7e | ||
|
|
ff355f3cd8 | ||
|
|
03dc7fa841 | ||
|
|
7aaaa346ab | ||
|
|
27309e61da | ||
|
|
61816acdaa | ||
|
|
c31410727b | ||
|
|
4557df86ed | ||
|
|
b8910f1b21 | ||
|
|
48e4f8da2a | ||
|
|
1e0158c27e | ||
|
|
4fb3ddd661 | ||
|
|
690005de06 | ||
|
|
23da976494 | ||
|
|
f0b8bb8b7f | ||
|
|
ecc9fa1959 | ||
|
|
7068a85dfb | ||
|
|
c73922c754 | ||
|
|
ae2179c01c | ||
|
|
09e056a3fb | ||
|
|
4abfc76865 | ||
|
|
aa48c958d7 | ||
|
|
2bd6d0a9e5 | ||
|
|
88275620f2 | ||
|
|
5a058250e6 | ||
|
|
afd7a10003 | ||
|
|
8eedd1e39d | ||
|
|
fedeb1a7e5 | ||
|
|
69b31a3be5 | ||
|
|
31d306ca05 | ||
|
|
1bfb716cea | ||
|
|
13b1524c56 | ||
|
|
b18599b6f4 | ||
|
|
473027c1ae | ||
|
|
3a77440996 | ||
|
|
731765c36c | ||
|
|
a86b1bd17b | ||
|
|
064b298968 | ||
|
|
2638f218f3 | ||
|
|
0f4f398b5a | ||
|
|
545993a8aa | ||
|
|
a1947dd09e | ||
|
|
018f1f5977 | ||
|
|
9a9e3d462e | ||
|
|
fc8b6f0dcf | ||
|
|
37890c2a22 | ||
|
|
096b682f0a | ||
|
|
e4d8bb2ada | ||
|
|
3becf046e6 | ||
|
|
a21b3257d4 | ||
|
|
5f9ab577bb | ||
|
|
0a969bb64d | ||
|
|
2d1d3d927b | ||
|
|
540028a22e | ||
|
|
14b0d51894 | ||
|
|
4334f926c0 | ||
|
|
1088972ff0 | ||
|
|
55e247ac71 | ||
|
|
05a2700718 | ||
|
|
06c11cdcd5 |
10
.github/ISSUE_TEMPLATE/internal.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/internal.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: "🛠️ Internal / Developer Issue"
|
||||
about: "Unstructured issue for project members only. Outside contributors: please use a standard template."
|
||||
title: "[INT]: "
|
||||
labels: ["internal"]
|
||||
assignees: []
|
||||
---
|
||||
|
||||
**Summary:**
|
||||
[Write here]
|
||||
432
.github/instructions/backend-app-api-handlers.instructions.md
vendored
Normal file
432
.github/instructions/backend-app-api-handlers.instructions.md
vendored
Normal file
@@ -0,0 +1,432 @@
|
||||
---
|
||||
applyTo: '/backend/app/api/handlers/**/*'
|
||||
---
|
||||
|
||||
# Backend API Handlers Instructions (`/backend/app/api/handlers/v1/`)
|
||||
|
||||
## Overview
|
||||
|
||||
API handlers are the HTTP layer that processes requests, calls services, and returns responses. All handlers use the V1 API pattern with Swagger documentation for auto-generation.
|
||||
|
||||
## Architecture Flow
|
||||
|
||||
```
|
||||
HTTP Request → Router → Middleware → Handler → Service → Repository → Database
|
||||
↓
|
||||
HTTP Response
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
backend/app/api/
|
||||
├── routes.go # Route definitions and middleware
|
||||
├── handlers/
|
||||
│ └── v1/
|
||||
│ ├── controller.go # V1Controller struct and dependencies
|
||||
│ ├── v1_ctrl_items.go # Item endpoints
|
||||
│ ├── v1_ctrl_users.go # User endpoints
|
||||
│ ├── v1_ctrl_locations.go # Location endpoints
|
||||
│ ├── v1_ctrl_auth.go # Authentication endpoints
|
||||
│ ├── helpers.go # HTTP helper functions
|
||||
│ ├── query_params.go # Query parameter parsing
|
||||
│ └── assets/ # Asset handling
|
||||
```
|
||||
|
||||
## Handler Structure
|
||||
|
||||
### V1Controller
|
||||
|
||||
All handlers are methods on `V1Controller`:
|
||||
|
||||
```go
|
||||
type V1Controller struct {
|
||||
svc *services.AllServices // Service layer
|
||||
repo *repo.AllRepos // Direct repo access (rare)
|
||||
bus *eventbus.EventBus // Event publishing
|
||||
}
|
||||
|
||||
func (ctrl *V1Controller) HandleItemCreate() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
// Handler logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Swagger Documentation
|
||||
|
||||
**CRITICAL:** Every handler must have Swagger comments for API doc generation:
|
||||
|
||||
```go
|
||||
// HandleItemsGetAll godoc
|
||||
//
|
||||
// @Summary Query All Items
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param q query string false "search string"
|
||||
// @Param page query int false "page number"
|
||||
// @Param pageSize query int false "items per page"
|
||||
// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{}
|
||||
// @Router /v1/items [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemsGetAll() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**After modifying Swagger comments, ALWAYS run:**
|
||||
```bash
|
||||
task generate # Regenerates Swagger docs and TypeScript types
|
||||
```
|
||||
|
||||
## Standard Handler Pattern
|
||||
|
||||
### 1. Decode Request
|
||||
|
||||
```go
|
||||
func (ctrl *V1Controller) HandleItemCreate() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
var itemData repo.ItemCreate
|
||||
if err := server.Decode(r, &itemData); err != nil {
|
||||
return validate.NewRequestError(err, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// ... rest of handler
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Extract Context
|
||||
|
||||
```go
|
||||
// Get current user from request (added by auth middleware)
|
||||
user := ctrl.CurrentUser(r)
|
||||
|
||||
// Create service context with group/user IDs
|
||||
ctx := services.NewContext(r.Context(), user)
|
||||
```
|
||||
|
||||
### 3. Call Service
|
||||
|
||||
```go
|
||||
result, err := ctrl.svc.Items.Create(ctx, itemData)
|
||||
if err != nil {
|
||||
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Return Response
|
||||
|
||||
```go
|
||||
return server.JSON(w, result, http.StatusCreated)
|
||||
```
|
||||
|
||||
## Common Handler Patterns
|
||||
|
||||
### GET - Single Item
|
||||
|
||||
```go
|
||||
// HandleItemGet godoc
|
||||
//
|
||||
// @Summary Get Item
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param id path string true "Item ID"
|
||||
// @Success 200 {object} repo.ItemOut
|
||||
// @Router /v1/items/{id} [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemGet() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
id, err := ctrl.RouteUUID(r, "id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := services.NewContext(r.Context(), ctrl.CurrentUser(r))
|
||||
item, err := ctrl.svc.Items.Get(ctx, id)
|
||||
if err != nil {
|
||||
return validate.NewRequestError(err, http.StatusNotFound)
|
||||
}
|
||||
|
||||
return server.JSON(w, item, http.StatusOK)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### GET - List with Pagination
|
||||
|
||||
```go
|
||||
func (ctrl *V1Controller) HandleItemsGetAll() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
// Parse query parameters
|
||||
query := extractItemQuery(r)
|
||||
|
||||
ctx := services.NewContext(r.Context(), ctrl.CurrentUser(r))
|
||||
items, err := ctrl.svc.Items.GetAll(ctx, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return server.JSON(w, items, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to extract query params
|
||||
func extractItemQuery(r *http.Request) repo.ItemQuery {
|
||||
params := r.URL.Query()
|
||||
return repo.ItemQuery{
|
||||
Page: queryIntOrNegativeOne(params.Get("page")),
|
||||
PageSize: queryIntOrNegativeOne(params.Get("pageSize")),
|
||||
Search: params.Get("q"),
|
||||
LocationIDs: queryUUIDList(params, "locations"),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST - Create
|
||||
|
||||
```go
|
||||
// HandleItemCreate godoc
|
||||
//
|
||||
// @Summary Create Item
|
||||
// @Tags Items
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param payload body repo.ItemCreate true "Item Data"
|
||||
// @Success 201 {object} repo.ItemOut
|
||||
// @Router /v1/items [POST]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemCreate() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
var data repo.ItemCreate
|
||||
if err := server.Decode(r, &data); err != nil {
|
||||
return validate.NewRequestError(err, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
ctx := services.NewContext(r.Context(), ctrl.CurrentUser(r))
|
||||
item, err := ctrl.svc.Items.Create(ctx, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return server.JSON(w, item, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PUT - Update
|
||||
|
||||
```go
|
||||
// HandleItemUpdate godoc
|
||||
//
|
||||
// @Summary Update Item
|
||||
// @Tags Items
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param payload body repo.ItemUpdate true "Item Data"
|
||||
// @Success 200 {object} repo.ItemOut
|
||||
// @Router /v1/items/{id} [PUT]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemUpdate() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
id, err := ctrl.RouteUUID(r, "id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var data repo.ItemUpdate
|
||||
if err := server.Decode(r, &data); err != nil {
|
||||
return validate.NewRequestError(err, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
ctx := services.NewContext(r.Context(), ctrl.CurrentUser(r))
|
||||
item, err := ctrl.svc.Items.Update(ctx, id, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return server.JSON(w, item, http.StatusOK)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### DELETE
|
||||
|
||||
```go
|
||||
// HandleItemDelete godoc
|
||||
//
|
||||
// @Summary Delete Item
|
||||
// @Tags Items
|
||||
// @Param id path string true "Item ID"
|
||||
// @Success 204
|
||||
// @Router /v1/items/{id} [DELETE]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemDelete() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
id, err := ctrl.RouteUUID(r, "id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := services.NewContext(r.Context(), ctrl.CurrentUser(r))
|
||||
err = ctrl.svc.Items.Delete(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return server.JSON(w, nil, http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### File Upload
|
||||
|
||||
```go
|
||||
func (ctrl *V1Controller) HandleItemAttachmentCreate() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
id, err := ctrl.RouteUUID(r, "id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse multipart form
|
||||
err = r.ParseMultipartForm(32 << 20) // 32MB max
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
ctx := services.NewContext(r.Context(), ctrl.CurrentUser(r))
|
||||
attachment, err := ctrl.svc.Items.CreateAttachment(ctx, id, file, header.Filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return server.JSON(w, attachment, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Routing
|
||||
|
||||
Routes are defined in `backend/app/api/routes.go`:
|
||||
|
||||
```go
|
||||
func (a *app) mountRoutes(repos *repo.AllRepos, svc *services.AllServices) {
|
||||
v1 := v1.NewControllerV1(svc, repos)
|
||||
|
||||
a.server.Get("/api/v1/items", v1.HandleItemsGetAll())
|
||||
a.server.Post("/api/v1/items", v1.HandleItemCreate())
|
||||
a.server.Get("/api/v1/items/{id}", v1.HandleItemGet())
|
||||
a.server.Put("/api/v1/items/{id}", v1.HandleItemUpdate())
|
||||
a.server.Delete("/api/v1/items/{id}", v1.HandleItemDelete())
|
||||
}
|
||||
```
|
||||
|
||||
## Helper Functions
|
||||
|
||||
### Query Parameter Parsing
|
||||
|
||||
Located in `query_params.go`:
|
||||
|
||||
```go
|
||||
func queryIntOrNegativeOne(s string) int
|
||||
func queryBool(s string) bool
|
||||
func queryUUIDList(params url.Values, key string) []uuid.UUID
|
||||
```
|
||||
|
||||
### Response Helpers
|
||||
|
||||
```go
|
||||
// From httpkit/server
|
||||
server.JSON(w, data, statusCode) // JSON response
|
||||
server.Respond(w, statusCode) // Empty response
|
||||
validate.NewRequestError(err, statusCode) // Error response
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
```go
|
||||
user := ctrl.CurrentUser(r) // Get authenticated user (from middleware)
|
||||
```
|
||||
|
||||
## Adding a New Endpoint
|
||||
|
||||
### 1. Create Handler
|
||||
|
||||
In `backend/app/api/handlers/v1/v1_ctrl_myentity.go`:
|
||||
|
||||
```go
|
||||
// HandleMyEntityCreate godoc
|
||||
//
|
||||
// @Summary Create MyEntity
|
||||
// @Tags MyEntity
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param payload body repo.MyEntityCreate true "Data"
|
||||
// @Success 201 {object} repo.MyEntityOut
|
||||
// @Router /v1/my-entity [POST]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleMyEntityCreate() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
var data repo.MyEntityCreate
|
||||
if err := server.Decode(r, &data); err != nil {
|
||||
return validate.NewRequestError(err, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
ctx := services.NewContext(r.Context(), ctrl.CurrentUser(r))
|
||||
result, err := ctrl.svc.MyEntity.Create(ctx, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return server.JSON(w, result, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Add Route
|
||||
|
||||
In `backend/app/api/routes.go`:
|
||||
|
||||
```go
|
||||
a.server.Post("/api/v1/my-entity", v1.HandleMyEntityCreate())
|
||||
```
|
||||
|
||||
### 3. Generate Docs
|
||||
|
||||
```bash
|
||||
task generate # Generates Swagger docs and TypeScript types
|
||||
```
|
||||
|
||||
### 4. Test
|
||||
|
||||
```bash
|
||||
task go:build # Verify builds
|
||||
task go:test # Run tests
|
||||
```
|
||||
|
||||
## Critical Rules
|
||||
|
||||
1. **ALWAYS add Swagger comments** - required for API docs and TypeScript type generation
|
||||
2. **Run `task generate` after handler changes** - updates API documentation
|
||||
3. **Use services, not repos directly** - handlers call services, services call repos
|
||||
4. **Always use `services.Context`** - includes auth and multi-tenancy
|
||||
5. **Handle errors properly** - use `validate.NewRequestError()` with appropriate status codes
|
||||
6. **Validate input** - decode and validate request bodies
|
||||
7. **Return correct status codes** - 200 OK, 201 Created, 204 No Content, 400 Bad Request, 404 Not Found
|
||||
|
||||
## Common Issues
|
||||
|
||||
- **"Missing Swagger docs"** → Add `@Summary`, `@Tags`, `@Router` comments, run `task generate`
|
||||
- **TypeScript types outdated** → Run `task generate` to regenerate
|
||||
- **Auth failures** → Ensure route has auth middleware and `@Security Bearer`
|
||||
- **CORS errors** → Check middleware configuration in `routes.go`
|
||||
341
.github/instructions/backend-internal-core-services.instructions.md
vendored
Normal file
341
.github/instructions/backend-internal-core-services.instructions.md
vendored
Normal file
@@ -0,0 +1,341 @@
|
||||
---
|
||||
applyTo: '/backend/internal/core/services/**/*'
|
||||
---
|
||||
|
||||
# Backend Services Layer Instructions (`/backend/internal/core/services/`)
|
||||
|
||||
## Overview
|
||||
|
||||
The services layer contains business logic that orchestrates between repositories and API handlers. Services handle complex operations, validation, and cross-cutting concerns.
|
||||
|
||||
## Architecture Pattern
|
||||
|
||||
```
|
||||
Handler (API) → Service (Business Logic) → Repository (Data Access) → Database
|
||||
```
|
||||
|
||||
**Separation of concerns:**
|
||||
- **Handlers** (`backend/app/api/handlers/v1/`) - HTTP request/response, routing, auth
|
||||
- **Services** (`backend/internal/core/services/`) - Business logic, orchestration
|
||||
- **Repositories** (`backend/internal/data/repo/`) - Database operations, queries
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
backend/internal/core/services/
|
||||
├── all.go # Service aggregation
|
||||
├── service_items.go # Item business logic
|
||||
├── service_items_attachments.go # Item attachments logic
|
||||
├── service_user.go # User management logic
|
||||
├── service_group.go # Group management logic
|
||||
├── service_background.go # Background tasks
|
||||
├── contexts.go # Service context types
|
||||
├── reporting/ # Reporting subsystem
|
||||
│ ├── eventbus/ # Event bus for notifications
|
||||
│ └── *.go # Report generation logic
|
||||
└── *_test.go # Service tests
|
||||
```
|
||||
|
||||
## Service Structure
|
||||
|
||||
### Standard Pattern
|
||||
|
||||
```go
|
||||
type ItemService struct {
|
||||
repo *repo.AllRepos // Access to all repositories
|
||||
filepath string // File storage path
|
||||
autoIncrementAssetID bool // Feature flags
|
||||
}
|
||||
|
||||
func (svc *ItemService) Create(ctx Context, item repo.ItemCreate) (repo.ItemOut, error) {
|
||||
// 1. Validation
|
||||
if item.Name == "" {
|
||||
return repo.ItemOut{}, errors.New("name required")
|
||||
}
|
||||
|
||||
// 2. Business logic
|
||||
if svc.autoIncrementAssetID {
|
||||
highest, err := svc.repo.Items.GetHighestAssetID(ctx, ctx.GID)
|
||||
if err != nil {
|
||||
return repo.ItemOut{}, err
|
||||
}
|
||||
item.AssetID = highest + 1
|
||||
}
|
||||
|
||||
// 3. Repository call
|
||||
return svc.repo.Items.Create(ctx, ctx.GID, item)
|
||||
}
|
||||
```
|
||||
|
||||
### Service Context
|
||||
|
||||
Services use a custom `Context` type that extends `context.Context`:
|
||||
|
||||
```go
|
||||
type Context struct {
|
||||
context.Context
|
||||
GID uuid.UUID // Group ID for multi-tenancy
|
||||
UID uuid.UUID // User ID for audit
|
||||
}
|
||||
```
|
||||
|
||||
**Always use `Context` from services package, not raw `context.Context`.**
|
||||
|
||||
## Common Service Patterns
|
||||
|
||||
### 1. CRUD with Business Logic
|
||||
|
||||
```go
|
||||
func (svc *ItemService) Update(ctx Context, id uuid.UUID, data repo.ItemUpdate) (repo.ItemOut, error) {
|
||||
// Fetch existing
|
||||
existing, err := svc.repo.Items.Get(ctx, id)
|
||||
if err != nil {
|
||||
return repo.ItemOut{}, err
|
||||
}
|
||||
|
||||
// Business rules
|
||||
if existing.Archived && data.Quantity != nil {
|
||||
return repo.ItemOut{}, errors.New("cannot modify archived items")
|
||||
}
|
||||
|
||||
// Update
|
||||
return svc.repo.Items.Update(ctx, id, data)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Orchestrating Multiple Repositories
|
||||
|
||||
```go
|
||||
func (svc *ItemService) CreateWithAttachment(ctx Context, item repo.ItemCreate, file io.Reader) (repo.ItemOut, error) {
|
||||
// Create item
|
||||
created, err := svc.repo.Items.Create(ctx, ctx.GID, item)
|
||||
if err != nil {
|
||||
return repo.ItemOut{}, err
|
||||
}
|
||||
|
||||
// Upload attachment
|
||||
attachment, err := svc.repo.Attachments.Create(ctx, created.ID, file)
|
||||
if err != nil {
|
||||
// Rollback - delete item
|
||||
_ = svc.repo.Items.Delete(ctx, created.ID)
|
||||
return repo.ItemOut{}, err
|
||||
}
|
||||
|
||||
created.Attachments = []repo.AttachmentOut{attachment}
|
||||
return created, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Background Tasks
|
||||
|
||||
```go
|
||||
func (svc *ItemService) EnsureAssetID(ctx context.Context, gid uuid.UUID) (int, error) {
|
||||
// Get items without asset IDs
|
||||
items, err := svc.repo.Items.GetAllZeroAssetID(ctx, gid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Batch assign
|
||||
highest := svc.repo.Items.GetHighestAssetID(ctx, gid)
|
||||
for _, item := range items {
|
||||
highest++
|
||||
_ = svc.repo.Items.Update(ctx, item.ID, repo.ItemUpdate{
|
||||
AssetID: &highest,
|
||||
})
|
||||
}
|
||||
|
||||
return len(items), nil
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Event Publishing
|
||||
|
||||
Services can publish events to the event bus:
|
||||
|
||||
```go
|
||||
func (svc *ItemService) Delete(ctx Context, id uuid.UUID) error {
|
||||
err := svc.repo.Items.Delete(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Publish event for notifications
|
||||
svc.repo.Bus.Publish(eventbus.Event{
|
||||
Type: "item.deleted",
|
||||
Data: map[string]interface{}{"id": id},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## Service Aggregation
|
||||
|
||||
All services are bundled in `all.go`:
|
||||
|
||||
```go
|
||||
type AllServices struct {
|
||||
User *UserService
|
||||
Group *GroupService
|
||||
Items *ItemService
|
||||
// ... other services
|
||||
}
|
||||
|
||||
func New(repos *repo.AllRepos, filepath string) *AllServices {
|
||||
return &AllServices{
|
||||
User: &UserService{repo: repos},
|
||||
Items: &ItemService{repo: repos, filepath: filepath},
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Accessed in handlers via:**
|
||||
```go
|
||||
ctrl.svc.Items.Create(ctx, itemData)
|
||||
```
|
||||
|
||||
## Working with Services from Handlers
|
||||
|
||||
Handlers call services, not repositories directly:
|
||||
|
||||
```go
|
||||
// In backend/app/api/handlers/v1/v1_ctrl_items.go
|
||||
func (ctrl *V1Controller) HandleItemCreate() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
var itemData repo.ItemCreate
|
||||
if err := server.Decode(r, &itemData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get context with group/user IDs
|
||||
ctx := services.NewContext(r.Context(), ctrl.CurrentUser(r))
|
||||
|
||||
// Call service (not repository)
|
||||
item, err := ctrl.svc.Items.Create(ctx, itemData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return server.JSON(w, item, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Services
|
||||
|
||||
Service tests mock repositories using interfaces:
|
||||
|
||||
```go
|
||||
func TestItemService_Create(t *testing.T) {
|
||||
mockRepo := &mockItemRepo{
|
||||
CreateFunc: func(ctx context.Context, gid uuid.UUID, data repo.ItemCreate) (repo.ItemOut, error) {
|
||||
return repo.ItemOut{ID: uuid.New(), Name: data.Name}, nil
|
||||
},
|
||||
}
|
||||
|
||||
svc := &ItemService{repo: &repo.AllRepos{Items: mockRepo}}
|
||||
|
||||
ctx := services.Context{GID: uuid.New(), UID: uuid.New()}
|
||||
result, err := svc.Create(ctx, repo.ItemCreate{Name: "Test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Test", result.Name)
|
||||
}
|
||||
```
|
||||
|
||||
**Run service tests:**
|
||||
```bash
|
||||
cd backend && go test ./internal/core/services -v
|
||||
```
|
||||
|
||||
## Adding a New Service
|
||||
|
||||
### 1. Create Service File
|
||||
|
||||
Create `backend/internal/core/services/service_myentity.go`:
|
||||
|
||||
```go
|
||||
package services
|
||||
|
||||
type MyEntityService struct {
|
||||
repo *repo.AllRepos
|
||||
}
|
||||
|
||||
func (svc *MyEntityService) Create(ctx Context, data repo.MyEntityCreate) (repo.MyEntityOut, error) {
|
||||
// Business logic here
|
||||
return svc.repo.MyEntity.Create(ctx, ctx.GID, data)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Add to AllServices
|
||||
|
||||
Edit `backend/internal/core/services/all.go`:
|
||||
|
||||
```go
|
||||
type AllServices struct {
|
||||
// ... existing services
|
||||
MyEntity *MyEntityService
|
||||
}
|
||||
|
||||
func New(repos *repo.AllRepos, filepath string) *AllServices {
|
||||
return &AllServices{
|
||||
// ... existing services
|
||||
MyEntity: &MyEntityService{repo: repos},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use in Handler
|
||||
|
||||
In `backend/app/api/handlers/v1/`:
|
||||
|
||||
```go
|
||||
func (ctrl *V1Controller) HandleMyEntityCreate() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
ctx := services.NewContext(r.Context(), ctrl.CurrentUser(r))
|
||||
result, err := ctrl.svc.MyEntity.Create(ctx, data)
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Run Tests
|
||||
|
||||
```bash
|
||||
task generate # If you modified schemas
|
||||
task go:test # Run all tests
|
||||
```
|
||||
|
||||
## Common Service Responsibilities
|
||||
|
||||
**Services should:**
|
||||
- ✅ Contain business logic and validation
|
||||
- ✅ Orchestrate multiple repository calls
|
||||
- ✅ Handle transactions (when needed)
|
||||
- ✅ Publish events for side effects
|
||||
- ✅ Enforce access control and multi-tenancy
|
||||
- ✅ Transform data between API and repository formats
|
||||
|
||||
**Services should NOT:**
|
||||
- ❌ Handle HTTP requests/responses (that's handlers)
|
||||
- ❌ Construct SQL queries (that's repositories)
|
||||
- ❌ Import handler packages (creates circular deps)
|
||||
- ❌ Directly access database (use repositories)
|
||||
|
||||
## Critical Rules
|
||||
|
||||
1. **Always use `services.Context`** - includes group/user IDs for multi-tenancy
|
||||
2. **Services call repos, handlers call services** - maintains layer separation
|
||||
3. **No direct database access** - always through repositories
|
||||
4. **Business logic goes here** - not in handlers or repositories
|
||||
5. **Test services independently** - mock repository dependencies
|
||||
|
||||
## Common Patterns to Follow
|
||||
|
||||
- **Validation:** Check business rules before calling repository
|
||||
- **Error wrapping:** Add context to repository errors
|
||||
- **Logging:** Use `log.Ctx(ctx)` for contextual logging
|
||||
- **Transactions:** Use `repo.WithTx()` for multi-step operations
|
||||
- **Events:** Publish to event bus for notifications/side effects
|
||||
239
.github/instructions/backend-internal-data.instructions.md
vendored
Normal file
239
.github/instructions/backend-internal-data.instructions.md
vendored
Normal file
@@ -0,0 +1,239 @@
|
||||
---
|
||||
applyTo: 'backend/internal/data/**/*'
|
||||
---
|
||||
|
||||
|
||||
# Backend Data Layer Instructions (`/backend/internal/data/`)
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains the data access layer using **Ent ORM** (entity framework). It follows a clear separation between schema definitions, generated code, and repository implementations.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
backend/internal/data/
|
||||
├── ent/ # Ent ORM generated code (DO NOT EDIT)
|
||||
│ ├── schema/ # Schema definitions (EDIT THESE)
|
||||
│ │ ├── item.go # Item entity schema
|
||||
│ │ ├── user.go # User entity schema
|
||||
│ │ ├── location.go # Location entity schema
|
||||
│ │ ├── label.go # Label entity schema
|
||||
│ │ └── mixins/ # Reusable schema mixins
|
||||
│ ├── *.go # Generated entity code
|
||||
│ └── migrate/ # Generated migrations
|
||||
├── repo/ # Repository pattern implementations
|
||||
│ ├── repos_all.go # Aggregates all repositories
|
||||
│ ├── repo_items.go # Item repository
|
||||
│ ├── repo_users.go # User repository
|
||||
│ ├── repo_locations.go # Location repository
|
||||
│ └── *_test.go # Repository tests
|
||||
├── migrations/ # Manual SQL migrations
|
||||
│ ├── sqlite3/ # SQLite-specific migrations
|
||||
│ └── postgres/ # PostgreSQL-specific migrations
|
||||
└── types/ # Custom data types
|
||||
```
|
||||
|
||||
## Ent ORM Workflow
|
||||
|
||||
### 1. Defining Schemas (`ent/schema/`)
|
||||
|
||||
**ALWAYS edit schema files here** - these define your database entities:
|
||||
|
||||
```go
|
||||
// Example: backend/internal/data/ent/schema/item.go
|
||||
type Item struct {
|
||||
ent.Schema
|
||||
}
|
||||
|
||||
func (Item) Fields() []ent.Field {
|
||||
return []ent.Field{
|
||||
field.String("name").NotEmpty(),
|
||||
field.Int("quantity").Default(1),
|
||||
field.Bool("archived").Default(false),
|
||||
}
|
||||
}
|
||||
|
||||
func (Item) Edges() []ent.Edge {
|
||||
return []ent.Edge{
|
||||
edge.From("location", Location.Type).Ref("items").Unique(),
|
||||
edge.From("labels", Label.Type).Ref("items"),
|
||||
}
|
||||
}
|
||||
|
||||
func (Item) Indexes() []ent.Index {
|
||||
return []ent.Index{
|
||||
index.Fields("name"),
|
||||
index.Fields("archived"),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Common schema patterns:**
|
||||
- Use `mixins.BaseMixin{}` for `id`, `created_at`, `updated_at` fields
|
||||
- Use `mixins.DetailsMixin{}` for `name` and `description` fields
|
||||
- Use `GroupMixin{ref: "items"}` to link entities to groups
|
||||
- Add indexes for frequently queried fields
|
||||
|
||||
### 2. Generating Code
|
||||
|
||||
**After modifying any schema file, ALWAYS run:**
|
||||
|
||||
```bash
|
||||
task generate
|
||||
```
|
||||
|
||||
This:
|
||||
1. Runs `go generate ./...` in `backend/internal/` (generates Ent code)
|
||||
2. Generates Swagger docs from API handlers
|
||||
3. Generates TypeScript types for frontend
|
||||
|
||||
**Generated files you'll see:**
|
||||
- `ent/*.go` - Entity types, builders, queries
|
||||
- `ent/migrate/migrate.go` - Auto migrations
|
||||
- `ent/predicate/predicate.go` - Query predicates
|
||||
|
||||
**NEVER edit generated files directly** - changes will be overwritten.
|
||||
|
||||
### 3. Using Generated Code in Repositories
|
||||
|
||||
Repositories in `repo/` use the generated Ent client:
|
||||
|
||||
```go
|
||||
// Example: backend/internal/data/repo/repo_items.go
|
||||
type ItemsRepository struct {
|
||||
db *ent.Client
|
||||
bus *eventbus.EventBus
|
||||
}
|
||||
|
||||
func (r *ItemsRepository) Create(ctx context.Context, gid uuid.UUID, data ItemCreate) (ItemOut, error) {
|
||||
entity, err := r.db.Item.Create().
|
||||
SetName(data.Name).
|
||||
SetQuantity(data.Quantity).
|
||||
SetGroupID(gid).
|
||||
Save(ctx)
|
||||
|
||||
return mapToItemOut(entity), err
|
||||
}
|
||||
```
|
||||
|
||||
## Repository Pattern
|
||||
|
||||
### Structure
|
||||
|
||||
Each entity typically has:
|
||||
- **Repository struct** (`ItemsRepository`) - holds DB client and dependencies
|
||||
- **Input types** (`ItemCreate`, `ItemUpdate`) - API input DTOs
|
||||
- **Output types** (`ItemOut`, `ItemSummary`) - API response DTOs
|
||||
- **Query types** (`ItemQuery`) - search/filter parameters
|
||||
- **Mapper functions** (`mapToItemOut`) - converts Ent entities to output DTOs
|
||||
|
||||
### Key Methods
|
||||
|
||||
Repositories typically implement:
|
||||
- `Create(ctx, gid, input)` - Create new entity
|
||||
- `Get(ctx, id)` - Get single entity by ID
|
||||
- `GetAll(ctx, gid, query)` - Query with pagination/filters
|
||||
- `Update(ctx, id, input)` - Update entity
|
||||
- `Delete(ctx, id)` - Delete entity
|
||||
|
||||
### Working with Ent Queries
|
||||
|
||||
**Loading relationships (edges):**
|
||||
```go
|
||||
items, err := r.db.Item.Query().
|
||||
WithLocation(). // Load location edge
|
||||
WithLabels(). // Load labels edge
|
||||
WithChildren(). // Load child items
|
||||
Where(item.GroupIDEQ(gid)).
|
||||
All(ctx)
|
||||
```
|
||||
|
||||
**Filtering:**
|
||||
```go
|
||||
query := r.db.Item.Query().
|
||||
Where(
|
||||
item.GroupIDEQ(gid),
|
||||
item.ArchivedEQ(false),
|
||||
item.NameContainsFold(search),
|
||||
)
|
||||
```
|
||||
|
||||
**Ordering and pagination:**
|
||||
```go
|
||||
items, err := query.
|
||||
Order(ent.Desc(item.FieldCreatedAt)).
|
||||
Limit(pageSize).
|
||||
Offset((page - 1) * pageSize).
|
||||
All(ctx)
|
||||
```
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Adding a New Entity
|
||||
|
||||
1. **Create schema:** `backend/internal/data/ent/schema/myentity.go`
|
||||
2. **Run:** `task generate` (generates Ent code)
|
||||
3. **Create repository:** `backend/internal/data/repo/repo_myentity.go`
|
||||
4. **Add to AllRepos:** Edit `repo/repos_all.go` to include new repo
|
||||
5. **Run tests:** `task go:test`
|
||||
|
||||
### Adding Fields to Existing Entity
|
||||
|
||||
1. **Edit schema:** `backend/internal/data/ent/schema/item.go`
|
||||
```go
|
||||
field.String("new_field").Optional()
|
||||
```
|
||||
2. **Run:** `task generate`
|
||||
3. **Update repository:** Add field to input/output types in `repo/repo_items.go`
|
||||
4. **Update mappers:** Ensure mapper functions handle new field
|
||||
5. **Run tests:** `task go:test`
|
||||
|
||||
### Adding Relationships (Edges)
|
||||
|
||||
1. **Edit both schemas:**
|
||||
```go
|
||||
// In item.go
|
||||
edge.From("location", Location.Type).Ref("items").Unique()
|
||||
|
||||
// In location.go
|
||||
edge.To("items", Item.Type)
|
||||
```
|
||||
2. **Run:** `task generate`
|
||||
3. **Use in queries:** `.WithLocation()` to load the edge
|
||||
4. **Run tests:** `task go:test`
|
||||
|
||||
## Testing
|
||||
|
||||
Repository tests use `enttest` for in-memory SQLite:
|
||||
|
||||
```go
|
||||
func TestItemRepo(t *testing.T) {
|
||||
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&_fk=1")
|
||||
defer client.Close()
|
||||
|
||||
repo := &ItemsRepository{db: client}
|
||||
// Test methods...
|
||||
}
|
||||
```
|
||||
|
||||
**Run repository tests:**
|
||||
```bash
|
||||
cd backend && go test ./internal/data/repo -v
|
||||
```
|
||||
|
||||
## Critical Rules
|
||||
|
||||
1. **ALWAYS run `task generate` after schema changes** - builds will fail otherwise
|
||||
2. **NEVER edit files in `ent/` except `ent/schema/`** - they're generated
|
||||
3. **Use repositories, not raw Ent queries in services/handlers** - maintains separation
|
||||
4. **Include `group_id` in all queries** - ensures multi-tenancy
|
||||
5. **Use `.WithX()` to load edges** - avoids N+1 queries
|
||||
6. **Test with both SQLite and PostgreSQL** - CI tests both
|
||||
|
||||
## Common Errors
|
||||
|
||||
- **"undefined: ent.ItemX"** → Run `task generate` after schema changes
|
||||
- **Migration conflicts** → Check `migrations/` for manual migration files
|
||||
- **Foreign key violations** → Ensure edges are properly defined in both schemas
|
||||
- **Slow queries** → Add indexes in schema `Indexes()` method
|
||||
157
.github/instructions/code.instructions.md
vendored
Normal file
157
.github/instructions/code.instructions.md
vendored
Normal file
@@ -0,0 +1,157 @@
|
||||
# Homebox Repository Instructions for Coding Agents
|
||||
|
||||
## Repository Overview
|
||||
|
||||
**Type**: Full-stack home inventory management web app (monorepo)
|
||||
**Size**: ~265 Go files, ~371 TypeScript/Vue files
|
||||
**Build Tool**: Task (Taskfile.yml) - **ALWAYS use `task` commands**
|
||||
**Database**: SQLite (default) or PostgreSQL
|
||||
|
||||
### Stack
|
||||
- **Backend** (`/backend`): Go 1.24+, Chi router, Ent ORM, port 7745
|
||||
- **Frontend** (`/frontend`): Nuxt 4, Vue 3, TypeScript, Tailwind CSS, pnpm 9.1.4+, dev proxies to backend
|
||||
|
||||
## Critical Build & Validation Commands
|
||||
|
||||
### Initial Setup (Run Once)
|
||||
```bash
|
||||
task setup # Installs swag, goose, Go deps, pnpm deps
|
||||
```
|
||||
|
||||
### Code Generation (Required Before Backend Work)
|
||||
```bash
|
||||
task generate # Generates Ent ORM, Swagger docs, TypeScript types
|
||||
```
|
||||
**ALWAYS run after**: schema changes, API handler changes, before backend server/tests
|
||||
**Note**: "TypeSpecDef is nil" warnings are normal - ignore them
|
||||
|
||||
### Backend Commands
|
||||
```bash
|
||||
task go:build # Build binary (60-90s)
|
||||
task go:test # Unit tests (5-10s)
|
||||
task go:lint # golangci-lint (6m timeout in CI)
|
||||
task go:all # Tidy + lint + test
|
||||
task go:run # Start server (SQLite)
|
||||
task pr # Full PR validation (3-5 min)
|
||||
```
|
||||
|
||||
### Frontend Commands
|
||||
```bash
|
||||
task ui:dev # Dev server port 3000
|
||||
task ui:check # Type checking
|
||||
task ui:fix # eslint --fix + prettier
|
||||
task ui:watch # Vitest watch mode
|
||||
```
|
||||
**Lint**: Max 1 warning in CI (`pnpm run lint:ci`)
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
task test:ci # Integration tests (15-30s + startup)
|
||||
task test:e2e # Playwright E2E (60s+ per shard, needs playwright install)
|
||||
task pr # Full PR validation: generate + go:all + ui:check + ui:fix + test:ci (3-5 min)
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Key Root Files
|
||||
- `Taskfile.yml` - All commands (always use `task`)
|
||||
- `docker-compose.yml`, `Dockerfile*` - Docker configs
|
||||
- `CONTRIBUTING.md` - Contribution guidelines
|
||||
|
||||
### Backend Structure (`/backend`)
|
||||
```
|
||||
backend/
|
||||
├── app/
|
||||
│ ├── api/ # Main API application
|
||||
│ │ ├── main.go # Entry point
|
||||
│ │ ├── routes.go # Route definitions
|
||||
│ │ ├── handlers/ # HTTP handlers (v1 API)
|
||||
│ │ ├── static/ # Swagger docs, embedded frontend
|
||||
│ │ └── providers/ # Service providers
|
||||
│ └── tools/
|
||||
│ └── typegen/ # TypeScript type generation tool
|
||||
├── internal/
|
||||
│ ├── core/
|
||||
│ │ └── services/ # Business logic layer
|
||||
│ ├── data/
|
||||
│ │ ├── ent/ # Ent ORM generated code + schemas
|
||||
│ │ │ └── schema/ # Schema definitions (edit these)
|
||||
│ │ └── repo/ # Repository pattern implementations
|
||||
│ ├── sys/ # System utilities (config, validation)
|
||||
│ └── web/ # Web middleware
|
||||
├── pkgs/ # Reusable packages
|
||||
├── go.mod, go.sum # Go dependencies
|
||||
└── .golangci.yml # Linter configuration
|
||||
```
|
||||
|
||||
**Patterns**: Schema/API changes → edit source → `task generate`. Never edit generated code in `ent/`.
|
||||
|
||||
### Frontend Structure (`/frontend`)
|
||||
```
|
||||
frontend/
|
||||
├── app.vue # Root component
|
||||
├── nuxt.config.ts # Nuxt configuration
|
||||
├── package.json # Frontend dependencies
|
||||
├── components/ # Vue components (auto-imported)
|
||||
├── pages/ # File-based routing
|
||||
├── layouts/ # Layout components
|
||||
├── composables/ # Vue composables (auto-imported)
|
||||
├── stores/ # Pinia state stores
|
||||
├── lib/
|
||||
│ └── api/
|
||||
│ └── types/ # Generated TypeScript API types
|
||||
├── locales/ # i18n translations
|
||||
├── test/ # Vitest + Playwright tests
|
||||
├── eslint.config.mjs # ESLint configuration
|
||||
└── tailwind.config.js # Tailwind configuration
|
||||
```
|
||||
|
||||
**Patterns**: Auto-imports for `components/` and `composables/`. API types auto-generated - never edit manually.
|
||||
|
||||
## CI/CD Workflows
|
||||
|
||||
PR checks (`.github/workflows/pull-requests.yaml`) on `main`/`vnext`:
|
||||
1. **Backend**: Go 1.24, golangci-lint, `task go:build`, `task go:coverage`
|
||||
2. **Frontend**: Lint (max 1 warning), typecheck, `task test:ci` (SQLite + PostgreSQL v15-17)
|
||||
3. **E2E**: 4 sharded Playwright runs (60min timeout)
|
||||
|
||||
All must pass before merge.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Missing tools**: Run `task setup` first (installs swag, goose, deps)
|
||||
2. **Stale generated code**: Always `task generate` after schema/API changes
|
||||
3. **Test failures**: Integration tests may fail first run (race condition) - retry
|
||||
4. **Port in use**: Backend uses 7745 - kill existing process
|
||||
5. **SQLite locked**: Delete `.data/homebox.db-*` files
|
||||
6. **Clean build**: `rm -rf build/ backend/app/api/static/public/ frontend/.nuxt`
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Backend defaults in `Taskfile.yml`:
|
||||
- `HBOX_LOG_LEVEL=debug`
|
||||
- `HBOX_DATABASE_DRIVER=sqlite3` (or `postgres`)
|
||||
- `HBOX_DATABASE_SQLITE_PATH=.data/homebox.db?_pragma=busy_timeout=1000&_pragma=journal_mode=WAL&_fk=1`
|
||||
- PostgreSQL: `HBOX_DATABASE_*` vars for username/password/host/port/database
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
Before PR:
|
||||
- [ ] `task generate` after schema/API changes
|
||||
- [ ] `task pr` passes (includes lint, test, typecheck)
|
||||
- [ ] No build artifacts committed (check `.gitignore`)
|
||||
- [ ] Code matches existing patterns
|
||||
|
||||
## Quick Reference
|
||||
|
||||
**Dev environment**: `task go:run` (terminal 1) + `task ui:dev` (terminal 2)
|
||||
|
||||
**API changes**: Edit handlers → add Swagger comments → `task generate` → `task go:build` → `task go:test`
|
||||
|
||||
**Schema changes**: Edit `ent/schema/*.go` → `task generate` → update repo methods → `task go:test`
|
||||
|
||||
**Specific tests**: `cd backend && go test ./path -v` or `cd frontend && pnpm run test:watch`
|
||||
|
||||
## Trust These Instructions
|
||||
|
||||
Instructions are validated and current. Only explore further if info is incomplete, incorrect, or you encounter undocumented errors. Use `task --list-all` for all commands.
|
||||
480
.github/instructions/frontend.instructions.md
vendored
Normal file
480
.github/instructions/frontend.instructions.md
vendored
Normal file
@@ -0,0 +1,480 @@
|
||||
---
|
||||
applyTo: 'frontend/**/*'
|
||||
---
|
||||
|
||||
|
||||
# Frontend Components & Pages Instructions (`/frontend/`)
|
||||
|
||||
## Overview
|
||||
|
||||
The frontend is a Nuxt 4 application with Vue 3 and TypeScript. It uses auto-imports for components and composables, file-based routing, and generated TypeScript types from the backend API.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── components/ # Vue components (auto-imported)
|
||||
│ ├── Item/ # Item-related components
|
||||
│ ├── Location/ # Location components
|
||||
│ ├── Label/ # Label components
|
||||
│ ├── Form/ # Form components
|
||||
│ └── ui/ # Shadcn-vue UI components
|
||||
├── pages/ # File-based routes (auto-routing)
|
||||
│ ├── index.vue # Home page (/)
|
||||
│ ├── items.vue # Items list (/items)
|
||||
│ ├── item/
|
||||
│ │ └── [id].vue # Item detail (/item/:id)
|
||||
│ ├── locations.vue # Locations list (/locations)
|
||||
│ └── profile.vue # User profile (/profile)
|
||||
├── composables/ # Vue composables (auto-imported)
|
||||
│ ├── use-api.ts # API client wrapper
|
||||
│ ├── use-auth.ts # Authentication
|
||||
│ └── use-user-api.ts # User API helpers
|
||||
├── stores/ # Pinia state management
|
||||
│ ├── auth.ts # Auth state
|
||||
│ └── preferences.ts # User preferences
|
||||
├── lib/
|
||||
│ └── api/
|
||||
│ └── types/ # Generated TypeScript types (DO NOT EDIT)
|
||||
├── layouts/ # Layout components
|
||||
│ └── default.vue # Default layout
|
||||
├── locales/ # i18n translations
|
||||
├── test/ # Tests (Vitest + Playwright)
|
||||
└── nuxt.config.ts # Nuxt configuration
|
||||
```
|
||||
|
||||
## Auto-Imports
|
||||
|
||||
### Components
|
||||
|
||||
Components in `components/` are **automatically imported** - no import statement needed:
|
||||
|
||||
```vue
|
||||
<!-- components/Item/Card.vue -->
|
||||
<template>
|
||||
<div class="item-card">{{ item.name }}</div>
|
||||
</template>
|
||||
|
||||
<!-- pages/items.vue - NO import needed -->
|
||||
<template>
|
||||
<ItemCard :item="item" />
|
||||
</template>
|
||||
```
|
||||
|
||||
**Naming convention:** Nested path becomes component name
|
||||
- `components/Item/Card.vue` → `<ItemCard />`
|
||||
- `components/Form/TextField.vue` → `<FormTextField />`
|
||||
|
||||
### Composables
|
||||
|
||||
Composables in `composables/` are **automatically imported**:
|
||||
|
||||
```ts
|
||||
// composables/use-items.ts
|
||||
export function useItems() {
|
||||
const api = useUserApi()
|
||||
|
||||
async function getItems() {
|
||||
const { data } = await api.items.getAll()
|
||||
return data
|
||||
}
|
||||
|
||||
return { getItems }
|
||||
}
|
||||
|
||||
// pages/items.vue - NO import needed
|
||||
const { getItems } = useItems()
|
||||
const items = await getItems()
|
||||
```
|
||||
|
||||
## File-Based Routing
|
||||
|
||||
Pages in `pages/` automatically become routes:
|
||||
|
||||
```
|
||||
pages/index.vue → /
|
||||
pages/items.vue → /items
|
||||
pages/item/[id].vue → /item/:id
|
||||
pages/locations.vue → /locations
|
||||
pages/location/[id].vue → /location/:id
|
||||
pages/profile.vue → /profile
|
||||
```
|
||||
|
||||
### Dynamic Routes
|
||||
|
||||
Use square brackets for dynamic segments:
|
||||
|
||||
```vue
|
||||
<!-- pages/item/[id].vue -->
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
const id = route.params.id
|
||||
|
||||
const { data: item } = await useUserApi().items.getOne(id)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1>{{ item.name }}</h1>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## API Integration
|
||||
|
||||
### Generated Types
|
||||
|
||||
API types are auto-generated from backend Swagger docs:
|
||||
|
||||
```ts
|
||||
// lib/api/types/data-contracts.ts (GENERATED - DO NOT EDIT)
|
||||
export interface ItemOut {
|
||||
id: string
|
||||
name: string
|
||||
quantity: number
|
||||
createdAt: Date | string
|
||||
updatedAt: Date | string
|
||||
}
|
||||
|
||||
export interface ItemCreate {
|
||||
name: string
|
||||
quantity?: number
|
||||
locationId?: string
|
||||
}
|
||||
```
|
||||
|
||||
**Regenerate after backend API changes:**
|
||||
```bash
|
||||
task generate # Runs in backend, updates frontend/lib/api/types/
|
||||
```
|
||||
|
||||
### Using the API Client
|
||||
|
||||
The `useUserApi()` composable provides typed API access:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import type { ItemCreate, ItemOut } from '~/lib/api/types/data-contracts'
|
||||
|
||||
const api = useUserApi()
|
||||
|
||||
// GET all items
|
||||
const { data: items } = await api.items.getAll({
|
||||
q: 'search term',
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
})
|
||||
|
||||
// GET single item
|
||||
const { data: item } = await api.items.getOne(itemId)
|
||||
|
||||
// POST create item
|
||||
const newItem: ItemCreate = {
|
||||
name: 'New Item',
|
||||
quantity: 1
|
||||
}
|
||||
const { data: created } = await api.items.create(newItem)
|
||||
|
||||
// PUT update item
|
||||
const { data: updated } = await api.items.update(itemId, {
|
||||
quantity: 5
|
||||
})
|
||||
|
||||
// DELETE item
|
||||
await api.items.delete(itemId)
|
||||
</script>
|
||||
```
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### Standard Vue 3 Composition API
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import type { ItemOut } from '~/lib/api/types/data-contracts'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
item: ItemOut
|
||||
editable?: boolean
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// Emits
|
||||
interface Emits {
|
||||
(e: 'update', item: ItemOut): void
|
||||
(e: 'delete', id: string): void
|
||||
}
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// State
|
||||
const isEditing = ref(false)
|
||||
const localItem = ref({ ...props.item })
|
||||
|
||||
// Computed
|
||||
const displayName = computed(() => {
|
||||
return props.item.name.toUpperCase()
|
||||
})
|
||||
|
||||
// Methods
|
||||
function handleSave() {
|
||||
emit('update', localItem.value)
|
||||
isEditing.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="item-card">
|
||||
<h3>{{ displayName }}</h3>
|
||||
<p v-if="!isEditing">Quantity: {{ item.quantity }}</p>
|
||||
|
||||
<input
|
||||
v-if="isEditing"
|
||||
v-model.number="localItem.quantity"
|
||||
type="number"
|
||||
/>
|
||||
|
||||
<button v-if="editable" @click="isEditing = !isEditing">
|
||||
{{ isEditing ? 'Cancel' : 'Edit' }}
|
||||
</button>
|
||||
<button v-if="isEditing" @click="handleSave">Save</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.item-card {
|
||||
padding: 1rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### Using Pinia Stores
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from '~/stores/auth'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// Access state
|
||||
const user = computed(() => authStore.user)
|
||||
const isLoggedIn = computed(() => authStore.isLoggedIn)
|
||||
|
||||
// Call actions
|
||||
async function logout() {
|
||||
await authStore.logout()
|
||||
navigateTo('/login')
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Form Handling
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { useForm } from 'vee-validate'
|
||||
import type { ItemCreate } from '~/lib/api/types/data-contracts'
|
||||
|
||||
const api = useUserApi()
|
||||
|
||||
const { values, errors, handleSubmit } = useForm<ItemCreate>({
|
||||
initialValues: {
|
||||
name: '',
|
||||
quantity: 1
|
||||
}
|
||||
})
|
||||
|
||||
const onSubmit = handleSubmit(async (values) => {
|
||||
try {
|
||||
const { data } = await api.items.create(values)
|
||||
navigateTo(`/item/${data.id}`)
|
||||
} catch (error) {
|
||||
console.error('Failed to create item:', error)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="onSubmit">
|
||||
<input v-model="values.name" type="text" placeholder="Item name" />
|
||||
<span v-if="errors.name">{{ errors.name }}</span>
|
||||
|
||||
<input v-model.number="values.quantity" type="number" />
|
||||
<span v-if="errors.quantity">{{ errors.quantity }}</span>
|
||||
|
||||
<button type="submit">Create Item</button>
|
||||
</form>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
### Tailwind CSS
|
||||
|
||||
The project uses Tailwind CSS for styling:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="flex items-center justify-between p-4 bg-white rounded-lg shadow-md">
|
||||
<h3 class="text-lg font-semibold text-gray-900">{{ item.name }}</h3>
|
||||
<span class="text-sm text-gray-500">Qty: {{ item.quantity }}</span>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Shadcn-vue Components
|
||||
|
||||
UI components from `components/ui/` (Shadcn-vue):
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h3>{{ item.name }}</h3>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>{{ item.description }}</p>
|
||||
<Button @click="handleEdit">Edit</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Vitest (Unit/Integration)
|
||||
|
||||
Tests use Vitest with the backend API running:
|
||||
|
||||
```ts
|
||||
// test/items.test.ts
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { useUserApi } from '~/composables/use-user-api'
|
||||
|
||||
describe('Items API', () => {
|
||||
it('should create and fetch item', async () => {
|
||||
const api = useUserApi()
|
||||
|
||||
// Create item
|
||||
const { data: created } = await api.items.create({
|
||||
name: 'Test Item',
|
||||
quantity: 1
|
||||
})
|
||||
|
||||
expect(created.name).toBe('Test Item')
|
||||
|
||||
// Fetch item
|
||||
const { data: fetched } = await api.items.getOne(created.id)
|
||||
expect(fetched.id).toBe(created.id)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
**Run tests:**
|
||||
```bash
|
||||
task ui:watch # Watch mode
|
||||
cd frontend && pnpm run test:ci # CI mode
|
||||
```
|
||||
|
||||
### Playwright (E2E)
|
||||
|
||||
E2E tests in `test/`:
|
||||
|
||||
```ts
|
||||
// test/e2e/items.spec.ts
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('should create new item', async ({ page }) => {
|
||||
await page.goto('/items')
|
||||
|
||||
await page.click('button:has-text("New Item")')
|
||||
await page.fill('input[name="name"]', 'Test Item')
|
||||
await page.fill('input[name="quantity"]', '5')
|
||||
await page.click('button:has-text("Save")')
|
||||
|
||||
await expect(page.locator('text=Test Item')).toBeVisible()
|
||||
})
|
||||
```
|
||||
|
||||
**Run E2E tests:**
|
||||
```bash
|
||||
task test:e2e # Full E2E suite
|
||||
```
|
||||
|
||||
## Adding a New Feature
|
||||
|
||||
### 1. Update Backend API
|
||||
|
||||
Make backend changes first (schema, service, handler):
|
||||
```bash
|
||||
# Edit backend files
|
||||
task generate # Regenerates TypeScript types
|
||||
```
|
||||
|
||||
### 2. Create Component
|
||||
|
||||
Create `components/MyFeature/Card.vue`:
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import type { MyFeatureOut } from '~/lib/api/types/data-contracts'
|
||||
|
||||
interface Props {
|
||||
feature: MyFeatureOut
|
||||
}
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>{{ feature.name }}</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 3. Create Page
|
||||
|
||||
Create `pages/my-feature/[id].vue`:
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
const api = useUserApi()
|
||||
|
||||
const { data: feature } = await api.myFeature.getOne(route.params.id)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MyFeatureCard :feature="feature" />
|
||||
</template>
|
||||
```
|
||||
|
||||
### 4. Test
|
||||
|
||||
```bash
|
||||
task ui:check # Type checking
|
||||
task ui:fix # Linting
|
||||
task ui:watch # Run tests
|
||||
```
|
||||
|
||||
## Critical Rules
|
||||
|
||||
1. **Never edit generated types** - `lib/api/types/` is auto-generated, run `task generate` after backend changes
|
||||
2. **No manual imports for components/composables** - auto-imported from `components/` and `composables/`
|
||||
3. **Use TypeScript** - all `.vue` files use `<script setup lang="ts">`
|
||||
4. **Follow file-based routing** - pages in `pages/` become routes automatically
|
||||
5. **Use `useUserApi()` for API calls** - provides typed, authenticated API client
|
||||
6. **Max 1 linting warning in CI** - run `task ui:fix` before committing
|
||||
7. **Test with backend running** - integration tests need API server
|
||||
|
||||
## Common Issues
|
||||
|
||||
- **"Type not found"** → Run `task generate` to regenerate types from backend
|
||||
- **Component not found** → Check naming (nested path = component name)
|
||||
- **API call fails** → Ensure backend is running (`task go:run`)
|
||||
- **Lint errors** → Run `task ui:fix` to auto-fix
|
||||
- **Type errors** → Run `task ui:check` for detailed errors
|
||||
349
.github/scripts/update_language_names.py
vendored
Executable file
349
.github/scripts/update_language_names.py
vendored
Executable file
@@ -0,0 +1,349 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to automatically update language names in the English translation file.
|
||||
Queries Weblate for translation completion and language names.
|
||||
Only adds languages with >=80% completion to en.json.
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import requests
|
||||
from babel import Locale, UnknownLocaleError
|
||||
|
||||
LOCALES_DIR = Path('frontend/locales')
|
||||
EN_JSON_PATH = LOCALES_DIR / 'en.json'
|
||||
WEBLATE_API_URL = 'https://translate.sysadminsmedia.com/api'
|
||||
WEBLATE_PROJECT = 'homebox'
|
||||
WEBLATE_COMPONENT = 'frontend'
|
||||
COMPLETION_THRESHOLD = 80.0 # Minimum completion percentage to include language
|
||||
TIMEOUT = 10 # seconds
|
||||
|
||||
|
||||
def setup_logging():
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s %(levelname)s: %(message)s'
|
||||
)
|
||||
|
||||
|
||||
def get_locale_files() -> List[str]:
|
||||
"""Get all locale codes from JSON files in the locales directory."""
|
||||
if not LOCALES_DIR.exists():
|
||||
logging.error("Locales directory not found: %s", LOCALES_DIR)
|
||||
return []
|
||||
|
||||
locale_codes = []
|
||||
for file in sorted(LOCALES_DIR.glob('*.json')):
|
||||
# Extract locale code from filename (e.g., "en.json" -> "en")
|
||||
locale_code = file.stem
|
||||
# Validate locale code format - should not contain dots
|
||||
if '.' not in locale_code:
|
||||
locale_codes.append(locale_code)
|
||||
else:
|
||||
logging.warning("Skipping invalid locale code: %s", locale_code)
|
||||
|
||||
logging.info("Found %d locale files", len(locale_codes))
|
||||
return sorted(locale_codes)
|
||||
|
||||
|
||||
def fetch_weblate_translations() -> Optional[Dict[str, Dict]]:
|
||||
"""
|
||||
Fetch translation statistics from Weblate API.
|
||||
|
||||
Returns:
|
||||
Dict mapping locale code to translation data (percent, name, native_name)
|
||||
or None if API is unavailable
|
||||
"""
|
||||
url = f"{WEBLATE_API_URL}/components/{WEBLATE_PROJECT}/{WEBLATE_COMPONENT}/translations/"
|
||||
|
||||
try:
|
||||
# Weblate API may require pagination
|
||||
translations = {}
|
||||
page_url = url
|
||||
|
||||
while page_url:
|
||||
logging.info("Fetching translations from Weblate: %s", page_url)
|
||||
resp = requests.get(page_url, timeout=TIMEOUT)
|
||||
|
||||
if resp.status_code != 200:
|
||||
logging.warning("Weblate API returned status %d", resp.status_code)
|
||||
return None
|
||||
|
||||
data = resp.json()
|
||||
|
||||
for trans in data.get('results', []):
|
||||
# Weblate uses underscores, we use hyphens
|
||||
locale_code = trans.get('language_code', '').replace('_', '-')
|
||||
percent = trans.get('translated_percent', 0.0)
|
||||
|
||||
lang_info = trans.get('language', {})
|
||||
english_name = lang_info.get('name', '')
|
||||
native_name = lang_info.get('native', '')
|
||||
|
||||
translations[locale_code] = {
|
||||
'percent': percent,
|
||||
'english_name': english_name,
|
||||
'native_name': native_name
|
||||
}
|
||||
|
||||
# Check for next page
|
||||
page_url = data.get('next')
|
||||
|
||||
logging.info("Fetched %d translations from Weblate", len(translations))
|
||||
return translations
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.warning("Failed to fetch from Weblate API: %s", e)
|
||||
return None
|
||||
except Exception as e:
|
||||
logging.error("Unexpected error fetching Weblate data: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def get_language_name_from_babel(locale_code: str) -> Optional[str]:
|
||||
"""
|
||||
Get the language name using Babel in format "English (Native)".
|
||||
Special handling for variants that need disambiguation (Portuguese, Chinese).
|
||||
|
||||
Args:
|
||||
locale_code: Language/locale code (e.g., 'en', 'pt-BR', 'zh-CN')
|
||||
|
||||
Returns:
|
||||
Language name in format "English (Native)" or None if cannot parse
|
||||
"""
|
||||
try:
|
||||
# Special handling for ar-AA (non-standard code, use standard 'ar')
|
||||
if locale_code == 'ar-AA':
|
||||
locale = Locale.parse('ar')
|
||||
else:
|
||||
# Parse locale code using Babel
|
||||
locale = Locale.parse(locale_code.replace('-', '_'))
|
||||
|
||||
# Get English display name
|
||||
english_name = locale.get_display_name('en')
|
||||
|
||||
# Get native display name
|
||||
native_name = locale.get_display_name(locale)
|
||||
|
||||
if not english_name:
|
||||
return None
|
||||
|
||||
# Special handling for Portuguese variants (distinguish Brazil vs Portugal)
|
||||
if locale_code == 'pt-BR':
|
||||
native_base = native_name.split('(')[0].strip() if '(' in native_name else native_name
|
||||
return f"Portuguese — Brazil ({native_base})"
|
||||
elif locale_code == 'pt-PT':
|
||||
native_base = native_name.split('(')[0].strip() if '(' in native_name else native_name
|
||||
return f"Portuguese — Portugal ({native_base})"
|
||||
|
||||
# Special handling for Chinese variants (distinguish Simplified/Traditional and regions)
|
||||
if locale_code == 'zh-CN':
|
||||
native_base = native_name.split('(')[0].strip() if '(' in native_name else native_name
|
||||
return f"Chinese — Simplified ({native_base})"
|
||||
elif locale_code == 'zh-TW':
|
||||
native_base = native_name.split('(')[0].strip() if '(' in native_name else native_name
|
||||
return f"Chinese — Traditional ({native_base})"
|
||||
elif locale_code == 'zh-HK':
|
||||
native_base = native_name.split('(')[0].strip() if '(' in native_name else native_name
|
||||
return f"Chinese — Hong Kong ({native_base})"
|
||||
elif locale_code == 'zh-MO':
|
||||
native_base = native_name.split('(')[0].strip() if '(' in native_name else native_name
|
||||
return f"Chinese — Macau ({native_base})"
|
||||
|
||||
# Format: "English (Native)" if native name differs and is available
|
||||
if native_name and native_name != english_name:
|
||||
# Clean up nested parentheses for complex locales
|
||||
if '(' in english_name and '(' in native_name:
|
||||
# For cases like "Japanese (Japan) (日本語 (日本))"
|
||||
# Simplify to "Japanese (日本語)"
|
||||
english_base = english_name.split('(')[0].strip()
|
||||
native_base = native_name.split('(')[0].strip()
|
||||
return f"{english_base} ({native_base})"
|
||||
else:
|
||||
return f"{english_name} ({native_name})"
|
||||
else:
|
||||
return english_name
|
||||
|
||||
except (UnknownLocaleError, ValueError) as e:
|
||||
logging.debug("Could not parse locale '%s' with Babel: %s", locale_code, e)
|
||||
return None
|
||||
|
||||
|
||||
def get_language_name(locale_code: str, weblate_data: Optional[Dict] = None) -> Optional[str]:
|
||||
"""
|
||||
Get the display name for a locale code.
|
||||
Priority: Weblate API > Babel > None
|
||||
|
||||
Args:
|
||||
locale_code: Language/locale code (e.g., 'en', 'pt-BR', 'zh-CN')
|
||||
weblate_data: Translation data from Weblate (if available)
|
||||
|
||||
Returns:
|
||||
Language name in format "English (Native)" or None if invalid
|
||||
"""
|
||||
# Validate locale code format
|
||||
if '.' in locale_code or locale_code.startswith('languages.'):
|
||||
logging.error("Invalid locale code format: %s", locale_code)
|
||||
return None
|
||||
|
||||
# Try Weblate first
|
||||
if weblate_data and locale_code in weblate_data:
|
||||
english_name = weblate_data[locale_code].get('english_name', '')
|
||||
native_name = weblate_data[locale_code].get('native_name', '')
|
||||
|
||||
if english_name:
|
||||
# Format: "English (Native)" if both names available and different
|
||||
if native_name and native_name != english_name:
|
||||
return f"{english_name} ({native_name})"
|
||||
else:
|
||||
return english_name
|
||||
|
||||
# Fallback to Babel
|
||||
babel_name = get_language_name_from_babel(locale_code)
|
||||
if babel_name:
|
||||
return babel_name
|
||||
|
||||
# If all else fails, return None (don't guess)
|
||||
logging.warning("Could not determine language name for: %s", locale_code)
|
||||
return None
|
||||
|
||||
|
||||
def load_en_json() -> dict:
|
||||
"""Load the English translation JSON file."""
|
||||
if not EN_JSON_PATH.exists():
|
||||
logging.error("English translation file not found: %s", EN_JSON_PATH)
|
||||
return {}
|
||||
|
||||
try:
|
||||
with EN_JSON_PATH.open('r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except (IOError, json.JSONDecodeError) as e:
|
||||
logging.error("Failed to load %s: %s", EN_JSON_PATH, e)
|
||||
return {}
|
||||
|
||||
|
||||
def save_en_json(data: dict):
|
||||
"""Save the English translation JSON file."""
|
||||
try:
|
||||
with EN_JSON_PATH.open('w', encoding='utf-8') as f:
|
||||
# Use 4-space indentation to match existing file format
|
||||
json.dump(data, f, ensure_ascii=False, indent=4)
|
||||
# Add newline at end of file
|
||||
f.write('\n')
|
||||
logging.info("Saved updated en.json")
|
||||
except IOError as e:
|
||||
logging.error("Failed to save %s: %s", EN_JSON_PATH, e)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def update_language_names(en_data: dict, locale_codes: List[str], weblate_data: Optional[Dict] = None) -> bool:
|
||||
"""
|
||||
Update the languages section in en.json.
|
||||
- Add new languages with >=80% completion (from Weblate) or that exist as locale files
|
||||
- Never remove existing entries (even if completion drops below 80%)
|
||||
|
||||
Args:
|
||||
en_data: The parsed en.json data
|
||||
locale_codes: List of all locale codes from files
|
||||
weblate_data: Translation data from Weblate (if available)
|
||||
|
||||
Returns:
|
||||
True if changes were made, False otherwise
|
||||
"""
|
||||
# Ensure languages section exists
|
||||
if 'languages' not in en_data:
|
||||
en_data['languages'] = {}
|
||||
logging.info("Created 'languages' section in en.json")
|
||||
|
||||
languages = en_data['languages']
|
||||
original_languages = languages.copy()
|
||||
|
||||
# Process each locale file
|
||||
added_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
for locale_code in locale_codes:
|
||||
# Skip if already in languages (never remove existing entries)
|
||||
if locale_code in languages:
|
||||
continue
|
||||
|
||||
# Check Weblate completion threshold if data available
|
||||
if weblate_data and locale_code in weblate_data:
|
||||
percent = weblate_data[locale_code].get('percent', 0.0)
|
||||
|
||||
if percent < COMPLETION_THRESHOLD:
|
||||
logging.info("Skipping %s: %.1f%% completion (threshold: %.1f%%)",
|
||||
locale_code, percent, COMPLETION_THRESHOLD)
|
||||
skipped_count += 1
|
||||
continue
|
||||
else:
|
||||
logging.info("Including %s: %.1f%% completion", locale_code, percent)
|
||||
else:
|
||||
# If Weblate data not available, include locale file but log warning
|
||||
logging.info("Including %s: Weblate data not available, locale file exists", locale_code)
|
||||
|
||||
# Get language name
|
||||
language_name = get_language_name(locale_code, weblate_data)
|
||||
|
||||
if language_name:
|
||||
languages[locale_code] = language_name
|
||||
logging.info("Added language: %s = %s", locale_code, language_name)
|
||||
added_count += 1
|
||||
else:
|
||||
logging.warning("Skipping %s: could not determine language name", locale_code)
|
||||
skipped_count += 1
|
||||
|
||||
# Sort languages alphabetically by key
|
||||
en_data['languages'] = dict(sorted(languages.items()))
|
||||
|
||||
# Check if anything changed
|
||||
changed = (original_languages != en_data['languages'])
|
||||
|
||||
if changed:
|
||||
logging.info("Updated %d language names, skipped %d", added_count, skipped_count)
|
||||
else:
|
||||
logging.info("All languages already present, no changes needed")
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
def main():
|
||||
setup_logging()
|
||||
logging.info("🔄 Starting language names update")
|
||||
|
||||
# Get all locale files
|
||||
locale_codes = get_locale_files()
|
||||
if not locale_codes:
|
||||
logging.error("No locale files found")
|
||||
sys.exit(1)
|
||||
|
||||
# Load English translation file
|
||||
en_data = load_en_json()
|
||||
if not en_data:
|
||||
logging.error("Failed to load English translation file")
|
||||
sys.exit(1)
|
||||
|
||||
# Fetch Weblate translation statistics
|
||||
weblate_data = fetch_weblate_translations()
|
||||
if weblate_data:
|
||||
logging.info("Successfully fetched Weblate data for %d languages", len(weblate_data))
|
||||
else:
|
||||
logging.warning("Weblate data not available, proceeding with locale files only")
|
||||
|
||||
# Update language names
|
||||
changed = update_language_names(en_data, locale_codes, weblate_data)
|
||||
|
||||
if changed:
|
||||
save_en_json(en_data)
|
||||
logging.info("✅ Language names updated successfully")
|
||||
else:
|
||||
logging.info("✅ No updates needed, en.json is already up-to-date")
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
259
.github/scripts/upgrade-test/README.md
vendored
Normal file
259
.github/scripts/upgrade-test/README.md
vendored
Normal file
@@ -0,0 +1,259 @@
|
||||
# HomeBox Upgrade Testing Workflow
|
||||
|
||||
This document describes the automated upgrade testing workflow for HomeBox.
|
||||
|
||||
## Overview
|
||||
|
||||
The upgrade test workflow is designed to ensure data integrity and functionality when upgrading HomeBox from one version to another. It automatically:
|
||||
|
||||
1. Deploys a stable version of HomeBox
|
||||
2. Creates test data (users, items, locations, labels, notifiers, attachments)
|
||||
3. Upgrades to the latest version from the main branch
|
||||
4. Verifies all data and functionality remain intact
|
||||
|
||||
## Workflow File
|
||||
|
||||
**Location**: `.github/workflows/upgrade-test.yaml`
|
||||
|
||||
## Trigger Conditions
|
||||
|
||||
The workflow runs:
|
||||
- **Daily**: Automatically at 2 AM UTC (via cron schedule)
|
||||
- **Manual**: Can be triggered manually via GitHub Actions UI
|
||||
- **On Push**: When changes are made to the workflow files or test scripts
|
||||
|
||||
## Test Scenarios
|
||||
|
||||
### 1. Environment Setup
|
||||
- Pulls the latest stable HomeBox Docker image from GHCR
|
||||
- Starts the application with test configuration
|
||||
- Ensures the service is healthy and ready
|
||||
|
||||
### 2. Data Creation
|
||||
|
||||
The workflow creates comprehensive test data using the `create-test-data.sh` script:
|
||||
|
||||
#### Users and Groups
|
||||
- **Group 1**: 5 users (user1@homebox.test through user5@homebox.test)
|
||||
- **Group 2**: 2 users (user6@homebox.test and user7@homebox.test)
|
||||
- All users have password: `TestPassword123!`
|
||||
|
||||
#### Locations
|
||||
- **Group 1**: Living Room, Garage
|
||||
- **Group 2**: Home Office
|
||||
|
||||
#### Labels
|
||||
- **Group 1**: Electronics, Important
|
||||
- **Group 2**: Work Equipment
|
||||
|
||||
#### Items
|
||||
- **Group 1**: 5 items (Laptop Computer, Power Drill, TV Remote, Tool Box, Coffee Maker)
|
||||
- **Group 2**: 2 items (Monitor, Keyboard)
|
||||
|
||||
#### Attachments
|
||||
- Multiple attachments added to various items (receipts, manuals, warranties)
|
||||
|
||||
#### Notifiers
|
||||
- **Group 1**: Test notifier named "TESTING"
|
||||
|
||||
### 3. Upgrade Process
|
||||
|
||||
1. Stops the stable version container
|
||||
2. Builds a fresh image from the current main branch
|
||||
3. Copies the database to a new location
|
||||
4. Starts the new version with the existing data
|
||||
|
||||
### 4. Verification Tests
|
||||
|
||||
The Playwright test suite (`upgrade-verification.spec.ts`) verifies:
|
||||
|
||||
- ✅ **User Authentication**: All 7 users can log in with their credentials
|
||||
- ✅ **Data Persistence**: All items, locations, and labels are present
|
||||
- ✅ **Attachments**: File attachments are correctly associated with items
|
||||
- ✅ **Notifiers**: The "TESTING" notifier is still configured
|
||||
- ✅ **UI Functionality**: Version display, theme switching work correctly
|
||||
- ✅ **Data Isolation**: Groups can only see their own data
|
||||
|
||||
## Test Data File
|
||||
|
||||
The setup script generates a JSON file at `/tmp/test-users.json` containing:
|
||||
|
||||
```json
|
||||
{
|
||||
"users": [
|
||||
{
|
||||
"email": "user1@homebox.test",
|
||||
"password": "TestPassword123!",
|
||||
"token": "...",
|
||||
"group": "1"
|
||||
},
|
||||
...
|
||||
],
|
||||
"locations": {
|
||||
"group1": ["location-id-1", "location-id-2"],
|
||||
"group2": ["location-id-3"]
|
||||
},
|
||||
"labels": {...},
|
||||
"items": {...},
|
||||
"notifiers": {...}
|
||||
}
|
||||
```
|
||||
|
||||
This file is used by the Playwright tests to verify data integrity.
|
||||
|
||||
## Scripts
|
||||
|
||||
### create-test-data.sh
|
||||
|
||||
**Location**: `.github/scripts/upgrade-test/create-test-data.sh`
|
||||
|
||||
**Purpose**: Creates all test data via the HomeBox REST API
|
||||
|
||||
**Environment Variables**:
|
||||
- `HOMEBOX_URL`: Base URL of the HomeBox instance (default: http://localhost:7745)
|
||||
- `TEST_DATA_FILE`: Path to output JSON file (default: /tmp/test-users.json)
|
||||
|
||||
**Requirements**:
|
||||
- `curl`: For API calls
|
||||
- `jq`: For JSON processing
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
export HOMEBOX_URL=http://localhost:7745
|
||||
./.github/scripts/upgrade-test/create-test-data.sh
|
||||
```
|
||||
|
||||
## Running Tests Locally
|
||||
|
||||
To run the upgrade tests locally:
|
||||
|
||||
### Prerequisites
|
||||
```bash
|
||||
# Install dependencies
|
||||
sudo apt-get install -y jq curl docker.io
|
||||
|
||||
# Install pnpm and Playwright
|
||||
cd frontend
|
||||
pnpm install
|
||||
pnpm exec playwright install --with-deps chromium
|
||||
```
|
||||
|
||||
### Run the test
|
||||
```bash
|
||||
# Start stable version
|
||||
docker run -d \
|
||||
--name homebox-test \
|
||||
-p 7745:7745 \
|
||||
-e HBOX_OPTIONS_ALLOW_REGISTRATION=true \
|
||||
-v /tmp/homebox-data:/data \
|
||||
ghcr.io/sysadminsmedia/homebox:latest
|
||||
|
||||
# Wait for startup
|
||||
sleep 10
|
||||
|
||||
# Create test data
|
||||
export HOMEBOX_URL=http://localhost:7745
|
||||
./.github/scripts/upgrade-test/create-test-data.sh
|
||||
|
||||
# Stop container
|
||||
docker stop homebox-test
|
||||
docker rm homebox-test
|
||||
|
||||
# Build new version
|
||||
docker build -t homebox:test .
|
||||
|
||||
# Start new version with existing data
|
||||
docker run -d \
|
||||
--name homebox-test \
|
||||
-p 7745:7745 \
|
||||
-e HBOX_OPTIONS_ALLOW_REGISTRATION=true \
|
||||
-v /tmp/homebox-data:/data \
|
||||
homebox:test
|
||||
|
||||
# Wait for startup
|
||||
sleep 10
|
||||
|
||||
# Run verification tests
|
||||
cd frontend
|
||||
TEST_DATA_FILE=/tmp/test-users.json \
|
||||
E2E_BASE_URL=http://localhost:7745 \
|
||||
pnpm exec playwright test \
|
||||
--project=chromium \
|
||||
test/upgrade/upgrade-verification.spec.ts
|
||||
|
||||
# Cleanup
|
||||
docker stop homebox-test
|
||||
docker rm homebox-test
|
||||
```
|
||||
|
||||
## Artifacts
|
||||
|
||||
The workflow produces several artifacts:
|
||||
|
||||
1. **playwright-report-upgrade-test**: HTML report of test results
|
||||
2. **playwright-traces**: Detailed traces for debugging failures
|
||||
3. **Docker logs**: Collected on failure for troubleshooting
|
||||
|
||||
## Failure Scenarios
|
||||
|
||||
The workflow will fail if:
|
||||
- The stable version fails to start
|
||||
- Test data creation fails
|
||||
- The new version fails to start with existing data
|
||||
- Any verification test fails
|
||||
- Database migrations fail
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Test Data Creation Fails
|
||||
|
||||
Check the Docker logs:
|
||||
```bash
|
||||
docker logs homebox-old
|
||||
```
|
||||
|
||||
Verify the API is accessible:
|
||||
```bash
|
||||
curl http://localhost:7745/api/v1/status
|
||||
```
|
||||
|
||||
### Verification Tests Fail
|
||||
|
||||
1. Download the Playwright report from GitHub Actions artifacts
|
||||
2. Review the HTML report for detailed failure information
|
||||
3. Check traces for visual debugging
|
||||
|
||||
### Database Issues
|
||||
|
||||
If migrations fail:
|
||||
```bash
|
||||
# Check database file
|
||||
ls -lh /tmp/homebox-data-new/homebox.db
|
||||
|
||||
# Check Docker logs for migration errors
|
||||
docker logs homebox-new
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements:
|
||||
- [ ] Test multiple upgrade paths (e.g., v0.10 → v0.11 → v0.12)
|
||||
- [ ] Test with PostgreSQL backend in addition to SQLite
|
||||
- [ ] Add performance benchmarks
|
||||
- [ ] Test with larger datasets
|
||||
- [ ] Add API-level verification in addition to UI tests
|
||||
- [ ] Test backup and restore functionality
|
||||
|
||||
## Related Files
|
||||
|
||||
- `.github/workflows/upgrade-test.yaml` - Main workflow definition
|
||||
- `.github/scripts/upgrade-test/create-test-data.sh` - Data generation script
|
||||
- `frontend/test/upgrade/upgrade-verification.spec.ts` - Playwright verification tests
|
||||
- `.github/workflows/e2e-partial.yaml` - Standard E2E test workflow (for reference)
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions about this workflow:
|
||||
1. Check the GitHub Actions run logs
|
||||
2. Review this documentation
|
||||
3. Open an issue in the repository
|
||||
413
.github/scripts/upgrade-test/create-test-data.sh
vendored
Executable file
413
.github/scripts/upgrade-test/create-test-data.sh
vendored
Executable file
@@ -0,0 +1,413 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to create test data in HomeBox for upgrade testing
|
||||
# This script creates users, items, attachments, notifiers, locations, and labels
|
||||
|
||||
set -e
|
||||
|
||||
HOMEBOX_URL="${HOMEBOX_URL:-http://localhost:7745}"
|
||||
API_URL="${HOMEBOX_URL}/api/v1"
|
||||
TEST_DATA_FILE="${TEST_DATA_FILE:-/tmp/test-users.json}"
|
||||
|
||||
echo "Creating test data in HomeBox at $HOMEBOX_URL"
|
||||
|
||||
# Function to make API calls with error handling
|
||||
api_call() {
|
||||
local method=$1
|
||||
local endpoint=$2
|
||||
local data=$3
|
||||
local token=$4
|
||||
|
||||
if [ -n "$token" ]; then
|
||||
if [ -n "$data" ]; then
|
||||
curl -s -X "$method" \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$data" \
|
||||
"$API_URL$endpoint"
|
||||
else
|
||||
curl -s -X "$method" \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$API_URL$endpoint"
|
||||
fi
|
||||
else
|
||||
if [ -n "$data" ]; then
|
||||
curl -s -X "$method" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$data" \
|
||||
"$API_URL$endpoint"
|
||||
else
|
||||
curl -s -X "$method" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$API_URL$endpoint"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to register a user and get token
|
||||
register_user() {
|
||||
local email=$1
|
||||
local name=$2
|
||||
local password=$3
|
||||
local group_token=$4
|
||||
|
||||
echo "Registering user: $email"
|
||||
|
||||
local payload="{\"email\":\"$email\",\"name\":\"$name\",\"password\":\"$password\""
|
||||
|
||||
if [ -n "$group_token" ]; then
|
||||
payload="$payload,\"groupToken\":\"$group_token\""
|
||||
fi
|
||||
|
||||
payload="$payload}"
|
||||
|
||||
local response=$(curl -s -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"$API_URL/users/register")
|
||||
|
||||
echo "$response"
|
||||
}
|
||||
|
||||
# Function to login and get token
|
||||
login_user() {
|
||||
local email=$1
|
||||
local password=$2
|
||||
|
||||
echo "Logging in user: $email" >&2
|
||||
|
||||
local response=$(curl -s -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"username\":\"$email\",\"password\":\"$password\"}" \
|
||||
"$API_URL/users/login")
|
||||
|
||||
echo "$response" | jq -r '.token // empty'
|
||||
}
|
||||
|
||||
# Function to create an item
|
||||
create_item() {
|
||||
local token=$1
|
||||
local name=$2
|
||||
local description=$3
|
||||
local location_id=$4
|
||||
|
||||
echo "Creating item: $name" >&2
|
||||
|
||||
local payload="{\"name\":\"$name\",\"description\":\"$description\""
|
||||
|
||||
if [ -n "$location_id" ]; then
|
||||
payload="$payload,\"locationId\":\"$location_id\""
|
||||
fi
|
||||
|
||||
payload="$payload}"
|
||||
|
||||
local response=$(curl -s -X POST \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"$API_URL/items")
|
||||
|
||||
echo "$response"
|
||||
}
|
||||
|
||||
# Function to create a location
|
||||
create_location() {
|
||||
local token=$1
|
||||
local name=$2
|
||||
local description=$3
|
||||
|
||||
echo "Creating location: $name" >&2
|
||||
|
||||
local response=$(curl -s -X POST \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"$name\",\"description\":\"$description\"}" \
|
||||
"$API_URL/locations")
|
||||
|
||||
echo "$response"
|
||||
}
|
||||
|
||||
# Function to create a label
|
||||
create_label() {
|
||||
local token=$1
|
||||
local name=$2
|
||||
local description=$3
|
||||
|
||||
echo "Creating label: $name" >&2
|
||||
|
||||
local response=$(curl -s -X POST \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"$name\",\"description\":\"$description\"}" \
|
||||
"$API_URL/labels")
|
||||
|
||||
echo "$response"
|
||||
}
|
||||
|
||||
# Function to create a notifier
|
||||
create_notifier() {
|
||||
local token=$1
|
||||
local name=$2
|
||||
local url=$3
|
||||
|
||||
echo "Creating notifier: $name" >&2
|
||||
|
||||
local response=$(curl -s -X POST \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"$name\",\"url\":\"$url\",\"isActive\":true}" \
|
||||
"$API_URL/groups/notifiers")
|
||||
|
||||
echo "$response"
|
||||
}
|
||||
|
||||
# Function to attach a file to an item (creates a dummy attachment)
|
||||
attach_file_to_item() {
|
||||
local token=$1
|
||||
local item_id=$2
|
||||
local filename=$3
|
||||
|
||||
echo "Creating attachment for item: $item_id" >&2
|
||||
|
||||
# Create a temporary file with some content
|
||||
local temp_file=$(mktemp)
|
||||
echo "This is a test attachment for $filename" > "$temp_file"
|
||||
|
||||
local response=$(curl -s -X POST \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-F "file=@$temp_file" \
|
||||
-F "type=attachment" \
|
||||
-F "name=$filename" \
|
||||
"$API_URL/items/$item_id/attachments")
|
||||
|
||||
rm -f "$temp_file"
|
||||
|
||||
echo "$response"
|
||||
}
|
||||
|
||||
# Initialize test data storage
|
||||
echo "{\"users\":[]}" > "$TEST_DATA_FILE"
|
||||
|
||||
echo "=== Step 1: Create first group with 5 users ==="
|
||||
|
||||
# Register first user (creates a new group)
|
||||
user1_response=$(register_user "user1@homebox.test" "User One" "TestPassword123!")
|
||||
user1_token=$(echo "$user1_response" | jq -r '.token // empty')
|
||||
group_token=$(echo "$user1_response" | jq -r '.group.inviteToken // empty')
|
||||
|
||||
if [ -z "$user1_token" ]; then
|
||||
echo "Failed to register first user"
|
||||
echo "Response: $user1_response"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "First user registered with token. Group token: $group_token"
|
||||
|
||||
# Store user1 data
|
||||
jq --arg email "user1@homebox.test" \
|
||||
--arg password "TestPassword123!" \
|
||||
--arg token "$user1_token" \
|
||||
--arg group "1" \
|
||||
'.users += [{"email":$email,"password":$password,"token":$token,"group":$group}]' \
|
||||
"$TEST_DATA_FILE" > "$TEST_DATA_FILE.tmp" && mv "$TEST_DATA_FILE.tmp" "$TEST_DATA_FILE"
|
||||
|
||||
# Register 4 more users in the same group
|
||||
for i in {2..5}; do
|
||||
echo "Registering user$i in group 1..."
|
||||
user_response=$(register_user "user${i}@homebox.test" "User $i" "TestPassword123!" "$group_token")
|
||||
user_token=$(echo "$user_response" | jq -r '.token // empty')
|
||||
|
||||
if [ -z "$user_token" ]; then
|
||||
echo "Failed to register user$i"
|
||||
echo "Response: $user_response"
|
||||
else
|
||||
echo "user$i registered successfully"
|
||||
# Store user data
|
||||
jq --arg email "user${i}@homebox.test" \
|
||||
--arg password "TestPassword123!" \
|
||||
--arg token "$user_token" \
|
||||
--arg group "1" \
|
||||
'.users += [{"email":$email,"password":$password,"token":$token,"group":$group}]' \
|
||||
"$TEST_DATA_FILE" > "$TEST_DATA_FILE.tmp" && mv "$TEST_DATA_FILE.tmp" "$TEST_DATA_FILE"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "=== Step 2: Create second group with 2 users ==="
|
||||
|
||||
# Register first user of second group
|
||||
user6_response=$(register_user "user6@homebox.test" "User Six" "TestPassword123!")
|
||||
user6_token=$(echo "$user6_response" | jq -r '.token // empty')
|
||||
group2_token=$(echo "$user6_response" | jq -r '.group.inviteToken // empty')
|
||||
|
||||
if [ -z "$user6_token" ]; then
|
||||
echo "Failed to register user6"
|
||||
echo "Response: $user6_response"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "user6 registered with token. Group 2 token: $group2_token"
|
||||
|
||||
# Store user6 data
|
||||
jq --arg email "user6@homebox.test" \
|
||||
--arg password "TestPassword123!" \
|
||||
--arg token "$user6_token" \
|
||||
--arg group "2" \
|
||||
'.users += [{"email":$email,"password":$password,"token":$token,"group":$group}]' \
|
||||
"$TEST_DATA_FILE" > "$TEST_DATA_FILE.tmp" && mv "$TEST_DATA_FILE.tmp" "$TEST_DATA_FILE"
|
||||
|
||||
# Register second user in group 2
|
||||
user7_response=$(register_user "user7@homebox.test" "User Seven" "TestPassword123!" "$group2_token")
|
||||
user7_token=$(echo "$user7_response" | jq -r '.token // empty')
|
||||
|
||||
if [ -z "$user7_token" ]; then
|
||||
echo "Failed to register user7"
|
||||
echo "Response: $user7_response"
|
||||
else
|
||||
echo "user7 registered successfully"
|
||||
# Store user7 data
|
||||
jq --arg email "user7@homebox.test" \
|
||||
--arg password "TestPassword123!" \
|
||||
--arg token "$user7_token" \
|
||||
--arg group "2" \
|
||||
'.users += [{"email":$email,"password":$password,"token":$token,"group":$group}]' \
|
||||
"$TEST_DATA_FILE" > "$TEST_DATA_FILE.tmp" && mv "$TEST_DATA_FILE.tmp" "$TEST_DATA_FILE"
|
||||
fi
|
||||
|
||||
echo "=== Step 3: Create locations for each group ==="
|
||||
|
||||
# Create locations for group 1 (using user1's token)
|
||||
location1=$(create_location "$user1_token" "Living Room" "Main living area")
|
||||
location1_id=$(echo "$location1" | jq -r '.id // empty')
|
||||
echo "Created location: Living Room (ID: $location1_id)"
|
||||
|
||||
location2=$(create_location "$user1_token" "Garage" "Storage and tools")
|
||||
location2_id=$(echo "$location2" | jq -r '.id // empty')
|
||||
echo "Created location: Garage (ID: $location2_id)"
|
||||
|
||||
# Create location for group 2 (using user6's token)
|
||||
location3=$(create_location "$user6_token" "Home Office" "Work from home space")
|
||||
location3_id=$(echo "$location3" | jq -r '.id // empty')
|
||||
echo "Created location: Home Office (ID: $location3_id)"
|
||||
|
||||
# Store locations
|
||||
jq --arg loc1 "$location1_id" \
|
||||
--arg loc2 "$location2_id" \
|
||||
--arg loc3 "$location3_id" \
|
||||
'.locations = {"group1":[$loc1,$loc2],"group2":[$loc3]}' \
|
||||
"$TEST_DATA_FILE" > "$TEST_DATA_FILE.tmp" && mv "$TEST_DATA_FILE.tmp" "$TEST_DATA_FILE"
|
||||
|
||||
echo "=== Step 4: Create labels for each group ==="
|
||||
|
||||
# Create labels for group 1
|
||||
label1=$(create_label "$user1_token" "Electronics" "Electronic devices")
|
||||
label1_id=$(echo "$label1" | jq -r '.id // empty')
|
||||
echo "Created label: Electronics (ID: $label1_id)"
|
||||
|
||||
label2=$(create_label "$user1_token" "Important" "High priority items")
|
||||
label2_id=$(echo "$label2" | jq -r '.id // empty')
|
||||
echo "Created label: Important (ID: $label2_id)"
|
||||
|
||||
# Create label for group 2
|
||||
label3=$(create_label "$user6_token" "Work Equipment" "Items for work")
|
||||
label3_id=$(echo "$label3" | jq -r '.id // empty')
|
||||
echo "Created label: Work Equipment (ID: $label3_id)"
|
||||
|
||||
# Store labels
|
||||
jq --arg lab1 "$label1_id" \
|
||||
--arg lab2 "$label2_id" \
|
||||
--arg lab3 "$label3_id" \
|
||||
'.labels = {"group1":[$lab1,$lab2],"group2":[$lab3]}' \
|
||||
"$TEST_DATA_FILE" > "$TEST_DATA_FILE.tmp" && mv "$TEST_DATA_FILE.tmp" "$TEST_DATA_FILE"
|
||||
|
||||
echo "=== Step 5: Create test notifier ==="
|
||||
|
||||
# Create notifier for group 1
|
||||
notifier1=$(create_notifier "$user1_token" "TESTING" "https://example.com/webhook")
|
||||
notifier1_id=$(echo "$notifier1" | jq -r '.id // empty')
|
||||
echo "Created notifier: TESTING (ID: $notifier1_id)"
|
||||
|
||||
# Store notifier
|
||||
jq --arg not1 "$notifier1_id" \
|
||||
'.notifiers = {"group1":[$not1]}' \
|
||||
"$TEST_DATA_FILE" > "$TEST_DATA_FILE.tmp" && mv "$TEST_DATA_FILE.tmp" "$TEST_DATA_FILE"
|
||||
|
||||
echo "=== Step 6: Create items for all users ==="
|
||||
|
||||
# Create items for users in group 1
|
||||
declare -A user_tokens
|
||||
user_tokens[1]=$user1_token
|
||||
user_tokens[2]=$(echo "$user1_token") # Users in same group share data, but we'll use user1 token
|
||||
user_tokens[3]=$(echo "$user1_token")
|
||||
user_tokens[4]=$(echo "$user1_token")
|
||||
user_tokens[5]=$(echo "$user1_token")
|
||||
|
||||
# Items for group 1 users
|
||||
echo "Creating items for group 1..."
|
||||
item1=$(create_item "$user1_token" "Laptop Computer" "Dell XPS 15 for work" "$location1_id")
|
||||
item1_id=$(echo "$item1" | jq -r '.id // empty')
|
||||
echo "Created item: Laptop Computer (ID: $item1_id)"
|
||||
|
||||
item2=$(create_item "$user1_token" "Power Drill" "DeWalt 20V cordless drill" "$location2_id")
|
||||
item2_id=$(echo "$item2" | jq -r '.id // empty')
|
||||
echo "Created item: Power Drill (ID: $item2_id)"
|
||||
|
||||
item3=$(create_item "$user1_token" "TV Remote" "Samsung TV remote control" "$location1_id")
|
||||
item3_id=$(echo "$item3" | jq -r '.id // empty')
|
||||
echo "Created item: TV Remote (ID: $item3_id)"
|
||||
|
||||
item4=$(create_item "$user1_token" "Tool Box" "Red metal tool box with tools" "$location2_id")
|
||||
item4_id=$(echo "$item4" | jq -r '.id // empty')
|
||||
echo "Created item: Tool Box (ID: $item4_id)"
|
||||
|
||||
item5=$(create_item "$user1_token" "Coffee Maker" "Breville espresso machine" "$location1_id")
|
||||
item5_id=$(echo "$item5" | jq -r '.id // empty')
|
||||
echo "Created item: Coffee Maker (ID: $item5_id)"
|
||||
|
||||
# Items for group 2 users
|
||||
echo "Creating items for group 2..."
|
||||
item6=$(create_item "$user6_token" "Monitor" "27 inch 4K monitor" "$location3_id")
|
||||
item6_id=$(echo "$item6" | jq -r '.id // empty')
|
||||
echo "Created item: Monitor (ID: $item6_id)"
|
||||
|
||||
item7=$(create_item "$user6_token" "Keyboard" "Mechanical keyboard" "$location3_id")
|
||||
item7_id=$(echo "$item7" | jq -r '.id // empty')
|
||||
echo "Created item: Keyboard (ID: $item7_id)"
|
||||
|
||||
# Store items
|
||||
jq --argjson group1_items "[\"$item1_id\",\"$item2_id\",\"$item3_id\",\"$item4_id\",\"$item5_id\"]" \
|
||||
--argjson group2_items "[\"$item6_id\",\"$item7_id\"]" \
|
||||
'.items = {"group1":$group1_items,"group2":$group2_items}' \
|
||||
"$TEST_DATA_FILE" > "$TEST_DATA_FILE.tmp" && mv "$TEST_DATA_FILE.tmp" "$TEST_DATA_FILE"
|
||||
|
||||
echo "=== Step 7: Add attachments to items ==="
|
||||
|
||||
# Add attachments for group 1 items
|
||||
echo "Adding attachments to group 1 items..."
|
||||
attach_file_to_item "$user1_token" "$item1_id" "laptop-receipt.pdf"
|
||||
attach_file_to_item "$user1_token" "$item1_id" "laptop-warranty.pdf"
|
||||
attach_file_to_item "$user1_token" "$item2_id" "drill-manual.pdf"
|
||||
attach_file_to_item "$user1_token" "$item3_id" "remote-guide.pdf"
|
||||
attach_file_to_item "$user1_token" "$item4_id" "toolbox-inventory.txt"
|
||||
|
||||
# Add attachments for group 2 items
|
||||
echo "Adding attachments to group 2 items..."
|
||||
attach_file_to_item "$user6_token" "$item6_id" "monitor-receipt.pdf"
|
||||
attach_file_to_item "$user6_token" "$item7_id" "keyboard-manual.pdf"
|
||||
|
||||
echo "=== Test Data Creation Complete ==="
|
||||
echo "Test data file saved to: $TEST_DATA_FILE"
|
||||
echo "Summary:"
|
||||
echo " - Users created: 7 (5 in group 1, 2 in group 2)"
|
||||
echo " - Locations created: 3"
|
||||
echo " - Labels created: 3"
|
||||
echo " - Notifiers created: 1"
|
||||
echo " - Items created: 7"
|
||||
echo " - Attachments created: 7"
|
||||
|
||||
# Display the test data file for verification
|
||||
echo ""
|
||||
echo "Test data:"
|
||||
cat "$TEST_DATA_FILE" | jq '.'
|
||||
|
||||
exit 0
|
||||
16
.github/workflows/binaries-publish.yaml
vendored
16
.github/workflows/binaries-publish.yaml
vendored
@@ -17,19 +17,17 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c
|
||||
with:
|
||||
go-version: "1.24"
|
||||
cache-dependency-path: backend/go.mod
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 9.15.3
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
|
||||
|
||||
- name: Build Frontend and Copy to Backend
|
||||
working-directory: frontend
|
||||
@@ -51,7 +49,7 @@ jobs:
|
||||
- name: Run GoReleaser
|
||||
id: releaser
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a
|
||||
with:
|
||||
workdir: "backend"
|
||||
distribution: goreleaser
|
||||
@@ -75,7 +73,7 @@ jobs:
|
||||
|
||||
- name: Run GoReleaser No Release
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a
|
||||
with:
|
||||
workdir: "backend"
|
||||
distribution: goreleaser
|
||||
@@ -93,7 +91,7 @@ jobs:
|
||||
actions: read # To read the workflow path.
|
||||
id-token: write # To sign the provenance.
|
||||
contents: write # To add assets to a release.
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.9.0
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a
|
||||
with:
|
||||
base64-subjects: "${{ needs.goreleaser.outputs.hashes }}"
|
||||
upload-assets: true # upload to a new release
|
||||
@@ -105,7 +103,7 @@ jobs:
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Install the verifier
|
||||
uses: slsa-framework/slsa-verifier/actions/installer@v2.4.0
|
||||
uses: slsa-framework/slsa-verifier/actions/installer@ea584f4502babc6f60d9bc799dbbb13c1caa9ee6
|
||||
|
||||
- name: Download assets
|
||||
env:
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
permissions:
|
||||
packages: write
|
||||
steps:
|
||||
- uses: dataaxiom/ghcr-cleanup-action@v1
|
||||
- uses: dataaxiom/ghcr-cleanup-action@cd0cdb900b5dbf3a6f2cc869f0dbb0b8211f50c4
|
||||
with:
|
||||
dry-run: true
|
||||
delete-ghost-images: true
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
permissions:
|
||||
packages: write
|
||||
steps:
|
||||
- uses: dataaxiom/ghcr-cleanup-action@v1
|
||||
- uses: dataaxiom/ghcr-cleanup-action@cd0cdb900b5dbf3a6f2cc869f0dbb0b8211f50c4
|
||||
with:
|
||||
dry-run: false
|
||||
delete-untagged: true
|
||||
|
||||
14
.github/workflows/copilot-setup-steps.yml
vendored
14
.github/workflows/copilot-setup-steps.yml
vendored
@@ -26,25 +26,23 @@ jobs:
|
||||
# If you do not check out your code, Copilot will do this for you.
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "24"
|
||||
|
||||
- uses: pnpm/action-setup@v3.0.0
|
||||
with:
|
||||
version: 9.12.2
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c
|
||||
with:
|
||||
go-version: "1.24"
|
||||
cache-dependency-path: backend/go.mod
|
||||
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v1
|
||||
uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
21
.github/workflows/docker-publish-hardened.yaml
vendored
21
.github/workflows/docker-publish-hardened.yaml
vendored
@@ -33,7 +33,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -43,10 +43,11 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- linux/arm/v7
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
- name: Enable Debug Logs
|
||||
@@ -56,7 +57,7 @@ jobs:
|
||||
ACTIONS_STEP_DEBUG: true
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
@@ -123,7 +124,7 @@ jobs:
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
|
||||
- name: Attest platform-specific images
|
||||
uses: actions/attest-build-provenance@v1
|
||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
subject-name: ${{ env.GHCR_REPO }}
|
||||
@@ -216,7 +217,7 @@ jobs:
|
||||
echo "digest=$digest" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Attest GHCR images
|
||||
uses: actions/attest-build-provenance@v1
|
||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
subject-name: ${{ env.GHCR_REPO }}
|
||||
@@ -240,9 +241,9 @@ jobs:
|
||||
echo "digest=$digest" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Attest Dockerhub images
|
||||
uses: actions/attest-build-provenance@v1
|
||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
subject-name: ${{ env.DOCKERHUB_REPO }}
|
||||
subject-name: docker.io/${{ env.DOCKERHUB_REPO }}
|
||||
subject-digest: ${{ steps.push-dockerhub.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
45
.github/workflows/docker-publish-rootless.yaml
vendored
45
.github/workflows/docker-publish-rootless.yaml
vendored
@@ -37,7 +37,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -47,10 +47,11 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- linux/arm/v7
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
- name: Enable Debug Logs
|
||||
@@ -60,7 +61,7 @@ jobs:
|
||||
ACTIONS_STEP_DEBUG: true
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
@@ -75,40 +76,40 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
|
||||
with:
|
||||
images: |
|
||||
name=${{ env.DOCKERHUB_REPO }},enable=${{ github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') }}
|
||||
name=${{ env.GHCR_REPO }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392
|
||||
with:
|
||||
image: ghcr.io/sysadminsmedia/binfmt:latest
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
|
||||
with:
|
||||
driver-opts: |
|
||||
image=ghcr.io/sysadminsmedia/buildkit:master
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
|
||||
with:
|
||||
context: . # Explicitly specify the build context
|
||||
file: ./Dockerfile.rootless # Explicitly specify the Dockerfile
|
||||
@@ -125,7 +126,7 @@ jobs:
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
|
||||
- name: Attest platform-specific images
|
||||
uses: actions/attest-build-provenance@v1
|
||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
subject-name: ${{ env.GHCR_REPO }}
|
||||
@@ -139,7 +140,7 @@ jobs:
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
@@ -159,35 +160,35 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
|
||||
with:
|
||||
driver-opts: |
|
||||
image=ghcr.io/sysadminsmedia/buildkit:master
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
|
||||
with:
|
||||
images: |
|
||||
name=${{ env.DOCKERHUB_REPO }},enable=${{ github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') }}
|
||||
@@ -218,7 +219,7 @@ jobs:
|
||||
echo "digest=$digest" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Attest GHCR images
|
||||
uses: actions/attest-build-provenance@v1
|
||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
subject-name: ${{ env.GHCR_REPO }}
|
||||
@@ -242,9 +243,9 @@ jobs:
|
||||
echo "digest=$digest" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Attest Dockerhub images
|
||||
uses: actions/attest-build-provenance@v1
|
||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
subject-name: ${{ env.DOCKERHUB_REPO }}
|
||||
subject-name: docker.io/${{ env.DOCKERHUB_REPO }}
|
||||
subject-digest: ${{ steps.push-dockerhub.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
45
.github/workflows/docker-publish.yaml
vendored
45
.github/workflows/docker-publish.yaml
vendored
@@ -37,7 +37,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read # Allows access to repository contents (read-only)
|
||||
packages: write # Allows pushing to GHCR
|
||||
@@ -47,14 +47,15 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- linux/arm/v7
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
@@ -70,40 +71,40 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
|
||||
with:
|
||||
images: |
|
||||
name=${{ env.DOCKERHUB_REPO }},enable=${{ github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') }}
|
||||
name=${{ env.GHCR_REPO }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392
|
||||
with:
|
||||
image: ghcr.io/sysadminsmedia/binfmt:latest
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
|
||||
with:
|
||||
driver-opts: |
|
||||
image=ghcr.io/sysadminsmedia/buildkit:latest
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
|
||||
with:
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -118,7 +119,7 @@ jobs:
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
|
||||
- name: Attest platform-specific images
|
||||
uses: actions/attest-build-provenance@v1
|
||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
subject-name: ${{ env.GHCR_REPO }}
|
||||
@@ -132,7 +133,7 @@ jobs:
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
@@ -152,35 +153,35 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
|
||||
with:
|
||||
driver-opts: |
|
||||
image=ghcr.io/sysadminsmedia/buildkit:master
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
|
||||
with:
|
||||
images: |
|
||||
name=${{ env.DOCKERHUB_REPO }},enable=${{ github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') }}
|
||||
@@ -209,7 +210,7 @@ jobs:
|
||||
echo "digest=$digest" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Attest GHCR images
|
||||
uses: actions/attest-build-provenance@v1
|
||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
subject-name: ${{ env.GHCR_REPO }}
|
||||
@@ -233,9 +234,9 @@ jobs:
|
||||
echo "digest=$digest" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Attest Dockerhub images
|
||||
uses: actions/attest-build-provenance@v1
|
||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
|
||||
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
subject-name: ${{ env.DOCKERHUB_REPO }}
|
||||
subject-name: docker.io/${{ env.DOCKERHUB_REPO }}
|
||||
subject-digest: ${{ steps.push-dockerhub.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
34
.github/workflows/e2e-partial.yaml
vendored
34
.github/workflows/e2e-partial.yaml
vendored
@@ -1,5 +1,11 @@
|
||||
name: E2E (Playwright)
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
checks: write
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
@@ -15,28 +21,26 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v1
|
||||
uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c
|
||||
with:
|
||||
go-version: "1.23"
|
||||
go-version: "1.24"
|
||||
cache-dependency-path: backend/go.mod
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- uses: pnpm/action-setup@v3.0.0
|
||||
with:
|
||||
version: 9.12.2
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
@@ -49,7 +53,7 @@ jobs:
|
||||
- name: Run E2E Tests
|
||||
run: task test:e2e -- --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
name: Upload partial Playwright report
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
@@ -64,20 +68,18 @@ jobs:
|
||||
name: Merge Playwright Reports
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
|
||||
with:
|
||||
node-version: lts/*
|
||||
- uses: pnpm/action-setup@v3.0.0
|
||||
with:
|
||||
version: 9.12.2
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
working-directory: frontend
|
||||
|
||||
- name: Download blob reports from GitHub Actions Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
path: frontend/all-blob-reports
|
||||
pattern: blob-report-*
|
||||
@@ -88,7 +90,7 @@ jobs:
|
||||
working-directory: frontend
|
||||
|
||||
- name: Upload HTML report
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: html-report--attempt-${{ github.run_attempt }}
|
||||
path: frontend/playwright-report
|
||||
|
||||
50
.github/workflows/issue-gatekeeper.yml
vendored
Normal file
50
.github/workflows/issue-gatekeeper.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
name: Issue Gatekeeper
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [ opened ]
|
||||
|
||||
jobs:
|
||||
check-permissions:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Verify Internal Template Use
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const issue_number = context.issue.number;
|
||||
const actor = context.payload.sender.login;
|
||||
|
||||
// 1. Get user permission level
|
||||
const { data: perms } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner,
|
||||
repo,
|
||||
username: actor
|
||||
});
|
||||
|
||||
const isMember = ['admin', 'write'].includes(perms.permission);
|
||||
const body = context.payload.issue.body || "";
|
||||
|
||||
// 2. Check if they used the internal template (or if the issue is blank)
|
||||
// We detect this by checking for our specific template string or the 'internal' label
|
||||
const usedInternal = context.payload.issue.labels.some(l => l.name === 'internal');
|
||||
|
||||
if (usedInternal && !isMember) {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
body: `@${actor}, the "Internal" template is restricted to project members. Please use one of the standard bug or feature templates for this repository.`
|
||||
});
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
state: 'closed'
|
||||
});
|
||||
}
|
||||
14
.github/workflows/partial-backend.yaml
vendored
14
.github/workflows/partial-backend.yaml
vendored
@@ -1,5 +1,11 @@
|
||||
name: Go Build/Test
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
checks: write
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
@@ -7,21 +13,21 @@ jobs:
|
||||
Go:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c
|
||||
with:
|
||||
go-version: "1.24"
|
||||
cache-dependency-path: backend/go.mod
|
||||
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v1
|
||||
uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v7
|
||||
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20
|
||||
with:
|
||||
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
|
||||
version: latest
|
||||
|
||||
42
.github/workflows/partial-frontend.yaml
vendored
42
.github/workflows/partial-frontend.yaml
vendored
@@ -1,5 +1,11 @@
|
||||
name: Frontend
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
checks: write
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
@@ -9,13 +15,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: pnpm/action-setup@v3.0.0
|
||||
with:
|
||||
version: 9.12.2
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
@@ -48,28 +52,26 @@ jobs:
|
||||
--health-retries 5
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v1
|
||||
uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c
|
||||
with:
|
||||
go-version: "1.23"
|
||||
go-version: "1.24"
|
||||
cache-dependency-path: backend/go.mod
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: lts/*
|
||||
|
||||
- uses: pnpm/action-setup@v3.0.0
|
||||
with:
|
||||
version: 9.12.2
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
@@ -99,28 +101,26 @@ jobs:
|
||||
- 5432:5432
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v1
|
||||
uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c
|
||||
with:
|
||||
go-version: "1.23"
|
||||
go-version: "1.24"
|
||||
cache-dependency-path: backend/go.mod
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- uses: pnpm/action-setup@v3.0.0
|
||||
with:
|
||||
version: 9.12.2
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
6
.github/workflows/pull-requests.yaml
vendored
6
.github/workflows/pull-requests.yaml
vendored
@@ -1,5 +1,11 @@
|
||||
name: Pull Request CI
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
checks: write
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
|
||||
6
.github/workflows/update-currencies.yml
vendored
6
.github/workflows/update-currencies.yml
vendored
@@ -15,12 +15,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548
|
||||
with:
|
||||
python-version: '3.8'
|
||||
cache: 'pip'
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
if: env.changed == 'true'
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: update-currencies
|
||||
|
||||
70
.github/workflows/update-language-names.yml
vendored
Normal file
70
.github/workflows/update-language-names.yml
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
name: Update Language Names
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'frontend/locales/*.json'
|
||||
- '.github/scripts/update_language_names.py'
|
||||
- '.github/workflows/update-language-names.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
update-language-names:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548
|
||||
with:
|
||||
python-version: '3.8'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: .github/workflows/update-languages/requirements.txt
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r .github/workflows/update-languages/requirements.txt
|
||||
|
||||
- name: Run language names update script
|
||||
run: python .github/scripts/update_language_names.py
|
||||
|
||||
- name: Check for en.json changes
|
||||
run: |
|
||||
if git diff --quiet -- frontend/locales/en.json; then
|
||||
echo "changed=false" >> $GITHUB_ENV
|
||||
else
|
||||
echo "changed=true" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Create Pull Request
|
||||
if: env.changed == 'true'
|
||||
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: automation/update-language-file
|
||||
base: main
|
||||
title: "Update language names in en.json"
|
||||
commit-message: "chore: update language names in en.json"
|
||||
body: |
|
||||
This PR automatically updates the language names in `frontend/locales/en.json` based on the available locale files.
|
||||
|
||||
New languages have been added to ensure all locale files have corresponding language names in the English translation file.
|
||||
|
||||
🤖 This PR was automatically created by the update-language-names workflow.
|
||||
path: .
|
||||
add-paths: |
|
||||
frontend/locales/en.json
|
||||
|
||||
- name: No updates needed
|
||||
if: env.changed == 'false'
|
||||
run: echo "✅ en.json language names are already up-to-date"
|
||||
2
.github/workflows/update-languages/requirements.txt
vendored
Normal file
2
.github/workflows/update-languages/requirements.txt
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
babel
|
||||
requests
|
||||
177
.github/workflows/upgrade-test.yaml
vendored
Normal file
177
.github/workflows/upgrade-test.yaml
vendored
Normal file
@@ -0,0 +1,177 @@
|
||||
#name: HomeBox Upgrade Test
|
||||
|
||||
# on:
|
||||
# schedule:
|
||||
# Run daily at 2 AM UTC
|
||||
# - cron: '0 2 * * *'
|
||||
# workflow_dispatch: # Allow manual trigger
|
||||
# push:
|
||||
# branches:
|
||||
# - main
|
||||
# paths:
|
||||
# - '.github/workflows/upgrade-test.yaml'
|
||||
# - '.github/scripts/upgrade-test/**'
|
||||
|
||||
jobs:
|
||||
upgrade-test:
|
||||
name: Test Upgrade Path
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read # Read repository contents
|
||||
packages: read # Pull Docker images from GHCR
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3.0.0
|
||||
with:
|
||||
version: 9.12.2
|
||||
|
||||
- name: Install Playwright
|
||||
run: |
|
||||
cd frontend
|
||||
pnpm install
|
||||
pnpm exec playwright install --with-deps chromium
|
||||
|
||||
- name: Create test data directory
|
||||
run: |
|
||||
mkdir -p /tmp/homebox-data-old
|
||||
mkdir -p /tmp/homebox-data-new
|
||||
chmod -R 777 /tmp/homebox-data-old
|
||||
chmod -R 777 /tmp/homebox-data-new
|
||||
|
||||
# Step 1: Pull and deploy latest stable version
|
||||
- name: Pull latest stable HomeBox image
|
||||
run: |
|
||||
docker pull ghcr.io/sysadminsmedia/homebox:latest
|
||||
|
||||
- name: Start HomeBox (stable version)
|
||||
run: |
|
||||
docker run -d \
|
||||
--name homebox-old \
|
||||
--restart unless-stopped \
|
||||
-p 7745:7745 \
|
||||
-e HBOX_LOG_LEVEL=debug \
|
||||
-e HBOX_OPTIONS_ALLOW_REGISTRATION=true \
|
||||
-e TZ=UTC \
|
||||
-v /tmp/homebox-data-old:/data \
|
||||
ghcr.io/sysadminsmedia/homebox:latest
|
||||
|
||||
# Wait for the service to be ready
|
||||
timeout 60 bash -c 'until curl -f http://localhost:7745/api/v1/status; do sleep 2; done'
|
||||
echo "HomeBox stable version is ready"
|
||||
|
||||
# Step 2: Create test data
|
||||
- name: Create test data
|
||||
run: |
|
||||
chmod +x .github/scripts/upgrade-test/create-test-data.sh
|
||||
.github/scripts/upgrade-test/create-test-data.sh
|
||||
env:
|
||||
HOMEBOX_URL: http://localhost:7745
|
||||
|
||||
- name: Verify initial data creation
|
||||
run: |
|
||||
echo "Verifying test data was created..."
|
||||
# Check if database file exists and has content
|
||||
if [ -f /tmp/homebox-data-old/homebox.db ]; then
|
||||
ls -lh /tmp/homebox-data-old/homebox.db
|
||||
echo "Database file exists"
|
||||
else
|
||||
echo "Database file not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Stop old HomeBox instance
|
||||
run: |
|
||||
docker stop homebox-old
|
||||
docker rm homebox-old
|
||||
|
||||
# Step 3: Build latest version from main branch
|
||||
- name: Build HomeBox from main branch
|
||||
run: |
|
||||
docker build \
|
||||
--build-arg VERSION=main \
|
||||
--build-arg COMMIT=${{ github.sha }} \
|
||||
--build-arg BUILD_TIME="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
|
||||
-t homebox:test \
|
||||
-f Dockerfile \
|
||||
.
|
||||
|
||||
# Step 4: Copy data and start new version
|
||||
- name: Copy data to new location
|
||||
run: |
|
||||
cp -r /tmp/homebox-data-old/* /tmp/homebox-data-new/
|
||||
chmod -R 777 /tmp/homebox-data-new
|
||||
|
||||
- name: Start HomeBox (new version)
|
||||
run: |
|
||||
docker run -d \
|
||||
--name homebox-new \
|
||||
--restart unless-stopped \
|
||||
-p 7745:7745 \
|
||||
-e HBOX_LOG_LEVEL=debug \
|
||||
-e HBOX_OPTIONS_ALLOW_REGISTRATION=true \
|
||||
-e TZ=UTC \
|
||||
-v /tmp/homebox-data-new:/data \
|
||||
homebox:test
|
||||
|
||||
# Wait for the service to be ready
|
||||
timeout 60 bash -c 'until curl -f http://localhost:7745/api/v1/status; do sleep 2; done'
|
||||
echo "HomeBox new version is ready"
|
||||
|
||||
# Step 5: Run verification tests with Playwright
|
||||
- name: Run verification tests
|
||||
run: |
|
||||
cd frontend
|
||||
TEST_DATA_FILE=/tmp/test-users.json \
|
||||
E2E_BASE_URL=http://localhost:7745 \
|
||||
pnpm exec playwright test \
|
||||
-c ./test/playwright.config.ts \
|
||||
--project=chromium \
|
||||
test/upgrade/upgrade-verification.spec.ts
|
||||
env:
|
||||
HOMEBOX_URL: http://localhost:7745
|
||||
|
||||
- name: Upload Playwright report
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-upgrade-test
|
||||
path: frontend/playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload test traces
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-traces
|
||||
path: frontend/test-results/
|
||||
retention-days: 7
|
||||
|
||||
- name: Collect logs on failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== Docker logs for new version ==="
|
||||
docker logs homebox-new || true
|
||||
echo "=== Database content ==="
|
||||
ls -la /tmp/homebox-data-new/ || true
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: |
|
||||
docker stop homebox-new || true
|
||||
docker rm homebox-new || true
|
||||
docker rmi homebox:test || true
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -67,3 +67,5 @@ frontend/test-results/
|
||||
frontend/playwright-report/
|
||||
frontend/blob-report/
|
||||
frontend/playwright/.cache/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
14
.scaffold/go.sum
Normal file
14
.scaffold/go.sum
Normal file
@@ -0,0 +1,14 @@
|
||||
entgo.io/ent v0.14.5 h1:Rj2WOYJtCkWyFo6a+5wB3EfBRP0rnx1fMk6gGA0UUe4=
|
||||
entgo.io/ent v0.14.5/go.mod h1:zTzLmWtPvGpmSwtkaayM2cm5m819NdM7z7tYPq3vN0U=
|
||||
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/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/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/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/sysadminsmedia/homebox/backend v0.0.0-20251228172914-2a6773d1d610 h1:kNLtnxaPaOryBUZ7RgUHPQVWxIExXYR/q9pYCbum5Vk=
|
||||
github.com/sysadminsmedia/homebox/backend v0.0.0-20251228172914-2a6773d1d610/go.mod h1:9zHHw5TNttw5Kn4Wks+SxwXmJPz6PgGNbnB4BtF1Z4c=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -17,8 +17,6 @@ builds:
|
||||
- freebsd
|
||||
goarch:
|
||||
- amd64
|
||||
- "386"
|
||||
- arm
|
||||
- arm64
|
||||
- riscv64
|
||||
flags:
|
||||
@@ -28,20 +26,9 @@ builds:
|
||||
- -X main.version={{.Version}}
|
||||
- -X main.commit={{.Commit}}
|
||||
- -X main.date={{.Date}}
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
- goos: windows
|
||||
goarch: "386"
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
- goos: freebsd
|
||||
goarch: "386"
|
||||
tags:
|
||||
- >-
|
||||
{{- if eq .Arch "riscv64" }}nodynamic
|
||||
{{- else if eq .Arch "arm" }}nodynamic
|
||||
{{- else if eq .Arch "386" }}nodynamic
|
||||
{{- else if eq .Os "freebsd" }}nodynamic
|
||||
{{ end }}
|
||||
|
||||
@@ -62,7 +49,6 @@ archives:
|
||||
{{ .ProjectName }}_
|
||||
{{- title .Os }}_
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else if eq .Arch "386" }}i386
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
# use zip for windows archives
|
||||
|
||||
@@ -2,6 +2,7 @@ package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"github.com/hay-kot/httpkit/server"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/core/services"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/core/services/reporting/eventbus"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/validate"
|
||||
)
|
||||
|
||||
@@ -94,3 +96,64 @@ func (ctrl *V1Controller) HandleSetPrimaryPhotos() errchain.HandlerFunc {
|
||||
func (ctrl *V1Controller) HandleCreateMissingThumbnails() errchain.HandlerFunc {
|
||||
return actionHandlerFactory("create missing thumbnails", ctrl.repo.Attachments.CreateMissingThumbnails)
|
||||
}
|
||||
|
||||
// WipeInventoryOptions represents the options for wiping inventory
|
||||
type WipeInventoryOptions struct {
|
||||
WipeLabels bool `json:"wipeLabels"`
|
||||
WipeLocations bool `json:"wipeLocations"`
|
||||
WipeMaintenance bool `json:"wipeMaintenance"`
|
||||
}
|
||||
|
||||
// HandleWipeInventory godoc
|
||||
//
|
||||
// @Summary Wipe Inventory
|
||||
// @Description Deletes all items in the inventory
|
||||
// @Tags Actions
|
||||
// @Produce json
|
||||
// @Param options body WipeInventoryOptions false "Wipe options"
|
||||
// @Success 200 {object} ActionAmountResult
|
||||
// @Router /v1/actions/wipe-inventory [Post]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleWipeInventory() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
if ctrl.isDemo {
|
||||
return validate.NewRequestError(errors.New("wipe inventory is not allowed in demo mode"), http.StatusForbidden)
|
||||
}
|
||||
|
||||
ctx := services.NewContext(r.Context())
|
||||
|
||||
// Check if user is owner
|
||||
if !ctx.User.IsOwner {
|
||||
return validate.NewRequestError(errors.New("only group owners can wipe inventory"), http.StatusForbidden)
|
||||
}
|
||||
|
||||
// Parse options from request body
|
||||
var options WipeInventoryOptions
|
||||
if err := server.Decode(r, &options); err != nil {
|
||||
// If no body provided, use default (false for all)
|
||||
options = WipeInventoryOptions{
|
||||
WipeLabels: false,
|
||||
WipeLocations: false,
|
||||
WipeMaintenance: false,
|
||||
}
|
||||
}
|
||||
|
||||
totalCompleted, err := ctrl.repo.Items.WipeInventory(ctx, ctx.GID, options.WipeLabels, options.WipeLocations, options.WipeMaintenance)
|
||||
if err != nil {
|
||||
log.Err(err).Str("action_ref", "wipe inventory").Msg("failed to run action")
|
||||
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Publish mutation events for wiped resources
|
||||
if ctrl.bus != nil {
|
||||
if options.WipeLabels {
|
||||
ctrl.bus.Publish(eventbus.EventLabelMutation, eventbus.GroupMutationEvent{GID: ctx.GID})
|
||||
}
|
||||
if options.WipeLocations {
|
||||
ctrl.bus.Publish(eventbus.EventLocationMutation, eventbus.GroupMutationEvent{GID: ctx.GID})
|
||||
}
|
||||
}
|
||||
|
||||
return server.JSON(w, http.StatusOK, ActionAmountResult{Completed: totalCompleted})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ func (ctrl *V1Controller) HandleAuthLogin(ps ...AuthProvider) errchain.HandlerFu
|
||||
return validate.NewUnauthorizedError()
|
||||
}
|
||||
|
||||
ctrl.setCookies(w, noPort(r.Host), newToken.Raw, newToken.ExpiresAt, true)
|
||||
ctrl.setCookies(w, noPort(r.Host), newToken.Raw, newToken.ExpiresAt, true, newToken.AttachmentToken)
|
||||
return server.JSON(w, http.StatusOK, TokenResponse{
|
||||
Token: "Bearer " + newToken.Raw,
|
||||
ExpiresAt: newToken.ExpiresAt,
|
||||
@@ -178,7 +178,7 @@ func (ctrl *V1Controller) HandleAuthRefresh() errchain.HandlerFunc {
|
||||
return validate.NewUnauthorizedError()
|
||||
}
|
||||
|
||||
ctrl.setCookies(w, noPort(r.Host), newToken.Raw, newToken.ExpiresAt, false)
|
||||
ctrl.setCookies(w, noPort(r.Host), newToken.Raw, newToken.ExpiresAt, false, newToken.AttachmentToken)
|
||||
return server.JSON(w, http.StatusOK, newToken)
|
||||
}
|
||||
}
|
||||
@@ -187,7 +187,7 @@ func noPort(host string) string {
|
||||
return strings.Split(host, ":")[0]
|
||||
}
|
||||
|
||||
func (ctrl *V1Controller) setCookies(w http.ResponseWriter, domain, token string, expires time.Time, remember bool) {
|
||||
func (ctrl *V1Controller) setCookies(w http.ResponseWriter, domain, token string, expires time.Time, remember bool, attachmentToken string) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: cookieNameRemember,
|
||||
Value: strconv.FormatBool(remember),
|
||||
@@ -219,6 +219,19 @@ func (ctrl *V1Controller) setCookies(w http.ResponseWriter, domain, token string
|
||||
HttpOnly: false,
|
||||
Path: "/",
|
||||
})
|
||||
|
||||
// Set attachment token cookie (accessible to frontend, not HttpOnly)
|
||||
if attachmentToken != "" {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "hb.auth.attachment_token",
|
||||
Value: attachmentToken,
|
||||
Expires: expires,
|
||||
Domain: domain,
|
||||
Secure: ctrl.cookieSecure,
|
||||
HttpOnly: false,
|
||||
Path: "/",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl *V1Controller) unsetCookies(w http.ResponseWriter, domain string) {
|
||||
@@ -252,6 +265,17 @@ func (ctrl *V1Controller) unsetCookies(w http.ResponseWriter, domain string) {
|
||||
HttpOnly: false,
|
||||
Path: "/",
|
||||
})
|
||||
|
||||
// Unset attachment token cookie
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "hb.auth.attachment_token",
|
||||
Value: "",
|
||||
Expires: time.Unix(0, 0),
|
||||
Domain: domain,
|
||||
Secure: ctrl.cookieSecure,
|
||||
HttpOnly: false,
|
||||
Path: "/",
|
||||
})
|
||||
}
|
||||
|
||||
// HandleOIDCLogin godoc
|
||||
@@ -310,7 +334,7 @@ func (ctrl *V1Controller) HandleOIDCCallback() errchain.HandlerFunc {
|
||||
}
|
||||
|
||||
// Set cookies and redirect to home
|
||||
ctrl.setCookies(w, noPort(r.Host), newToken.Raw, newToken.ExpiresAt, true)
|
||||
ctrl.setCookies(w, noPort(r.Host), newToken.Raw, newToken.ExpiresAt, true, newToken.AttachmentToken)
|
||||
http.Redirect(w, r, "/home", http.StatusFound)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ func run(cfg *config.Config) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.ToLower(cfg.Database.Driver) == "postgres" {
|
||||
if strings.ToLower(cfg.Database.Driver) == config.DriverPostgres {
|
||||
if !validatePostgresSSLMode(cfg.Database.SslMode) {
|
||||
log.Error().Str("sslmode", cfg.Database.SslMode).Msg("invalid sslmode")
|
||||
return fmt.Errorf("invalid sslmode: %s", cfg.Database.SslMode)
|
||||
|
||||
@@ -108,6 +108,7 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
|
||||
r.Post("/actions/ensure-import-refs", chain.ToHandlerFunc(v1Ctrl.HandleEnsureImportRefs(), userMW...))
|
||||
r.Post("/actions/set-primary-photos", chain.ToHandlerFunc(v1Ctrl.HandleSetPrimaryPhotos(), userMW...))
|
||||
r.Post("/actions/create-missing-thumbnails", chain.ToHandlerFunc(v1Ctrl.HandleCreateMissingThumbnails(), userMW...))
|
||||
r.Post("/actions/wipe-inventory", chain.ToHandlerFunc(v1Ctrl.HandleWipeInventory(), userMW...))
|
||||
|
||||
r.Get("/locations", chain.ToHandlerFunc(v1Ctrl.HandleLocationGetAll(), userMW...))
|
||||
r.Post("/locations", chain.ToHandlerFunc(v1Ctrl.HandleLocationCreate(), userMW...))
|
||||
|
||||
@@ -41,7 +41,7 @@ func setupStorageDir(cfg *config.Config) error {
|
||||
func setupDatabaseURL(cfg *config.Config) (string, error) {
|
||||
databaseURL := ""
|
||||
switch strings.ToLower(cfg.Database.Driver) {
|
||||
case "sqlite3":
|
||||
case config.DriverSqlite3:
|
||||
databaseURL = cfg.Database.SqlitePath
|
||||
dbFilePath := strings.Split(cfg.Database.SqlitePath, "?")[0]
|
||||
dbDir := filepath.Dir(dbFilePath)
|
||||
@@ -49,7 +49,7 @@ func setupDatabaseURL(cfg *config.Config) (string, error) {
|
||||
log.Error().Err(err).Str("path", dbDir).Msg("failed to create SQLite database directory")
|
||||
return "", fmt.Errorf("failed to create SQLite database directory: %w", err)
|
||||
}
|
||||
case "postgres":
|
||||
case config.DriverPostgres:
|
||||
databaseURL = fmt.Sprintf("host=%s port=%s dbname=%s sslmode=%s", cfg.Database.Host, cfg.Database.Port, cfg.Database.Database, cfg.Database.SslMode)
|
||||
if cfg.Database.Username != "" {
|
||||
databaseURL += fmt.Sprintf(" user=%s", cfg.Database.Username)
|
||||
|
||||
@@ -118,6 +118,41 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/actions/wipe-inventory": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"description": "Deletes all items in the inventory",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Actions"
|
||||
],
|
||||
"summary": "Wipe Inventory",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Wipe options",
|
||||
"name": "options",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.WipeInventoryOptions"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.ActionAmountResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/actions/zero-item-time-fields": {
|
||||
"post": {
|
||||
"security": [
|
||||
@@ -5184,6 +5219,20 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.WipeInventoryOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"wipeLabels": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"wipeLocations": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"wipeMaintenance": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.Wrapped": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -114,6 +114,42 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/actions/wipe-inventory": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"description": "Deletes all items in the inventory",
|
||||
"tags": [
|
||||
"Actions"
|
||||
],
|
||||
"summary": "Wipe Inventory",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/v1.WipeInventoryOptions"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Wipe options"
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/v1.ActionAmountResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/actions/zero-item-time-fields": {
|
||||
"post": {
|
||||
"security": [
|
||||
@@ -5381,6 +5417,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.WipeInventoryOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"wipeLabels": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"wipeLocations": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"wipeMaintenance": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.Wrapped": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -67,6 +67,27 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/v1.ActionAmountResult"
|
||||
/v1/actions/wipe-inventory:
|
||||
post:
|
||||
security:
|
||||
- Bearer: []
|
||||
description: Deletes all items in the inventory
|
||||
tags:
|
||||
- Actions
|
||||
summary: Wipe Inventory
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/v1.WipeInventoryOptions"
|
||||
description: Wipe options
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/v1.ActionAmountResult"
|
||||
/v1/actions/zero-item-time-fields:
|
||||
post:
|
||||
security:
|
||||
@@ -3449,6 +3470,15 @@ components:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
v1.WipeInventoryOptions:
|
||||
type: object
|
||||
properties:
|
||||
wipeLabels:
|
||||
type: boolean
|
||||
wipeLocations:
|
||||
type: boolean
|
||||
wipeMaintenance:
|
||||
type: boolean
|
||||
v1.Wrapped:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -116,6 +116,41 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/actions/wipe-inventory": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"description": "Deletes all items in the inventory",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Actions"
|
||||
],
|
||||
"summary": "Wipe Inventory",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Wipe options",
|
||||
"name": "options",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.WipeInventoryOptions"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.ActionAmountResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/actions/zero-item-time-fields": {
|
||||
"post": {
|
||||
"security": [
|
||||
@@ -5182,6 +5217,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.WipeInventoryOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"wipeLabels": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"wipeLocations": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"wipeMaintenance": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.Wrapped": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -1867,6 +1867,15 @@ definitions:
|
||||
token:
|
||||
type: string
|
||||
type: object
|
||||
v1.WipeInventoryOptions:
|
||||
properties:
|
||||
wipeLabels:
|
||||
type: boolean
|
||||
wipeLocations:
|
||||
type: boolean
|
||||
wipeMaintenance:
|
||||
type: boolean
|
||||
type: object
|
||||
v1.Wrapped:
|
||||
properties:
|
||||
item: {}
|
||||
@@ -1947,6 +1956,27 @@ paths:
|
||||
summary: Set Primary Photos
|
||||
tags:
|
||||
- Actions
|
||||
/v1/actions/wipe-inventory:
|
||||
post:
|
||||
description: Deletes all items in the inventory
|
||||
parameters:
|
||||
- description: Wipe options
|
||||
in: body
|
||||
name: options
|
||||
schema:
|
||||
$ref: '#/definitions/v1.WipeInventoryOptions'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/v1.ActionAmountResult'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Wipe Inventory
|
||||
tags:
|
||||
- Actions
|
||||
/v1/actions/zero-item-time-fields:
|
||||
post:
|
||||
description: Resets all item date fields to the beginning of the day
|
||||
|
||||
@@ -6,16 +6,16 @@ toolchain go1.24.3
|
||||
|
||||
require (
|
||||
entgo.io/ent v0.14.5
|
||||
github.com/ardanlabs/conf/v3 v3.9.0
|
||||
github.com/ardanlabs/conf/v3 v3.10.0
|
||||
github.com/containrrr/shoutrrr v0.8.0
|
||||
github.com/coreos/go-oidc/v3 v3.17.0
|
||||
github.com/evanoberholster/imagemeta v0.3.1
|
||||
github.com/gen2brain/avif v0.4.4
|
||||
github.com/gen2brain/heic v0.4.6
|
||||
github.com/gen2brain/heic v0.4.7
|
||||
github.com/gen2brain/jpegxl v0.4.5
|
||||
github.com/gen2brain/webp v0.5.5
|
||||
github.com/go-chi/chi/v5 v5.2.3
|
||||
github.com/go-playground/validator/v10 v10.28.0
|
||||
github.com/go-playground/validator/v10 v10.30.1
|
||||
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
|
||||
@@ -40,11 +40,11 @@ require (
|
||||
gocloud.dev/pubsub/kafkapubsub v0.44.0
|
||||
gocloud.dev/pubsub/natspubsub v0.44.0
|
||||
gocloud.dev/pubsub/rabbitpubsub v0.44.0
|
||||
golang.org/x/crypto v0.45.0
|
||||
golang.org/x/image v0.33.0
|
||||
golang.org/x/oauth2 v0.33.0
|
||||
golang.org/x/text v0.31.0
|
||||
modernc.org/sqlite v1.40.1
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/image v0.34.0
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
golang.org/x/text v0.32.0
|
||||
modernc.org/sqlite v1.41.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -54,9 +54,9 @@ require (
|
||||
cloud.google.com/go/auth v0.17.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.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.50.0 // indirect
|
||||
cloud.google.com/go/iam v1.5.3 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.3 // indirect
|
||||
cloud.google.com/go/pubsub v1.50.1 // indirect
|
||||
cloud.google.com/go/pubsub/v2 v2.2.1 // indirect
|
||||
cloud.google.com/go/storage v1.56.0 // indirect
|
||||
github.com/Azure/azure-amqp-common-go/v3 v3.2.3 // indirect
|
||||
@@ -111,15 +111,15 @@ require (
|
||||
github.com/fatih/color v1.18.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.11 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // 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.22.3 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.3 // indirect
|
||||
github.com/go-openapi/spec v0.22.1 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.4 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.4 // indirect
|
||||
github.com/go-openapi/spec v0.22.3 // indirect
|
||||
github.com/go-openapi/swag/conv v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
|
||||
@@ -135,7 +135,7 @@ require (
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/wire v0.7.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.18.1 // indirect
|
||||
@@ -153,12 +153,12 @@ require (
|
||||
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.47.0 // indirect
|
||||
github.com/nats-io/nats.go v1.48.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.12 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.23 // 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
|
||||
@@ -169,7 +169,7 @@ require (
|
||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
|
||||
github.com/swaggo/files/v2 v2.0.2 // indirect
|
||||
github.com/tetratelabs/wazero v1.10.1 // indirect
|
||||
github.com/tetratelabs/wazero v1.11.0 // indirect
|
||||
github.com/tinylib/msgp v1.6.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
@@ -181,29 +181,29 @@ require (
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.38.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.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
google.golang.org/api v0.257.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/grpc v1.77.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
google.golang.org/api v0.258.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
google.golang.org/grpc v1.78.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.67.1 // indirect
|
||||
modernc.org/libc v1.67.2 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
168
backend/go.sum
168
backend/go.sum
@@ -10,22 +10,22 @@ cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIi
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
|
||||
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
|
||||
cloud.google.com/go/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.50.0 h1:hnYpOIxVlgVD1Z8LN7est4DQZK3K6tvZNurZjIVjUe0=
|
||||
cloud.google.com/go/pubsub v1.50.0/go.mod h1:Di2Y+nqXBpIS+dXUEJPQzLh8PbIQZMLE9IVUFhf2zmM=
|
||||
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
|
||||
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
|
||||
cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY=
|
||||
cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw=
|
||||
cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E=
|
||||
cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=
|
||||
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
|
||||
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
|
||||
cloud.google.com/go/pubsub v1.50.1 h1:fzbXpPyJnSGvWXF1jabhQeXyxdbCIkXTpjXHy7xviBM=
|
||||
cloud.google.com/go/pubsub v1.50.1/go.mod h1:6YVJv3MzWJUVdvQXG081sFvS0dWQOdnV+oTo++q/xFk=
|
||||
cloud.google.com/go/pubsub/v2 v2.2.1 h1:3brZcshL3fIiD1qOxAE2QW9wxsfjioy014x4yC9XuYI=
|
||||
cloud.google.com/go/pubsub/v2 v2.2.1/go.mod h1:O5f0KHG9zDheZAd3z5rlCRhxt2JQtB+t/IYLKK3Bpvw=
|
||||
cloud.google.com/go/storage v1.56.0 h1:iixmq2Fse2tqxMbWhLWC9HfBj1qdxqAmiK8/eqtsLxI=
|
||||
cloud.google.com/go/storage v1.56.0/go.mod h1:Tpuj6t4NweCLzlNbw9Z9iwxEkrSem20AetIeH/shgVU=
|
||||
cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=
|
||||
cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
|
||||
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
|
||||
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
|
||||
entgo.io/ent v0.14.5 h1:Rj2WOYJtCkWyFo6a+5wB3EfBRP0rnx1fMk6gGA0UUe4=
|
||||
entgo.io/ent v0.14.5/go.mod h1:zTzLmWtPvGpmSwtkaayM2cm5m819NdM7z7tYPq3vN0U=
|
||||
github.com/Azure/azure-amqp-common-go/v3 v3.2.3 h1:uDF62mbd9bypXWi19V1bN5NZEO84JqgmI5G73ibAmrk=
|
||||
@@ -79,8 +79,8 @@ github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7l
|
||||
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
|
||||
github.com/ardanlabs/conf/v3 v3.9.0 h1:aRBYHeD39/OkuaEXYIEoi4wvF3OnS7jUAPxXyLfEu20=
|
||||
github.com/ardanlabs/conf/v3 v3.9.0/go.mod h1:XlL9P0quWP4m1weOVFmlezabinbZLI05niDof/+Ochk=
|
||||
github.com/ardanlabs/conf/v3 v3.10.0 h1:qIrJ/WBmH/hFQ/IX4xH9LX9LzwK44T9aEOy78M+4S+0=
|
||||
github.com/ardanlabs/conf/v3 v3.10.0/go.mod h1:XlL9P0quWP4m1weOVFmlezabinbZLI05niDof/+Ochk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0=
|
||||
@@ -172,12 +172,12 @@ github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzP
|
||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gen2brain/avif v0.4.4 h1:Ga/ss7qcWWQm2bxFpnjYjhJsNfZrWs5RsyklgFjKRSE=
|
||||
github.com/gen2brain/avif v0.4.4/go.mod h1:/XCaJcjZraQwKVhpu9aEd9aLOssYOawLvhMBtmHVGqk=
|
||||
github.com/gen2brain/heic v0.4.6 h1:sNh3mfaEZLmDJnFc5WoLxCzh/wj5GwfJScPfvF5CNJE=
|
||||
github.com/gen2brain/heic v0.4.6/go.mod h1:ECnpqbqLu0qSje4KSNWUUDK47UPXPzl80T27GWGEL5I=
|
||||
github.com/gen2brain/heic v0.4.7 h1:xw/e9R3HdIvb+uEhRDMRJdviYnB3ODe/VwL8SYLaMGc=
|
||||
github.com/gen2brain/heic v0.4.7/go.mod h1:ECnpqbqLu0qSje4KSNWUUDK47UPXPzl80T27GWGEL5I=
|
||||
github.com/gen2brain/jpegxl v0.4.5 h1:TWpVEn5xkIfsswzkjHBArd0Cc9AE0tbjBSoa0jDsrbo=
|
||||
github.com/gen2brain/jpegxl v0.4.5/go.mod h1:4kWYJ18xCEuO2vzocYdGpeqNJ990/Gjy3uLMg5TBN6I=
|
||||
github.com/gen2brain/webp v0.5.5 h1:MvQR75yIPU/9nSqYT5h13k4URaJK3gf9tgz/ksRbyEg=
|
||||
@@ -195,12 +195,12 @@ github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
|
||||
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
|
||||
github.com/go-openapi/jsonpointer v0.22.3 h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYAsPotrkn8=
|
||||
github.com/go-openapi/jsonpointer v0.22.3/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo=
|
||||
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
|
||||
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
|
||||
github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k=
|
||||
github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA=
|
||||
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
|
||||
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
|
||||
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
|
||||
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
|
||||
github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc=
|
||||
github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
|
||||
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
|
||||
@@ -228,8 +228,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
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=
|
||||
@@ -267,8 +267,8 @@ github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
|
||||
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
|
||||
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
|
||||
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
|
||||
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=
|
||||
@@ -325,8 +325,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
||||
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-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
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/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
||||
@@ -339,8 +337,8 @@ github.com/nats-io/jwt/v2 v2.5.0 h1:WQQ40AAlqqfx+f6ku+i0pOVm+ASirD4fUh+oQsiE9Ak=
|
||||
github.com/nats-io/jwt/v2 v2.5.0/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI=
|
||||
github.com/nats-io/nats-server/v2 v2.9.23 h1:6Wj6H6QpP9FMlpCyWUaNu2yeZ/qGj+mdRkZ1wbikExU=
|
||||
github.com/nats-io/nats-server/v2 v2.9.23/go.mod h1:wEjrEy9vnqIGE4Pqz4/c75v9Pmaq7My2IgFmnykc4C0=
|
||||
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
||||
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
|
||||
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
||||
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
@@ -349,16 +347,14 @@ github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOF
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/olahol/melody v1.4.0 h1:Pa5SdeZL/zXPi1tJuMAPDbl4n3gQOThSL6G1p4qZ4SI=
|
||||
github.com/olahol/melody v1.4.0/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
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/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pierrec/lz4/v4 v4.1.23 h1:oJE7T90aYBGtFNrI8+KbETnPymobAhzRrR8Mu8n1yfU=
|
||||
github.com/pierrec/lz4/v4 v4.1.23/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -393,10 +389,6 @@ github.com/shirou/gopsutil/v4 v4.25.11 h1:X53gB7muL9Gnwwo2evPSE+SfOrltMoR6V3xJAX
|
||||
github.com/shirou/gopsutil/v4 v4.25.11/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
|
||||
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/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -416,8 +408,8 @@ github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSy
|
||||
github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ=
|
||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||
github.com/tetratelabs/wazero v1.10.1 h1:2DugeJf6VVk58KTPszlNfeeN8AhhpwcZqkJj2wwFuH8=
|
||||
github.com/tetratelabs/wazero v1.10.1/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU=
|
||||
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
|
||||
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
|
||||
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
|
||||
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||
@@ -453,18 +445,18 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0 h1:6VjV6Et+1Hd2iLZEPtdV7vie80Yyqf7oikJLjQ/myi0=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0/go.mod h1:u8hcp8ji5gaM/RfcOo8z9NMnf1pVLfVY7lBY2VOGuUU=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
@@ -483,15 +475,15 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
|
||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
|
||||
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
|
||||
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
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=
|
||||
@@ -499,14 +491,14 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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=
|
||||
@@ -520,8 +512,8 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -529,33 +521,33 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA=
|
||||
google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4=
|
||||
google.golang.org/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-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
google.golang.org/api v0.258.0 h1:IKo1j5FBlN74fe5isA2PVozN3Y5pwNKriEgAXPOkDAc=
|
||||
google.golang.org/api v0.258.0/go.mod h1:qhOMTQEZ6lUps63ZNq9jhODswwjkjYYguA7fA3TBFww=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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=
|
||||
@@ -575,8 +567,8 @@ modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
||||
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.67.1 h1:bFaqOaa5/zbWYJo8aW0tXPX21hXsngG2M7mckCnFSVk=
|
||||
modernc.org/libc v1.67.1/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
|
||||
modernc.org/libc v1.67.2 h1:ZbNmly1rcbjhot5jlOZG0q4p5VwFfjwWqZ5rY2xxOXo=
|
||||
modernc.org/libc v1.67.2/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
@@ -585,8 +577,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.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY=
|
||||
modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
|
||||
modernc.org/sqlite v1.41.0 h1:bJXddp4ZpsqMsNN1vS0jWo4IJTZzb8nWpcgvyCFG9Ck=
|
||||
modernc.org/sqlite v1.41.0/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
5
backend/internal/data/ent/item_predicates.go
generated
5
backend/internal/data/ent/item_predicates.go
generated
@@ -4,6 +4,7 @@ import (
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/item"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/predicate"
|
||||
conf "github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
||||
"github.com/sysadminsmedia/homebox/backend/pkgs/textutils"
|
||||
)
|
||||
|
||||
@@ -24,7 +25,7 @@ func AccentInsensitiveContains(field string, searchValue string) predicate.Item
|
||||
dialect := s.Dialect()
|
||||
|
||||
switch dialect {
|
||||
case "sqlite3":
|
||||
case conf.DriverSqlite3:
|
||||
// For SQLite, we'll create a custom normalization function using REPLACE
|
||||
// to handle common accented characters
|
||||
normalizeFunc := buildSQLiteNormalizeExpression(s.C(field))
|
||||
@@ -32,7 +33,7 @@ func AccentInsensitiveContains(field string, searchValue string) predicate.Item
|
||||
"LOWER("+normalizeFunc+") LIKE ?",
|
||||
"%"+normalizedSearch+"%",
|
||||
))
|
||||
case "postgres":
|
||||
case conf.DriverPostgres:
|
||||
// For PostgreSQL, use REPLACE-based normalization to avoid unaccent dependency
|
||||
normalizeFunc := buildGenericNormalizeExpression(s.C(field))
|
||||
// Use sql.P() for proper PostgreSQL parameter binding ($1, $2, etc.)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
||||
)
|
||||
|
||||
//go:embed all:postgres
|
||||
@@ -21,9 +22,9 @@ var sqliteFiles embed.FS
|
||||
// embedded file system containing the migration files for the specified dialect.
|
||||
func Migrations(dialect string) (embed.FS, error) {
|
||||
switch dialect {
|
||||
case "postgres":
|
||||
case config.DriverPostgres:
|
||||
return postgresFiles, nil
|
||||
case "sqlite3":
|
||||
case config.DriverSqlite3:
|
||||
return sqliteFiles, nil
|
||||
default:
|
||||
log.Error().Str("dialect", dialect).Msg("unknown sql dialect")
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- +goose Up
|
||||
ALTER TABLE users ALTER COLUMN password DROP NOT NULL;
|
||||
@@ -1,5 +1,6 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
-- +goose no transaction
|
||||
PRAGMA foreign_keys=OFF;
|
||||
-- SQLite doesn't support ALTER COLUMN directly, so we need to recreate the table
|
||||
-- Create a temporary table with the new schema
|
||||
CREATE TABLE users_temp (
|
||||
@@ -21,7 +22,7 @@ CREATE TABLE users_temp (
|
||||
);
|
||||
|
||||
-- Copy data from the original table
|
||||
INSERT INTO users_temp SELECT * FROM users;
|
||||
INSERT INTO users_temp SELECT id, created_at, updated_at, name, email, password, is_superuser, superuser, role, activated_on, group_users FROM users;
|
||||
|
||||
-- Drop the original table
|
||||
DROP TABLE users;
|
||||
@@ -31,38 +32,4 @@ ALTER TABLE users_temp RENAME TO users;
|
||||
|
||||
-- Recreate the unique index
|
||||
CREATE UNIQUE INDEX users_email_key on users (email);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
-- Create the original table structure
|
||||
CREATE TABLE users_temp (
|
||||
id uuid not null
|
||||
primary key,
|
||||
created_at datetime not null,
|
||||
updated_at datetime not null,
|
||||
name text not null,
|
||||
email text not null,
|
||||
password text not null,
|
||||
is_superuser bool default false not null,
|
||||
superuser bool default false not null,
|
||||
role text default 'user' not null,
|
||||
activated_on datetime,
|
||||
group_users uuid not null
|
||||
constraint users_groups_users
|
||||
references groups
|
||||
on delete cascade
|
||||
);
|
||||
|
||||
-- Copy data from the current table (this will fail if there are NULL passwords)
|
||||
INSERT INTO users_temp SELECT * FROM users;
|
||||
|
||||
-- Drop the current table
|
||||
DROP TABLE users;
|
||||
|
||||
-- Rename the temporary table
|
||||
ALTER TABLE users_temp RENAME TO users;
|
||||
|
||||
-- Recreate the unique index
|
||||
CREATE UNIQUE INDEX users_email_key on users (email);
|
||||
-- +goose StatementEnd
|
||||
PRAGMA foreign_keys=ON;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- +goose Up
|
||||
-- Force the role and superuser flags, previous nullable password migration (prior to v0.22.2)
|
||||
-- caused them to flip-flop during migration for some users.
|
||||
UPDATE users SET role = 'owner', is_superuser = 0, superuser = 0;
|
||||
@@ -97,12 +97,35 @@ func ToItemAttachment(attachment *ent.Attachment) ItemAttachment {
|
||||
}
|
||||
}
|
||||
|
||||
// normalizePath converts backslashes to forward slashes and trims slashes from both ends
|
||||
// This ensures consistent path separators for blob storage which expects forward slashes
|
||||
func normalizePath(path string) string {
|
||||
path = strings.ReplaceAll(path, "\\", "/")
|
||||
return strings.Trim(path, "/")
|
||||
}
|
||||
|
||||
func (r *AttachmentRepo) path(gid uuid.UUID, hash string) string {
|
||||
return filepath.Join(gid.String(), "documents", hash)
|
||||
// Always use forward slashes for consistency across platforms
|
||||
// This ensures paths are stored in the database with forward slashes
|
||||
return fmt.Sprintf("%s/documents/%s", gid.String(), hash)
|
||||
}
|
||||
|
||||
func (r *AttachmentRepo) fullPath(relativePath string) string {
|
||||
return filepath.Join(r.storage.PrefixPath, relativePath)
|
||||
// Normalize path separators to forward slashes for blob storage
|
||||
// The blob library expects forward slashes in keys regardless of OS
|
||||
normalizedRelativePath := normalizePath(relativePath)
|
||||
|
||||
// Always use forward slashes when joining paths for blob storage
|
||||
if r.storage.PrefixPath == "" {
|
||||
return normalizedRelativePath
|
||||
}
|
||||
normalizedPrefix := normalizePath(r.storage.PrefixPath)
|
||||
|
||||
if normalizedPrefix == "" {
|
||||
return normalizedRelativePath
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/%s", normalizedPrefix, normalizedRelativePath)
|
||||
}
|
||||
|
||||
func (r *AttachmentRepo) GetFullPath(relativePath string) string {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/attachment"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
||||
)
|
||||
|
||||
func TestAttachmentRepo_Create(t *testing.T) {
|
||||
@@ -281,3 +282,58 @@ func TestAttachmentRepo_SettingPhotoPrimaryStillWorks(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.False(t, photo1.Primary, "Photo 1 should no longer be primary after setting Photo 2 as primary")
|
||||
}
|
||||
|
||||
func TestAttachmentRepo_PathNormalization(t *testing.T) {
|
||||
// Test that paths always use forward slashes
|
||||
repo := &AttachmentRepo{
|
||||
storage: config.Storage{
|
||||
PrefixPath: ".data",
|
||||
},
|
||||
}
|
||||
|
||||
testGUID := uuid.MustParse("eb6bf410-a1a8-478d-a803-ca3948368a0c")
|
||||
testHash := "f295eb01-18a9-4631-a797-70bd9623edd4.png"
|
||||
|
||||
// Test path() method - should always return forward slashes
|
||||
relativePath := repo.path(testGUID, testHash)
|
||||
assert.Equal(t, "eb6bf410-a1a8-478d-a803-ca3948368a0c/documents/f295eb01-18a9-4631-a797-70bd9623edd4.png", relativePath)
|
||||
assert.NotContains(t, relativePath, "\\", "path() should not contain backslashes")
|
||||
|
||||
// Test fullPath() with forward slash input (from database)
|
||||
fullPath := repo.fullPath("eb6bf410-a1a8-478d-a803-ca3948368a0c/documents/f295eb01-18a9-4631-a797-70bd9623edd4.png")
|
||||
assert.Equal(t, ".data/eb6bf410-a1a8-478d-a803-ca3948368a0c/documents/f295eb01-18a9-4631-a797-70bd9623edd4.png", fullPath)
|
||||
assert.NotContains(t, fullPath, "\\", "fullPath() should not contain backslashes")
|
||||
|
||||
// Test fullPath() with backslash input (legacy Windows paths from old database)
|
||||
fullPathWithBackslash := repo.fullPath("eb6bf410-a1a8-478d-a803-ca3948368a0c\\documents\\f295eb01-18a9-4631-a797-70bd9623edd4.png")
|
||||
assert.Equal(t, ".data/eb6bf410-a1a8-478d-a803-ca3948368a0c/documents/f295eb01-18a9-4631-a797-70bd9623edd4.png", fullPathWithBackslash)
|
||||
assert.NotContains(t, fullPathWithBackslash, "\\", "fullPath() should normalize backslashes to forward slashes")
|
||||
|
||||
// Test with Windows-style prefix path
|
||||
repoWindows := &AttachmentRepo{
|
||||
storage: config.Storage{
|
||||
PrefixPath: ".data",
|
||||
},
|
||||
}
|
||||
fullPathWindows := repoWindows.fullPath("eb6bf410-a1a8-478d-a803-ca3948368a0c/documents/f295eb01-18a9-4631-a797-70bd9623edd4.png")
|
||||
assert.NotContains(t, fullPathWindows, "\\", "fullPath() should normalize Windows paths")
|
||||
|
||||
// Test empty prefix
|
||||
repoNoPrefix := &AttachmentRepo{
|
||||
storage: config.Storage{
|
||||
PrefixPath: "",
|
||||
},
|
||||
}
|
||||
fullPathNoPrefix := repoNoPrefix.fullPath("eb6bf410-a1a8-478d-a803-ca3948368a0c/documents/f295eb01-18a9-4631-a797-70bd9623edd4.png")
|
||||
assert.Equal(t, "eb6bf410-a1a8-478d-a803-ca3948368a0c/documents/f295eb01-18a9-4631-a797-70bd9623edd4.png", fullPathNoPrefix)
|
||||
|
||||
// Test with single slash prefix (like in tests)
|
||||
repoSlashPrefix := &AttachmentRepo{
|
||||
storage: config.Storage{
|
||||
PrefixPath: "/",
|
||||
},
|
||||
}
|
||||
fullPathSlashPrefix := repoSlashPrefix.fullPath("eb6bf410-a1a8-478d-a803-ca3948368a0c/documents/f295eb01-18a9-4631-a797-70bd9623edd4.png")
|
||||
assert.Equal(t, "eb6bf410-a1a8-478d-a803-ca3948368a0c/documents/f295eb01-18a9-4631-a797-70bd9623edd4.png", fullPathSlashPrefix)
|
||||
assert.NotContains(t, fullPathSlashPrefix, "//", "fullPath() should not have double slashes")
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ type (
|
||||
DefaultWarrantyDetails *string `json:"defaultWarrantyDetails,omitempty" extensions:"x-nullable" validate:"omitempty,max=1000"`
|
||||
|
||||
// Default location and labels
|
||||
DefaultLocationID *uuid.UUID `json:"defaultLocationId,omitempty" extensions:"x-nullable"`
|
||||
DefaultLocationID uuid.UUID `json:"defaultLocationId,omitempty" extensions:"x-nullable"`
|
||||
DefaultLabelIDs *[]uuid.UUID `json:"defaultLabelIds,omitempty" extensions:"x-nullable"`
|
||||
|
||||
// Metadata flags
|
||||
@@ -82,7 +82,7 @@ type (
|
||||
DefaultWarrantyDetails *string `json:"defaultWarrantyDetails,omitempty" extensions:"x-nullable" validate:"omitempty,max=1000"`
|
||||
|
||||
// Default location and labels
|
||||
DefaultLocationID *uuid.UUID `json:"defaultLocationId,omitempty" extensions:"x-nullable"`
|
||||
DefaultLocationID uuid.UUID `json:"defaultLocationId,omitempty" extensions:"x-nullable"`
|
||||
DefaultLabelIDs *[]uuid.UUID `json:"defaultLabelIds,omitempty" extensions:"x-nullable"`
|
||||
|
||||
// Metadata flags
|
||||
@@ -262,6 +262,7 @@ func (r *ItemTemplatesRepository) GetOne(ctx context.Context, gid uuid.UUID, id
|
||||
|
||||
// Create creates a new template
|
||||
func (r *ItemTemplatesRepository) Create(ctx context.Context, gid uuid.UUID, data ItemTemplateCreate) (ItemTemplateOut, error) {
|
||||
// Set up create builder
|
||||
q := r.db.ItemTemplate.Create().
|
||||
SetName(data.Name).
|
||||
SetDescription(data.Description).
|
||||
@@ -277,9 +278,12 @@ func (r *ItemTemplatesRepository) Create(ctx context.Context, gid uuid.UUID, dat
|
||||
SetIncludeWarrantyFields(data.IncludeWarrantyFields).
|
||||
SetIncludePurchaseFields(data.IncludePurchaseFields).
|
||||
SetIncludeSoldFields(data.IncludeSoldFields).
|
||||
SetGroupID(gid).
|
||||
SetNillableLocationID(data.DefaultLocationID)
|
||||
SetGroupID(gid)
|
||||
|
||||
// If a default location was provided (uuid != Nil) set it, otherwise leave empty
|
||||
if data.DefaultLocationID != uuid.Nil {
|
||||
q.SetLocationID(data.DefaultLocationID)
|
||||
}
|
||||
// Set default label IDs (stored as JSON)
|
||||
if data.DefaultLabelIDs != nil && len(*data.DefaultLabelIDs) > 0 {
|
||||
q.SetDefaultLabelIds(*data.DefaultLabelIDs)
|
||||
@@ -340,9 +344,9 @@ func (r *ItemTemplatesRepository) Update(ctx context.Context, gid uuid.UUID, dat
|
||||
SetIncludePurchaseFields(data.IncludePurchaseFields).
|
||||
SetIncludeSoldFields(data.IncludeSoldFields)
|
||||
|
||||
// Update location
|
||||
if data.DefaultLocationID != nil {
|
||||
updateQ.SetLocationID(*data.DefaultLocationID)
|
||||
// Update location: set when provided (not uuid.Nil), otherwise clear
|
||||
if data.DefaultLocationID != uuid.Nil {
|
||||
updateQ.SetLocationID(data.DefaultLocationID)
|
||||
} else {
|
||||
updateQ.ClearLocation()
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ func TestItemTemplatesRepository_CreateWithLocation(t *testing.T) {
|
||||
|
||||
// Create template with location
|
||||
data := templateFactory()
|
||||
data.DefaultLocationID = &loc.ID
|
||||
data.DefaultLocationID = loc.ID
|
||||
|
||||
template, err := tRepos.ItemTemplates.Create(context.Background(), tGroup.ID, data)
|
||||
require.NoError(t, err)
|
||||
@@ -311,7 +311,7 @@ func TestItemTemplatesRepository_UpdateRemoveLocation(t *testing.T) {
|
||||
|
||||
// Create template with location
|
||||
data := templateFactory()
|
||||
data.DefaultLocationID = &loc.ID
|
||||
data.DefaultLocationID = loc.ID
|
||||
|
||||
template, err := tRepos.ItemTemplates.Create(context.Background(), tGroup.ID, data)
|
||||
require.NoError(t, err)
|
||||
@@ -323,7 +323,7 @@ func TestItemTemplatesRepository_UpdateRemoveLocation(t *testing.T) {
|
||||
ID: template.ID,
|
||||
Name: template.Name,
|
||||
DefaultQuantity: &qty,
|
||||
DefaultLocationID: nil, // Remove location
|
||||
DefaultLocationID: uuid.Nil, // Remove location
|
||||
}
|
||||
|
||||
updated, err := tRepos.ItemTemplates.Update(context.Background(), tGroup.ID, updateData)
|
||||
|
||||
@@ -809,6 +809,88 @@ func (e *ItemsRepository) DeleteByGroup(ctx context.Context, gid, id uuid.UUID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *ItemsRepository) WipeInventory(ctx context.Context, gid uuid.UUID, wipeLabels bool, wipeLocations bool, wipeMaintenance bool) (int, error) {
|
||||
deleted := 0
|
||||
|
||||
// Wipe maintenance records if requested
|
||||
// IMPORTANT: Must delete maintenance records BEFORE items since they are linked to items
|
||||
if wipeMaintenance {
|
||||
maintenanceCount, err := e.db.MaintenanceEntry.Delete().
|
||||
Where(maintenanceentry.HasItemWith(item.HasGroupWith(group.ID(gid)))).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to delete maintenance entries during wipe inventory")
|
||||
} else {
|
||||
log.Info().Int("count", maintenanceCount).Msg("deleted maintenance entries during wipe inventory")
|
||||
deleted += maintenanceCount
|
||||
}
|
||||
}
|
||||
|
||||
// Get all items for the group
|
||||
items, err := e.db.Item.Query().
|
||||
Where(item.HasGroupWith(group.ID(gid))).
|
||||
WithAttachments().
|
||||
All(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Delete each item with its attachments
|
||||
// Note: We manually delete attachments and items instead of calling DeleteByGroup
|
||||
// to continue processing remaining items even if some deletions fail
|
||||
for _, itm := range items {
|
||||
// Delete all attachments first
|
||||
for _, att := range itm.Edges.Attachments {
|
||||
err := e.attachments.Delete(ctx, gid, itm.ID, att.ID)
|
||||
if err != nil {
|
||||
log.Err(err).Str("attachment_id", att.ID.String()).Msg("failed to delete attachment during wipe inventory")
|
||||
// Continue with other attachments even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the item
|
||||
_, err = e.db.Item.
|
||||
Delete().
|
||||
Where(
|
||||
item.ID(itm.ID),
|
||||
item.HasGroupWith(group.ID(gid)),
|
||||
).Exec(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Str("item_id", itm.ID.String()).Msg("failed to delete item during wipe inventory")
|
||||
// Skip to next item without incrementing counter
|
||||
continue
|
||||
}
|
||||
|
||||
// Only increment counter if deletion succeeded
|
||||
deleted++
|
||||
}
|
||||
|
||||
// Wipe labels if requested
|
||||
if wipeLabels {
|
||||
labelCount, err := e.db.Label.Delete().Where(label.HasGroupWith(group.ID(gid))).Exec(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to delete labels during wipe inventory")
|
||||
} else {
|
||||
log.Info().Int("count", labelCount).Msg("deleted labels during wipe inventory")
|
||||
deleted += labelCount
|
||||
}
|
||||
}
|
||||
|
||||
// Wipe locations if requested
|
||||
if wipeLocations {
|
||||
locationCount, err := e.db.Location.Delete().Where(location.HasGroupWith(group.ID(gid))).Exec(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to delete locations during wipe inventory")
|
||||
} else {
|
||||
log.Info().Int("count", locationCount).Msg("deleted locations during wipe inventory")
|
||||
deleted += locationCount
|
||||
}
|
||||
}
|
||||
|
||||
e.publishMutationEvent(gid)
|
||||
return deleted, nil
|
||||
}
|
||||
|
||||
func (e *ItemsRepository) UpdateByGroup(ctx context.Context, gid uuid.UUID, data ItemUpdate) (ItemOut, error) {
|
||||
q := e.db.Item.Update().Where(item.ID(data.ID), item.HasGroupWith(group.ID(gid))).
|
||||
SetName(data.Name).
|
||||
|
||||
@@ -398,4 +398,161 @@ func TestItemsRepository_DeleteByGroupWithAttachments(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestItemsRepository_WipeInventory(t *testing.T) {
|
||||
// Create test data: items, labels, locations, and maintenance entries
|
||||
|
||||
// Create locations
|
||||
loc1, err := tRepos.Locations.Create(context.Background(), tGroup.ID, LocationCreate{
|
||||
Name: "Test Location 1",
|
||||
Description: "Test location for wipe test",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
loc2, err := tRepos.Locations.Create(context.Background(), tGroup.ID, LocationCreate{
|
||||
Name: "Test Location 2",
|
||||
Description: "Another test location",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create labels
|
||||
label1, err := tRepos.Labels.Create(context.Background(), tGroup.ID, LabelCreate{
|
||||
Name: "Test Label 1",
|
||||
Description: "Test label for wipe test",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
label2, err := tRepos.Labels.Create(context.Background(), tGroup.ID, LabelCreate{
|
||||
Name: "Test Label 2",
|
||||
Description: "Another test label",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create items
|
||||
item1, err := tRepos.Items.Create(context.Background(), tGroup.ID, ItemCreate{
|
||||
Name: "Test Item 1",
|
||||
Description: "Test item for wipe test",
|
||||
LocationID: loc1.ID,
|
||||
LabelIDs: []uuid.UUID{label1.ID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
item2, err := tRepos.Items.Create(context.Background(), tGroup.ID, ItemCreate{
|
||||
Name: "Test Item 2",
|
||||
Description: "Another test item",
|
||||
LocationID: loc2.ID,
|
||||
LabelIDs: []uuid.UUID{label2.ID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create maintenance entries for items
|
||||
_, err = tRepos.MaintEntry.Create(context.Background(), item1.ID, MaintenanceEntryCreate{
|
||||
CompletedDate: types.DateFromTime(time.Now()),
|
||||
Name: "Test Maintenance 1",
|
||||
Description: "Test maintenance entry",
|
||||
Cost: 100.0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = tRepos.MaintEntry.Create(context.Background(), item2.ID, MaintenanceEntryCreate{
|
||||
CompletedDate: types.DateFromTime(time.Now()),
|
||||
Name: "Test Maintenance 2",
|
||||
Description: "Another test maintenance entry",
|
||||
Cost: 200.0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test 1: Wipe inventory with all options enabled
|
||||
t.Run("wipe all including labels, locations, and maintenance", func(t *testing.T) {
|
||||
deleted, err := tRepos.Items.WipeInventory(context.Background(), tGroup.ID, true, true, true)
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, deleted, 0, "Should have deleted at least some entities")
|
||||
|
||||
// Verify items are deleted
|
||||
_, err = tRepos.Items.GetOneByGroup(context.Background(), tGroup.ID, item1.ID)
|
||||
require.Error(t, err, "Item 1 should be deleted")
|
||||
|
||||
_, err = tRepos.Items.GetOneByGroup(context.Background(), tGroup.ID, item2.ID)
|
||||
require.Error(t, err, "Item 2 should be deleted")
|
||||
|
||||
// Verify maintenance entries are deleted (query by item ID, should return empty)
|
||||
maint1List, err := tRepos.MaintEntry.GetMaintenanceByItemID(context.Background(), tGroup.ID, item1.ID, MaintenanceFilters{})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, maint1List, "Maintenance entry 1 should be deleted")
|
||||
|
||||
maint2List, err := tRepos.MaintEntry.GetMaintenanceByItemID(context.Background(), tGroup.ID, item2.ID, MaintenanceFilters{})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, maint2List, "Maintenance entry 2 should be deleted")
|
||||
|
||||
// Verify labels are deleted
|
||||
_, err = tRepos.Labels.GetOneByGroup(context.Background(), tGroup.ID, label1.ID)
|
||||
require.Error(t, err, "Label 1 should be deleted")
|
||||
|
||||
_, err = tRepos.Labels.GetOneByGroup(context.Background(), tGroup.ID, label2.ID)
|
||||
require.Error(t, err, "Label 2 should be deleted")
|
||||
|
||||
// Verify locations are deleted
|
||||
_, err = tRepos.Locations.Get(context.Background(), loc1.ID)
|
||||
require.Error(t, err, "Location 1 should be deleted")
|
||||
|
||||
_, err = tRepos.Locations.Get(context.Background(), loc2.ID)
|
||||
require.Error(t, err, "Location 2 should be deleted")
|
||||
})
|
||||
}
|
||||
|
||||
func TestItemsRepository_WipeInventory_OnlyItems(t *testing.T) {
|
||||
// Create test data
|
||||
loc, err := tRepos.Locations.Create(context.Background(), tGroup.ID, LocationCreate{
|
||||
Name: "Test Location",
|
||||
Description: "Test location for wipe test",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
label, err := tRepos.Labels.Create(context.Background(), tGroup.ID, LabelCreate{
|
||||
Name: "Test Label",
|
||||
Description: "Test label for wipe test",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
item, err := tRepos.Items.Create(context.Background(), tGroup.ID, ItemCreate{
|
||||
Name: "Test Item",
|
||||
Description: "Test item for wipe test",
|
||||
LocationID: loc.ID,
|
||||
LabelIDs: []uuid.UUID{label.ID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = tRepos.MaintEntry.Create(context.Background(), item.ID, MaintenanceEntryCreate{
|
||||
CompletedDate: types.DateFromTime(time.Now()),
|
||||
Name: "Test Maintenance",
|
||||
Description: "Test maintenance entry",
|
||||
Cost: 100.0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test: Wipe inventory with only items (no labels, locations, or maintenance)
|
||||
deleted, err := tRepos.Items.WipeInventory(context.Background(), tGroup.ID, false, false, false)
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, deleted, 0, "Should have deleted at least the item")
|
||||
|
||||
// Verify item is deleted
|
||||
_, err = tRepos.Items.GetOneByGroup(context.Background(), tGroup.ID, item.ID)
|
||||
require.Error(t, err, "Item should be deleted")
|
||||
|
||||
// Verify maintenance entry is deleted due to cascade
|
||||
maintList, err := tRepos.MaintEntry.GetMaintenanceByItemID(context.Background(), tGroup.ID, item.ID, MaintenanceFilters{})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, maintList, "Maintenance entry should be cascade deleted with item")
|
||||
|
||||
// Verify label still exists
|
||||
_, err = tRepos.Labels.GetOneByGroup(context.Background(), tGroup.ID, label.ID)
|
||||
require.NoError(t, err, "Label should still exist")
|
||||
|
||||
// Verify location still exists
|
||||
_, err = tRepos.Locations.Get(context.Background(), loc.ID)
|
||||
require.NoError(t, err, "Location should still exist")
|
||||
|
||||
// Cleanup
|
||||
_ = tRepos.Labels.DeleteByGroup(context.Background(), tGroup.ID, label.ID)
|
||||
_ = tRepos.Locations.delete(context.Background(), loc.ID)
|
||||
}
|
||||
|
||||
|
||||
194
backend/internal/data/repo/repo_wipe_integration_test.go
Normal file
194
backend/internal/data/repo/repo_wipe_integration_test.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/types"
|
||||
)
|
||||
|
||||
// TestWipeInventory_Integration tests the complete wipe inventory flow
|
||||
func TestWipeInventory_Integration(t *testing.T) {
|
||||
// Create test data: locations, labels, items with maintenance
|
||||
|
||||
// 1. Create locations
|
||||
loc1, err := tRepos.Locations.Create(context.Background(), tGroup.ID, LocationCreate{
|
||||
Name: "Test Garage",
|
||||
Description: "Garage location",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
loc2, err := tRepos.Locations.Create(context.Background(), tGroup.ID, LocationCreate{
|
||||
Name: "Test Basement",
|
||||
Description: "Basement location",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// 2. Create labels
|
||||
label1, err := tRepos.Labels.Create(context.Background(), tGroup.ID, LabelCreate{
|
||||
Name: "Test Electronics",
|
||||
Description: "Electronics label",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
label2, err := tRepos.Labels.Create(context.Background(), tGroup.ID, LabelCreate{
|
||||
Name: "Test Tools",
|
||||
Description: "Tools label",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// 3. Create items
|
||||
item1, err := tRepos.Items.Create(context.Background(), tGroup.ID, ItemCreate{
|
||||
Name: "Test Laptop",
|
||||
Description: "Work laptop",
|
||||
LocationID: loc1.ID,
|
||||
LabelIDs: []uuid.UUID{label1.ID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
item2, err := tRepos.Items.Create(context.Background(), tGroup.ID, ItemCreate{
|
||||
Name: "Test Drill",
|
||||
Description: "Power drill",
|
||||
LocationID: loc2.ID,
|
||||
LabelIDs: []uuid.UUID{label2.ID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
item3, err := tRepos.Items.Create(context.Background(), tGroup.ID, ItemCreate{
|
||||
Name: "Test Monitor",
|
||||
Description: "Computer monitor",
|
||||
LocationID: loc1.ID,
|
||||
LabelIDs: []uuid.UUID{label1.ID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// 4. Create maintenance entries
|
||||
_, err = tRepos.MaintEntry.Create(context.Background(), item1.ID, MaintenanceEntryCreate{
|
||||
CompletedDate: types.DateFromTime(time.Now()),
|
||||
Name: "Laptop cleaning",
|
||||
Description: "Cleaned keyboard and screen",
|
||||
Cost: 0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = tRepos.MaintEntry.Create(context.Background(), item2.ID, MaintenanceEntryCreate{
|
||||
CompletedDate: types.DateFromTime(time.Now()),
|
||||
Name: "Drill maintenance",
|
||||
Description: "Oiled motor",
|
||||
Cost: 5.00,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = tRepos.MaintEntry.Create(context.Background(), item3.ID, MaintenanceEntryCreate{
|
||||
CompletedDate: types.DateFromTime(time.Now()),
|
||||
Name: "Monitor calibration",
|
||||
Description: "Color calibration",
|
||||
Cost: 0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// 5. Verify items exist
|
||||
allItems, err := tRepos.Items.GetAll(context.Background(), tGroup.ID)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(allItems), 3, "Should have at least 3 items")
|
||||
|
||||
// 6. Verify maintenance entries exist
|
||||
maint1List, err := tRepos.MaintEntry.GetMaintenanceByItemID(context.Background(), tGroup.ID, item1.ID, MaintenanceFilters{})
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, maint1List, "Item 1 should have maintenance records")
|
||||
|
||||
maint2List, err := tRepos.MaintEntry.GetMaintenanceByItemID(context.Background(), tGroup.ID, item2.ID, MaintenanceFilters{})
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, maint2List, "Item 2 should have maintenance records")
|
||||
|
||||
// 7. Test wipe inventory with all options enabled
|
||||
deleted, err := tRepos.Items.WipeInventory(context.Background(), tGroup.ID, true, true, true)
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, deleted, 0, "Should have deleted entities")
|
||||
|
||||
// 8. Verify all items are deleted
|
||||
allItemsAfter, err := tRepos.Items.GetAll(context.Background(), tGroup.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, len(allItemsAfter), "All items should be deleted")
|
||||
|
||||
// 9. Verify maintenance entries are deleted
|
||||
maint1After, err := tRepos.MaintEntry.GetMaintenanceByItemID(context.Background(), tGroup.ID, item1.ID, MaintenanceFilters{})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, maint1After, "Item 1 maintenance records should be deleted")
|
||||
|
||||
// 10. Verify labels are deleted
|
||||
_, err = tRepos.Labels.GetOneByGroup(context.Background(), tGroup.ID, label1.ID)
|
||||
require.Error(t, err, "Label 1 should be deleted")
|
||||
|
||||
_, err = tRepos.Labels.GetOneByGroup(context.Background(), tGroup.ID, label2.ID)
|
||||
require.Error(t, err, "Label 2 should be deleted")
|
||||
|
||||
// 11. Verify locations are deleted
|
||||
_, err = tRepos.Locations.Get(context.Background(), loc1.ID)
|
||||
require.Error(t, err, "Location 1 should be deleted")
|
||||
|
||||
_, err = tRepos.Locations.Get(context.Background(), loc2.ID)
|
||||
require.Error(t, err, "Location 2 should be deleted")
|
||||
}
|
||||
|
||||
// TestWipeInventory_SelectiveWipe tests wiping only certain entity types
|
||||
func TestWipeInventory_SelectiveWipe(t *testing.T) {
|
||||
// Create test data
|
||||
loc, err := tRepos.Locations.Create(context.Background(), tGroup.ID, LocationCreate{
|
||||
Name: "Test Office",
|
||||
Description: "Office location",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
label, err := tRepos.Labels.Create(context.Background(), tGroup.ID, LabelCreate{
|
||||
Name: "Test Important",
|
||||
Description: "Important label",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
item, err := tRepos.Items.Create(context.Background(), tGroup.ID, ItemCreate{
|
||||
Name: "Test Computer",
|
||||
Description: "Desktop computer",
|
||||
LocationID: loc.ID,
|
||||
LabelIDs: []uuid.UUID{label.ID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = tRepos.MaintEntry.Create(context.Background(), item.ID, MaintenanceEntryCreate{
|
||||
CompletedDate: types.DateFromTime(time.Now()),
|
||||
Name: "System update",
|
||||
Description: "OS update",
|
||||
Cost: 0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test: Wipe only items (keep labels and locations)
|
||||
deleted, err := tRepos.Items.WipeInventory(context.Background(), tGroup.ID, false, false, false)
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, deleted, 0, "Should have deleted at least items")
|
||||
|
||||
// Verify item is deleted
|
||||
_, err = tRepos.Items.GetOneByGroup(context.Background(), tGroup.ID, item.ID)
|
||||
require.Error(t, err, "Item should be deleted")
|
||||
|
||||
// Verify maintenance is cascade deleted
|
||||
maintList, err := tRepos.MaintEntry.GetMaintenanceByItemID(context.Background(), tGroup.ID, item.ID, MaintenanceFilters{})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, maintList, "Maintenance should be cascade deleted")
|
||||
|
||||
// Verify label still exists
|
||||
_, err = tRepos.Labels.GetOneByGroup(context.Background(), tGroup.ID, label.ID)
|
||||
require.NoError(t, err, "Label should still exist")
|
||||
|
||||
// Verify location still exists
|
||||
_, err = tRepos.Locations.Get(context.Background(), loc.ID)
|
||||
require.NoError(t, err, "Location should still exist")
|
||||
|
||||
// Cleanup
|
||||
_ = tRepos.Labels.DeleteByGroup(context.Background(), tGroup.ID, label.ID)
|
||||
_ = tRepos.Locations.delete(context.Background(), loc.ID)
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
package config
|
||||
|
||||
const (
|
||||
DriverSqlite3 = "sqlite3"
|
||||
DriverSqlite3 = "sqlite3"
|
||||
DriverPostgres = "postgres"
|
||||
)
|
||||
|
||||
type Storage struct {
|
||||
|
||||
@@ -43,6 +43,7 @@ export default defineConfig({
|
||||
nav: [
|
||||
{ text: 'API Docs', link: '/en/api' },
|
||||
{ text: 'Demo', link: 'https://demo.homebox.software' },
|
||||
{ text: 'Blog', link: 'https://sysadminsjournal.com/tag/homebox/' }
|
||||
],
|
||||
|
||||
sidebar: {
|
||||
|
||||
@@ -6,6 +6,7 @@ export default [
|
||||
{text: 'Installation', link: '/en/installation'},
|
||||
{text: 'Configure', link: '/en/configure'},
|
||||
{text: 'Storage', link: '/en/configure/storage'},
|
||||
{text: 'OIDC', link: '/en/configure/oidc'},
|
||||
{text: 'Upgrade Guide', link: '/en/upgrade'},
|
||||
{text: 'Migration Guide', link: '/en/migration'},
|
||||
]
|
||||
@@ -20,7 +21,8 @@ export default [
|
||||
{
|
||||
text: 'Advanced',
|
||||
items: [
|
||||
{text: 'Import CSV', link: '/en/import-csv'},
|
||||
{text: 'Import CSV', link: '/en/advanced/import-csv'},
|
||||
{text: 'External Label Service', link: '/en/advanced/external-label-service'},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
53
docs/en/advanced/external-label-service.md
Normal file
53
docs/en/advanced/external-label-service.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# External Label Service
|
||||
|
||||
You can use an external web service to generate asset and location labels in homebox. This is useful if you have custom requirements for your labels and are happy to spin up a web service that can accept incoming requests and return an image file for homebox to use.
|
||||
|
||||
::: info "Note"
|
||||
|
||||
This service is not called to generate sheets of labels accessed via the label generator function. It is used when creating labels from an item or location.
|
||||
|
||||
:::
|
||||
|
||||
## Configuration
|
||||
|
||||
The extenal service is configured using the `HBOX_LABEL_MAKER_LABEL_SERVICE_URL` enviroment variable.
|
||||
|
||||
## Request
|
||||
|
||||
The service is called using an **HTTP `GET` request**. All parameters are passed as part of the **query string**.
|
||||
|
||||
#### Headers
|
||||
|
||||
- **User-Agent**: Homebox-LabelMaker/1.0
|
||||
|
||||
- **Accept**: image/*
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Parameter | Type | Description | Value |
|
||||
| --------------------- | ------ | -------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| AdditionalInformation | string | Extra free text to include on the label. | `HBOX_LABEL_MAKER_ADDITIONAL_INFORMATION` |
|
||||
| ComponentPadding | int | Padding around label components (pixels). | `HBOX_LABEL_MAKER_PADDING` |
|
||||
| DescriptionFontSize | float | Font size for the description text. | |
|
||||
| DescriptionText | string | Descriptive text, can be multi-line. | Item name or "Homebox Location" |
|
||||
| Dpi | float | Rendering resolution (dots per inch). | |
|
||||
| DynamicLength | bool | Whether the label length should auto-adjust. | `HBOX_LABEL_MAKER_DYNAMIC_LENGTH` |
|
||||
| Height | int | Label height in pixels. | `HBOX_LABEL_MAKER_HEIGHT` |
|
||||
| Margin | int | Margin around the label in pixels. | `HBOX_LABEL_MAKER_MARGIN` |
|
||||
| QrSize | int | Size of the QR code element in pixels. | |
|
||||
| TitleFontSize | float | Font size for the title text. | |
|
||||
| TitleText | string | Main label title (e.g. product code). | Asset ID or Location Name |
|
||||
| URL | string | URL to be encoded into the QR code. | Generated based on the configured homebox URL and Asset / Location ID |
|
||||
| Width | int | Label width in pixels. | `HBOX_LABEL_MAKER_WIDTH` |
|
||||
|
||||
## Response
|
||||
|
||||
The external service should respond with the following specifications;
|
||||
|
||||
- **Size:** Less than or equal to `HBOX_WEB_MAX_UPLOAD_SIZE` (Default: 10Mb)
|
||||
|
||||
- **Content-Type**: Specified in the response header should be of the type image/*
|
||||
|
||||
- **Time**: Within the time specified in `HBOX_LABEL_MAKER_LABEL_SERVICE_TIMEOUT` (Default 30s)
|
||||
|
||||
|
||||
@@ -116,6 +116,41 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/actions/wipe-inventory": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"description": "Deletes all items in the inventory",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Actions"
|
||||
],
|
||||
"summary": "Wipe Inventory",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Wipe options",
|
||||
"name": "options",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.WipeInventoryOptions"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.ActionAmountResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/actions/zero-item-time-fields": {
|
||||
"post": {
|
||||
"security": [
|
||||
@@ -4032,7 +4067,8 @@
|
||||
"properties": {
|
||||
"defaultDescription": {
|
||||
"type": "string",
|
||||
"maxLength": 1000
|
||||
"maxLength": 1000,
|
||||
"x-nullable": true
|
||||
},
|
||||
"defaultInsured": {
|
||||
"type": "boolean"
|
||||
@@ -4041,34 +4077,41 @@
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"x-nullable": true
|
||||
},
|
||||
"defaultLifetimeWarranty": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"defaultLocationId": {
|
||||
"description": "Default location and labels",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"x-nullable": true
|
||||
},
|
||||
"defaultManufacturer": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
"maxLength": 255,
|
||||
"x-nullable": true
|
||||
},
|
||||
"defaultModelNumber": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
"maxLength": 255,
|
||||
"x-nullable": true
|
||||
},
|
||||
"defaultName": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
"maxLength": 255,
|
||||
"x-nullable": true
|
||||
},
|
||||
"defaultQuantity": {
|
||||
"description": "Default values for items",
|
||||
"type": "integer"
|
||||
"type": "integer",
|
||||
"x-nullable": true
|
||||
},
|
||||
"defaultWarrantyDetails": {
|
||||
"type": "string",
|
||||
"maxLength": 1000
|
||||
"maxLength": 1000,
|
||||
"x-nullable": true
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
@@ -4209,7 +4252,8 @@
|
||||
"properties": {
|
||||
"defaultDescription": {
|
||||
"type": "string",
|
||||
"maxLength": 1000
|
||||
"maxLength": 1000,
|
||||
"x-nullable": true
|
||||
},
|
||||
"defaultInsured": {
|
||||
"type": "boolean"
|
||||
@@ -4218,34 +4262,41 @@
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"x-nullable": true
|
||||
},
|
||||
"defaultLifetimeWarranty": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"defaultLocationId": {
|
||||
"description": "Default location and labels",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"x-nullable": true
|
||||
},
|
||||
"defaultManufacturer": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
"maxLength": 255,
|
||||
"x-nullable": true
|
||||
},
|
||||
"defaultModelNumber": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
"maxLength": 255,
|
||||
"x-nullable": true
|
||||
},
|
||||
"defaultName": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
"maxLength": 255,
|
||||
"x-nullable": true
|
||||
},
|
||||
"defaultQuantity": {
|
||||
"description": "Default values for items",
|
||||
"type": "integer"
|
||||
"type": "integer",
|
||||
"x-nullable": true
|
||||
},
|
||||
"defaultWarrantyDetails": {
|
||||
"type": "string",
|
||||
"maxLength": 1000
|
||||
"maxLength": 1000,
|
||||
"x-nullable": true
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
@@ -5166,6 +5217,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.WipeInventoryOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"wipeLabels": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"wipeLocations": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"wipeMaintenance": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.Wrapped": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -1084,32 +1084,40 @@ definitions:
|
||||
defaultDescription:
|
||||
maxLength: 1000
|
||||
type: string
|
||||
x-nullable: true
|
||||
defaultInsured:
|
||||
type: boolean
|
||||
defaultLabelIds:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-nullable: true
|
||||
defaultLifetimeWarranty:
|
||||
type: boolean
|
||||
defaultLocationId:
|
||||
description: Default location and labels
|
||||
type: string
|
||||
x-nullable: true
|
||||
defaultManufacturer:
|
||||
maxLength: 255
|
||||
type: string
|
||||
x-nullable: true
|
||||
defaultModelNumber:
|
||||
maxLength: 255
|
||||
type: string
|
||||
x-nullable: true
|
||||
defaultName:
|
||||
maxLength: 255
|
||||
type: string
|
||||
x-nullable: true
|
||||
defaultQuantity:
|
||||
description: Default values for items
|
||||
type: integer
|
||||
x-nullable: true
|
||||
defaultWarrantyDetails:
|
||||
maxLength: 1000
|
||||
type: string
|
||||
x-nullable: true
|
||||
description:
|
||||
maxLength: 1000
|
||||
type: string
|
||||
@@ -1205,32 +1213,40 @@ definitions:
|
||||
defaultDescription:
|
||||
maxLength: 1000
|
||||
type: string
|
||||
x-nullable: true
|
||||
defaultInsured:
|
||||
type: boolean
|
||||
defaultLabelIds:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
x-nullable: true
|
||||
defaultLifetimeWarranty:
|
||||
type: boolean
|
||||
defaultLocationId:
|
||||
description: Default location and labels
|
||||
type: string
|
||||
x-nullable: true
|
||||
defaultManufacturer:
|
||||
maxLength: 255
|
||||
type: string
|
||||
x-nullable: true
|
||||
defaultModelNumber:
|
||||
maxLength: 255
|
||||
type: string
|
||||
x-nullable: true
|
||||
defaultName:
|
||||
maxLength: 255
|
||||
type: string
|
||||
x-nullable: true
|
||||
defaultQuantity:
|
||||
description: Default values for items
|
||||
type: integer
|
||||
x-nullable: true
|
||||
defaultWarrantyDetails:
|
||||
maxLength: 1000
|
||||
type: string
|
||||
x-nullable: true
|
||||
description:
|
||||
maxLength: 1000
|
||||
type: string
|
||||
@@ -1851,6 +1867,15 @@ definitions:
|
||||
token:
|
||||
type: string
|
||||
type: object
|
||||
v1.WipeInventoryOptions:
|
||||
properties:
|
||||
wipeLabels:
|
||||
type: boolean
|
||||
wipeLocations:
|
||||
type: boolean
|
||||
wipeMaintenance:
|
||||
type: boolean
|
||||
type: object
|
||||
v1.Wrapped:
|
||||
properties:
|
||||
item: {}
|
||||
@@ -1931,6 +1956,27 @@ paths:
|
||||
summary: Set Primary Photos
|
||||
tags:
|
||||
- Actions
|
||||
/v1/actions/wipe-inventory:
|
||||
post:
|
||||
description: Deletes all items in the inventory
|
||||
parameters:
|
||||
- description: Wipe options
|
||||
in: body
|
||||
name: options
|
||||
schema:
|
||||
$ref: '#/definitions/v1.WipeInventoryOptions'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/v1.ActionAmountResult'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Wipe Inventory
|
||||
tags:
|
||||
- Actions
|
||||
/v1/actions/zero-item-time-fields:
|
||||
post:
|
||||
description: Resets all item date fields to the beginning of the day
|
||||
|
||||
@@ -114,6 +114,42 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/actions/wipe-inventory": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"description": "Deletes all items in the inventory",
|
||||
"tags": [
|
||||
"Actions"
|
||||
],
|
||||
"summary": "Wipe Inventory",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/v1.WipeInventoryOptions"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Wipe options"
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/v1.ActionAmountResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/actions/zero-item-time-fields": {
|
||||
"post": {
|
||||
"security": [
|
||||
@@ -5381,6 +5417,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.WipeInventoryOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"wipeLabels": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"wipeLocations": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"wipeMaintenance": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.Wrapped": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -67,6 +67,27 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/v1.ActionAmountResult"
|
||||
/v1/actions/wipe-inventory:
|
||||
post:
|
||||
security:
|
||||
- Bearer: []
|
||||
description: Deletes all items in the inventory
|
||||
tags:
|
||||
- Actions
|
||||
summary: Wipe Inventory
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/v1.WipeInventoryOptions"
|
||||
description: Wipe options
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/v1.ActionAmountResult"
|
||||
/v1/actions/zero-item-time-fields:
|
||||
post:
|
||||
security:
|
||||
@@ -3449,6 +3470,15 @@ components:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
v1.WipeInventoryOptions:
|
||||
type: object
|
||||
properties:
|
||||
wipeLabels:
|
||||
type: boolean
|
||||
wipeLocations:
|
||||
type: boolean
|
||||
wipeMaintenance:
|
||||
type: boolean
|
||||
v1.Wrapped:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -116,6 +116,41 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/actions/wipe-inventory": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"description": "Deletes all items in the inventory",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Actions"
|
||||
],
|
||||
"summary": "Wipe Inventory",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Wipe options",
|
||||
"name": "options",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.WipeInventoryOptions"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.ActionAmountResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/actions/zero-item-time-fields": {
|
||||
"post": {
|
||||
"security": [
|
||||
@@ -5182,6 +5217,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.WipeInventoryOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"wipeLabels": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"wipeLocations": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"wipeMaintenance": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.Wrapped": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -1867,6 +1867,15 @@ definitions:
|
||||
token:
|
||||
type: string
|
||||
type: object
|
||||
v1.WipeInventoryOptions:
|
||||
properties:
|
||||
wipeLabels:
|
||||
type: boolean
|
||||
wipeLocations:
|
||||
type: boolean
|
||||
wipeMaintenance:
|
||||
type: boolean
|
||||
type: object
|
||||
v1.Wrapped:
|
||||
properties:
|
||||
item: {}
|
||||
@@ -1947,6 +1956,27 @@ paths:
|
||||
summary: Set Primary Photos
|
||||
tags:
|
||||
- Actions
|
||||
/v1/actions/wipe-inventory:
|
||||
post:
|
||||
description: Deletes all items in the inventory
|
||||
parameters:
|
||||
- description: Wipe options
|
||||
in: body
|
||||
name: options
|
||||
schema:
|
||||
$ref: '#/definitions/v1.WipeInventoryOptions'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/v1.ActionAmountResult'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Wipe Inventory
|
||||
tags:
|
||||
- Actions
|
||||
/v1/actions/zero-item-time-fields:
|
||||
post:
|
||||
description: Resets all item date fields to the beginning of the day
|
||||
|
||||
@@ -22,7 +22,7 @@ aside: false
|
||||
| HBOX_WEB_IDLE_TIMEOUT | 30s | Idle timeout of HTTP server |
|
||||
| HBOX_STORAGE_CONN_STRING | file:///./ | path to the data directory, do not change this if you're using docker |
|
||||
| HBOX_STORAGE_PREFIX_PATH | .data | prefix path for the storage, if not set the storage will be used as is |
|
||||
| HBOX_LOG_LEVEL | `info` | log level to use, can be one of `trace`, `debug`, `info`, `warn`, `error`, `critical` |
|
||||
| HBOX_LOG_LEVEL | `info` | log level to use, can be one of `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `panic` |
|
||||
| HBOX_LOG_FORMAT | `text` | log format to use, can be one of: `text`, `json` |
|
||||
| HBOX_MAILER_HOST | | email host to use, if not set no email provider will be used |
|
||||
| HBOX_MAILER_PORT | 587 | email port to use |
|
||||
@@ -71,11 +71,89 @@ aside: false
|
||||
| HBOX_THUMBNAIL_ENABLED | true | enable thumbnail generation for images, supports PNG, JPEG, AVIF, WEBP, GIF file types |
|
||||
| HBOX_THUMBNAIL_WIDTH | 500 | width for generated thumbnails in pixels |
|
||||
| HBOX_THUMBNAIL_HEIGHT | 500 | height for generated thumbnails in pixels |
|
||||
| HBOX_BARCODE_TOKEN_BARCODESPIDER | | API token for BarcodeSpider.com service used for barcode product lookups. If not set, barcode product lookups will not be performed. |
|
||||
|
||||
```sh
|
||||
Options:
|
||||
--barcode-token-barcodespider <string>
|
||||
--database-database <string>
|
||||
--database-driver <string> (default: sqlite3)
|
||||
--database-host <string>
|
||||
--database-password <string>
|
||||
--database-port <string>
|
||||
--database-pub-sub-conn-string <string> (default: mem://{{ .Topic }})
|
||||
--database-sqlite-path <string> (default: ./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite)
|
||||
--database-ssl-cert <string>
|
||||
--database-ssl-key <string>
|
||||
--database-ssl-mode <string> (default: require)
|
||||
--database-ssl-root-cert <string>
|
||||
--database-username <string>
|
||||
--debug-enabled <bool> (default: false)
|
||||
--debug-port <string> (default: 4000)
|
||||
--demo <bool>
|
||||
-h, --help display this help message
|
||||
--label-maker-additional-information <string>
|
||||
--label-maker-bold-font-path <string>
|
||||
--label-maker-dynamic-length <bool> (default: true)
|
||||
--label-maker-font-size <float> (default: 32.0)
|
||||
--label-maker-height <int> (default: 200)
|
||||
--label-maker-label-service-timeout <int>
|
||||
--label-maker-label-service-url <string>
|
||||
--label-maker-margin <int> (default: 32)
|
||||
--label-maker-padding <int> (default: 32)
|
||||
--label-maker-print-command <string>
|
||||
--label-maker-regular-font-path <string>
|
||||
--label-maker-width <int> (default: 526)
|
||||
--log-format <string> (default: text)
|
||||
--log-level <string> (default: info)
|
||||
--mailer-from <string>
|
||||
--mailer-host <string>
|
||||
--mailer-password <string>
|
||||
--mailer-port <int>
|
||||
--mailer-username <string>
|
||||
--mode <string> (default: development)
|
||||
--oidc-allowed-groups <string>
|
||||
--oidc-auto-redirect <bool> (default: false)
|
||||
--oidc-button-text <string> (default: Sign in with OIDC)
|
||||
--oidc-client-id <string>
|
||||
--oidc-client-secret <string>
|
||||
--oidc-email-claim <string> (default: email)
|
||||
--oidc-email-verified-claim <string> (default: email_verified)
|
||||
--oidc-enabled <bool> (default: false)
|
||||
--oidc-group-claim <string> (default: groups)
|
||||
--oidc-issuer-url <string>
|
||||
--oidc-name-claim <string> (default: name)
|
||||
--oidc-request-timeout <duration> (default: 30s)
|
||||
--oidc-scope <string> (default: openid profile email)
|
||||
--oidc-state-expiry <duration> (default: 10m)
|
||||
--oidc-verify-email <bool> (default: false)
|
||||
--options-allow-analytics <bool> (default: false)
|
||||
--options-allow-local-login <bool> (default: true)
|
||||
--options-allow-registration <bool> (default: true)
|
||||
--options-auto-increment-asset-id <bool> (default: true)
|
||||
--options-currency-config <string>
|
||||
--options-github-release-check <bool> (default: true)
|
||||
--options-hostname <string>
|
||||
--options-trust-proxy <bool> (default: false)
|
||||
--storage-conn-string <string> (default: file:///./)
|
||||
--storage-prefix-path <string> (default: .data)
|
||||
--thumbnail-enabled <bool> (default: true)
|
||||
--thumbnail-height <int> (default: 500)
|
||||
--thumbnail-width <int> (default: 500)
|
||||
-v, --version display version
|
||||
--web-host <string>
|
||||
--web-idle-timeout <duration> (default: 30s)
|
||||
--web-max-upload-size <int> (default: 10)
|
||||
--web-port <string> (default: 7745)
|
||||
--web-read-timeout <duration> (default: 10s)
|
||||
--web-write-timeout <duration> (default: 10s)
|
||||
```
|
||||
:::
|
||||
|
||||
### 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). |
|
||||
@@ -109,6 +187,7 @@ 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]
|
||||
@@ -124,6 +203,7 @@ WantedBy=sockets.target
|
||||
```
|
||||
|
||||
File: homebox.service
|
||||
|
||||
```systemd
|
||||
# /usr/local/lib/systemd/system/homebox.service
|
||||
[Unit]
|
||||
@@ -144,6 +224,7 @@ CapabilityBoundingSet=
|
||||
RestrictNamespaces=true
|
||||
SystemCallFilter=@system-service
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```bash
|
||||
@@ -169,105 +250,4 @@ For SQLite in production:
|
||||
|
||||
## OIDC Configuration
|
||||
|
||||
HomeBox supports OpenID Connect (OIDC) authentication, allowing users to login using external identity providers like Keycloak, Authentik, Google, Microsoft, etc.
|
||||
|
||||
### Basic OIDC Setup
|
||||
|
||||
1. **Enable OIDC**: Set `HBOX_OIDC_ENABLED=true`
|
||||
2. **Provider Configuration**: Set the required provider details:
|
||||
- `HBOX_OIDC_ISSUER_URL`: Your OIDC provider's issuer URL
|
||||
- `HBOX_OIDC_CLIENT_ID`: Client ID from your OIDC provider
|
||||
- `HBOX_OIDC_CLIENT_SECRET`: Client secret from your OIDC provider
|
||||
|
||||
3. **Configure Redirect URI**: In your OIDC provider, set the redirect URI to:
|
||||
`https://your-homebox-domain.com/api/v1/users/login/oidc/callback`
|
||||
|
||||
### Advanced OIDC Configuration
|
||||
|
||||
- **Group Authorization**: Use `HBOX_OIDC_ALLOWED_GROUPS` to restrict access to specific groups
|
||||
- **Custom Claims**: Configure `HBOX_OIDC_GROUP_CLAIM`, `HBOX_OIDC_EMAIL_CLAIM`, and `HBOX_OIDC_NAME_CLAIM` if your provider uses different claim names
|
||||
- **Auto Redirect to OIDC**: Set `HBOX_OIDC_AUTO_REDIRECT=true` to automatically redirect users directly to OIDC
|
||||
- **Local Login**: Set `HBOX_OPTIONS_ALLOW_LOCAL_LOGIN=false` to completely disable username/password login
|
||||
- **Email Verification**: Set `HBOX_OIDC_VERIFY_EMAIL=true` to require email verification from the OIDC provider
|
||||
|
||||
### Security Considerations
|
||||
|
||||
::: warning OIDC Security
|
||||
- Store `HBOX_OIDC_CLIENT_SECRET` securely (use environment variables, not config files)
|
||||
- Use HTTPS for production deployments
|
||||
- Configure proper redirect URIs in your OIDC provider
|
||||
- Consider setting `HBOX_OIDC_ALLOWED_GROUPS` for group-based access control
|
||||
:::
|
||||
|
||||
::: tip CLI Arguments
|
||||
If you're deploying without docker you can use command line arguments to configure the application. Run `homebox --help`
|
||||
for more information.
|
||||
|
||||
```sh
|
||||
Usage: api [options] [arguments]
|
||||
|
||||
OPTIONS
|
||||
--mode/$HBOX_MODE <string> (default: development)
|
||||
--web-port/$HBOX_WEB_PORT <string> (default: 7745)
|
||||
--web-host/$HBOX_WEB_HOST <string>
|
||||
--web-max-upload-size/$HBOX_WEB_MAX_UPLOAD_SIZE <int> (default: 10)
|
||||
--storage-conn-string/$HBOX_STORAGE_CONN_STRING <string> (default: file:///./)
|
||||
--storage-prefix-path/$HBOX_STORAGE_PREFIX_PATH <string> (default: .data)
|
||||
--log-level/$HBOX_LOG_LEVEL <string> (default: info)
|
||||
--log-format/$HBOX_LOG_FORMAT <string> (default: text)
|
||||
--mailer-host/$HBOX_MAILER_HOST <string>
|
||||
--mailer-port/$HBOX_MAILER_PORT <int>
|
||||
--mailer-username/$HBOX_MAILER_USERNAME <string>
|
||||
--mailer-password/$HBOX_MAILER_PASSWORD <string>
|
||||
--mailer-from/$HBOX_MAILER_FROM <string>
|
||||
--demo/$HBOX_DEMO <bool>
|
||||
--debug-enabled/$HBOX_DEBUG_ENABLED <bool> (default: false)
|
||||
--debug-port/$HBOX_DEBUG_PORT <string> (default: 4000)
|
||||
--database-driver/$HBOX_DATABASE_DRIVER <string> (default: sqlite3)
|
||||
--database-sqlite-path/$HBOX_DATABASE_SQLITE_PATH <string> (default: ./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite)
|
||||
--database-host/$HBOX_DATABASE_HOST <string>
|
||||
--database-port/$HBOX_DATABASE_PORT <string>
|
||||
--database-username/$HBOX_DATABASE_USERNAME <string>
|
||||
--database-password/$HBOX_DATABASE_PASSWORD <string>
|
||||
--database-database/$HBOX_DATABASE_DATABASE <string>
|
||||
--database-ssl-mode/$HBOX_DATABASE_SSL_MODE <string> (default: prefer)
|
||||
--options-allow-registration/$HBOX_OPTIONS_ALLOW_REGISTRATION <bool> (default: true)
|
||||
--options-auto-increment-asset-id/$HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID <bool> (default: true)
|
||||
--options-currency-config/$HBOX_OPTIONS_CURRENCY_CONFIG <string>
|
||||
--options-github-release-check/$HBOX_OPTIONS_GITHUB_RELEASE_CHECK <bool> (default: true)
|
||||
--options-allow-analytics/$HBOX_OPTIONS_ALLOW_ANALYTICS <bool> (default: false)
|
||||
--options-allow-local-login/$HBOX_OPTIONS_ALLOW_LOCAL_LOGIN <bool> (default: true)
|
||||
--options-trust-proxy/$HBOX_OPTIONS_TRUST_PROXY <bool> (default: false)
|
||||
--options-hostname/$HBOX_OPTIONS_HOSTNAME <string>
|
||||
--oidc-enabled/$HBOX_OIDC_ENABLED <bool> (default: false)
|
||||
--oidc-issuer-url/$HBOX_OIDC_ISSUER_URL <string>
|
||||
--oidc-client-id/$HBOX_OIDC_CLIENT_ID <string>
|
||||
--oidc-client-secret/$HBOX_OIDC_CLIENT_SECRET <string>
|
||||
--oidc-scope/$HBOX_OIDC_SCOPE <string> (default: openid profile email)
|
||||
--oidc-allowed-groups/$HBOX_OIDC_ALLOWED_GROUPS <string>
|
||||
--oidc-auto-redirect/$HBOX_OIDC_AUTO_REDIRECT <bool> (default: false)
|
||||
--oidc-verify-email/$HBOX_OIDC_VERIFY_EMAIL <bool> (default: false)
|
||||
--oidc-group-claim/$HBOX_OIDC_GROUP_CLAIM <string> (default: groups)
|
||||
--oidc-email-claim/$HBOX_OIDC_EMAIL_CLAIM <string> (default: email)
|
||||
--oidc-name-claim/$HBOX_OIDC_NAME_CLAIM <string> (default: name)
|
||||
--oidc-email-verified-claim/$HBOX_OIDC_EMAIL_VERIFIED_CLAIM <string> (default: email_verified)
|
||||
--oidc-button-text/$HBOX_OIDC_BUTTON_TEXT <string> (default: Sign in with OIDC)
|
||||
--oidc-state-expiry/$HBOX_OIDC_STATE_EXPIRY <duration> (default: 10m)
|
||||
--oidc-request-timeout/$HBOX_OIDC_REQUEST_TIMEOUT <duration> (default: 30s)
|
||||
--label-maker-width/$HBOX_LABEL_MAKER_WIDTH <int> (default: 526)
|
||||
--label-maker-height/$HBOX_LABEL_MAKER_HEIGHT <int> (default: 200)
|
||||
--label-maker-padding/$HBOX_LABEL_MAKER_PADDING <int> (default: 32)
|
||||
--label-maker-margin/$HBOX_LABEL_MAKER_MARGIN <int> (default: 32)
|
||||
--label-maker-font-size/$HBOX_LABEL_MAKER_FONT_SIZE <float> (default: 32.0)
|
||||
--label-maker-print-command/$HBOX_LABEL_MAKER_PRINT_COMMAND <string>
|
||||
--label-maker-dynamic-length/$HBOX_LABEL_MAKER_DYNAMIC_LENGTH <bool> (default: true)
|
||||
--label-maker-additional-information/$HBOX_LABEL_MAKER_ADDITIONAL_INFORMATION <string>
|
||||
--label-maker-regular-font-path/$HBOX_LABEL_MAKER_REGULAR_FONT_PATH <string>
|
||||
--label-maker-bold-font-path/$HBOX_LABEL_MAKER_BOLD_FONT_PATH <string>
|
||||
--thumbnail-enabled/$HBOX_THUMBNAIL_ENABLED <bool> (default: true)
|
||||
--thumbnail-width/$HBOX_THUMBNAIL_WIDTH <int> (default: 500)
|
||||
--thumbnail-height/$HBOX_THUMBNAIL_HEIGHT <int> (default: 500)
|
||||
--help/-h display this help message
|
||||
```
|
||||
|
||||
:::
|
||||
For configuring OpenID Connect (OIDC) authentication, refer to the [OIDC Configuration Guide](/en/configure/oidc).
|
||||
|
||||
44
docs/en/configure/oidc.md
Normal file
44
docs/en/configure/oidc.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Configure OIDC
|
||||
|
||||
HomeBox supports OpenID Connect (OIDC) authentication, allowing users to login using external identity providers like Keycloak, Authentik, Authelia, Google, Microsoft, etc.
|
||||
|
||||
::: tip OIDC Provider Documentation
|
||||
When configuring OIDC, always refer to the documentation provided by your identity provider for specific details and requirements.
|
||||
:::
|
||||
|
||||
## Basic OIDC Setup
|
||||
|
||||
1. **Enable OIDC**: Set `HBOX_OIDC_ENABLED=true`.
|
||||
2. **Provider Configuration**: Set the required provider details:
|
||||
- `HBOX_OIDC_ISSUER_URL`: Your OIDC provider's issuer URL.
|
||||
- Generally this URL should not have a trailing slash, though it may be required for some providers.
|
||||
- `HBOX_OIDC_CLIENT_ID`: Client ID from your OIDC provider.
|
||||
- `HBOX_OIDC_CLIENT_SECRET`: Client secret from your OIDC provider.
|
||||
- If you are using a reverse proxy, it may be necessary to set `HBOX_OPTIONS_TRUST_PROXY=true` to ensure `https` is correctly detected.
|
||||
- If you have set `HBOX_OPTIONS_HOSTNAME` make sure it is just the hostname and does not include `https://` or `http://`.
|
||||
|
||||
3. **Configure Redirect URI**: In your OIDC provider, set the redirect URI to:
|
||||
`https://your-homebox-domain.example.com/api/v1/users/login/oidc/callback`.
|
||||
|
||||
## Advanced OIDC Configuration
|
||||
|
||||
- **Group Authorization**: Use `HBOX_OIDC_ALLOWED_GROUPS` to restrict access to specific groups, e.g. `HBOX_OIDC_ALLOWED_GROUPS=admin,homebox`.
|
||||
- Some providers require the `groups` scope to return group claims, include it in `HBOX_OIDC_SCOPE` (e.g. `openid profile email groups`) or configure the provider to release the claim.
|
||||
- **Custom Claims**: Configure `HBOX_OIDC_GROUP_CLAIM`, `HBOX_OIDC_EMAIL_CLAIM`, and `HBOX_OIDC_NAME_CLAIM` if your provider uses different claim names.
|
||||
- These default to `HBOX_OIDC_GROUP_CLAIM=groups`, `HBOX_OIDC_EMAIL_CLAIM=email` and `HBOX_OIDC_NAME_CLAIM=name`.
|
||||
- **Auto Redirect to OIDC**: Set `HBOX_OIDC_AUTO_REDIRECT=true` to automatically redirect users directly to OIDC.
|
||||
- **Local Login**: Set `HBOX_OPTIONS_ALLOW_LOCAL_LOGIN=false` to completely disable username/password login.
|
||||
- **Email Verification**: Set `HBOX_OIDC_VERIFY_EMAIL=true` to require email verification from the OIDC provider.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
::: warning OIDC Security
|
||||
- Store `HBOX_OIDC_CLIENT_SECRET` securely (use environment variables, not config files).
|
||||
- Use HTTPS for production deployments.
|
||||
- Configure proper redirect URIs in your OIDC provider.
|
||||
- Consider setting `HBOX_OIDC_ALLOWED_GROUPS` for group-based access control.
|
||||
:::
|
||||
|
||||
::: tip CLI Arguments
|
||||
If you're deploying without docker you can use command line arguments to configure the application. Run `homebox --help` for more information.
|
||||
:::
|
||||
@@ -30,19 +30,19 @@ the bucket name in the connection string.
|
||||
### S3-Compatible Storage
|
||||
|
||||
You can also use S3-compatible storage by setting the `HBOX_STORAGE_CONN_STRING` to
|
||||
`s3://my-bucket?awssdk=v2&endpoint=http://my-s3-compatible-endpoint.tld&disableSSL=true&s3ForcePathStyle=true`.
|
||||
`s3://my-bucket?awssdk=v2&endpoint=http://my-s3-compatible-endpoint.tld&disable_https=true&s3ForcePathStyle=true`.
|
||||
|
||||
This allows you to connect to S3-compatible services like MinIO, DigitalOcean Spaces, or any other service that supports
|
||||
the S3 API. Configure the `disableSSL`, `s3ForcePathStyle`, and `endpoint` parameters as needed for your specific
|
||||
the S3 API. Configure the `disable_https`, `s3ForcePathStyle`, and `endpoint` parameters as needed for your specific
|
||||
service.
|
||||
|
||||
#### Tested S3-Compatible Storage
|
||||
|
||||
| Service | Working | Connection String |
|
||||
|---------------------|---------|--------------------------------------------------------------------------------------------------------------------------|
|
||||
| MinIO | Yes | `s3://my-bucket?awssdk=v2&endpoint=http://minio:9000&disableSSL=true&s3ForcePathStyle=true` |
|
||||
| Cloudflare R2 | Yes | `s3://my-bucket?awssdk=v2&endpoint=https://<account-id>.r2.cloudflarestorage.com&disableSSL=false&s3ForcePathStyle=true` |
|
||||
| Backblaze B2 | Yes | `s3://my-bucket?awssdk=v2&endpoint=https://s3.us-west-004.backblazeb2.com&disableSSL=false&s3ForcePathStyle=true` |
|
||||
| MinIO | Yes | `s3://my-bucket?awssdk=v2&endpoint=http://minio:9000&disable_https=true&s3ForcePathStyle=true` |
|
||||
| Cloudflare R2 | Yes | `s3://my-bucket?awssdk=v2&endpoint=https://<account-id>.r2.cloudflarestorage.com&disable_https=false&s3ForcePathStyle=true` |
|
||||
| Backblaze B2 | Yes | `s3://my-bucket?awssdk=v2&endpoint=https://s3.us-west-004.backblazeb2.com&disable_https=false&s3ForcePathStyle=true` |
|
||||
|
||||
::: info
|
||||
If you know of any other S3-compatible storage that works with Homebox, please let us know or create a pull request to update the table.
|
||||
@@ -57,7 +57,7 @@ Additionally, the parameters in the URL can be used to configure specific S3 set
|
||||
features.)
|
||||
- `endpoint`: The custom endpoint for S3-compatible storage services.
|
||||
- `s3ForcePathStyle`: Whether to force path-style access (set to `true` or `false`).
|
||||
- `disableSSL`: Whether to disable SSL (set to `true` or `false`).
|
||||
- `disable_https`: Whether to disable SSL (set to `true` or `false`).
|
||||
- `sseType`: The server-side encryption type (e.g., `AES256` or `aws:kms` or `aws:kms:dsse`).
|
||||
- `kmskeyid`: The KMS key ID for server-side encryption.
|
||||
- `fips`: Whether to use FIPS endpoints (set to `true` or `false`).
|
||||
|
||||
@@ -52,7 +52,7 @@ services:
|
||||
environment:
|
||||
- HBOX_LOG_LEVEL=info
|
||||
- HBOX_LOG_FORMAT=text
|
||||
- HBOX_WEB_MAX_FILE_UPLOAD=10
|
||||
- HBOX_WEB_MAX_UPLOAD_SIZE=10
|
||||
# Please consider allowing analytics to help us improve Homebox (basic computer information, no personal data)
|
||||
- HBOX_OPTIONS_ALLOW_ANALYTICS=false
|
||||
volumes:
|
||||
|
||||
@@ -81,17 +81,6 @@
|
||||
errorMessage.value = t("scanner.error");
|
||||
};
|
||||
|
||||
const checkPermissionsError = async () => {
|
||||
if (navigator.permissions) {
|
||||
const permissionStatus = await navigator.permissions.query({ name: "camera" as PermissionName });
|
||||
if (permissionStatus.state === "denied") {
|
||||
errorMessage.value = t("scanner.permission_denied");
|
||||
console.error("Camera permission denied");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleButtonClick = () => {
|
||||
openDialog(DialogID.ProductImport, { params: { barcode: detectedBarcode.value } });
|
||||
};
|
||||
@@ -103,11 +92,19 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (await checkPermissionsError()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Request camera permission first
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && err.name === "NotAllowedError") {
|
||||
errorMessage.value = t("scanner.permission_denied");
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
const devices = await codeReader.listVideoInputDevices();
|
||||
sources.value = devices;
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form class="flex flex-col gap-2" @submit.prevent="create()">
|
||||
<form class="flex min-w-0 flex-col gap-2" @submit.prevent="create()">
|
||||
<LocationSelector v-model="form.location" />
|
||||
|
||||
<!-- Template Info Display - Collapsible banner with distinct styling -->
|
||||
|
||||
@@ -6,12 +6,25 @@
|
||||
<Popover v-model:open="open">
|
||||
<PopoverTrigger as-child>
|
||||
<Button :id="id" variant="outline" role="combobox" :aria-expanded="open" class="w-full justify-between">
|
||||
<span>
|
||||
<span class="truncate text-left">
|
||||
<slot name="display" v-bind="{ item: value }">
|
||||
{{ displayValue(value) || localizedPlaceholder }}
|
||||
</slot>
|
||||
</span>
|
||||
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
|
||||
|
||||
<span class="ml-2 flex items-center">
|
||||
<button
|
||||
v-if="value"
|
||||
type="button"
|
||||
class="shrink-0 rounded p-1 hover:bg-primary/20"
|
||||
:aria-label="t('components.item.selector.clear')"
|
||||
@click.stop.prevent="clearSelection"
|
||||
>
|
||||
<X class="size-4" />
|
||||
</button>
|
||||
|
||||
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
|
||||
</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-[--reka-popper-anchor-width] p-0">
|
||||
@@ -44,7 +57,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { Check, ChevronsUpDown } from "lucide-vue-next";
|
||||
import { Check, ChevronsUpDown, X } from "lucide-vue-next";
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { useVModel } from "@vueuse/core";
|
||||
import { useI18n } from "vue-i18n";
|
||||
@@ -174,6 +187,12 @@
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
value.value = null;
|
||||
search.value = "";
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
const filtered = computed(() => {
|
||||
let baseItems = props.items;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<BaseModal :dialog-id="DialogID.CreateLabel" :title="$t('components.label.create_modal.title')">
|
||||
<form class="flex flex-col gap-2" @submit.prevent="create()">
|
||||
<form class="flex min-w-0 flex-col gap-2" @submit.prevent="create()">
|
||||
<FormTextField
|
||||
v-model="form.name"
|
||||
:trigger-focus="focused"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<BaseModal :dialog-id="DialogID.CreateLocation" :title="$t('components.location.create_modal.title')">
|
||||
<form class="flex flex-col gap-2" @submit.prevent="create()">
|
||||
<form class="flex min-w-0 flex-col gap-2" @submit.prevent="create()">
|
||||
<LocationSelector v-model="form.parent" />
|
||||
<FormTextField
|
||||
ref="locationNameRef"
|
||||
|
||||
@@ -7,8 +7,23 @@
|
||||
<Popover v-model:open="open">
|
||||
<PopoverTrigger as-child>
|
||||
<Button :id="id" variant="outline" role="combobox" :aria-expanded="open" class="w-full justify-between">
|
||||
{{ value && value.name ? value.name : $t("components.location.selector.select_location") }}
|
||||
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
|
||||
<span class="min-w-0 flex-auto truncate text-left">
|
||||
{{ value && value.name ? value.name : $t("components.location.selector.select_location") }}
|
||||
</span>
|
||||
|
||||
<span class="ml-2 flex items-center">
|
||||
<button
|
||||
v-if="value"
|
||||
type="button"
|
||||
class="shrink-0 rounded p-1 hover:bg-primary/20"
|
||||
:aria-label="$t('components.location.selector.clear')"
|
||||
@click.stop.prevent="clearSelection"
|
||||
>
|
||||
<X class="size-4" />
|
||||
</button>
|
||||
|
||||
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
|
||||
</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-[--reka-popper-anchor-width] p-0">
|
||||
@@ -46,7 +61,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, ChevronsUpDown } from "lucide-vue-next";
|
||||
import { Check, ChevronsUpDown, X } from "lucide-vue-next";
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "~/components/ui/command";
|
||||
@@ -79,6 +94,12 @@
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
value.value = null;
|
||||
search.value = "";
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
const filteredLocations = computed(() => {
|
||||
const filtered = fuzzysort.go(search.value, locations.value, { key: "name", all: true }).map(i => i.obj);
|
||||
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
|
||||
const state = useTreeState(props.treeId);
|
||||
|
||||
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });
|
||||
|
||||
const sortedChildren = computed(() => {
|
||||
const children = props.item.children ?? [];
|
||||
return [...children].sort((a, b) => collator.compare(a.name, b.name));
|
||||
});
|
||||
|
||||
const openRef = computed({
|
||||
get() {
|
||||
return state.value[nodeHash.value] ?? false;
|
||||
@@ -66,7 +73,7 @@
|
||||
<NuxtLink class="text-lg hover:underline" :to="link" @click.stop>{{ item.name }} </NuxtLink>
|
||||
</div>
|
||||
<div v-if="openRef" class="ml-4">
|
||||
<LocationTreeNode v-for="child in item.children" :key="child.id" :item="child" :tree-id="treeId" />
|
||||
<LocationTreeNode v-for="child in sortedChildren" :key="child.id" :item="child" :tree-id="treeId" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -7,14 +7,21 @@
|
||||
treeId: string;
|
||||
};
|
||||
|
||||
defineProps<Props>();
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });
|
||||
|
||||
const sortedLocs = computed(() => {
|
||||
const list = props.locs ?? [];
|
||||
return [...list].sort((a, b) => collator.compare(a.name, b.name));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p v-if="locs.length === 0" class="text-center text-sm">
|
||||
<p v-if="sortedLocs.length === 0" class="text-center text-sm">
|
||||
{{ $t("location.tree.no_locations") }}
|
||||
</p>
|
||||
<LocationTreeNode v-for="item in locs" :key="item.id" :item="item" :tree-id="treeId" />
|
||||
<LocationTreeNode v-for="item in sortedLocs" :key="item.id" :item="item" :tree-id="treeId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
defaultModelNumber: fullTemplate.defaultModelNumber,
|
||||
defaultLifetimeWarranty: fullTemplate.defaultLifetimeWarranty,
|
||||
defaultWarrantyDetails: fullTemplate.defaultWarrantyDetails,
|
||||
defaultLocationId: fullTemplate.defaultLocation?.id ?? "",
|
||||
defaultLocationId: fullTemplate.defaultLocation?.id ?? null,
|
||||
defaultLabelIds: fullTemplate.defaultLabels?.map(l => l.id) || [],
|
||||
includeWarrantyFields: fullTemplate.includeWarrantyFields,
|
||||
includePurchaseFields: fullTemplate.includePurchaseFields,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<BaseModal :dialog-id="DialogID.CreateTemplate" :title="$t('components.template.create_modal.title')">
|
||||
<form class="flex flex-col gap-2" @submit.prevent="create()">
|
||||
<form class="flex min-w-0 flex-col gap-2" @submit.prevent="create()">
|
||||
<FormTextField
|
||||
v-model="form.name"
|
||||
:autofocus="true"
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<Separator class="my-2" />
|
||||
<h3 class="text-sm font-medium">{{ $t("components.template.form.default_item_values") }}</h3>
|
||||
<div class="grid gap-2">
|
||||
<div class="flex min-w-0 flex-col gap-2">
|
||||
<FormTextField v-model="form.defaultName" :label="$t('components.template.form.item_name')" :max-length="255" />
|
||||
<FormTextArea
|
||||
v-model="form.defaultDescription"
|
||||
|
||||
@@ -38,6 +38,14 @@
|
||||
</div>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
<CommandSeparator v-if="value" />
|
||||
<CommandGroup v-if="value">
|
||||
<CommandItem v-if="value" value="clear-selection" @select="clearSelection">
|
||||
<div class="flex w-full">
|
||||
{{ $t("components.template.selector.clear") }}
|
||||
</div>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
@@ -79,6 +87,13 @@
|
||||
</div>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
<CommandSeparator />
|
||||
<CommandItem v-if="value" value="clear-selection" @select="clearSelection">
|
||||
<X :class="cn('mr-2 h-4 w-4')" />
|
||||
<div class="flex w-full">
|
||||
<span class="text-destructive">{{ $t("components.template.selector.clear") }}</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
@@ -87,10 +102,18 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, ChevronsUpDown } from "lucide-vue-next";
|
||||
import { Check, ChevronsUpDown, X } from "lucide-vue-next";
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "~/components/ui/command";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from "~/components/ui/command";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
|
||||
import { cn } from "~/lib/utils";
|
||||
@@ -132,6 +155,13 @@
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
value.value = null;
|
||||
emit("template-selected", null);
|
||||
search.value = "";
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
const filteredTemplates = computed(() => {
|
||||
if (!templates.value) return [];
|
||||
const filtered = fuzzysort.go(search.value, templates.value, { key: "name", all: true }).map(i => i.obj);
|
||||
|
||||
129
frontend/components/WipeInventoryDialog.vue
Normal file
129
frontend/components/WipeInventoryDialog.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<AlertDialog :open="dialog" @update:open="handleOpenChange">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ $t("tools.actions_set.wipe_inventory") }}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{{ $t("tools.actions_set.wipe_inventory_confirm") }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
id="wipe-labels-checkbox"
|
||||
v-model="wipeLabels"
|
||||
type="checkbox"
|
||||
class="size-4 rounded border-gray-300"
|
||||
/>
|
||||
<label for="wipe-labels-checkbox" class="cursor-pointer text-sm font-medium">
|
||||
{{ $t("tools.actions_set.wipe_inventory_labels") }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
id="wipe-locations-checkbox"
|
||||
v-model="wipeLocations"
|
||||
type="checkbox"
|
||||
class="size-4 rounded border-gray-300"
|
||||
/>
|
||||
<label for="wipe-locations-checkbox" class="cursor-pointer text-sm font-medium">
|
||||
{{ $t("tools.actions_set.wipe_inventory_locations") }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
id="wipe-maintenance-checkbox"
|
||||
v-model="wipeMaintenance"
|
||||
type="checkbox"
|
||||
class="size-4 rounded border-gray-300"
|
||||
/>
|
||||
<label for="wipe-maintenance-checkbox" class="cursor-pointer text-sm font-medium">
|
||||
{{ $t("tools.actions_set.wipe_inventory_maintenance") }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-600">
|
||||
{{ $t("tools.actions_set.wipe_inventory_note") }}
|
||||
</p>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel @click="close">
|
||||
{{ $t("global.cancel") }}
|
||||
</AlertDialogCancel>
|
||||
<Button @click="confirm">
|
||||
{{ $t("global.confirm") }}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DialogID } from "~/components/ui/dialog-provider/utils";
|
||||
import { useDialog } from "~/components/ui/dialog-provider";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
const { registerOpenDialogCallback, closeDialog, addAlert, removeAlert } = useDialog();
|
||||
|
||||
const dialog = ref(false);
|
||||
const wipeLabels = ref(false);
|
||||
const wipeLocations = ref(false);
|
||||
const wipeMaintenance = ref(false);
|
||||
const isConfirming = ref(false);
|
||||
|
||||
registerOpenDialogCallback(DialogID.WipeInventory, () => {
|
||||
dialog.value = true;
|
||||
wipeLabels.value = false;
|
||||
wipeLocations.value = false;
|
||||
wipeMaintenance.value = false;
|
||||
isConfirming.value = false;
|
||||
});
|
||||
|
||||
watch(
|
||||
dialog,
|
||||
val => {
|
||||
if (val) {
|
||||
addAlert("wipe-inventory-dialog");
|
||||
} else {
|
||||
removeAlert("wipe-inventory-dialog");
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
function handleOpenChange(open: boolean) {
|
||||
if (!open && !isConfirming.value) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
dialog.value = false;
|
||||
closeDialog(DialogID.WipeInventory, undefined);
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
isConfirming.value = true;
|
||||
const result = {
|
||||
wipeLabels: wipeLabels.value,
|
||||
wipeLocations: wipeLocations.value,
|
||||
wipeMaintenance: wipeMaintenance.value,
|
||||
};
|
||||
closeDialog(DialogID.WipeInventory, result);
|
||||
dialog.value = false;
|
||||
isConfirming.value = false;
|
||||
}
|
||||
</script>
|
||||
@@ -26,6 +26,7 @@ export enum DialogID {
|
||||
UpdateLocation = "update-location",
|
||||
UpdateTemplate = "update-template",
|
||||
ItemChangeDetails = "item-table-updater",
|
||||
WipeInventory = "wipe-inventory",
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,6 +72,7 @@ export type DialogResultMap = {
|
||||
[DialogID.ItemImage]?: { action: "delete"; id: string };
|
||||
[DialogID.EditMaintenance]?: boolean;
|
||||
[DialogID.ItemChangeDetails]?: boolean;
|
||||
[DialogID.WipeInventory]?: { wipeLabels: boolean; wipeLocations: boolean; wipeMaintenance: boolean };
|
||||
};
|
||||
|
||||
/** Helpers to split IDs by requirement */
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<ModalConfirm />
|
||||
<OutdatedModal v-if="status" :status="status" />
|
||||
<ItemCreateModal />
|
||||
<WipeInventoryDialog />
|
||||
<LabelCreateModal />
|
||||
<LocationCreateModal />
|
||||
<ItemBarcodeModal />
|
||||
@@ -47,7 +48,7 @@
|
||||
{{ btn.name.value }}
|
||||
<Shortcut
|
||||
v-if="btn.shortcut"
|
||||
class="ml-auto hidden group-hover:inline"
|
||||
class="invisible ml-auto group-hover:visible"
|
||||
:keys="btn.shortcut.replace('Shift', '⇧').split('+')"
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
@@ -216,6 +217,7 @@
|
||||
import ModalConfirm from "~/components/ModalConfirm.vue";
|
||||
import OutdatedModal from "~/components/App/OutdatedModal.vue";
|
||||
import ItemCreateModal from "~/components/Item/CreateModal.vue";
|
||||
import WipeInventoryDialog from "~/components/WipeInventoryDialog.vue";
|
||||
|
||||
import LabelCreateModal from "~/components/Label/CreateModal.vue";
|
||||
import LocationCreateModal from "~/components/Location/CreateModal.vue";
|
||||
|
||||
@@ -31,4 +31,14 @@ export class ActionsAPI extends BaseAPI {
|
||||
url: route("/actions/create-missing-thumbnails"),
|
||||
});
|
||||
}
|
||||
|
||||
wipeInventory(options?: { wipeLabels?: boolean; wipeLocations?: boolean; wipeMaintenance?: boolean }) {
|
||||
return this.http.post<
|
||||
{ wipeLabels?: boolean; wipeLocations?: boolean; wipeMaintenance?: boolean },
|
||||
ActionAmountResult
|
||||
>({
|
||||
url: route("/actions/wipe-inventory"),
|
||||
body: options || {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
6
frontend/lib/api/types/data-contracts.ts
generated
6
frontend/lib/api/types/data-contracts.ts
generated
@@ -1150,6 +1150,12 @@ export interface TokenResponse {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface WipeInventoryOptions {
|
||||
wipeLabels: boolean;
|
||||
wipeLocations: boolean;
|
||||
wipeMaintenance: boolean;
|
||||
}
|
||||
|
||||
export interface Wrapped {
|
||||
item: any;
|
||||
}
|
||||
|
||||
14
frontend/locales/ar-AA.json
Normal file
14
frontend/locales/ar-AA.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"create_modal": {
|
||||
"createAndAddAnother": "استخدم المفتاحين {shiftKey} + {enterKey} للحفظ و إضافة عنصر جديد.",
|
||||
"enter": "إدخال",
|
||||
"shift": "عالي"
|
||||
},
|
||||
"import_dialog": {
|
||||
"change_warning": "تم تغيير آلية الإستيراد بتواجد العمود import_refs. عند تواجد العمود import_refs في ملف الـCSV،\nسيتم تحديث محتوى العناصر عن طريق القيم المتوفرة في ملف الـCSV."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,19 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"create_modal": {
|
||||
"createAndAddAnother": "Koristite {shiftKey} + {enterKey} da kreirate i dodate novu stavku.",
|
||||
"enter": "Enter",
|
||||
"shift": "Shift"
|
||||
},
|
||||
"import_dialog": {
|
||||
"change_warning": "Način unosa podataka s postojećim import_refs se promjenio. Ako je import_ref prisutan u CSV fajlu,\npredmet će se ažurirati s vrijednostima iz CSV fajla.",
|
||||
"title": "Importuj CSV fajl"
|
||||
"change_warning": "Način unosa podataka s postojećim import_refs se promijenio. Ako je import_ref prisutan u CSV fajlu,\npredmet će se ažurirati s vrijednostima iz CSV fajla.",
|
||||
"title": "Importuj CSV fajl",
|
||||
"toast": {
|
||||
"import_failed": "Import neuspješan. Molim vas pokušajte ponovo kasnije.",
|
||||
"import_success": "Import uspješan!",
|
||||
"please_select_file": "Molim odaberite fajl za import."
|
||||
}
|
||||
},
|
||||
"outdated": {
|
||||
"current_version": "Trenutna verzija",
|
||||
@@ -13,6 +23,18 @@
|
||||
"new_version_available_link": "Klikni ovdje za pregled bilješke o izdanju"
|
||||
}
|
||||
},
|
||||
"color_selector": {
|
||||
"clear": "Poništi izbor boje",
|
||||
"color": "Boja",
|
||||
"no_color": "Bez boje",
|
||||
"no_color_selected": "Nije odabrana boja",
|
||||
"randomize": "Slučajni izbor boje"
|
||||
},
|
||||
"form": {
|
||||
"password": {
|
||||
"toggle_show": "Prikaži/sakrij šifru"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"copy_text": {
|
||||
"documentation": "dokumentacija",
|
||||
@@ -33,6 +55,9 @@
|
||||
"minute": "minuta",
|
||||
"minutes": "minuta",
|
||||
"months": "mjeseci",
|
||||
"next-month": "idući mjesec",
|
||||
"next-week": "iduća hefta",
|
||||
"next-year": "iduća godina",
|
||||
"second": "sekunda",
|
||||
"seconds": "sekundi",
|
||||
"tomorrow": "sutra",
|
||||
@@ -40,9 +65,62 @@
|
||||
"weeks": "sedmica/e",
|
||||
"years": "godine/a",
|
||||
"yesterday": "jučer"
|
||||
},
|
||||
"label_maker": {
|
||||
"browser_print": "Štampaj iz browsera",
|
||||
"confirm_description": "Da li ste sigurni da želite štampati ovu oznaku?",
|
||||
"download": "Preuzmi oznaku",
|
||||
"print": "Štampaj oznaku",
|
||||
"server_print": "Štampaj na server",
|
||||
"titles": "Oznake",
|
||||
"toast": {
|
||||
"load_status_failed": "Ne mogu da učitam status",
|
||||
"print_failed": "Ne mogu da štampam oznaku",
|
||||
"print_success": "Oznaka otštampana"
|
||||
}
|
||||
},
|
||||
"page_qr_code": {
|
||||
"page_url": "Link stranice",
|
||||
"qr_tooltip": "Prikaži QR kod"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "Kompleksnost šifre"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"attachments_list": {
|
||||
"download": "Preuzimanje",
|
||||
"open_new_tab": "Otvori u novom tabu"
|
||||
},
|
||||
"create_modal": {
|
||||
"delete_photo": "Obriši fotografiju",
|
||||
"item_description": "Opis stavke",
|
||||
"item_name": "Naziv stavke",
|
||||
"item_photo": "Fotografija stavke 📷",
|
||||
"item_quantity": "Količina stavke",
|
||||
"product_tooltip_input_barcode": "Automatski popuni s ručno unesenim barkodom",
|
||||
"product_tooltip_scan_barcode": "Automatski popuni s barkodom sa 📷",
|
||||
"rotate_photo": "Okreni fotografiju",
|
||||
"title": "Kreiraj stavku",
|
||||
"toast": {
|
||||
"already_creating": "Već kreiram stavku",
|
||||
"create_failed": "Ne mogu kreirati stavku",
|
||||
"create_success": "Stavka kreirana",
|
||||
"no_canvas_support": "Vaš browser ne podržava canvas operacije",
|
||||
"please_select_location": "Molim odaberite lokaciju.",
|
||||
"rotate_failed": "Ne mogu rotirati fotografiju: { error }",
|
||||
"rotate_process_failed": "Ne mogu obraditi rotiranu fotografiju",
|
||||
"some_photos_failed": "{count, plural, =0 {Nema fotografija za upload.} =1 {1 fotografiju nije moguće uplodovati.} other {Neke fotografije nije moguće uploadovati.}}",
|
||||
"upload_failed": "Ne mogu uploadovati fotografiju: { photoName }",
|
||||
"upload_success": "{count, plural, =0 {Nema fotografija za upload.} =1 {Fotografija uspješno uploadovana.} other {Sve fotografije uspješno uploadovane.}}",
|
||||
"uploading_photos": "{count, plural, =0 {Nema fotografija za upload.} =1 {Uploadujem 1 fotografiju…} other {Uploadujem {count} fotografija…}}"
|
||||
},
|
||||
"upload_photos": "Uploaduj fotografije",
|
||||
"uploaded": "Uploadovane fotografije"
|
||||
},
|
||||
"product_import": {
|
||||
"barcode": "Barkod proizvoda"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"card": "Karta",
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
"open_new_tab": "Otevřít na nové kartě"
|
||||
},
|
||||
"create_modal": {
|
||||
"clear_template": "Vymazat šablonu",
|
||||
"delete_photo": "Smazat fotku",
|
||||
"item_description": "Popis položky",
|
||||
"item_name": "Jméno položky",
|
||||
@@ -134,19 +135,55 @@
|
||||
"selector": {
|
||||
"no_results": "Nebyly nalezeny žádné výsledky",
|
||||
"placeholder": "Vyberte…",
|
||||
"search_placeholder": "Pište pro vyhledávání…"
|
||||
"search_placeholder": "Pište pro vyhledávání…",
|
||||
"searching": "Hledám…"
|
||||
},
|
||||
"view": {
|
||||
"change_details": {
|
||||
"add_labels": "Přidat štítky",
|
||||
"failed_to_update_item": "Aktualizace položky se nezdařila",
|
||||
"remove_labels": "Odebrat štítky",
|
||||
"title": "Změnit podrobnosti o položce"
|
||||
},
|
||||
"selectable": {
|
||||
"card": "Karta",
|
||||
"items": "Položky",
|
||||
"no_items": "Žádné položky k zobrazení",
|
||||
"select_all": "Vybrat vše",
|
||||
"select_card": "Vybrat kartu",
|
||||
"select_row": "Vyberte řádek",
|
||||
"table": "Tabulka"
|
||||
},
|
||||
"table": {
|
||||
"dropdown": {
|
||||
"actions": "Akce",
|
||||
"change_labels": "Změnit štítky",
|
||||
"change_labels_success": "Štítky změněny",
|
||||
"change_location": "Změnit umístění",
|
||||
"change_location_success": "Umístění bylo změněno",
|
||||
"create_maintenance_item": "Vytvořit záznam údržby pro položku",
|
||||
"create_maintenance_selected": "Vytvořit záznam údržby pro vybrané položky",
|
||||
"create_maintenance_success": "Vytvořené záznamy údržby",
|
||||
"delete_confirmation": "Opravdu chcete smazat vybrané položky? Tuto akci nelze vrátit zpět.",
|
||||
"delete_item": "Odstranit položku",
|
||||
"delete_selected": "Smazat vybrané položky",
|
||||
"download_csv": "Stáhnout tabulku jako CSV",
|
||||
"download_json": "Stáhnout tabulku jako JSON",
|
||||
"duplicate_item": "Duplikovat tuto položku",
|
||||
"duplicate_selected": "Duplikovat vybrané položky",
|
||||
"error_deleting": "Chyba při mazání položky",
|
||||
"error_duplicating": "Chyba při duplikování položky",
|
||||
"open_menu": "Otevřít menu",
|
||||
"open_multi_tab_warning": "Z bezpečnostních důvodů prohlížeče ve výchozím nastavení neumožňují otevření více karet najednou. Chcete-li to změnit, postupujte podle dokumentace:",
|
||||
"toggle_expand": "Přepnout rozbalit",
|
||||
"view_item": "Zobrazit položku",
|
||||
"view_items": "Zobrazit položky"
|
||||
},
|
||||
"headers": "Záhlaví",
|
||||
"page": "Stránka",
|
||||
"quick_actions": "Povolit rychlé akce a výběr",
|
||||
"rows_per_page": "Řádků na stránku",
|
||||
"selected_rows": "{selected} z {total} řádků vybráno.",
|
||||
"table_settings": "Nastavení tabulky",
|
||||
"view_item": "Zobrazit položku"
|
||||
}
|
||||
@@ -193,6 +230,65 @@
|
||||
"quick_menu": {
|
||||
"no_results": "Nebyly nalezeny žádné výsledky.",
|
||||
"shortcut_hint": "Pomocí číselných tlačítek rychle vyberte akci."
|
||||
},
|
||||
"template": {
|
||||
"apply_template": "Použít šablonu",
|
||||
"card": {
|
||||
"delete": "Smazat šablonu",
|
||||
"duplicate": "Duplikovat šablonu",
|
||||
"edit": "Upravit šablonu"
|
||||
},
|
||||
"confirm_delete": "Smazat tuto šablonu?",
|
||||
"create_modal": {
|
||||
"title": "Vytvořit šablonu"
|
||||
},
|
||||
"detail": {
|
||||
"default_values": "Výchozí hodnoty",
|
||||
"updated": "Aktualizováno"
|
||||
},
|
||||
"edit_modal": {
|
||||
"title": "Upravit šablonu"
|
||||
},
|
||||
"empty_value": "(prázdné)",
|
||||
"form": {
|
||||
"custom_fields": "Vlastní pole",
|
||||
"default_item_values": "Výchozí hodnoty položek",
|
||||
"default_location": "Výchozí umístění",
|
||||
"default_value": "Výchozí hodnota",
|
||||
"field_name": "Název pole",
|
||||
"item_description": "Popis položky",
|
||||
"item_name": "Název položky",
|
||||
"lifetime_warranty": "Doživotní záruka",
|
||||
"location": "Umístění",
|
||||
"manufacturer": "Výrobce",
|
||||
"model_number": "Číslo modelu",
|
||||
"no_custom_fields": "Žádná vlastní pole.",
|
||||
"template_description": "Popis šablony",
|
||||
"template_name": "Název šablony"
|
||||
},
|
||||
"hide_defaults": "Skrýt výchozí hodnoty",
|
||||
"save_as_template": "Uložit jako šablonu",
|
||||
"selector": {
|
||||
"label": "Šablona (volitelné)",
|
||||
"not_found": "Nenalezena žádná šablona",
|
||||
"search": "Vyhledávání šablon…",
|
||||
"select": "Vyberte šablonu…"
|
||||
},
|
||||
"show_defaults": "Zobrazit výchozí hodnoty",
|
||||
"toast": {
|
||||
"applied": "Šablona „{name}“ použita",
|
||||
"create_failed": "Nepodařilo se vytvořit šablonu",
|
||||
"created": "Šablona vytvořena",
|
||||
"delete_failed": "Nepodařilo se odstranit šablonu",
|
||||
"deleted": "Šablona smazána",
|
||||
"duplicate_failed": "Nepodařilo se duplikovat šablonu",
|
||||
"duplicated": "Šablona duplikována jako „{name}“",
|
||||
"load_failed": "Nepodařilo se načíst podrobnosti šablony",
|
||||
"saved_as_template": "Položka uložena jako šablona „{name}“",
|
||||
"update_failed": "Nepodařilo se aktualizovat šablonu",
|
||||
"updated": "Šablona aktualizována"
|
||||
},
|
||||
"using_template": "Používání šablony: {name}"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
@@ -230,7 +326,9 @@
|
||||
"maintenance": "Údržba",
|
||||
"name": "Jméno",
|
||||
"navigate": "Navigovat",
|
||||
"no": "Ne",
|
||||
"password": "Heslo",
|
||||
"preview": "Náhled",
|
||||
"quantity": "Množství",
|
||||
"read_docs": "Přečtěte si dokumentaci",
|
||||
"return_home": "Zpět domů",
|
||||
@@ -243,7 +341,8 @@
|
||||
"updating": "Aktualizuji",
|
||||
"value": "Hodnota",
|
||||
"version": "Verze: { version }",
|
||||
"welcome": "Vítejte, { username }"
|
||||
"welcome": "Vítejte, { username }",
|
||||
"yes": "Ano"
|
||||
},
|
||||
"home": {
|
||||
"labels": "Štítky",
|
||||
@@ -260,6 +359,7 @@
|
||||
"dont_join_group": "Nechcete se přidat do skupiny?",
|
||||
"joining_group": "Přidáváte se do existující skupiny!",
|
||||
"login": "Přihlásit se",
|
||||
"or": "nebo",
|
||||
"register": "Registrovat se",
|
||||
"remember_me": "Zapamatovat si mě",
|
||||
"set_email": "Jaký je váš email?",
|
||||
@@ -271,6 +371,14 @@
|
||||
"invalid_email": "Neplatná e-mailová adresa",
|
||||
"invalid_email_password": "Neplatný e-mail nebo heslo",
|
||||
"login_success": "Úspěšně přihlášen",
|
||||
"oidc_access_denied": "Přístup odepřen: Váš účet nemá požadovanou roli/členství ve skupině",
|
||||
"oidc_auth_failed": "OIDC ověření selhalo",
|
||||
"oidc_invalid_response": "Byla přijata neplatná odpověď OIDC",
|
||||
"oidc_provider_error": "Poskytovatel OIDC vrátil chybu",
|
||||
"oidc_security_error": "Chyba zabezpečení OIDC – možný útok CSRF",
|
||||
"oidc_session_expired": "OIDC relace vypršela",
|
||||
"oidc_token_expired": "Token OIDC vypršel",
|
||||
"oidc_token_invalid": "Podpis tokenu OIDC je neplatný",
|
||||
"problem_registering": "Problém s registrací uživatele",
|
||||
"user_registered": "Uživatel registrován"
|
||||
}
|
||||
@@ -507,8 +615,15 @@
|
||||
"profile": "Profil",
|
||||
"scanner": "Skener",
|
||||
"search": "Vyhledávání",
|
||||
"templates": "Šablony",
|
||||
"tools": "Nástroje"
|
||||
},
|
||||
"pages": {
|
||||
"templates": {
|
||||
"no_templates": "Zatím žádné šablony.",
|
||||
"title": "Šablony"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"active": "Aktivní",
|
||||
"change_password": "Změnit heslo",
|
||||
@@ -526,6 +641,7 @@
|
||||
"group_settings_sub": "Nastavení sdílené skupiny. Je možné, že bude nutné obnovit prohlížeč, aby se některá nastavení použila.",
|
||||
"inactive": "Neaktivní",
|
||||
"language": "Jazyk",
|
||||
"legacy_image_fit": "{ currentValue, select, true {Zakázat starší přizpůsobení: přizpůsobit obrázek s pruhy} false {Povolit starší přizpůsobení: vyplnit obrázek oříznutím} other {Not Hit}}",
|
||||
"new_password": "Nové heslo",
|
||||
"no_notifiers": "Nejsou nakonfigurováni žádní oznamovatelé",
|
||||
"no_override": "Žádné přepsání",
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
"item_photo": "Vare Foto 📷",
|
||||
"item_quantity": "Vare Antal",
|
||||
"parent_item": "Overordnet element",
|
||||
"product_tooltip_input_barcode": "Autoudfyld med en manuelt angivet stregkode",
|
||||
"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",
|
||||
@@ -124,27 +125,64 @@
|
||||
"product_import": {
|
||||
"barcode": "Produkts stregkode",
|
||||
"db_source": "DB kilde",
|
||||
"error_exception": "Der opstod en undtagelse under hentning af varens stregkode: ",
|
||||
"error_invalid_barcode": "Ugyldig stregkode angivet",
|
||||
"error_not_found": "Intet produkt fundet med angivet stregkode.",
|
||||
"error_not_found": "Intet produkt fundet med den angivne 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…"
|
||||
"search_placeholder": "Skriv for at søge…",
|
||||
"searching": "Søger…"
|
||||
},
|
||||
"view": {
|
||||
"change_details": {
|
||||
"add_labels": "Tilføj etiketter",
|
||||
"failed_to_update_item": "Kunne ikke opdatere elementet",
|
||||
"remove_labels": "Fjern Etiketter",
|
||||
"title": "Skift elementoplysninger"
|
||||
},
|
||||
"selectable": {
|
||||
"card": "Kort",
|
||||
"items": "Genstande",
|
||||
"no_items": "Ingen genstande at vise",
|
||||
"select_all": "Vælg alle",
|
||||
"select_card": "Vælg kort",
|
||||
"select_row": "Vælg række",
|
||||
"table": "Tabel"
|
||||
},
|
||||
"table": {
|
||||
"dropdown": {
|
||||
"actions": "Handlinger",
|
||||
"change_labels": "Skift etiketter",
|
||||
"change_labels_success": "Etiketter ændret",
|
||||
"change_location": "Skift placering",
|
||||
"change_location_success": "Placering ændret",
|
||||
"create_maintenance_item": "Opret vedligeholdelsespost for vare",
|
||||
"create_maintenance_selected": "Opret vedligeholdelsespost for valgte varer",
|
||||
"create_maintenance_success": "Vedligeholdelsespost(er) oprettet",
|
||||
"delete_confirmation": "Er du sikker på, at du vil slette det/de valgte element(er)? Denne handling kan ikke fortrydes.",
|
||||
"delete_item": "Slet element",
|
||||
"delete_selected": "Slet valgte elementer",
|
||||
"download_csv": "Download tabel som CSV",
|
||||
"download_json": "Download tabel som JSON",
|
||||
"duplicate_item": "Dupliker element",
|
||||
"duplicate_selected": "Dupliker valgte elementer",
|
||||
"error_deleting": "Fejl ved sletning af element",
|
||||
"error_duplicating": "Fejl ved duplikering af element",
|
||||
"open_menu": "Åbn menu",
|
||||
"open_multi_tab_warning": "Af sikkerhedsmæssige årsager tillader browsere som standard ikke, at flere faner åbnes på én gang. For at ændre dette skal du følge dokumentationen:",
|
||||
"toggle_expand": "Udvid til/fra",
|
||||
"view_item": "Vis element",
|
||||
"view_items": "Vis elementer"
|
||||
},
|
||||
"headers": "Overskrifter",
|
||||
"page": "Side",
|
||||
"quick_actions": "Aktivér hurtige handlinger og valg",
|
||||
"rows_per_page": "Rækker per side",
|
||||
"selected_rows": "{selected} af {total} række(r) valgt.",
|
||||
"table_settings": "Tabel Indstillinger",
|
||||
"view_item": "Se vare"
|
||||
}
|
||||
@@ -229,6 +267,7 @@
|
||||
"name": "Navn",
|
||||
"navigate": "Naviger",
|
||||
"password": "Adgangskode",
|
||||
"preview": "Forhåndsvisning",
|
||||
"quantity": "Mængde",
|
||||
"read_docs": "Læs Docs",
|
||||
"return_home": "Vend hjem",
|
||||
@@ -289,6 +328,18 @@
|
||||
"description": "Beskrivelse",
|
||||
"details": "Detaljer",
|
||||
"drag_and_drop": "Træk og slip filer her, eller klik for at vælge filer",
|
||||
"duplicate": {
|
||||
"copy_attachments": "Kopiér vedhæftede filer",
|
||||
"copy_custom_fields": "Kopiér brugerdefinerede felter",
|
||||
"copy_maintenance": "Kopier vedligeholdelse",
|
||||
"custom_prefix": "Kopiér præfiks",
|
||||
"enable_custom_prefix": "Aktivér brugerdefineret præfiks",
|
||||
"override_instructions": "Hold Shift nede, når du klikker på duplikatknappen for at tilsidesætte disse indstillinger.",
|
||||
"prefix": "Kopi af ",
|
||||
"prefix_instructions": "Dette præfiks vil blive tilføjet i begyndelsen af det duplikerede elements navn. Medtag et mellemrum i slutningen af præfikset for at tilføje et mellemrum mellem præfikset og elementnavnet.",
|
||||
"temporary_title": "Midlertidige indstillinger",
|
||||
"title": "Dupliker indstillinger"
|
||||
},
|
||||
"edit": {
|
||||
"edit_attachment_dialog": {
|
||||
"attachment_title": "Titel på vedhæftet fil",
|
||||
@@ -297,7 +348,8 @@
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"view_image": "Vis billede"
|
||||
},
|
||||
"edit_details": "Rediger detaljer",
|
||||
"field_selector": "Feltvælger",
|
||||
@@ -395,6 +447,7 @@
|
||||
"update_label": "Opdater etiket"
|
||||
},
|
||||
"languages": {
|
||||
"bs-BA": "Bosnisk (Bosnien-Hercegovina)",
|
||||
"ca": "Catalansk",
|
||||
"cs-CZ": "Tjekkisk",
|
||||
"de": "Tysk",
|
||||
@@ -422,6 +475,7 @@
|
||||
"th-TH": "Thailandsk",
|
||||
"tr": "Tyrkisk",
|
||||
"uk-UA": "Ukrainsk",
|
||||
"vi-VN": "Vietnamesisk",
|
||||
"zh-CN": "Kinesisk (simplificeret)",
|
||||
"zh-HK": "Kinesisk (Hong Kong)",
|
||||
"zh-MO": "Kinesisk (Macao)",
|
||||
@@ -509,6 +563,7 @@
|
||||
"group_settings_sub": "Indstillinger for delt gruppe. Det kan være nødvendigt at genindlæse din browser før nogle indstillinger træder i kraft.",
|
||||
"inactive": "Inaktiv",
|
||||
"language": "Sprog",
|
||||
"legacy_image_fit": "{ currentValue, select, true {Deaktiver Legacy Fit: Tilpas billede med bjælker} false {Aktiver Legacy Fit: Fyld billede med beskæring} other {Ikke ramt}}",
|
||||
"new_password": "Ny Adgangskode",
|
||||
"no_notifiers": "Ingen notifikationer konfiguret",
|
||||
"no_override": "Ingen tilsidesættelse",
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
"open_new_tab": "In einem neuen Tab öffnen"
|
||||
},
|
||||
"create_modal": {
|
||||
"clear_template": "Vorlage leeren",
|
||||
"delete_photo": "Photo löschen",
|
||||
"item_description": "Gegenstandsbezeichnung",
|
||||
"item_name": "Gegenstandsname",
|
||||
@@ -115,7 +116,7 @@
|
||||
"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.}}",
|
||||
"upload_failed": "Hochladen des Bildes Fehlgeschlagen: { photoName }",
|
||||
"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...}}"
|
||||
},
|
||||
@@ -134,19 +135,55 @@
|
||||
"selector": {
|
||||
"no_results": "Keine Ergebnisse gefunden",
|
||||
"placeholder": "Auswählen…",
|
||||
"search_placeholder": "Für Suche tippen…"
|
||||
"search_placeholder": "Für Suche tippen…",
|
||||
"searching": "Suche…"
|
||||
},
|
||||
"view": {
|
||||
"change_details": {
|
||||
"add_labels": "Labels hinzufügen",
|
||||
"failed_to_update_item": "Artikel konnte nicht aktualisiert werden",
|
||||
"remove_labels": "Labels entfernen",
|
||||
"title": "Artikeldetails ändern"
|
||||
},
|
||||
"selectable": {
|
||||
"card": "Karte",
|
||||
"items": "Gegenstände",
|
||||
"no_items": "Keine Gegenstände anzuzeigen",
|
||||
"select_all": "Alles auswählen",
|
||||
"select_card": "Karte auswählen",
|
||||
"select_row": "Zeile auswählen",
|
||||
"table": "Tabelle"
|
||||
},
|
||||
"table": {
|
||||
"dropdown": {
|
||||
"actions": "Aktionen",
|
||||
"change_labels": "Labels ändern",
|
||||
"change_labels_success": "Etiketten geändert",
|
||||
"change_location": "Standort ändern",
|
||||
"change_location_success": "Standort geändert",
|
||||
"create_maintenance_item": "Wartungsposten für Artikel erstellen",
|
||||
"create_maintenance_selected": "Wartungsposten für ausgewählte Artikel erstellen",
|
||||
"create_maintenance_success": "Wartungseinträge erstellt",
|
||||
"delete_confirmation": "Möchten Sie die ausgewählten Elemente wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"delete_item": "Löschen",
|
||||
"delete_selected": "Ausgewählte Elemente löschen",
|
||||
"download_csv": "Tabelle als CSV herunterladen",
|
||||
"download_json": "Tabelle als json herunterladen",
|
||||
"duplicate_item": "Duplizieren",
|
||||
"duplicate_selected": "Ausgewählte Elemente duplizieren",
|
||||
"error_deleting": "Fehler beim Löschen",
|
||||
"error_duplicating": "Fehler beim Duplizieren des Elements",
|
||||
"open_menu": "Menü öffnen",
|
||||
"open_multi_tab_warning": "Aus Sicherheitsgründen erlauben Browser standardmäßig nicht, dass mehrere Registerkarten gleichzeitig geöffnet werden. Um dies zu ändern, folgen Sie bitte der Dokumentation:",
|
||||
"toggle_expand": "Ausklappen",
|
||||
"view_item": "Artikel anzeigen",
|
||||
"view_items": "Einträge anzeigen"
|
||||
},
|
||||
"headers": "Kopfzeilen",
|
||||
"page": "Seite",
|
||||
"quick_actions": "Schnellaktionen und Auswahl aktivieren",
|
||||
"rows_per_page": "Zeilen pro Seite",
|
||||
"selected_rows": "{selected} von {total} Zeilen ausgewählt.",
|
||||
"table_settings": "Tabellen Einstellungen",
|
||||
"view_item": "Artikel anzeigen"
|
||||
}
|
||||
@@ -193,6 +230,65 @@
|
||||
"quick_menu": {
|
||||
"no_results": "Keine Ergebnisse gefunden.",
|
||||
"shortcut_hint": "Verwenden Sie die Zifferntasten, um schnell eine Aktion auszuwählen."
|
||||
},
|
||||
"template": {
|
||||
"apply_template": "Eine Vorlage anwenden",
|
||||
"card": {
|
||||
"delete": "Vorlage löschen",
|
||||
"duplicate": "Vorlage duplizieren",
|
||||
"edit": "Vorlage bearbeiten"
|
||||
},
|
||||
"confirm_delete": "Diese Vorlage löschen?",
|
||||
"create_modal": {
|
||||
"title": "Vorlage erstellen"
|
||||
},
|
||||
"detail": {
|
||||
"default_values": "Standardwerte",
|
||||
"updated": "Aktualisiert"
|
||||
},
|
||||
"edit_modal": {
|
||||
"title": "Vorlage bearbeiten"
|
||||
},
|
||||
"empty_value": "(leer)",
|
||||
"form": {
|
||||
"custom_fields": "Eigene Felder",
|
||||
"default_item_values": "Standard Gegenstandswerte",
|
||||
"default_location": "Standard Lagerort",
|
||||
"default_value": "Standardwert",
|
||||
"field_name": "Feldname",
|
||||
"item_description": "Gegenstandsbeschreibung",
|
||||
"item_name": "Gegenstandsname",
|
||||
"lifetime_warranty": "Lebenslange Garantie",
|
||||
"location": "Lagerort",
|
||||
"manufacturer": "Hersteller",
|
||||
"model_number": "Modellnummer",
|
||||
"no_custom_fields": "Keine eigenen Felder.",
|
||||
"template_description": "Vorlagen Beschreibung",
|
||||
"template_name": "Vorlagen Name"
|
||||
},
|
||||
"hide_defaults": "Standardwerte ausblenden",
|
||||
"save_as_template": "Als Vorlage speichern",
|
||||
"selector": {
|
||||
"label": "Vorlage (optional)",
|
||||
"not_found": "Keine Vorlage gefunden",
|
||||
"search": "Vorlagen suchen…",
|
||||
"select": "Vorlage auswählen…"
|
||||
},
|
||||
"show_defaults": "Standardwerte anzeigen",
|
||||
"toast": {
|
||||
"applied": "Vorlage \"{name}\" angewendet",
|
||||
"create_failed": "Erstellen der Vorlage fehlgeschlagen",
|
||||
"created": "Vorlage erstellt",
|
||||
"delete_failed": "Löschen der Vorlage fehlgeschlagen",
|
||||
"deleted": "Vorlage gelöscht",
|
||||
"duplicate_failed": "Duplizieren der Vorlage fehlgeschlagen",
|
||||
"duplicated": "Vorlage dupliziert als \"{name}\"",
|
||||
"load_failed": "Laden der Vorlagen Details fehlgeschlagen",
|
||||
"saved_as_template": "Objekt als Vorlage \"{name}\" gespeichert",
|
||||
"update_failed": "Aktualisieren der Vorlage fehlgeschlagen",
|
||||
"updated": "Vorlage aktualisiert"
|
||||
},
|
||||
"using_template": "Verwendet Vorlage: {name}"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
@@ -230,7 +326,9 @@
|
||||
"maintenance": "Wartung",
|
||||
"name": "Name",
|
||||
"navigate": "Navigieren",
|
||||
"no": "Nein",
|
||||
"password": "Passwort",
|
||||
"preview": "Vorschau",
|
||||
"quantity": "Menge",
|
||||
"read_docs": "Dokumentation lesen",
|
||||
"return_home": "Startbildschirm",
|
||||
@@ -243,7 +341,8 @@
|
||||
"updating": "Aktualisiere",
|
||||
"value": "Wert",
|
||||
"version": "Version: { version }",
|
||||
"welcome": "Willkommen, { username }"
|
||||
"welcome": "Willkommen, { username }",
|
||||
"yes": "Ja"
|
||||
},
|
||||
"home": {
|
||||
"labels": "Labels",
|
||||
@@ -260,6 +359,7 @@
|
||||
"dont_join_group": "Möchtest du nicht einer Gruppe beitreten?",
|
||||
"joining_group": "Du trittst einer bereits bestehenden Gruppe bei!",
|
||||
"login": "Anmelden",
|
||||
"or": "oder",
|
||||
"register": "Registrieren",
|
||||
"remember_me": "Angemeldet bleiben",
|
||||
"set_email": "Was ist deine E-Mail?",
|
||||
@@ -271,6 +371,14 @@
|
||||
"invalid_email": "Ungültige Email-Adresse",
|
||||
"invalid_email_password": "Ungültige Email oder Passwort",
|
||||
"login_success": "Erfolgreich eingeloggt",
|
||||
"oidc_access_denied": "Zugriff verweigert: Dein Konto hat nicht die erforderliche Rollen-/Gruppenmitgliedschaft",
|
||||
"oidc_auth_failed": "OIDC authentifizierung fehlgeschlagen",
|
||||
"oidc_invalid_response": "Ungültige OIDC-Antwort erhalten",
|
||||
"oidc_provider_error": "OIDC-Anbieter hat einen Fehler zurückgegeben",
|
||||
"oidc_security_error": "OIDC-Sicherheitsfehler - möglicher CSRF-Angriff",
|
||||
"oidc_session_expired": "OIDC-Sitzung ist abgelaufen",
|
||||
"oidc_token_expired": "OIDC-Token ist abgelaufen",
|
||||
"oidc_token_invalid": "OIDC-Token-Signatur ist ungültig",
|
||||
"problem_registering": "Problem bei der Benutzerregistrierung",
|
||||
"user_registered": "Benutzer registriert"
|
||||
}
|
||||
@@ -498,7 +606,7 @@
|
||||
"total_entries": "Gesamteinträge"
|
||||
},
|
||||
"menu": {
|
||||
"create_item": "Artikel / Vermögenswert",
|
||||
"create_item": "Artikel / Objekt",
|
||||
"create_label": "Label",
|
||||
"create_location": "Standort",
|
||||
"home": "Home",
|
||||
@@ -507,8 +615,15 @@
|
||||
"profile": "Profil",
|
||||
"scanner": "Der Scanner",
|
||||
"search": "Suche",
|
||||
"templates": "Vorlagen",
|
||||
"tools": "Extras"
|
||||
},
|
||||
"pages": {
|
||||
"templates": {
|
||||
"no_templates": "Noch keine Vorlagen.",
|
||||
"title": "Vorlagen"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"active": "Aktiv",
|
||||
"change_password": "Passwort ändern",
|
||||
@@ -526,6 +641,7 @@
|
||||
"group_settings_sub": "Gemeinsame Gruppeneinstellungen. Möglicherweise müssen Sie Ihren Browser aktualisieren, damit die Einstellungen wirksam werden.",
|
||||
"inactive": "Inaktiv",
|
||||
"language": "Sprache",
|
||||
"legacy_image_fit": "{ currentValue, select, true {Legacy-Anpassung deaktivieren: Bild mit Balken anpassen} false {Legacy-Anpassung aktivieren: Bild mit Zuschneiden füllen} other {Nicht getroffen}}",
|
||||
"new_password": "Neues Passwort",
|
||||
"no_notifiers": "Keine Benachrichtigungen konfiguriert",
|
||||
"no_override": "Kein Überschreiben",
|
||||
|
||||
12
frontend/locales/el-GR.json
Normal file
12
frontend/locales/el-GR.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"create_modal": {
|
||||
"createAndAddAnother": "Χρησιμοποιήστε τα πλήκτρα {shiftKey} + {enterKey} για να δημιουργήσετε και να προσθέσετε ένα άλλο."
|
||||
},
|
||||
"import_dialog": {
|
||||
"change_warning": "Η συμπεριφορά για τις εισαγωγές με υπάρχοντα import_refs έχει αλλάξει. Εάν υπάρχει ένα import_ref στο αρχείο CSV, \nτο στοιχείο θα ενημερωθεί με τις τιμές στο αρχείο CSV."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,7 @@
|
||||
"open_new_tab": "Open in new tab"
|
||||
},
|
||||
"create_modal": {
|
||||
"clear_template": "Clear Template",
|
||||
"delete_photo": "Delete photo",
|
||||
"item_description": "Item Description",
|
||||
"item_name": "Item Name",
|
||||
@@ -105,7 +106,6 @@
|
||||
"rotate_photo": "Rotate photo",
|
||||
"set_as_primary_photo": "Set as { isPrimary, select, true {non-} false {} other {}}primary photo",
|
||||
"title": "Create Item",
|
||||
"clear_template": "Clear Template",
|
||||
"toast": {
|
||||
"already_creating": "Already creating an item",
|
||||
"create_failed": "Couldn't create item",
|
||||
@@ -136,56 +136,57 @@
|
||||
"no_results": "No Results Found",
|
||||
"placeholder": "Select…",
|
||||
"search_placeholder": "Type to search…",
|
||||
"searching": "Searching…"
|
||||
"searching": "Searching…",
|
||||
"clear": "Clear Item Selection"
|
||||
},
|
||||
"view": {
|
||||
"change_details": {
|
||||
"title": "Change Item Details",
|
||||
"failed_to_update_item": "Failed to update item",
|
||||
"add_labels": "Add Labels",
|
||||
"remove_labels": "Remove Labels"
|
||||
"failed_to_update_item": "Failed to update item",
|
||||
"remove_labels": "Remove Labels",
|
||||
"title": "Change Item Details"
|
||||
},
|
||||
"selectable": {
|
||||
"card": "Card",
|
||||
"items": "Items",
|
||||
"no_items": "No Items to Display",
|
||||
"table": "Table",
|
||||
"select_all": "Select All",
|
||||
"select_card": "Select Card",
|
||||
"select_row": "Select Row",
|
||||
"select_card": "Select Card"
|
||||
"table": "Table"
|
||||
},
|
||||
"table": {
|
||||
"headers": "Headers",
|
||||
"page": "Page",
|
||||
"rows_per_page": "Rows per page",
|
||||
"quick_actions": "Enable Quick Actions & Selection",
|
||||
"table_settings": "Table Settings",
|
||||
"view_item": "View Item",
|
||||
"selected_rows": "{selected} of {total} row(s) selected.",
|
||||
"dropdown": {
|
||||
"open_menu": "Open menu",
|
||||
"actions": "Actions",
|
||||
"view_item": "View item",
|
||||
"view_items": "View items",
|
||||
"toggle_expand": "Toggle Expand",
|
||||
"download_csv": "Download Table as CSV",
|
||||
"download_json": "Download Table as JSON",
|
||||
"delete_selected": "Delete Selected Items",
|
||||
"delete_item": "Delete Item",
|
||||
"error_deleting": "Error Deleting Item",
|
||||
"delete_confirmation": "Are you sure you want to delete the selected item(s)? This action cannot be undone.",
|
||||
"duplicate_selected": "Duplicate Selected Items",
|
||||
"duplicate_item": "Duplicate Item",
|
||||
"error_duplicating": "Error Duplicating Item",
|
||||
"open_multi_tab_warning": "For security reasons browsers do not allow multiple tabs to be opened at once by default, to change this please follow the documentation:",
|
||||
"create_maintenance_selected": "Create Maintenance Entry for Selected Items",
|
||||
"create_maintenance_item": "Create Maintenance Entry for Item",
|
||||
"create_maintenance_success": "Maintenance Entry(s) Created",
|
||||
"change_labels": "Change Labels",
|
||||
"change_labels_success": "Labels Changed",
|
||||
"change_location": "Change Location",
|
||||
"change_location_success": "Location Changed",
|
||||
"change_labels": "Change Labels",
|
||||
"change_labels_success": "Labels Changed"
|
||||
}
|
||||
"create_maintenance_item": "Create Maintenance Entry for Item",
|
||||
"create_maintenance_selected": "Create Maintenance Entry for Selected Items",
|
||||
"create_maintenance_success": "Maintenance Entrys Created",
|
||||
"delete_confirmation": "Are you sure you want to delete the selected items? This action cannot be undone.",
|
||||
"delete_item": "Delete Item",
|
||||
"delete_selected": "Delete Selected Items",
|
||||
"download_csv": "Download Table as CSV",
|
||||
"download_json": "Download Table as JSON",
|
||||
"duplicate_item": "Duplicate Item",
|
||||
"duplicate_selected": "Duplicate Selected Items",
|
||||
"error_deleting": "Error Deleting Item",
|
||||
"error_duplicating": "Error Duplicating Item",
|
||||
"open_menu": "Open menu",
|
||||
"open_multi_tab_warning": "For security reasons browsers do not allow multiple tabs to be opened at once by default, to change this please follow the documentation:",
|
||||
"toggle_expand": "Toggle Expand",
|
||||
"view_item": "View item",
|
||||
"view_items": "View items"
|
||||
},
|
||||
"headers": "Headers",
|
||||
"page": "Page",
|
||||
"quick_actions": "Enable Quick Actions & Selection",
|
||||
"rows_per_page": "Rows per page",
|
||||
"selected_rows": "{selected} of {total} rows selected.",
|
||||
"table_settings": "Table Settings",
|
||||
"view_item": "View Item"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -221,7 +222,8 @@
|
||||
"no_location_found": "No location found",
|
||||
"parent_location": "Parent Location",
|
||||
"search_location": "Search Locations",
|
||||
"select_location": "Select a Location"
|
||||
"select_location": "Select a Location",
|
||||
"clear": "Clear Location Selection"
|
||||
},
|
||||
"tree": {
|
||||
"no_locations": "No locations available. Add new locations through the\n '<span class=\"link-primary\">'Create'</span>' button on the navigation bar."
|
||||
@@ -232,6 +234,7 @@
|
||||
"shortcut_hint": "Use the number keys to quickly select an action."
|
||||
},
|
||||
"template": {
|
||||
"apply_template": "Apply a template",
|
||||
"card": {
|
||||
"delete": "Delete template",
|
||||
"duplicate": "Duplicate template",
|
||||
@@ -248,6 +251,7 @@
|
||||
"edit_modal": {
|
||||
"title": "Edit Template"
|
||||
},
|
||||
"empty_value": "(empty)",
|
||||
"form": {
|
||||
"custom_fields": "Custom Fields",
|
||||
"default_item_values": "Default Item Values",
|
||||
@@ -264,12 +268,16 @@
|
||||
"template_description": "Template Description",
|
||||
"template_name": "Template Name"
|
||||
},
|
||||
"hide_defaults": "Hide defaults",
|
||||
"save_as_template": "Save as Template",
|
||||
"selector": {
|
||||
"label": "Template (Optional)",
|
||||
"not_found": "No template found",
|
||||
"search": "Search templates...",
|
||||
"select": "Select template..."
|
||||
"select": "Select template...",
|
||||
"clear": "Clear Template Selection"
|
||||
},
|
||||
"show_defaults": "Show defaults",
|
||||
"toast": {
|
||||
"applied": "Template \"{name}\" applied",
|
||||
"create_failed": "Failed to create template",
|
||||
@@ -283,12 +291,7 @@
|
||||
"update_failed": "Failed to update template",
|
||||
"updated": "Template updated"
|
||||
},
|
||||
"apply_template": "Apply a template",
|
||||
"using_template": "Using template: {name}",
|
||||
"show_defaults": "Show defaults",
|
||||
"hide_defaults": "Hide defaults",
|
||||
"empty_value": "(empty)",
|
||||
"save_as_template": "Save as Template"
|
||||
"using_template": "Using template: {name}"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
@@ -326,7 +329,9 @@
|
||||
"maintenance": "Maintenance",
|
||||
"name": "Name",
|
||||
"navigate": "Navigate",
|
||||
"no": "No",
|
||||
"password": "Password",
|
||||
"preview": "Preview",
|
||||
"quantity": "Quantity",
|
||||
"read_docs": "Read the Docs",
|
||||
"return_home": "Return Home",
|
||||
@@ -340,9 +345,7 @@
|
||||
"value": "Value",
|
||||
"version": "Version: { version }",
|
||||
"welcome": "Welcome, { username }",
|
||||
"preview": "Preview",
|
||||
"yes": "Yes",
|
||||
"no": "No"
|
||||
"yes": "Yes"
|
||||
},
|
||||
"home": {
|
||||
"labels": "Labels",
|
||||
@@ -397,20 +400,20 @@
|
||||
"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",
|
||||
"duplicate": {
|
||||
"copy_attachments": "Copy Attachments",
|
||||
"copy_custom_fields": "Copy Custom Fields",
|
||||
"copy_maintenance": "Copy Maintenance",
|
||||
"custom_prefix": "Copy Prefix",
|
||||
"enable_custom_prefix": "Enable Custom Prefix",
|
||||
"override_instructions": "Hold shift when clicking the duplicate button to override these settings.",
|
||||
"prefix": "Copy of ",
|
||||
"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"
|
||||
},
|
||||
"edit": {
|
||||
"edit_attachment_dialog": {
|
||||
"attachment_title": "Attachment Title",
|
||||
@@ -518,42 +521,45 @@
|
||||
"update_label": "Update Label"
|
||||
},
|
||||
"languages": {
|
||||
"bs-BA": "Bosnian (Bosnia and Herzegovina)",
|
||||
"ca": "Catalan",
|
||||
"cs-CZ": "Czech",
|
||||
"da-DK": "Danish",
|
||||
"de": "German",
|
||||
"ar-AA": "Arabic (العربية)",
|
||||
"bs-BA": "Bosnian (bosanski)",
|
||||
"ca": "Catalan (català)",
|
||||
"cs-CZ": "Czech (čeština)",
|
||||
"da-DK": "Danish (dansk)",
|
||||
"de": "German (Deutsch)",
|
||||
"el-GR": "Greek (Ελληνικά)",
|
||||
"en": "English",
|
||||
"es": "Spanish",
|
||||
"fi-FI": "Finnish",
|
||||
"fr": "French",
|
||||
"hu": "Hungarian",
|
||||
"id-ID": "Indonesian",
|
||||
"it": "Italian",
|
||||
"ja-JP": "Japanese",
|
||||
"ko-KR": "Korean",
|
||||
"lb-LU": "Luxembourgish (Luxembourg)",
|
||||
"lt-LT": "Lithuanian (Lithuania)",
|
||||
"nb-NO": "Norwegian Bokmål",
|
||||
"nl": "Dutch",
|
||||
"pl": "Polish",
|
||||
"pt-BR": "Portuguese (Brazil)",
|
||||
"pt-PT": "Portuguese (Portugal)",
|
||||
"ro-RO": "Romanian",
|
||||
"ru": "Russian",
|
||||
"sk-SK": "Slovak",
|
||||
"sl": "Slovenian",
|
||||
"sq-AL": "Albanian",
|
||||
"sv": "Swedish",
|
||||
"ta-IN": "Tamil",
|
||||
"th-TH": "Thai",
|
||||
"tr": "Turkish",
|
||||
"uk-UA": "Ukrainian",
|
||||
"vi-VN": "Vietnamese",
|
||||
"zh-CN": "Chinese (Simplified)",
|
||||
"zh-HK": "Chinese (Hong Kong)",
|
||||
"zh-MO": "Chinese (Macau)",
|
||||
"zh-TW": "Chinese (Traditional)"
|
||||
"es": "Spanish (español)",
|
||||
"fi-FI": "Finnish (suomi)",
|
||||
"fr": "French (français)",
|
||||
"hu": "Hungarian (magyar)",
|
||||
"id-ID": "Indonesian (Indonesia)",
|
||||
"it": "Italian (italiano)",
|
||||
"ja-JP": "Japanese (日本語)",
|
||||
"ko-KR": "Korean (한국어)",
|
||||
"lb-LU": "Luxembourgish (Lëtzebuergesch)",
|
||||
"lt-LT": "Lithuanian (lietuvių)",
|
||||
"nb-NO": "Norwegian Bokmål (norsk bokmål)",
|
||||
"nl": "Dutch (Nederlands)",
|
||||
"pl": "Polish (polski)",
|
||||
"pt-BR": "Portuguese - Brazil (português)",
|
||||
"pt-PT": "Portuguese - Portugal (português)",
|
||||
"ro-RO": "Romanian (română)",
|
||||
"ru": "Russian (русский)",
|
||||
"sk-SK": "Slovak (slovenčina)",
|
||||
"sl": "Slovenian (slovenščina)",
|
||||
"sq-AL": "Albanian (shqip)",
|
||||
"sv": "Swedish (svenska)",
|
||||
"ta-IN": "Tamil (தமிழ்)",
|
||||
"te-IN": "Telugu (తెలుగు)",
|
||||
"th-TH": "Thai (ไทย)",
|
||||
"tr": "Turkish (Türkçe)",
|
||||
"uk-UA": "Ukrainian (українська)",
|
||||
"vi-VN": "Vietnamese (Tiếng Việt)",
|
||||
"zh-CN": "Chinese - Simplified (中文)",
|
||||
"zh-HK": "Chinese - Hong Kong (中文)",
|
||||
"zh-MO": "Chinese - Macau (中文)",
|
||||
"zh-TW": "Chinese - Traditional (中文)"
|
||||
},
|
||||
"locations": {
|
||||
"child_locations": "Child Locations",
|
||||
@@ -620,8 +626,8 @@
|
||||
},
|
||||
"pages": {
|
||||
"templates": {
|
||||
"title": "Templates",
|
||||
"no_templates": "No templates yet."
|
||||
"no_templates": "No templates yet.",
|
||||
"title": "Templates"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
@@ -634,7 +640,6 @@
|
||||
"delete_account_sub": "Delete your account and all its associated data. This can not be undone.",
|
||||
"delete_notifier_confirm": "Are you sure you want to delete this notifier?",
|
||||
"display_legacy_header": "{ currentValue, select, true {Disable Legacy Header} false {Enable Legacy Header} other {Not Hit}}",
|
||||
"legacy_image_fit": "{ currentValue, select, true {Disable Legacy Fit: Fit Image with Bars} false {Enable Legacy Fit: Fill Image with Crop} other {Not Hit}}",
|
||||
"enabled": "Enabled",
|
||||
"example": "Example",
|
||||
"gen_invite": "Generate Invite Link",
|
||||
@@ -642,6 +647,7 @@
|
||||
"group_settings_sub": "Shared Group Settings. You may need to refresh your browser for some settings to apply.",
|
||||
"inactive": "Inactive",
|
||||
"language": "Language",
|
||||
"legacy_image_fit": "{ currentValue, select, true {Disable Legacy Fit: Fit Image with Bars} false {Enable Legacy Fit: Fill Image with Crop} other {Not Hit}}",
|
||||
"new_password": "New Password",
|
||||
"no_notifiers": "No notifiers configured",
|
||||
"no_override": "No override",
|
||||
@@ -732,12 +738,23 @@
|
||||
"set_primary_photo_button": "Set Primary Photo",
|
||||
"set_primary_photo_confirm": "Are you sure you want to set primary photos? This can take a while and cannot be undone.",
|
||||
"set_primary_photo_sub": "In version v0.10.0 of Homebox, the primary image field was added to attachments of type photo. This action will set the primary image field to the first image in the attachments array in the database, if it is not already set. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/pull/576\">'See GitHub PR #576'</a>'",
|
||||
"wipe_inventory": "Wipe Inventory",
|
||||
"wipe_inventory_button": "Wipe Inventory",
|
||||
"wipe_inventory_confirm": "Are you sure you want to wipe your entire inventory? This will delete all items and cannot be undone.",
|
||||
"wipe_inventory_labels": "Also wipe all labels (tags)",
|
||||
"wipe_inventory_locations": "Also wipe all locations",
|
||||
"wipe_inventory_maintenance": "Also wipe all maintenance records",
|
||||
"wipe_inventory_note": "Note: Only group owners can perform this action.",
|
||||
"wipe_inventory_sub": "Permanently deletes all items in your inventory. This action is irreversible and will remove all item data including attachments and photos.",
|
||||
"zero_datetimes": "Zero Item Date Times",
|
||||
"zero_datetimes_button": "Zero Item Date Times",
|
||||
"zero_datetimes_confirm": "Are you sure you want to reset all date and time values? This can take a while and cannot be undone.",
|
||||
"zero_datetimes_sub": "Resets the time value for all date time fields in your inventory to the beginning of the date. This is to fix a bug that was introduced early on in the development of the site that caused the time value to be stored with the time which caused issues with date fields displaying accurate values. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/issues/236\" target=\"_blank\">'See Github Issue #236 for more details.'</a>'"
|
||||
},
|
||||
"actions_sub": "Apply Actions to your inventory in bulk. These are irreversible actions. '<b>'Be careful.'</b>'",
|
||||
"demo_mode_error": {
|
||||
"wipe_inventory": "Inventory, labels, locations and maintenance records cannot be wiped whilst Homebox is in demo mode. Please ensure that you are not in demo mode and try again."
|
||||
},
|
||||
"import_export": "Import/Export",
|
||||
"import_export_set": {
|
||||
"export": "Export Inventory",
|
||||
@@ -765,7 +782,9 @@
|
||||
"failed_ensure_ids": "Failed to ensure asset IDs.",
|
||||
"failed_ensure_import_refs": "Failed to ensure import refs.",
|
||||
"failed_set_primary_photos": "Failed to set primary photos.",
|
||||
"failed_zero_datetimes": "Failed to reset date and time values."
|
||||
"failed_wipe_inventory": "Failed to wipe inventory.",
|
||||
"failed_zero_datetimes": "Failed to reset date and time values.",
|
||||
"wipe_inventory_success": "Successfully wiped inventory. { results } items deleted."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
"open_new_tab": "Abrir en nueva pestaña"
|
||||
},
|
||||
"create_modal": {
|
||||
"clear_template": "Vaciar Plantilla",
|
||||
"delete_photo": "Eliminar foto",
|
||||
"item_description": "Descripción del artículo",
|
||||
"item_name": "Nombre del artículo",
|
||||
@@ -127,26 +128,62 @@
|
||||
"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.",
|
||||
"error_not_found": "No se ha encontrado ningún producto con el código de barras proporcionado.",
|
||||
"search_item": "Buscar producto",
|
||||
"title": "Importar producto"
|
||||
},
|
||||
"selector": {
|
||||
"no_results": "Resultados No Encontrados",
|
||||
"placeholder": "Seleccionar…",
|
||||
"search_placeholder": "Escribe para buscar…"
|
||||
"search_placeholder": "Escribe para buscar…",
|
||||
"searching": "Buscando…"
|
||||
},
|
||||
"view": {
|
||||
"change_details": {
|
||||
"add_labels": "Añadir etiquetas",
|
||||
"failed_to_update_item": "No se pudo actualizar el artículo",
|
||||
"remove_labels": "Eliminar etiquetas",
|
||||
"title": "Cambiar detalles del artículo"
|
||||
},
|
||||
"selectable": {
|
||||
"card": "Tarjeta",
|
||||
"items": "Elementos",
|
||||
"no_items": "No hay elementos para mostrar",
|
||||
"select_all": "Seleccionar todo",
|
||||
"select_card": "Seleccionar tarjeta",
|
||||
"select_row": "Seleccionar fila",
|
||||
"table": "Tabla"
|
||||
},
|
||||
"table": {
|
||||
"dropdown": {
|
||||
"actions": "Acciones",
|
||||
"change_labels": "Cambiar etiquetas",
|
||||
"change_labels_success": "Etiquetas cambiadas",
|
||||
"change_location": "Cambiar ubicación",
|
||||
"change_location_success": "Ubicación cambiada",
|
||||
"create_maintenance_item": "Crear entrada de mantenimiento para el elemento",
|
||||
"create_maintenance_selected": "Crear entrada de mantenimiento para los elementos seleccionados",
|
||||
"create_maintenance_success": "Entradas de mantenimiento creadas",
|
||||
"delete_confirmation": "¿Está seguro de que desea eliminar los elementos seleccionados? Esta acción no se puede deshacer.",
|
||||
"delete_item": "Borrar elemento",
|
||||
"delete_selected": "Borrar los elementos seleccionados",
|
||||
"download_csv": "Descargar tabla como CSV",
|
||||
"download_json": "Descargar tabla como JSON",
|
||||
"duplicate_item": "Duplicar Elemento",
|
||||
"duplicate_selected": "Duplicar Elementos Seleccionados",
|
||||
"error_deleting": "Error al borrar el elemento",
|
||||
"error_duplicating": "Error al duplicar el elemento",
|
||||
"open_menu": "Abrir menú",
|
||||
"open_multi_tab_warning": "Por razones de seguridad, los navegadores no permiten abrir varias pestañas a la vez de forma predeterminada. Para cambiar esto, siga la documentación:",
|
||||
"toggle_expand": "Alternar Expandir",
|
||||
"view_item": "Ver elemento",
|
||||
"view_items": "Ver elementos"
|
||||
},
|
||||
"headers": "Encabezados",
|
||||
"page": "Página",
|
||||
"quick_actions": "Habilitar \"Acciones Rápidas y Selección\"",
|
||||
"rows_per_page": "Filas por página",
|
||||
"selected_rows": "{selected} de {total} filas seleccionadas.",
|
||||
"table_settings": "Configuración de Tabla",
|
||||
"view_item": "Ver Elemento"
|
||||
}
|
||||
@@ -193,6 +230,65 @@
|
||||
"quick_menu": {
|
||||
"no_results": "Sin resultados.",
|
||||
"shortcut_hint": "Usa las teclas numéricas para seleccionar rápidamente una acción."
|
||||
},
|
||||
"template": {
|
||||
"apply_template": "Aplicar una plantilla",
|
||||
"card": {
|
||||
"delete": "Eliminar Plantilla",
|
||||
"duplicate": "Duplicar plantilla",
|
||||
"edit": "Editar plantilla"
|
||||
},
|
||||
"confirm_delete": "¿Eliminar esta plantilla?",
|
||||
"create_modal": {
|
||||
"title": "Crear Plantilla"
|
||||
},
|
||||
"detail": {
|
||||
"default_values": "Valores Predeterminados",
|
||||
"updated": "Actualizado"
|
||||
},
|
||||
"edit_modal": {
|
||||
"title": "Editar Plantilla"
|
||||
},
|
||||
"empty_value": "(vacío)",
|
||||
"form": {
|
||||
"custom_fields": "Campos Personalizados",
|
||||
"default_item_values": "Valores Predeterminados de Elementos",
|
||||
"default_location": "Ubicación Predeterminada",
|
||||
"default_value": "Valor Predeterminado",
|
||||
"field_name": "Nombre del Campo",
|
||||
"item_description": "Descripción del Elemento",
|
||||
"item_name": "Nombre del Elemento",
|
||||
"lifetime_warranty": "Garantía de por vida",
|
||||
"location": "Ubicación",
|
||||
"manufacturer": "Fabricante",
|
||||
"model_number": "Número de Modelo",
|
||||
"no_custom_fields": "Sin campos personalizados.",
|
||||
"template_description": "Descripción de la Plantilla",
|
||||
"template_name": "Nombre de la Plantilla"
|
||||
},
|
||||
"hide_defaults": "Ocultar valores predeterminados",
|
||||
"save_as_template": "Guardar como Plantilla",
|
||||
"selector": {
|
||||
"label": "Plantilla (Opcional)",
|
||||
"not_found": "No se ha encontrado ninguna plantilla",
|
||||
"search": "Buscar plantillas…",
|
||||
"select": "Seleccionar plantilla…"
|
||||
},
|
||||
"show_defaults": "Mostrar valores predeterminados",
|
||||
"toast": {
|
||||
"applied": "Plantilla \"{name}\" aplicada",
|
||||
"create_failed": "No se pudo crear la plantilla",
|
||||
"created": "Plantilla creada",
|
||||
"delete_failed": "No se ha podido eliminar la plantilla",
|
||||
"deleted": "Plantilla eliminada",
|
||||
"duplicate_failed": "No se pudo duplicar la plantilla",
|
||||
"duplicated": "Plantilla duplicada como \"{name}\"",
|
||||
"load_failed": "No se han podido cargar los detalles de la plantilla",
|
||||
"saved_as_template": "Elemento guardado como plantilla \"{name}\"",
|
||||
"update_failed": "No se ha podido actualizar la plantilla",
|
||||
"updated": "Plantilla actualizada"
|
||||
},
|
||||
"using_template": "Usando plantilla: {name}"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
@@ -230,7 +326,9 @@
|
||||
"maintenance": "Mantenimiento",
|
||||
"name": "Nombre",
|
||||
"navigate": "Navegar",
|
||||
"no": "No",
|
||||
"password": "Contraseña",
|
||||
"preview": "Vista previa",
|
||||
"quantity": "Cantidad",
|
||||
"read_docs": "Lee la Documentación",
|
||||
"return_home": "Regresar a Inicio",
|
||||
@@ -243,7 +341,8 @@
|
||||
"updating": "Actualizando",
|
||||
"value": "Valor",
|
||||
"version": "Versión: { version }",
|
||||
"welcome": "Bienvenido/a, { username }"
|
||||
"welcome": "Bienvenido/a, { username }",
|
||||
"yes": "Sí"
|
||||
},
|
||||
"home": {
|
||||
"labels": "Etiquetas",
|
||||
@@ -260,6 +359,7 @@
|
||||
"dont_join_group": "¿No quieres unirte a un grupo?",
|
||||
"joining_group": "¡Te estás uniendo a un grupo existente!",
|
||||
"login": "Iniciar sesión",
|
||||
"or": "o",
|
||||
"register": "Registrarse",
|
||||
"remember_me": "Recuérdame",
|
||||
"set_email": "¿Cuál es tu email?",
|
||||
@@ -271,6 +371,14 @@
|
||||
"invalid_email": "Dirección de correo electrónico no válida",
|
||||
"invalid_email_password": "E-mail y/o contraseña no válido",
|
||||
"login_success": "Sesión iniciada correctamente",
|
||||
"oidc_access_denied": "Acceso denegado: Tu cuenta no tiene el rol/pertenencia al grupo requerido",
|
||||
"oidc_auth_failed": "Autenticación OIDC fallida",
|
||||
"oidc_invalid_response": "Se recibió una respuesta OIDC no válida",
|
||||
"oidc_provider_error": "El proveedor OIDC devolvió un error",
|
||||
"oidc_security_error": "Error de seguridad OIDC - posible ataque CSRF",
|
||||
"oidc_session_expired": "La sesión OIDC ha caducado",
|
||||
"oidc_token_expired": "El token OIDC ha caducado",
|
||||
"oidc_token_invalid": "La firma del token OIDC no es válida",
|
||||
"problem_registering": "Problema al registrar al usuario",
|
||||
"user_registered": "Usuario registrado"
|
||||
}
|
||||
@@ -291,6 +399,18 @@
|
||||
"description": "Descripción",
|
||||
"details": "Detalles",
|
||||
"drag_and_drop": "Arrastra y suelta los archivos aquí o selecciona los archivos",
|
||||
"duplicate": {
|
||||
"copy_attachments": "Copiar Adjuntos",
|
||||
"copy_custom_fields": "Copiar Campos Personalizados",
|
||||
"copy_maintenance": "Copiar Mantenimiento",
|
||||
"custom_prefix": "Copiar Prefijo",
|
||||
"enable_custom_prefix": "Habilitar Prefijo Personalizado",
|
||||
"override_instructions": "Mantén pulsada la tecla Mayús mientras haces clic en el botón Duplicar para anular estos ajustes.",
|
||||
"prefix": "Copia de ",
|
||||
"prefix_instructions": "Este prefijo se añadirá al principio del nombre del elemento duplicado. Incluye un espacio al final del prefijo para añadir un espacio entre el prefijo y el nombre del elemento.",
|
||||
"temporary_title": "Configuración temporal",
|
||||
"title": "Configuración Duplicados"
|
||||
},
|
||||
"edit": {
|
||||
"edit_attachment_dialog": {
|
||||
"attachment_title": "Título del Adjunto",
|
||||
@@ -299,7 +419,8 @@
|
||||
"primary_photo_sub": "Esta opción sólo está disponible para las fotos. Sólo puede haber una foto principal. Si seleccionas esta opción, la foto principal actual, si existe, se deseleccionará.",
|
||||
"select_type": "Seleccionar un tipo",
|
||||
"title": "Editar Adjunto"
|
||||
}
|
||||
},
|
||||
"view_image": "Ver imagen"
|
||||
},
|
||||
"edit_details": "Editar detalles",
|
||||
"field_selector": "Selector de Campo",
|
||||
@@ -397,6 +518,7 @@
|
||||
"update_label": "Actualizar Etiqueta"
|
||||
},
|
||||
"languages": {
|
||||
"bs-BA": "Bosnio (Bosnia y Herzegovina)",
|
||||
"ca": "Catalán",
|
||||
"cs-CZ": "Checo",
|
||||
"de": "Alemán",
|
||||
@@ -423,6 +545,7 @@
|
||||
"th-TH": "Tailandés",
|
||||
"tr": "Turco",
|
||||
"uk-UA": "Ucraniano",
|
||||
"vi-VN": "Vietnamita",
|
||||
"zh-CN": "Chino (Simplificado)",
|
||||
"zh-HK": "Chino (Hong Kong)",
|
||||
"zh-MO": "Chino (Macao)",
|
||||
@@ -492,8 +615,15 @@
|
||||
"profile": "Perfil",
|
||||
"scanner": "Escáner",
|
||||
"search": "Buscar",
|
||||
"templates": "Plantillas",
|
||||
"tools": "Herramientas"
|
||||
},
|
||||
"pages": {
|
||||
"templates": {
|
||||
"no_templates": "Aún no hay plantillas.",
|
||||
"title": "Plantillas"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"active": "Activo",
|
||||
"change_password": "Cambiar Contraseña",
|
||||
@@ -511,6 +641,7 @@
|
||||
"group_settings_sub": "Configuración de Grupo Compartido. Es posible que tengas que actualizar tu navegador para que se apliquen algunos ajustes.",
|
||||
"inactive": "Inactivo",
|
||||
"language": "Idioma",
|
||||
"legacy_image_fit": "{ currentValue, select, true {Disable Legacy Fit: Fit Image with Bars} false {Enable Legacy Fit: Fill Image with Crop} other {Not Hit}}",
|
||||
"new_password": "Nueva Contraseña",
|
||||
"no_notifiers": "No hay notificadores configurados",
|
||||
"no_override": "No reemplazar",
|
||||
@@ -550,7 +681,7 @@
|
||||
"generate_page": "Generar Página",
|
||||
"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_2": "Como tal, estas etiquetas funcionan imprimiendo un código QR de URL e información ID de Activo en una etiqueta. Si has desactivado\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>'",
|
||||
"label_height": "Altura de la Etiqueta",
|
||||
"label_width": "Ancho de la Etiqueta",
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
"select_location": "Valitse sijainti"
|
||||
},
|
||||
"tree": {
|
||||
"no_locations": "Sijainteja ei ole saatavilla. Lisää uusia sijainteja\n '<'span class=\" link-primary \"`>`Luo`<`/span`> ' - painike navigointipalkissa."
|
||||
"no_locations": "Sijainteja ei ole saatavilla. Lisää uusia sijainteja\n '<span class=\"link-primary\">'Luo'</span>' - painike navigointipalkissa."
|
||||
}
|
||||
},
|
||||
"quick_menu": {
|
||||
|
||||
@@ -232,6 +232,7 @@
|
||||
"name": "Nom",
|
||||
"navigate": "Naviguer",
|
||||
"password": "Mot de passe",
|
||||
"preview": "Aperçu",
|
||||
"quantity": "Quantité",
|
||||
"read_docs": "Lire la documentation",
|
||||
"return_home": "Retour à l'accueil",
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"create_modal": {
|
||||
"createAndAddAnother": "Gunakan {shiftKey} + {enterKey} untuk membuat dan menambahkan data.",
|
||||
"enter": "Enter",
|
||||
"shift": "Shift"
|
||||
},
|
||||
"import_dialog": {
|
||||
"change_warning": "Ada perubahan pada logika impor untuk data dengan import_ref yang sudah ada. Jika sebuah import_ref ditemukan di file CSV,\ndata tersebut akan diperbarui menggunakan nilai-nilai yang ada di file CSV.",
|
||||
"description": "Impor file CSV yang berisi item, label, dan lokasi Anda. Lihat dokumentasi untuk informasi lebih lanjut mengenai\nformat yang diperlukan.",
|
||||
"title": "Impor CSV"
|
||||
"title": "Impor CSV",
|
||||
"toast": {
|
||||
"import_failed": "Impor gagal. Silahkan coba kembali beberapa saat lagi.",
|
||||
"import_success": "Import berhasil!",
|
||||
"please_select_file": "Silakan pilih file untuk diimpor."
|
||||
}
|
||||
},
|
||||
"outdated": {
|
||||
"current_version": "Versi Terkini",
|
||||
@@ -14,6 +24,18 @@
|
||||
"new_version_available_link": "Klik di sini untuk melihat informasi rilis"
|
||||
}
|
||||
},
|
||||
"color_selector": {
|
||||
"clear": "Reset warna",
|
||||
"color": "Warna",
|
||||
"no_color": "Tanpa warna",
|
||||
"no_color_selected": "Tidak ada warna yang dipilih",
|
||||
"randomize": "Acak warna"
|
||||
},
|
||||
"form": {
|
||||
"password": {
|
||||
"toggle_show": "Tampilkan kata sandi"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"copy_text": {
|
||||
"documentation": "dokumentasi",
|
||||
@@ -46,20 +68,51 @@
|
||||
"yesterday": "kemaren"
|
||||
},
|
||||
"label_maker": {
|
||||
"download": "Unduh Label"
|
||||
"browser_print": "Cetak dari Browser",
|
||||
"confirm_description": "Anda yakin ingin mencetak label ini?",
|
||||
"download": "Unduh Label",
|
||||
"print": "Cetak Label",
|
||||
"server_print": "Cetak di Server",
|
||||
"titles": "Label",
|
||||
"toast": {
|
||||
"load_status_failed": "Gagal memuat status",
|
||||
"print_failed": "Gagal mencetak label",
|
||||
"print_success": "Label telah dicetak"
|
||||
}
|
||||
},
|
||||
"page_qr_code": {
|
||||
"page_url": "Halaman URL"
|
||||
"page_url": "Halaman URL",
|
||||
"qr_tooltip": "Tampilkan kode QR"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "Kompleksitas kata sandi"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"attachments_list": {
|
||||
"download": "Unduh",
|
||||
"open_new_tab": "Buka di tab baru"
|
||||
},
|
||||
"create_modal": {
|
||||
"delete_photo": "Hapus foto",
|
||||
"item_description": "Deskripsi item",
|
||||
"item_name": "Nama item",
|
||||
"title": "Buat item"
|
||||
"item_photo": "Foto Item 📷",
|
||||
"item_quantity": "Jumlah Item",
|
||||
"parent_item": "Item Induk",
|
||||
"product_tooltip_input_barcode": "Isi otomatis dengan barcode yang disediakan secara manual",
|
||||
"product_tooltip_scan_barcode": "Isi otomatis dengan barcode dari 📷",
|
||||
"rotate_photo": "Putar foto",
|
||||
"set_as_primary_photo": "Atur sebagai { isPrimary, select, true {non} false {} other {}} foto utama",
|
||||
"title": "Buat item",
|
||||
"toast": {
|
||||
"already_creating": "Sudah membuat item",
|
||||
"create_failed": "Tidak dapat membuat item",
|
||||
"create_success": "Item dibuat",
|
||||
"failed_load_parent": "Gagal memuat item induk - silakan pilih secara manual",
|
||||
"no_canvas_support": "Browser Anda tidak mendukung operasi kanvas",
|
||||
"please_select_location": "Silakan pilih Lokasi."
|
||||
}
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
@@ -69,6 +122,9 @@
|
||||
"table": "Tabel"
|
||||
},
|
||||
"table": {
|
||||
"dropdown": {
|
||||
"change_labels_success": "Label Diubah"
|
||||
},
|
||||
"page": "Halaman",
|
||||
"rows_per_page": "Baris per halaman"
|
||||
}
|
||||
@@ -76,59 +132,96 @@
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"label_description": "Keterangan/Deskripsi",
|
||||
"label_name": "Nama",
|
||||
"title": "Buat label"
|
||||
"label_color": "Warna label",
|
||||
"label_description": "Deskripsi label",
|
||||
"label_name": "Nama Label",
|
||||
"title": "Buat label",
|
||||
"toast": {
|
||||
"already_creating": "Sudah membuat label",
|
||||
"create_failed": "Tidak dapat membuat label",
|
||||
"create_success": "Label telah dibuat",
|
||||
"label_name_too_long": "Nama label tidak boleh lebih dari 50 karakter"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"select_labels": "Pilih Label"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"location_description": "Deskripsi lokasi",
|
||||
"location_name": "Nama lokasi",
|
||||
"title": "Tambah Lokasi"
|
||||
"title": "Tambah Lokasi",
|
||||
"toast": {
|
||||
"already_creating": "Lokasi telah dibuat",
|
||||
"create_failed": "Tidak dapat membuat lokasi",
|
||||
"create_success": "Lokasi telah dibuat"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"parent_location": "Lokasi Induk"
|
||||
"no_location_found": "Lokasi tidak ditemukan",
|
||||
"parent_location": "Lokasi Induk",
|
||||
"search_location": "Mencari Lokasi",
|
||||
"select_location": "Pilih lokasi"
|
||||
},
|
||||
"tree": {
|
||||
"no_locations": "Tidak ada lokasi yang tersedia. Tambahkan lokasi melalui tombol\n`<`span class=\"link-primary\"`>`Buat`<`/span`>` menu navigasi."
|
||||
"no_locations": "Tidak ada lokasi yang tersedia. Tambahkan lokasi melalui tombol\n'<span class=\"link-primary\">'Buat'</span>' menu navigasi."
|
||||
}
|
||||
},
|
||||
"quick_menu": {
|
||||
"no_results": "Tidak ada hasil yang ditemukan.",
|
||||
"shortcut_hint": "Gunakan tombol angka untuk memilih."
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"api_failure": "Gagal melakukan panggilan pada API: "
|
||||
},
|
||||
"global": {
|
||||
"add": "Tambah",
|
||||
"build": "Kompilasi: { build }",
|
||||
"archived": "Diarsipkan",
|
||||
"build": "Build: { build }",
|
||||
"cancel": "Batal",
|
||||
"confirm": "Konfirmasi",
|
||||
"create": "Buat",
|
||||
"create_and_add": "Buat dan Tambah Baru",
|
||||
"create_subitem": "Buat Subitem",
|
||||
"created": "Behasil dibuat",
|
||||
"delete": "Hapus",
|
||||
"delete_confirm": "Anda yakin akan menghapus butir ini? ",
|
||||
"demo_instance": "Ini adalah aplikasi demo",
|
||||
"details": "Detail",
|
||||
"duplicate": "Duplikat",
|
||||
"edit": "Sunting",
|
||||
"email": "Email",
|
||||
"follow_dev": "Ikuti Pengembang",
|
||||
"github": "Github project",
|
||||
"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\"'>' Versi: { version } Build: { build } '</a>'"
|
||||
},
|
||||
"github": "Proyek GitHub",
|
||||
"insured": "Diasuransikan",
|
||||
"items": "Barang",
|
||||
"join_discord": "Bergabunglah dengan Discord",
|
||||
"labels": "Label",
|
||||
"loading": "Memuat…",
|
||||
"locations": "Lokasi",
|
||||
"maintenance": "Perbaikan",
|
||||
"name": "Nama",
|
||||
"navigate": "Navigasi",
|
||||
"password": "Kata Sandi",
|
||||
"quantity": "Jumlah",
|
||||
"read_docs": "Baca Dokumen",
|
||||
"return_home": "Kembali ke halaman depan",
|
||||
"save": "Simpan",
|
||||
"search": "Cari",
|
||||
"sign_out": "Keluar",
|
||||
"submit": "Kirim",
|
||||
"unknown": "Tidak Diketahui",
|
||||
"update": "Perbaharui",
|
||||
"updating": "Memperbaharui",
|
||||
"value": "Nilai",
|
||||
"version": "Versi:{ version }",
|
||||
"welcome": "Selamay datang, { username }"
|
||||
"welcome": "Selamat datang, { username }"
|
||||
},
|
||||
"home": {
|
||||
"labels": "Label",
|
||||
@@ -149,22 +242,45 @@
|
||||
"remember_me": "Ingat Saya",
|
||||
"set_email": "Apa email Anda?",
|
||||
"set_name": "Apa nama anda?",
|
||||
"set_password": "Password Anda",
|
||||
"tagline": "Lacak, Atur, dan Kelola Barang-barangmu."
|
||||
"set_password": "Tentukan kata sandi",
|
||||
"tagline": "Lacak, Atur, dan Kelola Barang-barangmu.",
|
||||
"title": "Atur dan Tandai Barang Anda",
|
||||
"toast": {
|
||||
"invalid_email": "Alamat email tidak valid",
|
||||
"invalid_email_password": "Email atau kata sandi salah",
|
||||
"login_success": "Anda berhasil login",
|
||||
"problem_registering": "Masalah saat mendaftarkan pengguna",
|
||||
"user_registered": "Terdaftar"
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"add": "Tambah",
|
||||
"advanced": "Tingkat Lanjut",
|
||||
"archived": "Diarsipkan",
|
||||
"asset_id": "ID Aset",
|
||||
"associated_with_multiple": "Id Aset ini terhubung dengan beberapa item",
|
||||
"attachment": "Lampiran",
|
||||
"attachments": "Lampiran",
|
||||
"changes_persisted_immediately": "Perubahan lampiran akan segera disimpan",
|
||||
"created_at": "Dibuat Pada",
|
||||
"custom_fields": "Informasi Tambahan",
|
||||
"delete_attachment_confirm": "Apa Anda yakin ingin menghapus ini?",
|
||||
"delete_item_confirm": "Anda yakin akan menghapus butir ini?",
|
||||
"description": "Deskripsi",
|
||||
"details": "Detail",
|
||||
"drag_and_drop": "Seret dan lepas file di sini atau klik untuk memilih file",
|
||||
"duplicate": {
|
||||
"copy_attachments": "Salin Lampiran",
|
||||
"copy_custom_fields": "Salin info tambahan",
|
||||
"copy_maintenance": "Salin Pemeliharaan",
|
||||
"custom_prefix": "Salin awalan",
|
||||
"enable_custom_prefix": "Aktifkan Prefiks Kustom",
|
||||
"override_instructions": "Tahan shift saat mengklik tombol duplikat untuk menimpa pengaturan ini.",
|
||||
"prefix": "Salinan dari ",
|
||||
"prefix_instructions": "Awalan ini akan ditambahkan pada awal penamaan item yang diduplikat. Termasuk spasi pada akhir awalan untuk menambahn pemisah antara awalan dan nama item.",
|
||||
"temporary_title": "Pengaturan Sementara",
|
||||
"title": "Duplikasi Pengaturan"
|
||||
},
|
||||
"edit_details": "Edit Detail",
|
||||
"field_selector": "Selektor",
|
||||
"field_value": "Nilai",
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
},
|
||||
"product_import": {
|
||||
"barcode": "Codice a barre del prodotto",
|
||||
"db_source": "DB sorgente",
|
||||
"error_exception": "Si è verificato un errore durante il recupero del codice a barre dell'articolo: ",
|
||||
"error_invalid_barcode": "Il codice a barre fornito non è valido",
|
||||
"error_not_found": "Nessun prodotto trovato con il codice a barre fornito.",
|
||||
@@ -133,19 +134,55 @@
|
||||
"selector": {
|
||||
"no_results": "Nessun risultato trovato",
|
||||
"placeholder": "Seleziona…",
|
||||
"search_placeholder": "Scrivi per cercare…"
|
||||
"search_placeholder": "Scrivi per cercare…",
|
||||
"searching": "Ricerca in corso…"
|
||||
},
|
||||
"view": {
|
||||
"change_details": {
|
||||
"add_labels": "Aggiungi Etichette",
|
||||
"failed_to_update_item": "Aggiornamento oggetto fallito",
|
||||
"remove_labels": "Rimuovi Etichette",
|
||||
"title": "Modifica Dettaglio Oggetto"
|
||||
},
|
||||
"selectable": {
|
||||
"card": "Scheda",
|
||||
"items": "Articoli",
|
||||
"no_items": "Nessun Articolo da Visualizzare",
|
||||
"select_all": "Seleziona tutto",
|
||||
"select_card": "Seleziona Card",
|
||||
"select_row": "Seleziona Riga",
|
||||
"table": "Tabella"
|
||||
},
|
||||
"table": {
|
||||
"dropdown": {
|
||||
"actions": "Azioni",
|
||||
"change_labels": "Cambia etichette",
|
||||
"change_labels_success": "Etichette cambiate",
|
||||
"change_location": "Cambia posizione",
|
||||
"change_location_success": "Posizione cambiata",
|
||||
"create_maintenance_item": "Inserisci Manutenzione per l'elemento",
|
||||
"create_maintenance_selected": "Inserisci in Manutenzione gli elementi selezionati",
|
||||
"create_maintenance_success": "Record di Manutenzione creato",
|
||||
"delete_confirmation": "Sei sicuro di voler eliminare gli elementi selezionati? Questa operazione è irreversibile.",
|
||||
"delete_item": "Elimina Oggetto",
|
||||
"delete_selected": "Elimina Oggetti Selezionati",
|
||||
"download_csv": "Scarica Tabella come CSV",
|
||||
"download_json": "Scarica Tabella come JSON",
|
||||
"duplicate_item": "Duplica Oggetto",
|
||||
"duplicate_selected": "Duplica Oggetti Selezionati",
|
||||
"error_deleting": "Errore durante l'eliminazione dell'Oggetto",
|
||||
"error_duplicating": "Errore durante la duplicazione dell'oggetto",
|
||||
"open_menu": "Apri menu",
|
||||
"open_multi_tab_warning": "Per ragioni di sicurezza, i browsers non permettono di aprire schede multiple in un colpo solo, per modificare questa configurazione utilizza la seguente documentazione",
|
||||
"toggle_expand": "Apri/Chiudi Espandi",
|
||||
"view_item": "Visualizza Oggetto",
|
||||
"view_items": "Visualizza Oggetti"
|
||||
},
|
||||
"headers": "Intestazioni",
|
||||
"page": "Pagina",
|
||||
"quick_actions": "Abilita Selezione & Azioni Rapide",
|
||||
"rows_per_page": "Righe per pagina",
|
||||
"selected_rows": "{selected} di {total} riga/righe selezionate.",
|
||||
"table_settings": "Impostazioni Tabella",
|
||||
"view_item": "Visualizzare articolo"
|
||||
}
|
||||
@@ -230,6 +267,7 @@
|
||||
"name": "Nome",
|
||||
"navigate": "Naviga",
|
||||
"password": "Password",
|
||||
"preview": "Anteprima",
|
||||
"quantity": "Quantità",
|
||||
"read_docs": "Leggi la Documentazione",
|
||||
"return_home": "Ritorna alla Home",
|
||||
@@ -293,10 +331,14 @@
|
||||
"duplicate": {
|
||||
"copy_attachments": "Copia allegati",
|
||||
"copy_custom_fields": "Copia Campi Personalizzati",
|
||||
"copy_maintenance": "Copia Manutenzione",
|
||||
"custom_prefix": "Copia Prefisso",
|
||||
"enable_custom_prefix": "Abilita prefisso personalizzato",
|
||||
"override_instructions": "Tieni premuto il tasto \"shift\" quando effettui un click sul pulsante \"duplica\" per sovrascrivere le impostazioni.",
|
||||
"prefix": "Copia di ",
|
||||
"temporary_title": "Impostazioni temporanee"
|
||||
"prefix_instructions": "Questo prefisso verrà aggiunto all'inizio del nome dell'elemento duplicato. Includi uno spazio alla fine del prefisso per aggiungere uno spazio tra il prefisso e il nome dell'elemento.",
|
||||
"temporary_title": "Impostazioni temporanee",
|
||||
"title": "Duplica Impostazioni"
|
||||
},
|
||||
"edit": {
|
||||
"edit_attachment_dialog": {
|
||||
@@ -306,7 +348,8 @@
|
||||
"primary_photo_sub": "Questa opzione è disponibile solo per le foto. Solo una foto può essere principale. Se selezioni questa opzione, l'attuale foto principale, se presente, sarà deselezionata.",
|
||||
"select_type": "Seleziona un tipo",
|
||||
"title": "Modifica allegato"
|
||||
}
|
||||
},
|
||||
"view_image": "Visualizza Immagine"
|
||||
},
|
||||
"edit_details": "Modifica dettagli",
|
||||
"field_selector": "Selezione in base ai campi",
|
||||
@@ -361,7 +404,30 @@
|
||||
"tips": "Suggerimenti",
|
||||
"tips_sub": "Suggerimenti per la Ricerca",
|
||||
"toast": {
|
||||
"quantity_cannot_negative": "La quantità non può essere negativa"
|
||||
"asset_not_found": "Asset non trovato",
|
||||
"attachment_deleted": "Allegato cancellato",
|
||||
"attachment_updated": "Allegato aggiornato",
|
||||
"attachment_uploaded": "Allegato caricato",
|
||||
"child_items_location_no_longer_synced": "Le posizioni degli elementi contenuti non saranno più sincronizzate con questo elemento.",
|
||||
"child_items_location_synced": "Le posizioni degli elementi contenuti sono sincronizzate con questo elemento.",
|
||||
"child_location_desync": "La modifica della posizione lo desincronizzerà dalla posizione dell'elemento che lo contiene.",
|
||||
"error_loading_parent_data": "Qualcosa e' andato storto durantre il caricamento dei dati dell'elemento padre",
|
||||
"failed_adjust_quantity": "Impossibile modificare la quantita'",
|
||||
"failed_delete_attachment": "Impossibile cancellare l'allegato",
|
||||
"failed_delete_item": "Impossibile cancellare l'elemento",
|
||||
"failed_duplicate_item": "Impossibile duplicare l'elemento",
|
||||
"failed_load_asset": "Impossibile caricare l'asset",
|
||||
"failed_load_item": "Impossibile caricare l'elemento",
|
||||
"failed_load_items": "Impossibile caricare gli elementi",
|
||||
"failed_save": "Impossibile salvare l'elemento",
|
||||
"failed_save_no_location": "Impossibile salvare l'elemento: non e' stata selezionata alcuna posizione",
|
||||
"failed_search_items": "Impossibile cercare gli elementi",
|
||||
"failed_update_attachment": "Impossibile aggiornare l'allegato",
|
||||
"failed_upload_attachment": "Impossibile caricare l'allegato",
|
||||
"item_deleted": "Elemento cancellato",
|
||||
"item_saved": "Elemento salvato",
|
||||
"quantity_cannot_negative": "La quantità non può essere negativa",
|
||||
"sync_child_location": "Il contenitore selezionato ha sincronizzato le posizioni dei suoi elementi interni. La posizione è stata aggiornata."
|
||||
},
|
||||
"updated_at": "Aggiornato Il",
|
||||
"warranty": "Garanzia",
|
||||
@@ -369,7 +435,15 @@
|
||||
"warranty_expires": "La garanzia scade il"
|
||||
},
|
||||
"labels": {
|
||||
"label_delete_confirm": "Sei sicuro di voler eliminare questa etichetta? Questa azione non può essere annullata.",
|
||||
"no_results": "Nessuna etichetta trovata",
|
||||
"toast": {
|
||||
"failed_delete_label": "Impossibile cancellare l'etichetta",
|
||||
"failed_load_label": "Impossibile caricare l'etichetta",
|
||||
"failed_update_label": "Impossibile aggiornare l'etichetta",
|
||||
"label_deleted": "Etichetta cancellata",
|
||||
"label_updated": "Etichetta aggiornata"
|
||||
},
|
||||
"update_label": "Aggiorna etichetta"
|
||||
},
|
||||
"languages": {
|
||||
@@ -414,7 +488,15 @@
|
||||
"child_locations": "Ubicazione figlia",
|
||||
"collapse_tree": "Contrai albero",
|
||||
"expand_tree": "Espandi albero",
|
||||
"location_items_delete_confirm": "Sei sicuro di voler eliminare questa posizione e tutti i relativi elementi? Questa azione non può essere annullata.",
|
||||
"no_results": "Nessuna posizione trovata",
|
||||
"toast": {
|
||||
"failed_delete_location": "Impossibile cancellare la posizione",
|
||||
"failed_load_location": "Impossibile caricare la posizione",
|
||||
"failed_update_location": "Impossibile aggiornare la posizione",
|
||||
"location_deleted": "Posizione cancellata",
|
||||
"location_updated": "Posizione aggiornata"
|
||||
},
|
||||
"update_location": "Aggiorna ubicazione"
|
||||
},
|
||||
"maintenance": {
|
||||
@@ -470,7 +552,9 @@
|
||||
"currency_format": "Formato Valuta",
|
||||
"current_password": "Password Corrente",
|
||||
"delete_account": "Elimina Account",
|
||||
"delete_account_confirm": "Sei sicuro di voler eliminare il tuo account? Se sei l’ultimo membro del tuo gruppo, tutti i tuoi dati verranno cancellati. Questa azione non può essere annullata.",
|
||||
"delete_account_sub": "Elimina il tuo account e tutti i dati associati. Questa operazione non può essere annullata.",
|
||||
"delete_notifier_confirm": "Sei sicuro di voler eliminare questo promemoria?",
|
||||
"display_legacy_header": "{ currentValue, select, true {Disable Legacy Header} false {Enable Legacy Header} other {Not Hit}}",
|
||||
"enabled": "Abilitato",
|
||||
"example": "Esempio",
|
||||
@@ -489,6 +573,20 @@
|
||||
"test": "Test",
|
||||
"theme_settings": "Impostazioni Tema",
|
||||
"theme_settings_sub": "Le impostazioni del tema sono memorizzate nella memoria locale del tuo browser. Puoi cambiare il tema \nin qualsiasi momento. Se hai problemi a impostare il tuo tema, prova a ricaricare la pagina.",
|
||||
"toast": {
|
||||
"account_deleted": "Il tuo account e' stato cancellato.",
|
||||
"failed_change_password": "Impossibile cambiare password.",
|
||||
"failed_create_notifier": "Impossibile creare il promemoria.",
|
||||
"failed_delete_account": "Impossibile cancellare l'account.",
|
||||
"failed_delete_notifier": "Impossibile cancellare il promemoria.",
|
||||
"failed_get_currencies": "Impossibile ottenere le valute",
|
||||
"failed_test_notifier": "Impossibile testare il promemoria.",
|
||||
"failed_update_group": "Impossibile aggiornare il gruppo",
|
||||
"failed_update_notifier": "Impossibile aggiornare il promemoria.",
|
||||
"group_updated": "Gruppo aggiornato",
|
||||
"notifier_test_success": "Promemoria testato con successo.",
|
||||
"password_changed": "Password cambiata con successo."
|
||||
},
|
||||
"update_group": "Aggiorna Gruppo",
|
||||
"update_language": "Aggiorna Lingua",
|
||||
"url": "URL",
|
||||
@@ -497,16 +595,38 @@
|
||||
},
|
||||
"reports": {
|
||||
"label_generator": {
|
||||
"asset_end": "Fine Asset",
|
||||
"asset_start": "Inizio Asset",
|
||||
"base_url": "URL base",
|
||||
"bordered_labels": "Etichette con bordo",
|
||||
"generate_page": "Genera Pagina",
|
||||
"input_placeholder": "Scrivi qui",
|
||||
"instruction_1": "Il Generatore di Etichette di Homebox è uno strumento che ti aiuta a stampare le etichette per il tuo inventario Homebox. Sono pensate come etichette da\nstampare in anticipo, così potrai stamparne molte e averle pronte da applicare",
|
||||
"instruction_2": "Queste etichette funzionano stampando un codice QR con un URL e le informazioni dell’AssetID sull’etichetta. Se hai disabilitato\n gli AssetID nelle impostazioni di Homebox, puoi comunque usare questo strumento, ma gli AssetID non faranno riferimento a nessun elemento",
|
||||
"instruction_3": "Questa funzione è nelle fasi iniziali di sviluppo e potrebbe cambiare nelle versioni future. Se hai suggerimenti,\n ti invitiamo a condividerli nella '<a href=\"https://github.com/sysadminsmedia/homebox/discussions/53\">Discussione GitHub</a>'",
|
||||
"label_height": "Altezza dell'etichetta",
|
||||
"label_width": "Larghezza dell'etichetta",
|
||||
"measure_type": "Tipo di misurazione",
|
||||
"page_bottom_padding": "Margine inferiore della pagina",
|
||||
"page_height": "Altezza della pagina",
|
||||
"page_left_padding": "Spaziatura Sinistra",
|
||||
"page_right_padding": "Spaziatura Destra",
|
||||
"page_top_padding": "Spaziatura in alto",
|
||||
"page_width": "Larghezza pagina",
|
||||
"qr_code_example": "Esempio di codice QR",
|
||||
"tip_1": "Le impostazioni predefinite qui sono configurate per i\n'<a href=\"https://www.avery.com/templates/5260\">'fogli di etichette ''Avery 5260 '</a>'. Se stai utilizzando un foglio differente,\n devi modificare le impostazioni affinchè corrispondano al tuo foglio."
|
||||
"tip_1": "Le impostazioni predefinite qui sono configurate per i\n'<a href=\"https://www.avery.com/templates/5260\">'fogli di etichette ''Avery 5260 '</a>'. Se stai utilizzando un foglio differente,\n devi modificare le impostazioni affinchè corrispondano al tuo foglio.",
|
||||
"tip_2": "Se stai personalizzando il tuo foglio, le dimensioni sono espresse in pollici. Quando ho creato il foglio 5260, ho notato che le\n dimensioni indicate nel loro modello non corrispondevano a quelle necessarie per stampare correttamente all’interno dei riquadri.\n '<b>'Preparati a qualche tentativo ed errore.'</b>'",
|
||||
"tip_3": "Quando stampi, assicurati di:\n<ol><li>Impostare i margini su 0 o Nessuno</li><li>Impostare la scala al 100%</li><li>Disattivare la stampa fronte/retro</li><li>Stampare una pagina di prova prima di stampare più pagine</li></ol>",
|
||||
"tips": "Suggerimenti",
|
||||
"title": "Generatore di etichette",
|
||||
"toast": {
|
||||
"page_too_small_card": "La dimensione della pagina è troppo piccola per il formato della scheda"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner": {
|
||||
"barcode_detected_message": "Codice a barre del prodotto rilevato",
|
||||
"barcode_fetch_data": "Recupera dati del prodotto",
|
||||
"error": "Si è verificato un errore durante la scansione",
|
||||
"invalid_url": "URL del codice a barre non valido",
|
||||
"no_sources": "Nessuna sorgente video disponibile",
|
||||
@@ -518,17 +638,24 @@
|
||||
"tools": {
|
||||
"actions": "Azioni Inventario",
|
||||
"actions_set": {
|
||||
"create_missing_thumbnails": "Crea miniature mancanti",
|
||||
"create_missing_thumbnails_button": "Crea miniature",
|
||||
"create_missing_thumbnails_confirm": "Sei sicuro di voler creare le miniature mancanti? Questa operazione potrebbe richiedere un po' di tempo e non può essere messa in pausa.",
|
||||
"create_missing_thumbnails_sub": "Crea miniature per tutti gli allegati supportati dalla configurazione corrente. Questo è utile per gli allegati caricati prima del rilascio della versione v0.20.0 di Homebox. Non sovrascriverà le miniature esistenti, ma creerà nuove miniature solo per gli allegati che non ne hanno una. Tieni presente che le miniature vengono generate in background e l’operazione potrebbe richiedere del tempo.",
|
||||
"ensure_ids": "Verifica ID delle risorse",
|
||||
"ensure_ids_button": "Verifica ID delle risorse",
|
||||
"ensure_ids_confirm": "Sei sicuro di voler assegnare un ID a tutti gli asset? Questa operazione potrebbe richiedere tempo e non può essere annullata.",
|
||||
"ensure_ids_sub": "Garantisce che tutti gli articoli nel tuo inventario abbiano un campo asset_id valido. Questo viene fatto trovando il campo asset_id corrente più alto nel database e applicando il valore successivo a ogni articolo che ha un campo asset_id non impostato. Questo viene fatto per il campo created_at.",
|
||||
"ensure_import_refs": "Verifica riferimenti di importazione",
|
||||
"ensure_import_refs_button": "Verifica riferimenti di importazione",
|
||||
"ensure_import_refs_sub": "Verifica che tutti gli articoli nel tuo inventario abbiano un campo import_ref valido. Questo viene fatto generando in modo casuale una stringa di 8 caratteri per ogni articolo che ha un campo import_ref non impostato.",
|
||||
"set_primary_photo": "Imposta foto principale",
|
||||
"set_primary_photo_button": "Imposta immagine principale",
|
||||
"set_primary_photo_confirm": "Sei sicuro di voler impostare le foto principali? Questa operazione potrebbe richiedere tempo e non può essere annullata.",
|
||||
"set_primary_photo_sub": "Nella versione v0.10.0 di Homebox, il campo immagine principale è stato aggiunto agli allegati di tipo foto. Questa azione imposterà il campo immagine principale alla prima immagine nella matrice allegati nel database, se non è già impostato. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/pull/576\">'Vedi GitHub PR #576'</a>'",
|
||||
"zero_datetimes": "Azzera Data e Orario articolo",
|
||||
"zero_datetimes_button": "Azzera Date e Ora articolo",
|
||||
"zero_datetimes_confirm": "Sei sicuro di voler reimpostare tutti i valori di data e ora? Questa operazione potrebbe richiedere tempo e non può essere annullata.",
|
||||
"zero_datetimes_sub": "Reimposta il valore dell'ora per tutti i campi data e ora dell'inventario all'inizio della data. Questo è per correggere un bug che è stato introdotto all'inizio dello sviluppo del sito che ha causato il valore di orario memorizzato con il tempo che ha causato problemi con i campi data visualizzazione dei valori esatti. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/issues/236\" target=\"_blank\">'Vedi Github Issue #236 per maggiori dettagli.'</a>'"
|
||||
},
|
||||
"actions_sub": "Applica Azioni massive al tuo inventario. Questo sono azioni irreversibili. '<b>'Presta attenzione.'</b>'",
|
||||
@@ -539,6 +666,7 @@
|
||||
"export_sub": "Esporta il formato CSV standard per Homebox. Questo esporterà tutti gli articoli del tuo inventario.",
|
||||
"import": "Importa Inventario",
|
||||
"import_button": "Importa Inventario",
|
||||
"import_ref_confirm": "Sei sicuro di voler garantire che tutti gli asset abbiano un import_ref? Questa operazione potrebbe richiedere tempo e non può essere annullata.",
|
||||
"import_sub": "Importa il formato CSV standard per Homebox. Senza una colonna '<code>'HB.import_ref'</code>' questo '<b>'non'</b>' sovrascriverà gli articoli esistenti nel tuo inventario, aggiungerà solamente nuovi articoli. Le righe con una colonna '<code>'HB.import_ref'</code>' saranno unite agli articoli esistenti con lo stesso import_ref, se presente."
|
||||
},
|
||||
"import_export_sub": "Importa ed esporta il tuo inventario da e verso un file CSV. Questo è utile per migrare il tuo inventario verso una nuova istanza di Homebox.",
|
||||
@@ -551,6 +679,14 @@
|
||||
"bill_of_materials_button": "Genera BOM",
|
||||
"bill_of_materials_sub": "Genera un file CSV (Valori Separati dalla Virgola) che può essere importato in un foglio di calcolo. Questo è un sommario del tuo inventario con informazioni di base su articoli e prezzi."
|
||||
},
|
||||
"reports_sub": "Genera diversi report per il tuo inventario."
|
||||
"reports_sub": "Genera diversi report per il tuo inventario.",
|
||||
"toast": {
|
||||
"asset_success": "{ results } assets sono stati aggiornati.",
|
||||
"failed_create_missing_thumbnails": "Impossibile creare le miniature mancanti.",
|
||||
"failed_ensure_ids": "Impossibile garantire gli ID degli asset.",
|
||||
"failed_ensure_import_refs": "Impossibile garantire i riferimenti di importazione.",
|
||||
"failed_set_primary_photos": "Impossibile configurare le foto pricipali.",
|
||||
"failed_zero_datetimes": "Impossibile reimpostare i valori di data e ora."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
"select_location": "Velg en lokasjon"
|
||||
},
|
||||
"tree": {
|
||||
"no_locations": "Ingen tilgjengelige lokasjoner. Legg til en ny lokasjon via\n `<`span class=\"link-primary\"`>`Opprett`<`/span`>`-knappen på navigasjonslinjen."
|
||||
"no_locations": "Ingen tilgjengelige lokasjoner. Legg til en ny lokasjon via\n '<span class=\"link-primary\">'Opprett'</span>'-knappen på navigasjonslinjen."
|
||||
}
|
||||
},
|
||||
"quick_menu": {
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
"open_new_tab": "Openen in een nieuw tabblad"
|
||||
},
|
||||
"create_modal": {
|
||||
"clear_template": "Template wissen",
|
||||
"delete_photo": "Foto verwijderen",
|
||||
"item_description": "Artikelomschrijving",
|
||||
"item_name": "Artikelnaam",
|
||||
@@ -138,16 +139,51 @@
|
||||
"searching": "Zoeken…"
|
||||
},
|
||||
"view": {
|
||||
"change_details": {
|
||||
"add_labels": "Voeg labels toe",
|
||||
"failed_to_update_item": "Bijwerken van item mislukt",
|
||||
"remove_labels": "Verwijder labels",
|
||||
"title": "Itemgegevens wijzigen"
|
||||
},
|
||||
"selectable": {
|
||||
"card": "Kaart",
|
||||
"items": "Objecten",
|
||||
"no_items": "Geen objecten om te tonen",
|
||||
"select_all": "Alles selecteren",
|
||||
"select_card": "Selecteer kaart",
|
||||
"select_row": "Rij selecteren",
|
||||
"table": "Tabel"
|
||||
},
|
||||
"table": {
|
||||
"dropdown": {
|
||||
"actions": "Acties",
|
||||
"change_labels": "Labels wijzigen",
|
||||
"change_labels_success": "Labels gewijzigd",
|
||||
"change_location": "Locatie wijzigen",
|
||||
"change_location_success": "Locatie gewijzigd",
|
||||
"create_maintenance_item": "Onderhoudsinvoer aanmaken voor item",
|
||||
"create_maintenance_selected": "Onderhoudsinvoer aanmaken voor geselecteerde items",
|
||||
"create_maintenance_success": "Onderhoudsinvoeren aangemaakt",
|
||||
"delete_confirmation": "Weet u zeker dat u de geselecteerde items wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
|
||||
"delete_item": "Item verwijderen",
|
||||
"delete_selected": "Geselecteerde items verwijderen",
|
||||
"download_csv": "Download tabel als CSV",
|
||||
"download_json": "Download tabel als JSON",
|
||||
"duplicate_item": "Dupliceer item",
|
||||
"duplicate_selected": "Geselecteerde items dupliceren",
|
||||
"error_deleting": "Fout bij verwijderen item",
|
||||
"error_duplicating": "Fout bij dupliceren van item",
|
||||
"open_menu": "Open menu",
|
||||
"open_multi_tab_warning": "Om veiligheidsredenen staan browsers standaard niet toe dat meerdere tabbladen tegelijk worden geopend. Volg de documentatie om dit te wijzigen:",
|
||||
"toggle_expand": "Uitvouwen in-/uitschakelen",
|
||||
"view_item": "Bekijk item",
|
||||
"view_items": "Bekijk items"
|
||||
},
|
||||
"headers": "Kopteksten",
|
||||
"page": "Pagina",
|
||||
"quick_actions": "Snelle acties en selectie inschakelen",
|
||||
"rows_per_page": "Rijen per pagina",
|
||||
"selected_rows": "{selected} van {total} rijen geselecteerd.",
|
||||
"table_settings": "Tabel instellingen",
|
||||
"view_item": "Toon Item"
|
||||
}
|
||||
@@ -194,6 +230,65 @@
|
||||
"quick_menu": {
|
||||
"no_results": "Geen resultaten gevonden.",
|
||||
"shortcut_hint": "Gebruik de numerieke toetsen om snel een actie te selecteren."
|
||||
},
|
||||
"template": {
|
||||
"apply_template": "Template toepassen",
|
||||
"card": {
|
||||
"delete": "Template verwijderen",
|
||||
"duplicate": "Template dupliceren",
|
||||
"edit": "Template bewerken"
|
||||
},
|
||||
"confirm_delete": "Deze template verwijderen?",
|
||||
"create_modal": {
|
||||
"title": "Template maken"
|
||||
},
|
||||
"detail": {
|
||||
"default_values": "Standaardinstellingen",
|
||||
"updated": "Bijgewerkt"
|
||||
},
|
||||
"edit_modal": {
|
||||
"title": "Template bewerken"
|
||||
},
|
||||
"empty_value": "(leeg)",
|
||||
"form": {
|
||||
"custom_fields": "Aangepaste velden",
|
||||
"default_item_values": "Standaardwaarden item",
|
||||
"default_location": "Standaard locatie",
|
||||
"default_value": "Standaard waarde",
|
||||
"field_name": "Veldnaam",
|
||||
"item_description": "Artikelomschrijving",
|
||||
"item_name": "Artikelnaam",
|
||||
"lifetime_warranty": "Levenslange garantie",
|
||||
"location": "Locatie",
|
||||
"manufacturer": "Fabrikant",
|
||||
"model_number": "Modelnummer",
|
||||
"no_custom_fields": "Geen aangepaste velden.",
|
||||
"template_description": "Template beschrijving",
|
||||
"template_name": "Template naam"
|
||||
},
|
||||
"hide_defaults": "Standaard verbergen",
|
||||
"save_as_template": "Opslaan als Template",
|
||||
"selector": {
|
||||
"label": "Template (Optioneel)",
|
||||
"not_found": "Geen template gevonden",
|
||||
"search": "Templates zoeken…",
|
||||
"select": "Template selecteren…"
|
||||
},
|
||||
"show_defaults": "Standaard weergeven",
|
||||
"toast": {
|
||||
"applied": "Template \"{name}\" toegepast",
|
||||
"create_failed": "Template aanmaken mislukt",
|
||||
"created": "Template aangemaakt",
|
||||
"delete_failed": "Template verwijderen mislukt",
|
||||
"deleted": "Template verwijderd",
|
||||
"duplicate_failed": "Template dupliceren mislukt",
|
||||
"duplicated": "Template gedupliceerd als \"{name}\"",
|
||||
"load_failed": "Template details laden mislukt",
|
||||
"saved_as_template": "Item opgeslagen als template \"{name}\"",
|
||||
"update_failed": "Template bijwerken mislukt",
|
||||
"updated": "Template bijgewerkt"
|
||||
},
|
||||
"using_template": "Gebruik template: {name}"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
@@ -231,7 +326,9 @@
|
||||
"maintenance": "Onderhoud",
|
||||
"name": "Naam",
|
||||
"navigate": "Navigeer",
|
||||
"no": "Nee",
|
||||
"password": "Wachtwoord",
|
||||
"preview": "Preview",
|
||||
"quantity": "Aantal",
|
||||
"read_docs": "Lees de documentatie",
|
||||
"return_home": "Terug naar startpagina",
|
||||
@@ -244,7 +341,8 @@
|
||||
"updating": "Aan het updaten",
|
||||
"value": "Waarde",
|
||||
"version": "Versie: { version }",
|
||||
"welcome": "Welkom, { username }"
|
||||
"welcome": "Welkom, { username }",
|
||||
"yes": "Ja"
|
||||
},
|
||||
"home": {
|
||||
"labels": "Labels",
|
||||
@@ -261,6 +359,7 @@
|
||||
"dont_join_group": "Wil je je niet bij een groep aansluiten?",
|
||||
"joining_group": "Je sluit je aan bij een bestaande groep!",
|
||||
"login": "Log in",
|
||||
"or": "of",
|
||||
"register": "Registreer",
|
||||
"remember_me": "Onthoud mij",
|
||||
"set_email": "Wat is je mailadres?",
|
||||
@@ -272,6 +371,14 @@
|
||||
"invalid_email": "Ongeldig e-mailadres",
|
||||
"invalid_email_password": "Ongeldig e-mailadres of wachtwoord",
|
||||
"login_success": "Aangemeld",
|
||||
"oidc_access_denied": "Toegang geweigerd: uw account heeft niet de vereiste rol/groepslidmaatschap",
|
||||
"oidc_auth_failed": "OIDC authenticatie mislukt",
|
||||
"oidc_invalid_response": "Ongeldig OIDC-antwoord ontvangen",
|
||||
"oidc_provider_error": "OIDC-provider heeft een fout geretourneerd",
|
||||
"oidc_security_error": "OIDC-beveiligingsfout - mogelijke CSRF-aanval",
|
||||
"oidc_session_expired": "OIDC sessie is verlopen",
|
||||
"oidc_token_expired": "OIDC token is verlopen",
|
||||
"oidc_token_invalid": "OIDC token signature is ongeldig",
|
||||
"problem_registering": "Probleem bij het registreren van de gebruiker",
|
||||
"user_registered": "Gebruiker geregistreerd"
|
||||
}
|
||||
@@ -508,8 +615,15 @@
|
||||
"profile": "Profiel",
|
||||
"scanner": "Scanner",
|
||||
"search": "Zoeken",
|
||||
"templates": "Templates",
|
||||
"tools": "Hulpmiddelen"
|
||||
},
|
||||
"pages": {
|
||||
"templates": {
|
||||
"no_templates": "Nog geen templates.",
|
||||
"title": "Templates"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"active": "Actief",
|
||||
"change_password": "Verander Wachtwoord",
|
||||
@@ -527,6 +641,7 @@
|
||||
"group_settings_sub": "Gedeelde groepsinstellingen. Mogelijk moet u uw browser vernieuwen om sommige instellingen toe te passen.",
|
||||
"inactive": "Inactief",
|
||||
"language": "Taal",
|
||||
"legacy_image_fit": "{ currentValue, select, true {Legacy fit uitschakelen: afbeelding aanpassen met balken} false {Legacy fit inschakelen: afbeelding vullen met bijsnijden} other {Overig}}",
|
||||
"new_password": "Nieuw Wachtwoord",
|
||||
"no_notifiers": "Geen melders geconfigureerd",
|
||||
"no_override": "Niet overschrijven",
|
||||
|
||||
@@ -134,19 +134,55 @@
|
||||
"selector": {
|
||||
"no_results": "Brak rezultatów",
|
||||
"placeholder": "Wybierz…",
|
||||
"search_placeholder": "Wpisz, aby wyszukać…"
|
||||
"search_placeholder": "Wpisz, aby wyszukać…",
|
||||
"searching": "Szukam…"
|
||||
},
|
||||
"view": {
|
||||
"change_details": {
|
||||
"add_labels": "Dodaj etykiety",
|
||||
"failed_to_update_item": "Nie udało się zaktualizować danych przedmiotu",
|
||||
"remove_labels": "Usuń etykiety",
|
||||
"title": "Zmień dane przedmiotu"
|
||||
},
|
||||
"selectable": {
|
||||
"card": "Karta",
|
||||
"items": "Przedmioty",
|
||||
"no_items": "Brak przedmiotów do wyświetlenia",
|
||||
"select_all": "Zaznacz wszystko",
|
||||
"select_card": "Zaznacz kartę",
|
||||
"select_row": "Zaznacz wiersz",
|
||||
"table": "Tabela"
|
||||
},
|
||||
"table": {
|
||||
"dropdown": {
|
||||
"actions": "Akcje",
|
||||
"change_labels": "Zmień etykiety tekstowe:",
|
||||
"change_labels_success": "Zmieniono etykiety",
|
||||
"change_location": "Zmień lokalizację",
|
||||
"change_location_success": "Oznaczenie lokalizacji zmienione",
|
||||
"create_maintenance_item": "Utwórz wpis konserwacyjny dla pozycji",
|
||||
"create_maintenance_selected": "Utwórz wpis konserwacyjny dla wybranych elementów",
|
||||
"create_maintenance_success": "Utworzono wpis(y) dot. konserwacji",
|
||||
"delete_confirmation": "Czy na pewno chcesz usunąć wybrane przedmioty? Tej czynności nie można cofnąć.",
|
||||
"delete_item": "Usuń przedmiot",
|
||||
"delete_selected": "Usuń zaznaczone przedmioty",
|
||||
"download_csv": "Pobierz tabelę jako CSV",
|
||||
"download_json": "Pobierz tabelę jako JSON",
|
||||
"duplicate_item": "Duplikuj pozycję",
|
||||
"duplicate_selected": "Powiel wybrane przedmioty",
|
||||
"error_deleting": "Bląd usuwania przedmiotu",
|
||||
"error_duplicating": "Problem podczas duplikowania pozycji",
|
||||
"open_menu": "Otwórz menu",
|
||||
"open_multi_tab_warning": "Ze względów bezpieczeństwa przeglądarki domyślnie nie zezwalają na jednoczesne otwieranie wielu kart. Aby to zmienić, postępuj zgodnie z dokumentacją:",
|
||||
"toggle_expand": "Włącz/wyłącz rozwiń",
|
||||
"view_item": "Pokaż przedmiot",
|
||||
"view_items": "Pokaż przedmioty"
|
||||
},
|
||||
"headers": "Nagłówki",
|
||||
"page": "Strona",
|
||||
"quick_actions": "Aktywuj Szybkie Akcje i Zaznaczenie",
|
||||
"rows_per_page": "Ilość wierszy na stronę",
|
||||
"selected_rows": "Zaznaczono {selected} z {total} wierszy.",
|
||||
"table_settings": "Ustawienia Tabeli",
|
||||
"view_item": "Zobacz przedmiot"
|
||||
}
|
||||
@@ -231,6 +267,7 @@
|
||||
"name": "Nazwa",
|
||||
"navigate": "Nawiguj",
|
||||
"password": "Hasło",
|
||||
"preview": "Podgląd",
|
||||
"quantity": "Ilość",
|
||||
"read_docs": "Przeczytaj dokumentację",
|
||||
"return_home": "Powrót do strony głównej",
|
||||
@@ -292,7 +329,16 @@
|
||||
"details": "Szczegóły",
|
||||
"drag_and_drop": "Przeciągnij i upuść pliki tutaj lub kliknij, aby wybrać pliki",
|
||||
"duplicate": {
|
||||
"prefix": "Kopia z "
|
||||
"copy_attachments": "Kopiuj załączniki",
|
||||
"copy_custom_fields": "Kopiuj pola niestandardowe",
|
||||
"copy_maintenance": "Kopiuj konserwację",
|
||||
"custom_prefix": "Kopiuj prefiks",
|
||||
"enable_custom_prefix": "Włącz niestandardowy prefiks",
|
||||
"override_instructions": "Aby zastąpić te ustawienia, przytrzymaj klawisz Shift podczas klikania przycisku duplikowania.",
|
||||
"prefix": "Kopia z ",
|
||||
"prefix_instructions": "Ten prefiks zostanie dodany na początku nazwy duplikatu. Dodaj spację na końcu prefiksu, aby dodać spację między prefiksem a nazwą elementu.",
|
||||
"temporary_title": "Ustawienia tymczasowe",
|
||||
"title": "Duplikuj ustawienia"
|
||||
},
|
||||
"edit": {
|
||||
"edit_attachment_dialog": {
|
||||
@@ -302,7 +348,8 @@
|
||||
"primary_photo_sub": "Ta opcja jest dostępna tylko dla zdjęć. Tylko jedno zdjęcie może być zdjęciem głównym. Po wybraniu tej opcji, bieżące zdjęcie główne (jeśli takie istnieje) zostanie odznaczone.",
|
||||
"select_type": "Wybierz typ",
|
||||
"title": "Edycja załącznika"
|
||||
}
|
||||
},
|
||||
"view_image": "Zobacz obraz"
|
||||
},
|
||||
"edit_details": "Edytuj szczegóły",
|
||||
"field_selector": "Selektor pól",
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"shift": "Continuar"
|
||||
},
|
||||
"import_dialog": {
|
||||
"change_warning": "O comportamento das importações com import_refs existentes foi alterado. Se houver um import_ref presente no arquivo CSV, o \nitem será atualizado com os valores presentes no arquivo CSV.",
|
||||
"description": "Importe um arquivo CSV contendo seus itens, etiquetas e locais. Consulte a documentação para mais informações \nsobre a formatação necessária.",
|
||||
"change_warning": "O comportamento das importações com import_refs existentes foi alterado. Se houver um import_ref presente no arquivo CSV, o\nitem será atualizado com os valores presentes no arquivo CSV.",
|
||||
"description": "Importe um arquivo CSV contendo seus itens, etiquetas e locais. Consulte a documentação para mais informações\nsobre a formatação necessária.",
|
||||
"title": "Importar arquivo CSV",
|
||||
"toast": {
|
||||
"import_failed": "Importação falhou. Tente novamente mais tarde.",
|
||||
@@ -94,12 +94,15 @@
|
||||
"open_new_tab": "Abrir em uma nova aba"
|
||||
},
|
||||
"create_modal": {
|
||||
"clear_template": "Template limpo",
|
||||
"delete_photo": "Apagar foto",
|
||||
"item_description": "Descrição do Item",
|
||||
"item_name": "Nome do Item",
|
||||
"item_photo": "Foto do Item 📷",
|
||||
"item_quantity": "Quantidade de Itens",
|
||||
"parent_item": "Item Pai",
|
||||
"product_tooltip_input_barcode": "Preenchimento automático com um código de barras fornecido manualmente",
|
||||
"product_tooltip_scan_barcode": "Preenchimento automático com um código de barras da 📷",
|
||||
"rotate_photo": "Girar imagem",
|
||||
"set_as_primary_photo": "Definir como { isPrimary, select, true {non-} false {} other {}} foto primária",
|
||||
"title": "Criar Item",
|
||||
@@ -109,7 +112,7 @@
|
||||
"create_success": "Item criado",
|
||||
"failed_load_parent": "Falhou ao carregar item pai - por favor selecione manualmente",
|
||||
"no_canvas_support": "Seu navegador não suporta operações de tela",
|
||||
"please_select_location": "Por favor seleciona a localização",
|
||||
"please_select_location": "Por favor, selecione uma localização",
|
||||
"rotate_failed": "Falha ao rotacionar imagem: { error }",
|
||||
"rotate_process_failed": "Falhou ao processar imagem rotacionada",
|
||||
"some_photos_failed": "{count, plural, =0 {Nenhuma foto para upload.} =1 {1 imagem falhou no upload.} other {Algumas fotos falharam durante upload.}}",
|
||||
@@ -120,22 +123,67 @@
|
||||
"upload_photos": "Carregar Fotos",
|
||||
"uploaded": "Fotos enviadas"
|
||||
},
|
||||
"product_import": {
|
||||
"barcode": "Código de barras do produto",
|
||||
"db_source": "Fonte DB",
|
||||
"error_exception": "Ocorreu uma exceção ao recuperar o código de barras do item: ",
|
||||
"error_invalid_barcode": "Código de barras fornecido inválido",
|
||||
"error_not_found": "Nenhum produto encontrado com o código de barras fornecido.",
|
||||
"search_item": "Pesquisar produto",
|
||||
"title": "Importar Produto"
|
||||
},
|
||||
"selector": {
|
||||
"no_results": "Nenhum Resultado Encontrado",
|
||||
"placeholder": "Selecione…",
|
||||
"search_placeholder": "Digite para pesquisar…"
|
||||
"search_placeholder": "Digite para pesquisar…",
|
||||
"searching": "Pesquisando…"
|
||||
},
|
||||
"view": {
|
||||
"change_details": {
|
||||
"add_labels": "Adicionar Etiquetas",
|
||||
"failed_to_update_item": "Falha ao atualizar item",
|
||||
"remove_labels": "Remover Etiquetas",
|
||||
"title": "Alterar detalhes do item"
|
||||
},
|
||||
"selectable": {
|
||||
"card": "Cartão",
|
||||
"items": "Items",
|
||||
"no_items": "Nenhum Item para Exibir",
|
||||
"select_all": "Selecionar tudo",
|
||||
"select_card": "Selecionar cartão",
|
||||
"select_row": "Selecionar a linha",
|
||||
"table": "Tabela"
|
||||
},
|
||||
"table": {
|
||||
"dropdown": {
|
||||
"actions": "Ações",
|
||||
"change_labels": "Trocar etiquetas",
|
||||
"change_labels_success": "Etiquetas alteradas",
|
||||
"change_location": "Trocar localização",
|
||||
"change_location_success": "Localização alterada",
|
||||
"create_maintenance_item": "Criar entrada de manutenção para o item",
|
||||
"create_maintenance_selected": "Criar entrada de manutenção para itens selecionados",
|
||||
"create_maintenance_success": "Entrada(s) de manutenção criada(s)",
|
||||
"delete_confirmation": "Tem certeza de que deseja excluir o (s) item(ns) selecionado (s)? Esta ação não pode ser desfeita.",
|
||||
"delete_item": "Deletar item",
|
||||
"delete_selected": "Excluir itens selecionados",
|
||||
"download_csv": "Baixar a tabela como CSV",
|
||||
"download_json": "Baixar tabela como JSON",
|
||||
"duplicate_item": "Duplicar Item",
|
||||
"duplicate_selected": "Duplicar itens selecionados",
|
||||
"error_deleting": "Erro ao deletar item",
|
||||
"error_duplicating": "Erro ao duplicar item",
|
||||
"open_menu": "Abrir menu",
|
||||
"open_multi_tab_warning": "Por razões de segurança, os navegadores não permitem que várias guias sejam abertas de uma só vez por padrão. Para alterar isso, siga a documentação:",
|
||||
"toggle_expand": "Expandir",
|
||||
"view_item": "Ver item",
|
||||
"view_items": "Ver Items"
|
||||
},
|
||||
"headers": "Cabeçalhos",
|
||||
"page": "Página",
|
||||
"quick_actions": "Ativar ações e seleção rápidas",
|
||||
"rows_per_page": "Linhas por página",
|
||||
"selected_rows": "{selected} de {total} linha(s) selecionada (s).",
|
||||
"table_settings": "Configurações da Tabela",
|
||||
"view_item": "Visualizar item"
|
||||
}
|
||||
@@ -182,8 +230,59 @@
|
||||
"quick_menu": {
|
||||
"no_results": "Nenhum resultado encontrado.",
|
||||
"shortcut_hint": "Use as teclas numéricas para selecionar uma ação rapidamente."
|
||||
},
|
||||
"template": {
|
||||
"card": {
|
||||
"delete": "Deletar template",
|
||||
"duplicate": "Clonar template",
|
||||
"edit": "Editar template"
|
||||
},
|
||||
"confirm_delete": "Deletar este template?",
|
||||
"create_modal": {
|
||||
"title": "Criar Template"
|
||||
},
|
||||
"detail": {
|
||||
"default_values": "Valores padrão",
|
||||
"updated": "Atualizado"
|
||||
},
|
||||
"edit_modal": {
|
||||
"title": "Editar Template"
|
||||
},
|
||||
"form": {
|
||||
"custom_fields": "Campos personalizados",
|
||||
"default_item_values": "Valores padrão de itens",
|
||||
"default_location": "Local padrão",
|
||||
"default_value": "Valor padrão",
|
||||
"field_name": "Nome do campo",
|
||||
"item_description": "Descrição do item",
|
||||
"item_name": "Nome do item",
|
||||
"lifetime_warranty": "Garantia vitalícia",
|
||||
"location": "Local",
|
||||
"manufacturer": "Fabricante",
|
||||
"model_number": "Número de modelo",
|
||||
"no_custom_fields": "Sem campos personalizados",
|
||||
"template_description": "Descrição do template",
|
||||
"template_name": "Nome do template"
|
||||
},
|
||||
"selector": {
|
||||
"label": "Template (opcional)",
|
||||
"not_found": "Nenhum template encontrado",
|
||||
"search": "Procurando templates…",
|
||||
"select": "Selecionar template…"
|
||||
},
|
||||
"toast": {
|
||||
"applied": "Template \"{name}\" aplicado",
|
||||
"create_failed": "Falha ao criar template",
|
||||
"created": "Template criado",
|
||||
"delete_failed": "Falha ao deletar template",
|
||||
"deleted": "Template deletado",
|
||||
"duplicate_failed": "Falha ao clonar template"
|
||||
}
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"api_failure": "Falha na chamada da API de back-end: "
|
||||
},
|
||||
"global": {
|
||||
"add": "Adicionar",
|
||||
"archived": "Arquivado",
|
||||
@@ -217,6 +316,7 @@
|
||||
"name": "Nome",
|
||||
"navigate": "Navegar",
|
||||
"password": "Senha",
|
||||
"preview": "Pré-visualização",
|
||||
"quantity": "Quantidade",
|
||||
"read_docs": "Leia a Documentação",
|
||||
"return_home": "Retornar para Home",
|
||||
@@ -277,6 +377,18 @@
|
||||
"description": "Descrição",
|
||||
"details": "Detalhes",
|
||||
"drag_and_drop": "Arraste e solte os arquivos aqui ou clique para selecionar os arquivos",
|
||||
"duplicate": {
|
||||
"copy_attachments": "Copiar Anexos",
|
||||
"copy_custom_fields": "Copiar campos personalizados",
|
||||
"copy_maintenance": "Manutenção de cópias",
|
||||
"custom_prefix": "Copiar prefixo",
|
||||
"enable_custom_prefix": "Ativar prefixo personalizado",
|
||||
"override_instructions": "Segure shift ao clicar no botão de duplicação para sobrescrever essas configurações.",
|
||||
"prefix": "Cópia de ",
|
||||
"prefix_instructions": "Este prefixo será adicionado ao início do nome do item duplicado. Inclua um espaço no final do prefixo para adicionar um espaço entre o prefixo e o nome do item.",
|
||||
"temporary_title": "Configurações temporárias",
|
||||
"title": "Duplicar Configurações"
|
||||
},
|
||||
"edit": {
|
||||
"edit_attachment_dialog": {
|
||||
"attachment_title": "Título do Anexo",
|
||||
@@ -285,7 +397,8 @@
|
||||
"primary_photo_sub": "Esta opção está disponível apenas para fotos. Apenas uma foto pode ser a principal. Se você selecionar esta opção, a foto principal atual, se houver, será desmarcada.",
|
||||
"select_type": "Selecione um tipo",
|
||||
"title": "Editar Anexo"
|
||||
}
|
||||
},
|
||||
"view_image": "Ver imagem"
|
||||
},
|
||||
"edit_details": "Detalhes da Edição",
|
||||
"field_selector": "Seletor de Campo",
|
||||
@@ -383,6 +496,7 @@
|
||||
"update_label": "Atualizar Etiqueta"
|
||||
},
|
||||
"languages": {
|
||||
"bs-BA": "Bósnio (Bósnia e Herzegovina)",
|
||||
"ca": "Catalão",
|
||||
"cs-CZ": "Tcheco",
|
||||
"de": "Alemão",
|
||||
@@ -410,6 +524,7 @@
|
||||
"th-TH": "Tailandês",
|
||||
"tr": "Turco",
|
||||
"uk-UA": "Ucraniano",
|
||||
"vi-VN": "Vietnamese",
|
||||
"zh-CN": "Chinês (Simplificado)",
|
||||
"zh-HK": "Chinês (Hong Kong)",
|
||||
"zh-MO": "Chinês (Macau)",
|
||||
@@ -497,6 +612,7 @@
|
||||
"group_settings_sub": "Configurações de Grupo Compartilhado. É possível que tenha que recarregar a página para que alguns ajustes sejam aplicados.",
|
||||
"inactive": "Inativo",
|
||||
"language": "Idioma",
|
||||
"legacy_image_fit": "{ currentValue, select, true {Desabilitar Ajuste legado: Ajuste a imagem com barras} false {Habilitar ajuste legado: Preencher imagem com recorte} other {Not Hit}}",
|
||||
"new_password": "Nova Senha",
|
||||
"no_notifiers": "Nenhum notificador configurado",
|
||||
"no_override": "Não sobrepor",
|
||||
@@ -559,6 +675,8 @@
|
||||
}
|
||||
},
|
||||
"scanner": {
|
||||
"barcode_detected_message": "código de barras do produto detectado",
|
||||
"barcode_fetch_data": "Buscar dados do produto",
|
||||
"error": "Ocorreu um erro durante a digitalização",
|
||||
"invalid_url": "Código de barras inválido",
|
||||
"no_sources": "Nenhuma fonte de vídeo disponível",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"shift": "Shift"
|
||||
},
|
||||
"import_dialog": {
|
||||
"change_warning": "O comportamento para importações com import_refs existente foi alterado. Se um import_ref estiver presente \nno ficheiro CSV, o item será atualizado com os valores do ficheiro CSV.",
|
||||
"change_warning": "O comportamento para importações com import_refs existente foi alterado. Se um import_ref estiver presente no ficheiro CSV, \no item será atualizado com os valores do ficheiro CSV.",
|
||||
"description": "Importe um ficheiro CSV contendo os seus itens, etiquetas e localizações. Consulte a documentação para mais \ninformações sobre o formato necessário.",
|
||||
"title": "Importar Ficheiro CSV",
|
||||
"toast": {
|
||||
@@ -24,6 +24,13 @@
|
||||
"new_version_available_link": "Clique aqui para ver as notas da versão"
|
||||
}
|
||||
},
|
||||
"color_selector": {
|
||||
"clear": "Limpar cor",
|
||||
"color": "Cor",
|
||||
"no_color": "Sem cor",
|
||||
"no_color_selected": "Nenhuma cor selecionada",
|
||||
"randomize": "Cor aleatória"
|
||||
},
|
||||
"form": {
|
||||
"password": {
|
||||
"toggle_show": "Alternar Visibilidade da Palavra-passe"
|
||||
@@ -93,6 +100,8 @@
|
||||
"item_photo": "Foto do Item 📷",
|
||||
"item_quantity": "Quantidade do Item",
|
||||
"parent_item": "Item Principal",
|
||||
"product_tooltip_input_barcode": "Preenchimento automático com um código de barras fornecido manualmente",
|
||||
"product_tooltip_scan_barcode": "Preenchimento automático com um código de barras da 📷",
|
||||
"rotate_photo": "Rodar foto",
|
||||
"set_as_primary_photo": "Definir como foto { isPrimary, select, true {não-} false {} other {}}principal",
|
||||
"title": "Criar Item",
|
||||
@@ -108,33 +117,72 @@
|
||||
"some_photos_failed": "{count, plural, =0 {Sem fotos para carregar.} =1 {1 foto falhou ao carregar.} other {Algumas fotos falharam ao carregar.}}",
|
||||
"upload_failed": "Falha ao carregar foto: { photoName }",
|
||||
"upload_success": "{count, plural, =0 {Sem fotos carregadas.} =1 {Foto carregada com sucesso.} other {Todas as fotos foram carregadas com sucesso.}}",
|
||||
"uploading_photos": "{count, plural, =0 {Sem fotos para carregar} =1 {A carregar 1 foto...} other {A carregar {count} fotos...}}"
|
||||
"uploading_photos": "{count, plural, =0 {Sem fotos para carregar} =1 {A carregar 1 foto…} other {A carregar {count} fotos…}}"
|
||||
},
|
||||
"upload_photos": "Carregar Fotos",
|
||||
"uploaded": "Foto Carregada"
|
||||
},
|
||||
"product_import": {
|
||||
"barcode": "Código de Barras do Produto",
|
||||
"error_not_found": "Nenhum produto encontrado com código de barras.",
|
||||
"db_source": "Fonte DB",
|
||||
"error_exception": "Ocorreu uma exceção ao recuperar o código de barras do item: ",
|
||||
"error_invalid_barcode": "Código de barras fornecido inválido",
|
||||
"error_not_found": "Nenhum produto encontrado com código de barras fornecido.",
|
||||
"search_item": "Pesquisar produto",
|
||||
"title": "Importar produto"
|
||||
},
|
||||
"selector": {
|
||||
"no_results": "Nenhum Resultado Encontrado",
|
||||
"placeholder": "Selecionar…",
|
||||
"search_placeholder": "Escreva para pesquisar…"
|
||||
"search_placeholder": "Escreva para pesquisar…",
|
||||
"searching": "A procurar…"
|
||||
},
|
||||
"view": {
|
||||
"change_details": {
|
||||
"add_labels": "Adicionar Labels",
|
||||
"failed_to_update_item": "Falha ao atualizar o artigo",
|
||||
"remove_labels": "Remover Rótulos",
|
||||
"title": "Alterar Detalhes do Item"
|
||||
},
|
||||
"selectable": {
|
||||
"card": "Cartão",
|
||||
"items": "Itens",
|
||||
"no_items": "Sem Itens para Exibir",
|
||||
"select_all": "Selecionar Tudo",
|
||||
"select_card": "Selecionar Cartão",
|
||||
"select_row": "Selecionar Linha",
|
||||
"table": "Tabela"
|
||||
},
|
||||
"table": {
|
||||
"dropdown": {
|
||||
"actions": "Ações",
|
||||
"change_labels": "Alterar etiquetas",
|
||||
"change_labels_success": "Etiquetas alteradas",
|
||||
"change_location": "Alterar Localização",
|
||||
"change_location_success": "Localização alterada",
|
||||
"create_maintenance_item": "Criar entrada de manutenção para o item",
|
||||
"create_maintenance_selected": "Criar entrada de manutenção para itens selecionados",
|
||||
"create_maintenance_success": "Entrada(s) de Manutenção Criada(s)",
|
||||
"delete_confirmation": "Tem a certeza de que pretende eliminar o(s) item(s) selecionado(s)? Esta ação não pode ser desfeita.",
|
||||
"delete_item": "Eliminar Item",
|
||||
"delete_selected": "Eliminar Items Selecionados",
|
||||
"download_csv": "Transferir Tabela como CSV",
|
||||
"download_json": "Transferir Tabela como JSON",
|
||||
"duplicate_item": "Duplicar item",
|
||||
"duplicate_selected": "Duplicar Itens Selecionados",
|
||||
"error_deleting": "Erro ao eliminar item",
|
||||
"error_duplicating": "Erro ao duplicar item",
|
||||
"open_menu": "Abrir menu",
|
||||
"open_multi_tab_warning": "Por razões de segurança, os navegadores não permitem que vários separadores sejam abertos de uma só vez por padrão. Para alterar isso, siga a documentação:",
|
||||
"toggle_expand": "Alternar Expandir",
|
||||
"view_item": "Ver item",
|
||||
"view_items": "Ver items"
|
||||
},
|
||||
"headers": "Cabeçalhos",
|
||||
"page": "Página",
|
||||
"quick_actions": "Ativar Ações Rápidas e Seleção",
|
||||
"rows_per_page": "Linhas por página",
|
||||
"selected_rows": "{selected} de {total} linha(s) selecionada(s).",
|
||||
"table_settings": "Definições da Tabela",
|
||||
"view_item": "Ver item"
|
||||
}
|
||||
@@ -142,6 +190,7 @@
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"label_color": "Cor da Etiqueta",
|
||||
"label_description": "Descrição da Etiqueta",
|
||||
"label_name": "Nome da Etiqueta",
|
||||
"title": "Criar Etiqueta",
|
||||
@@ -182,6 +231,9 @@
|
||||
"shortcut_hint": "Use as teclas numéricas para selecionar rapidamente uma ação."
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"api_failure": "Falha na chamada da API de back-end: "
|
||||
},
|
||||
"global": {
|
||||
"add": "Adicionar",
|
||||
"archived": "Arquivado",
|
||||
@@ -215,6 +267,7 @@
|
||||
"name": "Nome",
|
||||
"navigate": "Navegar",
|
||||
"password": "Palavra-passe",
|
||||
"preview": "Pré-visualização",
|
||||
"quantity": "Quantidade",
|
||||
"read_docs": "Ler a Documentação",
|
||||
"return_home": "Voltar à Página Inicial",
|
||||
@@ -275,6 +328,18 @@
|
||||
"description": "Descrição",
|
||||
"details": "Detalhes",
|
||||
"drag_and_drop": "Arraste e largue ficheiros aqui ou clique para selecionar ficheiros",
|
||||
"duplicate": {
|
||||
"copy_attachments": "Copiar Anexos",
|
||||
"copy_custom_fields": "Copiar Campos Personalizados",
|
||||
"copy_maintenance": "Copiar Manutenção",
|
||||
"custom_prefix": "Copiar Prefixo",
|
||||
"enable_custom_prefix": "Ativar Prefixo Personalizado",
|
||||
"override_instructions": "Segure shift ao clicar no botão duplicar para substituir estas configurações.",
|
||||
"prefix": "Cópia de ",
|
||||
"prefix_instructions": "Este prefixo será adicionado ao início do nome do item duplicado. Inclua um espaço no fim do prefixo para adicionar um espaço entre o prefixo e o nome do item.",
|
||||
"temporary_title": "Definições Temporárias",
|
||||
"title": "Duplicar Definições"
|
||||
},
|
||||
"edit": {
|
||||
"edit_attachment_dialog": {
|
||||
"attachment_title": "Título do Anexo",
|
||||
@@ -283,7 +348,8 @@
|
||||
"primary_photo_sub": "Esta opção só está disponível para fotos. Apenas uma foto pode ser principal. Ao selecionar esta, a atual será desmarcada.",
|
||||
"select_type": "Selecionar um tipo",
|
||||
"title": "Editar Anexo"
|
||||
}
|
||||
},
|
||||
"view_image": "Ver Imagem"
|
||||
},
|
||||
"edit_details": "Editar Detalhes",
|
||||
"field_selector": "Seletor de Campo",
|
||||
@@ -381,6 +447,7 @@
|
||||
"update_label": "Atualizar Etiqueta"
|
||||
},
|
||||
"languages": {
|
||||
"bs-BA": "Bósnio (Bósnia e Herzegovina)",
|
||||
"ca": "Catalão",
|
||||
"cs-CZ": "Checo",
|
||||
"da-DK": "Dinamarquês",
|
||||
@@ -411,6 +478,7 @@
|
||||
"th-TH": "Tailandês",
|
||||
"tr": "Turco",
|
||||
"uk-UA": "Ucraniano",
|
||||
"vi-VN": "Vietnamita",
|
||||
"zh-CN": "Chinês (Simplificado)",
|
||||
"zh-HK": "Chinês (Hong Kong)",
|
||||
"zh-MO": "Chinês (Macau)",
|
||||
@@ -495,6 +563,7 @@
|
||||
"group_settings_sub": "Definições Partilhadas do Grupo. Pode ser necessário atualizar a página para aplicar algumas definições.",
|
||||
"inactive": "Inativo",
|
||||
"language": "Idioma",
|
||||
"legacy_image_fit": "{ currentValue, select, true {Desativar ajuste herdado: ajustar imagem com barras} false {Ativar ajuste herdado: preencher imagem com recorte} other {Not Hit}}",
|
||||
"new_password": "Nova Palavra-passe",
|
||||
"no_notifiers": "Nenhum notificador configurado",
|
||||
"no_override": "Sem substituição",
|
||||
@@ -557,6 +626,8 @@
|
||||
}
|
||||
},
|
||||
"scanner": {
|
||||
"barcode_detected_message": "código de barras do produto detectado",
|
||||
"barcode_fetch_data": "Buscar dados do produto",
|
||||
"error": "Ocorreu um erro ao digitalizar",
|
||||
"invalid_url": "URL de código de barras inválido",
|
||||
"no_sources": "Sem fontes de vídeo disponíveis",
|
||||
@@ -568,6 +639,10 @@
|
||||
"tools": {
|
||||
"actions": "Ações de Inventário",
|
||||
"actions_set": {
|
||||
"create_missing_thumbnails": "Criar miniaturas em falta",
|
||||
"create_missing_thumbnails_button": "Criar miniaturas",
|
||||
"create_missing_thumbnails_confirm": "Tem a certeza que pretende criar miniaturas em falta? Esta ação poderá demorar e não pode ser colocada em pausa.",
|
||||
"create_missing_thumbnails_sub": "Cria miniaturas para todos os anexos que são suportados pela configuração atual. Isto é útil para anexos que foram enviados antes do lançamento da v0.20.0 de Homebox. Isto não irá substituir as miniaturas existentes, apenas irá criar umas novas para anexos que não têm miniatura. Por favor note que as miniaturas são criadas em segundo plano e podem demorar a finalizar.",
|
||||
"ensure_ids": "Garantir IDs dos Ativos",
|
||||
"ensure_ids_button": "Garantir IDs dos Ativos",
|
||||
"ensure_ids_confirm": "Tem a certeza de que deseja garantir que todos os ativos têm um ID? Isto pode demorar e não pode ser desfeito.",
|
||||
@@ -608,6 +683,7 @@
|
||||
"reports_sub": "Gerar diferentes relatórios para o seu inventário.",
|
||||
"toast": {
|
||||
"asset_success": "{ results } ativos foram atualizados.",
|
||||
"failed_create_missing_thumbnails": "Falha ao criar miniaturas em falta.",
|
||||
"failed_ensure_ids": "Falha ao garantir IDs dos ativos.",
|
||||
"failed_ensure_import_refs": "Falha ao garantir referências de importação.",
|
||||
"failed_set_primary_photos": "Falha ao definir fotos principais.",
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
"open_new_tab": "Открыть в новой вкладке"
|
||||
},
|
||||
"create_modal": {
|
||||
"clear_template": "Очистить шаблон",
|
||||
"delete_photo": "Удалить фото",
|
||||
"item_description": "Описание элемента",
|
||||
"item_name": "Имя элемента",
|
||||
@@ -134,19 +135,55 @@
|
||||
"selector": {
|
||||
"no_results": "Результаты не найдены",
|
||||
"placeholder": "Выберите…",
|
||||
"search_placeholder": "Введите для поиска…"
|
||||
"search_placeholder": "Введите для поиска…",
|
||||
"searching": "Поиск…"
|
||||
},
|
||||
"view": {
|
||||
"change_details": {
|
||||
"add_labels": "Добавить метки",
|
||||
"failed_to_update_item": "Не удалось обновить элемент",
|
||||
"remove_labels": "Удалить метки",
|
||||
"title": "Изменить данные элемента"
|
||||
},
|
||||
"selectable": {
|
||||
"card": "Карточка",
|
||||
"items": "Элементы",
|
||||
"no_items": "Нет элементов для отображения",
|
||||
"select_all": "Выбрать всё",
|
||||
"select_card": "Выбрать карточку",
|
||||
"select_row": "Выбрать строку",
|
||||
"table": "Таблица"
|
||||
},
|
||||
"table": {
|
||||
"dropdown": {
|
||||
"actions": "Действия",
|
||||
"change_labels": "Изменить метки",
|
||||
"change_labels_success": "Метки изменены",
|
||||
"change_location": "Изменить расположение",
|
||||
"change_location_success": "Расположение изменено",
|
||||
"create_maintenance_item": "Создать запись об обслуживании для элемента",
|
||||
"create_maintenance_selected": "Создать запись об обслуживании для выбранных элементов",
|
||||
"create_maintenance_success": "Записи об обслуживании созданы",
|
||||
"delete_confirmation": "Вы уверены, что хотите удалить выбранные элементы? Это действие нельзя отменить.",
|
||||
"delete_item": "Удалить элемент",
|
||||
"delete_selected": "Удалить выбранные элементы",
|
||||
"download_csv": "Скачать таблицу в формате CSV",
|
||||
"download_json": "Скачать таблицу в формате JSON",
|
||||
"duplicate_item": "Дублировать элемент",
|
||||
"duplicate_selected": "Дублировать выбранные элементы",
|
||||
"error_deleting": "Ошибка при удалении элемента",
|
||||
"error_duplicating": "Ошибка при дублировании элемента",
|
||||
"open_menu": "Открыть меню",
|
||||
"open_multi_tab_warning": "В целях безопасности браузеры по умолчанию не разрешают одновременное открытие нескольких вкладок. Чтобы изменить это, следуйте документации:",
|
||||
"toggle_expand": "Развернуть/Свернуть",
|
||||
"view_item": "Просмотреть элемент",
|
||||
"view_items": "Просмотреть элементы"
|
||||
},
|
||||
"headers": "Заголовки",
|
||||
"page": "Страница",
|
||||
"quick_actions": "Включить быстрые действия и выбор",
|
||||
"rows_per_page": "Строк на странице",
|
||||
"selected_rows": "{selected} из {total} строк выбрано.",
|
||||
"table_settings": "Настройки таблицы",
|
||||
"view_item": "Просмотр элемента"
|
||||
}
|
||||
@@ -193,6 +230,65 @@
|
||||
"quick_menu": {
|
||||
"no_results": "Результаты не найдены.",
|
||||
"shortcut_hint": "Используйте цифровые клавиши, чтобы быстро выбрать действие."
|
||||
},
|
||||
"template": {
|
||||
"apply_template": "Применить шаблон",
|
||||
"card": {
|
||||
"delete": "Удалить шаблон",
|
||||
"duplicate": "Дублировать шаблон",
|
||||
"edit": "Редактировать шаблон"
|
||||
},
|
||||
"confirm_delete": "Удалить этот шаблон?",
|
||||
"create_modal": {
|
||||
"title": "Создать шаблон"
|
||||
},
|
||||
"detail": {
|
||||
"default_values": "Стандартное значение",
|
||||
"updated": "Обновлено"
|
||||
},
|
||||
"edit_modal": {
|
||||
"title": "Редактировать Шаблон"
|
||||
},
|
||||
"empty_value": "(пусто)",
|
||||
"form": {
|
||||
"custom_fields": "Пользовательские Поля",
|
||||
"default_item_values": "Значения элементов по умолчанию",
|
||||
"default_location": "Местоположение по умолчанию",
|
||||
"default_value": "Значение по умолчанию",
|
||||
"field_name": "Название поля",
|
||||
"item_description": "Описание элемента",
|
||||
"item_name": "Название Элемента",
|
||||
"lifetime_warranty": "Пожизненная гарантия",
|
||||
"location": "Местоположение",
|
||||
"manufacturer": "Производитель",
|
||||
"model_number": "Номер модели",
|
||||
"no_custom_fields": "Пользовательские поля отсутствуют.",
|
||||
"template_description": "Описание шаблона",
|
||||
"template_name": "Название шаблона"
|
||||
},
|
||||
"hide_defaults": "Скрыть значения по умолчанию",
|
||||
"save_as_template": "Сохранить как Шаблон",
|
||||
"selector": {
|
||||
"label": "Шаблон (необязательно)",
|
||||
"not_found": "Шаблон не найден",
|
||||
"search": "Поиск шаблонов…",
|
||||
"select": "Выбрать шаблон…"
|
||||
},
|
||||
"show_defaults": "Показать значения по умолчанию",
|
||||
"toast": {
|
||||
"applied": "Шаблон \"{name}\" применён",
|
||||
"create_failed": "Ошибка создания шаблона",
|
||||
"created": "Шаблон создан",
|
||||
"delete_failed": "Ошибка при удалении шаблона",
|
||||
"deleted": "Шаблон удалён",
|
||||
"duplicate_failed": "Не удалось дублировать шаблон",
|
||||
"duplicated": "Шаблон продублирован как \"{name}\"",
|
||||
"load_failed": "Не удалось загрузить детали шаблона",
|
||||
"saved_as_template": "Предмет сохранен как шаблон \"{name}\"",
|
||||
"update_failed": "Ошибка обновления шаблона",
|
||||
"updated": "Шаблон обновлен"
|
||||
},
|
||||
"using_template": "Используется шаблон: {name}"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
@@ -230,7 +326,9 @@
|
||||
"maintenance": "Техническое обслуживание и ремонт",
|
||||
"name": "Имя",
|
||||
"navigate": "Навигация",
|
||||
"no": "Нет",
|
||||
"password": "Пароль",
|
||||
"preview": "Предпросмотр",
|
||||
"quantity": "Количество",
|
||||
"read_docs": "Прочитать документацию",
|
||||
"return_home": "Вернуться домой",
|
||||
@@ -243,7 +341,8 @@
|
||||
"updating": "Обновление",
|
||||
"value": "Значение",
|
||||
"version": "Версия: { version }",
|
||||
"welcome": "Добро пожаловать, { username }"
|
||||
"welcome": "Добро пожаловать, { username }",
|
||||
"yes": "Да"
|
||||
},
|
||||
"home": {
|
||||
"labels": "Метки",
|
||||
@@ -260,6 +359,7 @@
|
||||
"dont_join_group": "Не хотите ли вступить в группу?",
|
||||
"joining_group": "Вы присоединяетесь к уже существующей группе!",
|
||||
"login": "Войти",
|
||||
"or": "или",
|
||||
"register": "Зарегистрироваться",
|
||||
"remember_me": "Запомнить меня",
|
||||
"set_email": "Какой у вас адрес электронной почты?",
|
||||
@@ -271,6 +371,14 @@
|
||||
"invalid_email": "Неверный адрес электронной почты",
|
||||
"invalid_email_password": "Неверный email или пароль",
|
||||
"login_success": "Вход выполнен успешно",
|
||||
"oidc_access_denied": "Доступ запрещен: ваша учетная запись не имеет необходимой роли/членства в группе",
|
||||
"oidc_auth_failed": "Ошибка OIDC аутентификации",
|
||||
"oidc_invalid_response": "Получен недопустимый ответ OIDC",
|
||||
"oidc_provider_error": "Поставщик OIDC вернул ошибку",
|
||||
"oidc_security_error": "Ошибка безопасности OIDC - возможная атака CSRF",
|
||||
"oidc_session_expired": "OIDC сессия истекла",
|
||||
"oidc_token_expired": "OIDC токен истек",
|
||||
"oidc_token_invalid": "Недействительная подпись токена OIDC",
|
||||
"problem_registering": "Проблема при регистрации пользователя",
|
||||
"user_registered": "Пользователь зарегистрирован"
|
||||
}
|
||||
@@ -291,6 +399,18 @@
|
||||
"description": "Описание",
|
||||
"details": "Подробнее",
|
||||
"drag_and_drop": "Перетащите файлы сюда или нажмите, чтобы выбрать файлы",
|
||||
"duplicate": {
|
||||
"copy_attachments": "Копировать вложения",
|
||||
"copy_custom_fields": "Копировать настраиваемые поля",
|
||||
"copy_maintenance": "Копировать данные об обслуживании",
|
||||
"custom_prefix": "Копировать префикс",
|
||||
"enable_custom_prefix": "Включить пользовательский префикс",
|
||||
"override_instructions": "Чтобы переопределить эти настройки, удерживайте Shift при нажатии кнопки дублирования.",
|
||||
"prefix": "Копия ",
|
||||
"prefix_instructions": "Этот префикс будет добавлен в начало имени дублирующегося элемента.",
|
||||
"temporary_title": "Временные настройки",
|
||||
"title": "Настройки дублирования"
|
||||
},
|
||||
"edit": {
|
||||
"edit_attachment_dialog": {
|
||||
"attachment_title": "Название вложения",
|
||||
@@ -299,7 +419,8 @@
|
||||
"primary_photo_sub": "Эта функция доступна только для фотографий. Только одно фото может быть основным. Если вы выберете эту опцию, текущее основное фото, если таковое имеется, будет отменено.",
|
||||
"select_type": "Выберите тип",
|
||||
"title": "Редактирование вложения"
|
||||
}
|
||||
},
|
||||
"view_image": "Просмотреть изображение"
|
||||
},
|
||||
"edit_details": "Редактирование деталей",
|
||||
"field_selector": "Поле выбора",
|
||||
@@ -343,7 +464,7 @@
|
||||
"select_field": "Выберите поле",
|
||||
"serial_number": "Серийный номер",
|
||||
"show_advanced_view_options": "Показать дополнительные параметры",
|
||||
"sold_at": "Место продажи",
|
||||
"sold_at": "Дата продажи",
|
||||
"sold_details": "Детали продажи",
|
||||
"sold_price": "Цена продажи",
|
||||
"sold_to": "Покупатель",
|
||||
@@ -397,6 +518,7 @@
|
||||
"update_label": "Обновить метку"
|
||||
},
|
||||
"languages": {
|
||||
"bs-BA": "Боснийский (Босния и Герцеговина)",
|
||||
"ca": "Каталанский",
|
||||
"cs-CZ": "Чешский",
|
||||
"de": "Немецкий",
|
||||
@@ -423,6 +545,7 @@
|
||||
"th-TH": "Тайский",
|
||||
"tr": "Турецкий",
|
||||
"uk-UA": "Украинский",
|
||||
"vi-VN": "Вьетнамский",
|
||||
"zh-CN": "Китайский (упрощенный)",
|
||||
"zh-HK": "Китайский (Гонконг)",
|
||||
"zh-MO": "Китайский (Макао)",
|
||||
@@ -492,8 +615,15 @@
|
||||
"profile": "Профиль",
|
||||
"scanner": "Сканер",
|
||||
"search": "Поиск",
|
||||
"templates": "Шаблоны",
|
||||
"tools": "Инструменты"
|
||||
},
|
||||
"pages": {
|
||||
"templates": {
|
||||
"no_templates": "Шаблонов пока нет.",
|
||||
"title": "Шаблоны"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"active": "Активный",
|
||||
"change_password": "Изменить пароль",
|
||||
@@ -511,6 +641,7 @@
|
||||
"group_settings_sub": "Настройки общей группы. Для применения изменений возможно потребуется перезагрузить страницу.",
|
||||
"inactive": "Неактивный",
|
||||
"language": "Язык",
|
||||
"legacy_image_fit": "{ currentValue, select, true {Отключить старый режим: Вписать с полями} false {Включить старый режим: Заполнить с обрезкой} other {Ошибка}}",
|
||||
"new_password": "Новый пароль",
|
||||
"no_notifiers": "Нет настроенных уведомлений",
|
||||
"no_override": "Без переопределения",
|
||||
@@ -620,7 +751,7 @@
|
||||
"import_export_sub": "Импортировать или экспортировать ваш инвентарь в или из CSV файла. Это полезно при миграции вашего инвентаря в новый экземпляр Homebox.",
|
||||
"reports": "Отчеты",
|
||||
"reports_set": {
|
||||
"asset_labels": "Метки с ID активов",
|
||||
"asset_labels": "Метки инвентарных объектов",
|
||||
"asset_labels_button": "Генератор меток",
|
||||
"asset_labels_sub": "Генерирует PDF с метками для диапазона ID активов. Отсутствует привязка к Вашему инвентарю, так что вы можете напечатать метки заранее и применить их к вашему инвентарю позже.",
|
||||
"bill_of_materials": "Ведомость материалов",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user