Compare commits

..

91 Commits

Author SHA1 Message Date
Phil
9bc6b4519c Fix duplicated language names 2025-12-28 21:45:57 +00:00
copilot-swe-agent[bot]
6f77eae638 fix: Add region qualifiers to Portuguese and Chinese variants for clarity
Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-28 21:43:52 +00:00
Phil
6926aabd62 Remove redundant imports 2025-12-28 21:31:52 +00:00
copilot-swe-agent[bot]
b735ad12fd chore: Remove pycache and update gitignore
Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-28 21:27:31 +00:00
copilot-swe-agent[bot]
f3e817e139 fix: Address feedback - add Weblate API, native names, validation, and completion threshold
Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-28 21:27:06 +00:00
copilot-swe-agent[bot]
153ecd1094 feat: Add automated workflow to update language names in en.json
Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-28 21:14:49 +00:00
copilot-swe-agent[bot]
0be54da9cf Initial plan 2025-12-28 21:09:09 +00:00
Phil
e4aa38b264 Merge pull request #1186 from sysadminsmedia/copilot/fix-wipe-inventory-issue 2025-12-28 20:31:41 +00:00
Phil
e60f005990 Merge branch 'main' into copilot/fix-wipe-inventory-issue 2025-12-28 20:31:06 +00:00
Copilot
7dfaa0298b Add repository onboarding instructions for Copilot coding agents (#1187)
* Initial plan

* Initial plan for repository onboarding instructions

Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>

* Add comprehensive coding agent instructions for Homebox repository

Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>

* Optimize instructions to meet 2-page limit while maintaining completeness

Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>

* Add 4 path-specific instruction files for backend and frontend architecture

Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>

* Path application for instructions

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>
Co-authored-by: Matt <tankerkiller125@users.noreply.github.com>
2025-12-28 15:03:51 -05:00
Phil
fbe7382acd Merge branch 'main' into copilot/fix-wipe-inventory-issue 2025-12-28 19:27:00 +00:00
copilot-swe-agent[bot]
1003223b47 Add E2E test for wipe inventory functionality
Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-28 19:24:14 +00:00
Logan Miller
3c532896f5 fix(items): update quantity display immediately on increment/decrement (#1181)
Use API response data to update item state instead of directly mutating the quantity property.
Fixes #1180
2025-12-28 19:22:21 +00:00
copilot-swe-agent[bot]
4ba1a263c8 Publish mutation events when wiping labels and locations
Co-authored-by: tonyaellie <46281725+tonyaellie@users.noreply.github.com>
2025-12-28 19:06:04 +00:00
copilot-swe-agent[bot]
94f0123d9c Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com> 2025-12-28 18:19:00 +00:00
copilot-swe-agent[bot]
1f6782f8be Final documentation update
Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-28 18:12:23 +00:00
copilot-swe-agent[bot]
ec8703114f Reset isConfirming flag after dialog closes
Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-28 18:08:54 +00:00
copilot-swe-agent[bot]
5cd7792701 Fix callback order and restore isConfirming flag
Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-28 18:05:29 +00:00
copilot-swe-agent[bot]
d82c52df26 Fix wipe inventory by replacing AlertDialogAction with Button
Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-28 18:02:08 +00:00
copilot-swe-agent[bot]
033c17552b Update plan with root cause analysis
Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-28 18:01:24 +00:00
copilot-swe-agent[bot]
2355438962 Initial plan 2025-12-28 17:45:18 +00:00
Phil
2a6773d1d6 Fix wipe inventory action to use correct onclose 2025-12-28 17:29:14 +00:00
copilot-swe-agent[bot]
c8c07e2878 Fix code review nitpick: remove empty comment line
Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-28 17:27:01 +00:00
copilot-swe-agent[bot]
a3c05c3497 Fix WipeInventoryDialog to use DialogProvider onClose correctly
Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-28 17:22:57 +00:00
copilot-swe-agent[bot]
ab0647fe68 Add comprehensive integration tests for wipe inventory
Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-28 17:20:17 +00:00
copilot-swe-agent[bot]
0b616225a6 Code review and security checks complete
Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-28 17:08:07 +00:00
copilot-swe-agent[bot]
dc9c7b76f2 Add tests for WipeInventory functionality
Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-28 17:05:10 +00:00
copilot-swe-agent[bot]
b99102e093 Fix wipe inventory: delete maintenance records before items
Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-28 17:01:12 +00:00
copilot-swe-agent[bot]
3077602f93 Initial plan 2025-12-28 16:50:34 +00:00
Phil
2bd6ff580a Add option to wipe inventory 2025-12-28 16:32:53 +00:00
copilot-swe-agent[bot]
35941583c8 Fix frontend linting errors in WipeInventoryDialog
- Replaced BaseModal and BaseButton with AlertDialog components from ui library
- Added proper imports for AlertDialog, AlertDialogContent, AlertDialogHeader, etc.
- Fixed prettier formatting issues (auto-fixed by eslint --fix)
- Fixed Tailwind CSS shorthand (h-4 w-4 -> size-4)
- Added addAlert/removeAlert for dialog provider integration
- All linting and type-checking errors resolved

Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-28 16:26:09 +00:00
copilot-swe-agent[bot]
d576c89c7e Add maintenance records checkbox to wipe inventory dialog
- Added wipeMaintenance parameter to backend WipeInventory method
- Added checkbox in WipeInventoryDialog for wiping maintenance records
- Updated demo mode error message to include maintenance records
- Added translation key "wipe_inventory_maintenance" to en.json
- Maintenance records are linked to items via HasItemWith query
- Follows same structure as labels and locations checkboxes

Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-28 16:08:33 +00:00
copilot-swe-agent[bot]
ff355f3cd8 Add demo mode check for wipe inventory action
Added frontend check to display error modal when user attempts to wipe inventory in demo mode. The modal shows: "Inventory, labels and locations cannot be wiped whilst Homebox is in demo mode. Please ensure that you are not in demo mode and try again."

Backend already had demo mode protection returning 403 Forbidden.

Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-28 15:56:42 +00:00
copilot-swe-agent[bot]
03dc7fa841 Include label and location counts in wipe inventory total
Added labelCount and locationCount to the deleted counter to provide accurate total count of all deleted entities (items, labels, locations) in the wipe inventory result.

Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-28 15:45:49 +00:00
copilot-swe-agent[bot]
7aaaa346ab Add wipe inventory options for labels/locations and owner-only restriction
- Added WipeInventoryDialog component with checkboxes for wiping labels and locations
- Modified backend WipeInventory method to accept wipeLabels and wipeLocations parameters
- Added owner check in HandleWipeInventory to restrict action to group owners only
- Updated frontend API client to send wipe options
- Added new translation keys for checkbox labels and owner note
- Integrated dialog into app layout and updated tools.vue to use new dialog

Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-28 15:41:13 +00:00
Phil
27309e61da Add error import to v1_ctrl_actions.go 2025-12-28 15:29:14 +00:00
Phil
61816acdaa Merge branch 'main' into copilot/add-wipe-inventory-action 2025-12-28 15:28:07 +00:00
Phil
c31410727b Update backend/app/api/handlers/v1/v1_ctrl_actions.go
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-12-28 15:19:19 +00:00
Matt
4557df86ed Remove 32bit builds (#1000)
* Remove 32bit builds

* Use native Github runners

* Fix arm builds not getting runner

* Fix lint job
2025-12-28 00:28:20 -05:00
Matthew Kilgore
b8910f1b21 This should wipe out action related security flags 2025-12-27 19:09:27 -05:00
Matthew Kilgore
48e4f8da2a That should be all the actions pinned 2025-12-27 19:05:33 -05:00
Matthew Kilgore
1e0158c27e Fix copilot action 2025-12-27 18:52:12 -05:00
Matthew Kilgore
4fb3ddd661 Pin github copilot actions 2025-12-27 18:51:12 -05:00
Matthew Kilgore
690005de06 Harden all github actions 2025-12-27 18:46:14 -05:00
copilot-swe-agent[bot]
23da976494 Remove translation changes from non-English locale files
Only en.json should have the new translation keys. Translators will add translations to other language files later.

Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>
2025-12-27 22:29:18 +00:00
Harrison Conlin
f0b8bb8b7f refactor(backend): use constants for database driver names (#1177)
magic constants are bad m'kay
2025-12-27 16:16:48 -05:00
Katos
ecc9fa1959 Disable triggers in upgrade-test.yaml
Comment out the workflow triggers in upgrade-test.yaml
2025-12-27 20:28:31 +00:00
Katos
7068a85dfb Merge pull request #1178 from sysadminsmedia/copilot/create-ci-cd-pipeline
Add CI/CD workflow for upgrade testing with data integrity verification
2025-12-27 20:18:37 +00:00
Katos
c73922c754 Merge branch 'main' into copilot/create-ci-cd-pipeline 2025-12-27 20:16:41 +00:00
Matthew Kilgore
ae2179c01c Add blog link 2025-12-27 12:05:05 -05:00
copilot-swe-agent[bot]
09e056a3fb Move upgrade verification tests to separate directory
- Move upgrade-verification.spec.ts from test/e2e/ to test/upgrade/
- This prevents the test from running during normal E2E CI runs
- The upgrade test is only meant for the upgrade-test workflow
- Update workflow and documentation to reflect new location

Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-27 16:12:51 +00:00
Katos
4abfc76865 Fix CodeRabbit date quoting issue 2025-12-27 16:01:57 +00:00
copilot-swe-agent[bot]
aa48c958d7 Improve error handling and comments in WipeInventory method
Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-27 15:03:43 +00:00
copilot-swe-agent[bot]
2bd6d0a9e5 Add Wipe Inventory feature with backend and frontend implementation
Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-27 14:52:04 +00:00
copilot-swe-agent[bot]
88275620f2 Initial plan for Wipe Inventory feature
Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-27 14:47:42 +00:00
copilot-swe-agent[bot]
5a058250e6 Initial plan 2025-12-27 14:43:31 +00:00
copilot-swe-agent[bot]
afd7a10003 Fix TypeScript null check in upgrade-verification test
Add null check for pageContent before accessing length property

Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-27 14:34:59 +00:00
copilot-swe-agent[bot]
8eedd1e39d Fix ESLint errors in upgrade-verification.spec.ts
- Remove unused 'path' import
- Replace 'any' types with proper TypeScript interfaces
- Fix all Prettier formatting issues

Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-27 14:32:57 +00:00
copilot-swe-agent[bot]
fedeb1a7e5 Add proper GITHUB_TOKEN permissions to workflow
Set minimal required permissions (contents:read, packages:read) to follow security best practices

Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-27 14:14:52 +00:00
copilot-swe-agent[bot]
69b31a3be5 Improve test reliability and fix security issues
- Replace waitForTimeout with waitForSelector and waitForLoadState
- Remove eval security risk in bash script
- Use proper wait mechanisms for better test reliability

Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-27 14:12:22 +00:00
copilot-swe-agent[bot]
31d306ca05 Add comprehensive documentation for upgrade test workflow
Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-27 14:09:37 +00:00
copilot-swe-agent[bot]
1bfb716cea Add upgrade test workflow with data generation and verification
Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-27 14:08:13 +00:00
copilot-swe-agent[bot]
13b1524c56 Initial plan for upgrade test workflow
Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com>
2025-12-27 14:06:05 +00:00
copilot-swe-agent[bot]
b18599b6f4 Initial plan 2025-12-27 14:02:31 +00:00
Matthew Kilgore
473027c1ae Fix notifiers gettings wiped 2025-12-26 17:27:18 -05:00
Matthew Kilgore
3a77440996 Fix flip flopped columns 2025-12-26 15:50:04 -05:00
Matthew Kilgore
731765c36c Make sure the right columns get migrated into the correct columns 2025-12-26 15:09:02 -05:00
Matthew Kilgore
a86b1bd17b Update dependencies 2025-12-26 09:51:33 -05:00
Matthew Kilgore
064b298968 Merge branch 'main' into main-weblate
# Conflicts:
#	frontend/locales/en.json
2025-12-26 09:28:10 -05:00
Tonya
2638f218f3 fix: templates that dont have a location set (#1160) 2025-12-24 17:30:46 +00:00
Dan
0f4f398b5a Added documentation for the external label service feature. (#1018)
* Added documentation for the external label service feature. Re-ordered the columns in the config page to make it easier to read.

* Update docs/en/configure/index.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: Matt <tankerkiller125@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-12-23 21:36:19 +00:00
Copilot
545993a8aa Fix Windows attachment path encoding in blob storage operations (#1144)
* Initial plan

* Initial plan for fixing Windows attachment path issue

Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>

* Fix Windows attachment path encoding issue by normalizing to forward slashes

Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>

* Refactor path normalization into helper function per code review

Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>

* Update progress - all checks complete

Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>
2025-12-23 10:27:42 -05:00
tonyaellie
a1947dd09e feat: autosave after image upload 2025-12-22 23:46:29 +00:00
tonyaellie
018f1f5977 fix: use logical sorting for locations 2025-12-22 23:34:29 +00:00
tonyaellie
9a9e3d462e feat: add a clear button for selectors and stop create modal overflow 2025-12-22 23:24:01 +00:00
Weblate
fc8b6f0dcf Translated using Weblate (German)
Currently translated at 100.0% (609 of 609 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 95.4% (581 of 609 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (609 of 609 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (609 of 609 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (609 of 609 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (609 of 609 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (609 of 609 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (609 of 609 strings)

Translated using Weblate (Russian)

Currently translated at 96.2% (586 of 609 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (609 of 609 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (609 of 609 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (609 of 609 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (609 of 609 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (609 of 609 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (609 of 609 strings)

Translated using Weblate (Czech)

Currently translated at 99.8% (608 of 609 strings)

Translated using Weblate (German)

Currently translated at 99.3% (605 of 609 strings)

Translated using Weblate (German)

Currently translated at 99.1% (604 of 609 strings)

Translated using Weblate (German)

Currently translated at 99.1% (604 of 609 strings)

Translated using Weblate (German)

Currently translated at 90.3% (550 of 609 strings)

Translated using Weblate (German)

Currently translated at 90.3% (550 of 609 strings)

Translated using Weblate (German)

Currently translated at 90.1% (549 of 609 strings)

Translated using Weblate (German)

Currently translated at 89.9% (548 of 609 strings)

Translated using Weblate (Indonesian)

Currently translated at 60.0% (366 of 609 strings)

Translated using Weblate (Thai)

Currently translated at 22.0% (134 of 609 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 80.4% (490 of 609 strings)

Translated using Weblate (Slovak)

Currently translated at 84.8% (517 of 609 strings)

Translated using Weblate (Finnish)

Currently translated at 53.3% (325 of 609 strings)

Translated using Weblate (Ukrainian)

Currently translated at 59.7% (364 of 609 strings)

Translated using Weblate (English)

Currently translated at 100.0% (609 of 609 strings)

Translated using Weblate (Greek)

Currently translated at 0.3% (2 of 551 strings)

Added translation using Weblate (Greek)

Translated using Weblate (Italian)

Currently translated at 99.8% (550 of 551 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (550 of 551 strings)

Translated using Weblate (Telugu)

Currently translated at 0.9% (5 of 551 strings)

Translated using Weblate (Telugu)

Currently translated at 0.9% (5 of 551 strings)

Added translation using Weblate (Telugu)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Bosnian)

Currently translated at 21.9% (121 of 551 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Turkish)

Currently translated at 91.6% (505 of 551 strings)

Translated using Weblate (Turkish)

Currently translated at 89.2% (492 of 551 strings)

Translated using Weblate (Turkish)

Currently translated at 89.2% (492 of 551 strings)

Translated using Weblate (Russian)

Currently translated at 99.2% (547 of 551 strings)

Translated using Weblate (Ukrainian)

Currently translated at 65.8% (363 of 551 strings)

Translated using Weblate (Ukrainian)

Currently translated at 64.4% (355 of 551 strings)

Translated using Weblate (Ukrainian)

Currently translated at 62.7% (346 of 551 strings)

Translated using Weblate (Ukrainian)

Currently translated at 62.4% (344 of 551 strings)

Translated using Weblate (Ukrainian)

Currently translated at 61.1% (337 of 551 strings)

Translated using Weblate (Ukrainian)

Currently translated at 60.4% (333 of 551 strings)

Translated using Weblate (Ukrainian)

Currently translated at 59.3% (327 of 551 strings)

Translated using Weblate (Thai)

Currently translated at 24.1% (133 of 551 strings)

Translated using Weblate (Thai)

Currently translated at 24.1% (133 of 551 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (French)

Currently translated at 94.1% (519 of 551 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (550 of 551 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (German)

Currently translated at 99.8% (550 of 551 strings)

Translated using Weblate (German)

Currently translated at 99.8% (550 of 551 strings)

Translated using Weblate (German)

Currently translated at 99.8% (550 of 551 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.9% (545 of 551 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.9% (545 of 551 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.9% (545 of 551 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.9% (545 of 551 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.9% (545 of 551 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.9% (545 of 551 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.9% (545 of 551 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.9% (545 of 551 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.9% (545 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (551 of 551 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (550 of 550 strings)

Translated using Weblate (Polish)

Currently translated at 98.7% (543 of 550 strings)

Translated using Weblate (Polish)

Currently translated at 98.7% (543 of 550 strings)

Translated using Weblate (Polish)

Currently translated at 98.7% (543 of 550 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 98.5% (542 of 550 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 98.5% (542 of 550 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 98.3% (541 of 550 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 98.3% (541 of 550 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.3% (365 of 550 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.3% (365 of 550 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.3% (365 of 550 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.3% (365 of 550 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.3% (365 of 550 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.3% (365 of 550 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.3% (365 of 550 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.3% (365 of 550 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 93.4% (514 of 550 strings)

Translated using Weblate (Swedish)

Currently translated at 68.3% (376 of 550 strings)

Translated using Weblate (Swedish)

Currently translated at 68.3% (376 of 550 strings)

Translated using Weblate (Swedish)

Currently translated at 68.3% (376 of 550 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 98.1% (540 of 550 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (550 of 550 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (550 of 550 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (550 of 550 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (550 of 550 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (550 of 550 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (550 of 550 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (550 of 550 strings)

Translated using Weblate (German)

Currently translated at 96.0% (528 of 550 strings)

Translated using Weblate (German)

Currently translated at 96.0% (528 of 550 strings)

Translated using Weblate (Turkish)

Currently translated at 87.7% (482 of 549 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (518 of 518 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (518 of 518 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (518 of 518 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (518 of 518 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (518 of 518 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (518 of 518 strings)

Translated using Weblate (Arabic)

Currently translated at 0.7% (4 of 518 strings)

Translated using Weblate (Arabic)

Currently translated at 0.5% (3 of 518 strings)

Added translation using Weblate (Arabic)

Translated using Weblate (Thai)

Currently translated at 22.9% (119 of 518 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (518 of 518 strings)

Translated using Weblate (Swedish)

Currently translated at 71.2% (369 of 518 strings)

Translated using Weblate (Swedish)

Currently translated at 71.2% (369 of 518 strings)

Co-authored-by: Adam Havránek <adamhavra@seznam.cz>
Co-authored-by: Aniruddh Kotte <aniruddhkotte@gmail.com>
Co-authored-by: BoneGear <bonegear@hotmail.com>
Co-authored-by: DevHrytsan <3axapHrytsan@gmail.com>
Co-authored-by: Eisa Al Shamsi <awwase@gmail.com>
Co-authored-by: Hannes Salen <hannes.salen@gmail.com>
Co-authored-by: Heine Olsen <olsen10051988@gmail.com>
Co-authored-by: Henrique dos Santos Wisniewski <henriqueswisniewski@gmail.com>
Co-authored-by: Jackxwb <xwb9606@163.com>
Co-authored-by: Jan Fader <jan.fader@web.de>
Co-authored-by: JorgeS15 <jorgea15santos@gmail.com>
Co-authored-by: Loffa <jesperfalk94@gmail.com>
Co-authored-by: Marcelo Sandrini <sandrini.marcelo@gmail.com>
Co-authored-by: Matthew Kilgore <matthew@kilgore.dev>
Co-authored-by: Matvey <mrspanky@yandex.ru>
Co-authored-by: Mikolaj Wolicki <MIKOLAJW1997@gmail.com>
Co-authored-by: Mirad Maglic <mirad.maglic@gmail.com>
Co-authored-by: Muhammad Ikhsan <pararang@gmail.com>
Co-authored-by: Mutagenic <mkardas@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: Ricardo González <notorius28@gmail.com>
Co-authored-by: Robert Eggl <robert@eggl.dev>
Co-authored-by: Sara Wattanasombat <saraten2@gmail.com>
Co-authored-by: Simone Girardi <s.girardi92@gmail.com>
Co-authored-by: Slydite4 <39199098+Slydite4@users.noreply.github.com>
Co-authored-by: Stratos Palaiologos <stpa03@betssongroup.com>
Co-authored-by: Supphakorn <supphakorn5343@gmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: WilliamStark <yujinghao007@163.com>
Co-authored-by: Yao Yimeng <yym900902@gmail.com>
Co-authored-by: akrstlv <zmilex@gmail.com>
Co-authored-by: arsenius88 <arsenovich_andr@ukr.net>
Co-authored-by: buzz <buzz.eclair@gmail.com>
Co-authored-by: dARK raVEr <Dark.Raver@gmx.net>
Co-authored-by: efe <vastly-fax-brim@duck.com>
Co-authored-by: fjrefluxx <julianzobel@gmail.com>
Co-authored-by: jesper rezler lang <jesper.rezler.lang@gmail.com>
Co-authored-by: jjxxzz <jaro689@gmail.com>
Co-authored-by: noxmyn <vladcraft93@gmail.com>
Co-authored-by: sg4r3z <giovannigln@gmail.com>
Co-authored-by: swedishpete <nyhetsutskick@outlook.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ar/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/bs/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/cs/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/da/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/de/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/el/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/en/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/es/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/fi/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/fr/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/id/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/it/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/nb_NO/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/nl/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/pl/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/pt_BR/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/pt_PT/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ru/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/sk/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/sv/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/te/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/th/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/tr/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/uk/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/zh_Hans/
Translation: Homebox/Frontend
2025-12-22 11:19:55 +00:00
tonyaellie
37890c2a22 docs: update OIDC configuration details 2025-12-22 11:19:49 +00:00
Tonya
096b682f0a Improve oidc docs and fix attachment issue (#1153)
* fix: sort auth issues for oidc

* feat: improve oidc docs
2025-12-21 22:11:38 +00:00
Tonya
e4d8bb2ada chore: use example.com for example
better safe than sorry
2025-12-20 21:50:44 +00:00
Katos
3becf046e6 Merge pull request #1147 from sysadminsmedia/katos/docs-variable
Update max file upload environment variable
2025-12-20 16:01:04 +00:00
Katos
a21b3257d4 Update max file upload environment variable 2025-12-20 15:57:14 +00:00
Robert Eggl
5f9ab577bb fix: request camera permission in ScannerModal (#1113)
* feat: request camera permission in ScannerModal

* chore: simplify source code
2025-12-19 21:47:37 +00:00
Robert Eggl
0a969bb64d fix(sidebar): prevent dropdown menu layout shift on hover (#1116) 2025-12-19 21:38:06 +00:00
Sarun Nuntaviriyakul
2d1d3d927b Update log level options in configuration documentation (#1127) 2025-12-12 13:33:12 -05:00
Matthew Kilgore
540028a22e fix: broken docker.io attestation 2025-12-11 22:24:11 -05:00
Nelson Cabete
14b0d51894 Update docs to reference disable_https instead of disableSsl on Storage Configuration page (#1124)
Co-authored-by: Nelson Cabete <me@ncabete.com>
2025-12-09 20:56:05 -05:00
Matt
4334f926c0 Fix postgres nullable password migration to be at end 2025-12-09 14:44:53 -05:00
Robert Eggl
1088972ff0 docs: add missing barcode spider env var (#1114) 2025-12-08 20:17:45 -05:00
Matthew Kilgore
55e247ac71 Fix missing postgres OIDC migration 2025-12-08 20:10:36 -05:00
Matthew Kilgore
05a2700718 Merge remote-tracking branch 'origin/main' 2025-12-06 18:14:12 -05:00
Matthew Kilgore
06c11cdcd5 Ensure options are up to date in docs 2025-12-06 18:14:06 -05:00
117 changed files with 9289 additions and 2317 deletions

10
.github/ISSUE_TEMPLATE/internal.md vendored Normal file
View 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]

View 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`

View 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

View 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

View 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.

View 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
View 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
View 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

View 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

View File

@@ -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:

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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'
});
}

View File

@@ -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

View File

@@ -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

View File

@@ -1,5 +1,11 @@
name: Pull Request CI
permissions:
contents: read
actions: read
checks: write
pull-requests: write
on:
pull_request:
branches:

View File

@@ -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

View 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"

View File

@@ -0,0 +1,2 @@
babel
requests

177
.github/workflows/upgrade-test.yaml vendored Normal file
View 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
View File

@@ -67,3 +67,5 @@ frontend/test-results/
frontend/playwright-report/
frontend/blob-report/
frontend/playwright/.cache/
__pycache__/
*.pyc

14
.scaffold/go.sum Normal file
View 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=

View File

@@ -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

View File

@@ -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})
}
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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...))

View File

@@ -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)

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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:

View File

@@ -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": {

View File

@@ -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

View File

@@ -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
)

View File

@@ -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=

View File

@@ -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.)

View File

@@ -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")

View File

@@ -0,0 +1,2 @@
-- +goose Up
ALTER TABLE users ALTER COLUMN password DROP NOT NULL;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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")
}

View File

@@ -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()
}

View File

@@ -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)

View File

@@ -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).

View File

@@ -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)
}

View 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)
}

View File

@@ -1,7 +1,8 @@
package config
const (
DriverSqlite3 = "sqlite3"
DriverSqlite3 = "sqlite3"
DriverPostgres = "postgres"
)
type Storage struct {

View File

@@ -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: {

View File

@@ -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'},
]
},
{

View 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)

View File

@@ -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": {

View File

@@ -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

View File

@@ -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": {

View File

@@ -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:

View File

@@ -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": {

View File

@@ -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

View File

@@ -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
View 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.
:::

View File

@@ -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`).

View File

@@ -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:

View File

@@ -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;

View File

@@ -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 -->

View File

@@ -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;

View File

@@ -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"

View File

@@ -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"

View File

@@ -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);

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,

View File

@@ -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"

View File

@@ -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);

View 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>

View File

@@ -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 */

View File

@@ -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";

View File

@@ -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 || {},
});
}
}

View File

@@ -1150,6 +1150,12 @@ export interface TokenResponse {
token: string;
}
export interface WipeInventoryOptions {
wipeLabels: boolean;
wipeLocations: boolean;
wipeMaintenance: boolean;
}
export interface Wrapped {
item: any;
}

View 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."
}
}
}
}

View File

@@ -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",

View File

@@ -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í",

View File

@@ -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",

View File

@@ -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",

View File

@@ -0,0 +1,12 @@
{
"components": {
"app": {
"create_modal": {
"createAndAddAnother": "Χρησιμοποιήστε τα πλήκτρα {shiftKey} + {enterKey} για να δημιουργήσετε και να προσθέσετε ένα άλλο."
},
"import_dialog": {
"change_warning": "Η συμπεριφορά για τις εισαγωγές με υπάρχοντα import_refs έχει αλλάξει. Εάν υπάρχει ένα import_ref στο αρχείο CSV,  \nτο στοιχείο θα ενημερωθεί με τις τιμές στο αρχείο CSV."
}
}
}
}

View File

@@ -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."
}
}
}

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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",

View File

@@ -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 lultimo 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 dellAssetID sulletichetta. 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 allinterno 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 loperazione 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."
}
}
}

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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.",

View File

@@ -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