Compare commits

..

344 Commits

Author SHA1 Message Date
Matthew Kilgore
5131630640 Try keyless signing for blobs 2025-08-23 18:06:15 -04:00
Matthew Kilgore
b7369b00ee Try keyless signing for blobs 2025-08-23 17:51:55 -04:00
Matthew Kilgore
62ed3fabc2 Fix broken test version of binary build 2025-08-23 17:29:21 -04:00
Matthew Kilgore
304fc7f11f Fix YAML maybe 2025-08-23 17:24:10 -04:00
Matthew Kilgore
1b7a7a1999 Fix YAML maybe 2025-08-23 17:22:29 -04:00
Matthew Kilgore
a63f08ad87 Fix YAML maybe 2025-08-23 17:21:21 -04:00
Matthew Kilgore
9cb1a3f83c Fix YAML maybe 2025-08-23 17:21:01 -04:00
Matthew Kilgore
f86d38412b Fix YAML maybe 2025-08-23 17:20:16 -04:00
Matthew Kilgore
cbbe056d01 Let us test binary builds without publishing new tags 2025-08-23 17:17:10 -04:00
Katos
5f6b1a0805 Update binaries-publish.yaml
Add COSIGN_PWD and COSIGN_YES to workflow to rectify issues with binaries building on Action
2025-08-23 20:07:12 +01:00
Tonya
27e9eb2277 improve dialogs, option to open image dialog in edit then delete (#951)
* fix: change Content-Disposition to inline for proper document display in attachments

* feat: overhaul how dialog system works, add delete to image dialog and add button to open image dialog on edit page

* chore: remove unneeded console log

* fix: ensure cleanup of dialog callbacks on unmount in BarcodeModal, CreateModal, and ImageDialog components
2025-08-23 18:22:33 +00:00
tonyaellie
6fcd10d796 feat: move theme picker to its own component and improve contrast on login screen 2025-08-23 18:05:00 +00:00
Michael Manganiello
377c6c6e0d fix: Remove log.Fatal in favor of returning errors (#953)
* fix: Remove log.Fatal in favor of returning errors

This change is useful for including error tracking, which needs the
application to not terminate immediately, and instead give the tracer
time to capture and flush errors.

* Fix CodeRabbit issues

---------

Co-authored-by: Matthew Kilgore <matthew@kilgore.dev>
2025-08-23 13:09:40 -04:00
Matt
7980e8e90a Create hardened docker image (#955)
* Create hardened docker image

* Remove healthcheck that can't work

* Pin action dependencies

* Further cleanup and hardening

* Fix broken hardened build

* Enhance Dockerfile with healthcheck and optimizations

Added healthcheck helper using a small Go file module and improved Dockerfile structure for readability.

---------

Co-authored-by: Katos <7927609+katosdev@users.noreply.github.com>
2025-08-23 12:57:51 -04:00
Tonya
788d0b1c7e feat: improved duplicate (#927)
* feat: improved duplicate

* feat: enhance item duplication process with transaction handling and error logging for attachments and fields

* feat: add error logging during transaction rollback in item duplication process for better debugging

* feat: don't try and rollback is the commit succeeded

* feat: add customizable duplication options for items, including prefix and field copying settings in API and UI

* fix: simplify duplication checks for custom fields, attachments, and maintenance entries in ItemsRepository duplication method

* refactor: import DuplicateSettings type from composables and sort import issues
2025-08-23 16:17:15 +01:00
Weblate
8b711eda99 Translated using Weblate (Norwegian Bokmål)
Currently translated at 96.6% (489 of 506 strings)

Translated using Weblate (Slovak)

Currently translated at 97.2% (492 of 506 strings)

Translated using Weblate (Ukrainian)

Currently translated at 64.0% (324 of 506 strings)

Translated using Weblate (Hungarian)

Currently translated at 99.4% (503 of 506 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (505 of 506 strings)

Translated using Weblate (Catalan)

Currently translated at 54.5% (276 of 506 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.4% (503 of 506 strings)

Translated using Weblate (Spanish)

Currently translated at 99.4% (503 of 506 strings)

Translated using Weblate (Turkish)

Currently translated at 86.1% (436 of 506 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (506 of 506 strings)

Co-authored-by: Matthew Kilgore <matthew@kilgore.dev>
Co-authored-by: Michael Manganiello <mike@fmanganiello.com.ar>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ca/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/es/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
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/sk/
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-08-22 04:53:59 +00:00
Weblate
bba0d26480 Merge branch 'origin/main' into Weblate. 2025-08-21 23:23:32 +00:00
Matthew Kilgore
789e27e67b Merge remote-tracking branch 'origin/main' 2025-08-21 19:22:49 -04:00
Weblate
1828eae2c3 Translated using Weblate (French)
Currently translated at 96.8% (490 of 506 strings)

Translated using Weblate (English)

Currently translated at 100.0% (506 of 506 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: buzz <buzz.eclair@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/en/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/fr/
Translation: Homebox/Frontend
2025-08-21 19:20:28 -04:00
Natalí Paura
8c87cda9ab Fix label name length (#822)
* Fix label name length

The labels name were shortened to the max length of 20 characters and not taking advantage of extra space. And it was difficult to distinguish between labels with the same prefix.

* run task ui:fix

* fix label selector when creating an item

* feat: sort styles for line wrapping

---------

Co-authored-by: Tonya <tonya@tokia.dev>
2025-08-21 18:52:10 +00:00
Tonya
900604661b fix: change Content-Disposition to inline for proper document display in attachments (#950) 2025-08-21 14:59:13 +00:00
Michael Manganiello
8af1e8fcba fix: Allow up to 1000 characters for label description (#948)
The database schema already supports 1,000 characters for label
description, so this seems just like an oversight.
2025-08-20 15:29:49 -04:00
Weblate
ed7c3dd3f5 Translated using Weblate (French)
Currently translated at 96.8% (490 of 506 strings)

Translated using Weblate (English)

Currently translated at 100.0% (506 of 506 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: buzz <buzz.eclair@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/en/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/fr/
Translation: Homebox/Frontend
2025-08-19 21:58:40 +00:00
Matthew Kilgore
e810571bf1 Merge Bugged Translation Commits 2025-08-19 10:44:22 -04:00
Weblate
1bce1905b6 Translated using Weblate (Japanese)
Currently translated at 97.6% (494 of 506 strings)

Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 07:18:08 +00:00
Weblate
607507ad20 Translated using Weblate (Japanese)
Currently translated at 97.6% (494 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 97.6% (494 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 06:58:53 +00:00
Weblate
ed1b1a2765 Translated using Weblate (Japanese)
Currently translated at 95.4% (483 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 95.4% (483 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 06:41:30 +00:00
Weblate
5f140b34e6 Translated using Weblate (Japanese)
Currently translated at 95.4% (483 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 95.4% (483 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 06:39:32 +00:00
Weblate
3fbf154589 Translated using Weblate (Japanese)
Currently translated at 95.4% (483 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 95.4% (483 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 06:37:59 +00:00
Weblate
2bfd612971 Translated using Weblate (Japanese)
Currently translated at 95.4% (483 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 06:36:34 +00:00
Weblate
fe37c5acc7 Translated using Weblate (Japanese)
Currently translated at 95.4% (483 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 06:34:33 +00:00
Weblate
6be9c18f68 Translated using Weblate (Japanese)
Currently translated at 95.4% (483 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 95.4% (483 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 06:33:36 +00:00
Weblate
7d5d4e7dc7 Translated using Weblate (Japanese)
Currently translated at 95.0% (481 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 06:30:41 +00:00
Weblate
ec7051672f Translated using Weblate (Japanese)
Currently translated at 95.0% (481 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 95.0% (481 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 06:30:14 +00:00
Weblate
008725b300 Translated using Weblate (Japanese)
Currently translated at 94.0% (476 of 506 strings)

Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 06:18:16 +00:00
Weblate
3fb828ee1a Translated using Weblate (Japanese)
Currently translated at 93.4% (473 of 506 strings)

Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 06:03:16 +00:00
Weblate
0adebeaf8d Translated using Weblate (Japanese)
Currently translated at 93.2% (472 of 506 strings)

Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 06:00:52 +00:00
Weblate
c1a944411c Translated using Weblate (Japanese)
Currently translated at 91.6% (464 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 05:00:23 +00:00
Weblate
1aaab56045 Translated using Weblate (Japanese)
Currently translated at 91.6% (464 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:58:45 +00:00
Weblate
87ecb217fb Translated using Weblate (Japanese)
Currently translated at 91.6% (464 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 91.6% (464 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:58:33 +00:00
Weblate
91e4df652d Translated using Weblate (Japanese)
Currently translated at 91.5% (463 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:58:27 +00:00
Weblate
40ee154508 Translated using Weblate (Japanese)
Currently translated at 91.3% (462 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:56:40 +00:00
Weblate
1925167407 Translated using Weblate (Japanese)
Currently translated at 90.7% (459 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:56:18 +00:00
Weblate
b8bdf23d05 Translated using Weblate (Japanese)
Currently translated at 90.7% (459 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 90.7% (459 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:56:10 +00:00
Weblate
ca49a4cd82 Translated using Weblate (Japanese)
Currently translated at 90.5% (458 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 90.5% (458 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:55:42 +00:00
Weblate
c8c1a4f573 Translated using Weblate (Japanese)
Currently translated at 90.1% (456 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 90.1% (456 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:54:59 +00:00
Weblate
9f5fb82c47 Translated using Weblate (Japanese)
Currently translated at 89.9% (455 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 89.9% (455 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:54:45 +00:00
Weblate
d87c46a464 Translated using Weblate (Japanese)
Currently translated at 89.7% (454 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 89.7% (454 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:54:26 +00:00
Weblate
7e5567bd2f Translated using Weblate (Japanese)
Currently translated at 89.5% (453 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 89.5% (453 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:54:15 +00:00
Weblate
5589301c9d Translated using Weblate (Japanese)
Currently translated at 89.3% (452 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 89.3% (452 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:54:06 +00:00
Weblate
b489593e62 Translated using Weblate (Japanese)
Currently translated at 89.1% (451 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 89.1% (451 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:53:51 +00:00
Weblate
38413ddef4 Translated using Weblate (Japanese)
Currently translated at 88.9% (450 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 88.9% (450 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:53:38 +00:00
Weblate
273520fd96 Translated using Weblate (Japanese)
Currently translated at 88.7% (449 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 88.7% (449 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:52:35 +00:00
Weblate
4704b42b6d Translated using Weblate (Japanese)
Currently translated at 88.5% (448 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 88.5% (448 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:52:21 +00:00
Weblate
29c84e3071 Translated using Weblate (Japanese)
Currently translated at 88.3% (447 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 88.3% (447 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:52:06 +00:00
Weblate
6d3967383e Translated using Weblate (Japanese)
Currently translated at 88.1% (446 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 88.1% (446 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:51:48 +00:00
Weblate
c7af7720ea Translated using Weblate (Japanese)
Currently translated at 87.9% (445 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 87.9% (445 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:51:25 +00:00
Weblate
44ea3aef1b Translated using Weblate (Japanese)
Currently translated at 87.7% (444 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:48:40 +00:00
Weblate
414599503f Translated using Weblate (Japanese)
Currently translated at 87.7% (444 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:47:50 +00:00
Weblate
5eda237014 Translated using Weblate (Japanese)
Currently translated at 87.5% (443 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 87.5% (443 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:47:41 +00:00
Weblate
6e2b0f2d32 Translated using Weblate (Japanese)
Currently translated at 87.1% (441 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 87.1% (441 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:44:58 +00:00
Weblate
2fc9d40419 Translated using Weblate (Japanese)
Currently translated at 86.9% (440 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 86.9% (440 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:40:27 +00:00
Weblate
5ed5d69d34 Translated using Weblate (Japanese)
Currently translated at 86.7% (439 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 86.7% (439 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:40:16 +00:00
Weblate
19605bc242 Translated using Weblate (Japanese)
Currently translated at 86.5% (438 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 86.5% (438 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:40:04 +00:00
Weblate
523c3af677 Translated using Weblate (Japanese)
Currently translated at 86.3% (437 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 86.3% (437 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:39:55 +00:00
Weblate
c2d64388b2 Translated using Weblate (Japanese)
Currently translated at 86.1% (436 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 86.1% (436 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:39:47 +00:00
Weblate
2c8bc77aaa Translated using Weblate (Japanese)
Currently translated at 85.9% (435 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 85.9% (435 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:39:37 +00:00
Weblate
284e38c92c Translated using Weblate (Japanese)
Currently translated at 85.7% (434 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 85.7% (434 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:36:53 +00:00
Weblate
85fc35a382 Translated using Weblate (Japanese)
Currently translated at 85.1% (431 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 85.1% (431 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:35:55 +00:00
Weblate
9ffe8ec399 Translated using Weblate (Japanese)
Currently translated at 84.7% (429 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 84.7% (429 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:34:37 +00:00
Weblate
1e4902d8ae Translated using Weblate (Japanese)
Currently translated at 84.5% (428 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 84.5% (428 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:33:43 +00:00
Weblate
6585a271f6 Translated using Weblate (Japanese)
Currently translated at 83.3% (422 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 83.3% (422 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:32:55 +00:00
Weblate
faa9e09efe Translated using Weblate (Japanese)
Currently translated at 83.2% (421 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 83.2% (421 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:32:13 +00:00
Weblate
55b73418b8 Translated using Weblate (Japanese)
Currently translated at 83.0% (420 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 83.0% (420 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:31:35 +00:00
Weblate
8be61d9e36 Translated using Weblate (Japanese)
Currently translated at 82.8% (419 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 82.8% (419 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:30:50 +00:00
Weblate
174286b701 Translated using Weblate (Japanese)
Currently translated at 82.2% (416 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 82.2% (416 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:30:33 +00:00
Weblate
385baf1068 Translated using Weblate (Japanese)
Currently translated at 82.0% (415 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 82.0% (415 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:30:17 +00:00
Weblate
25104465ca Translated using Weblate (Japanese)
Currently translated at 81.8% (414 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 81.8% (414 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:29:58 +00:00
Weblate
dbdc9f6531 Translated using Weblate (Japanese)
Currently translated at 81.4% (412 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:29:36 +00:00
Weblate
2fe3cd9041 Translated using Weblate (Japanese)
Currently translated at 81.2% (411 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 81.2% (411 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:29:25 +00:00
Weblate
9c8a9d32b6 Translated using Weblate (Japanese)
Currently translated at 81.0% (410 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 81.0% (410 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:29:09 +00:00
Weblate
4b68162b1d Translated using Weblate (Japanese)
Currently translated at 80.8% (409 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 80.8% (409 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:29:02 +00:00
Weblate
3fa0ff5214 Translated using Weblate (Japanese)
Currently translated at 80.4% (407 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 80.4% (407 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:28:50 +00:00
Weblate
59c2074343 Translated using Weblate (Japanese)
Currently translated at 80.2% (406 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 80.2% (406 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:28:36 +00:00
Weblate
2c7d7b9d53 Translated using Weblate (Japanese)
Currently translated at 80.0% (405 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 80.0% (405 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:28:07 +00:00
Weblate
741baeb7fb Translated using Weblate (Japanese)
Currently translated at 79.8% (404 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 79.8% (404 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:27:43 +00:00
Weblate
65c1d20f17 Translated using Weblate (Japanese)
Currently translated at 79.6% (403 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 79.6% (403 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:27:34 +00:00
Weblate
23eec20e97 Translated using Weblate (Japanese)
Currently translated at 79.4% (402 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 79.4% (402 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:27:14 +00:00
Weblate
e9e0ccca99 Translated using Weblate (Japanese)
Currently translated at 79.2% (401 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 79.2% (401 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:26:56 +00:00
Weblate
00a1efce1d Translated using Weblate (Japanese)
Currently translated at 78.6% (398 of 506 strings)

Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:17:46 +00:00
Weblate
de7345f326 Translated using Weblate (Japanese)
Currently translated at 78.6% (398 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 78.6% (398 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:17:20 +00:00
Weblate
10564bfc9f Translated using Weblate (Japanese)
Currently translated at 78.4% (397 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 78.4% (397 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:17:00 +00:00
Weblate
508c5ee116 Translated using Weblate (Japanese)
Currently translated at 78.2% (396 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 78.2% (396 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:16:41 +00:00
Weblate
0dfc634d1b Translated using Weblate (Japanese)
Currently translated at 78.0% (395 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 78.0% (395 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:16:20 +00:00
Weblate
e92eb80aec Translated using Weblate (Japanese)
Currently translated at 77.8% (394 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 77.8% (394 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:16:09 +00:00
Weblate
5d84cc2899 Translated using Weblate (Japanese)
Currently translated at 77.6% (393 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 77.6% (393 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:15:51 +00:00
Weblate
19db9f5623 Translated using Weblate (Japanese)
Currently translated at 77.4% (392 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 77.4% (392 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:15:39 +00:00
Weblate
0f163e48e2 Translated using Weblate (Japanese)
Currently translated at 77.2% (391 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 77.2% (391 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 04:12:03 +00:00
Weblate
fb6df194d5 Translated using Weblate (Japanese)
Currently translated at 76.8% (389 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 76.8% (389 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:53:32 +00:00
Weblate
762a309e4b Translated using Weblate (Japanese)
Currently translated at 76.8% (389 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 76.8% (389 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:51:53 +00:00
Weblate
cf7f703f69 Translated using Weblate (Japanese)
Currently translated at 76.6% (388 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 76.6% (388 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:51:12 +00:00
Weblate
0e71f59086 Translated using Weblate (Japanese)
Currently translated at 76.2% (386 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 76.2% (386 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:49:33 +00:00
Weblate
b0829b7f4d Translated using Weblate (Japanese)
Currently translated at 75.8% (384 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 75.8% (384 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:48:12 +00:00
Weblate
305207fcd7 Translated using Weblate (Japanese)
Currently translated at 75.6% (383 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 75.6% (383 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:47:24 +00:00
Weblate
6deda72650 Translated using Weblate (Japanese)
Currently translated at 75.2% (381 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 75.2% (381 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:31:28 +00:00
Weblate
e8e6d6e81b Translated using Weblate (Japanese)
Currently translated at 75.0% (380 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 75.0% (380 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:30:29 +00:00
Weblate
1e06a6e4e0 Translated using Weblate (Japanese)
Currently translated at 74.9% (379 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 74.9% (379 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:30:19 +00:00
Weblate
064c945d9c Translated using Weblate (Japanese)
Currently translated at 74.7% (378 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 74.7% (378 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:30:01 +00:00
Weblate
8814d63655 Translated using Weblate (Japanese)
Currently translated at 74.3% (376 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:29:40 +00:00
Weblate
4954b79cbd Translated using Weblate (Japanese)
Currently translated at 74.1% (375 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 74.1% (375 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:29:16 +00:00
Weblate
6fa331307a Translated using Weblate (Japanese)
Currently translated at 73.3% (371 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 73.3% (371 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:28:28 +00:00
Weblate
1a95ff4854 Translated using Weblate (Japanese)
Currently translated at 72.7% (368 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:27:40 +00:00
Weblate
c77f2eb119 Translated using Weblate (Japanese)
Currently translated at 72.3% (366 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 72.3% (366 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 72.3% (366 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: Weblate Translation Memory <noreply-mt-weblate-translation-memory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:27:21 +00:00
Weblate
79b04203b9 Translated using Weblate (Japanese)
Currently translated at 70.5% (357 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 70.5% (357 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:18:16 +00:00
Weblate
32258535a5 Translated using Weblate (Japanese)
Currently translated at 70.1% (355 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 70.1% (355 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:15:04 +00:00
Weblate
4fb61bc4a5 Translated using Weblate (Japanese)
Currently translated at 69.5% (352 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 69.5% (352 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:13:58 +00:00
Weblate
55fed18582 Translated using Weblate (Japanese)
Currently translated at 69.3% (351 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 69.3% (351 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:13:48 +00:00
Weblate
408391d31f Translated using Weblate (Japanese)
Currently translated at 69.1% (350 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 69.1% (350 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:13:39 +00:00
Weblate
0087d810ae Translated using Weblate (Japanese)
Currently translated at 68.5% (347 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:12:16 +00:00
Weblate
be907f72ff Translated using Weblate (Japanese)
Currently translated at 68.3% (346 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 68.3% (346 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:12:00 +00:00
Weblate
669543989a Translated using Weblate (Japanese)
Currently translated at 68.1% (345 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 68.1% (345 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:11:40 +00:00
Weblate
484744c0f9 Translated using Weblate (Japanese)
Currently translated at 67.9% (344 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 67.9% (344 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:11:16 +00:00
Weblate
912a11f27d Translated using Weblate (Japanese)
Currently translated at 67.7% (343 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 67.7% (343 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:10:46 +00:00
Weblate
a49e6e4f92 Translated using Weblate (Japanese)
Currently translated at 67.5% (342 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 67.5% (342 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:10:33 +00:00
Weblate
f94167cb34 Translated using Weblate (Japanese)
Currently translated at 67.1% (340 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 67.1% (340 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:10:03 +00:00
Weblate
4aa6f12df4 Translated using Weblate (Japanese)
Currently translated at 66.9% (339 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 66.9% (339 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:08:54 +00:00
Weblate
2ac5c08f76 Translated using Weblate (Japanese)
Currently translated at 66.7% (338 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 66.7% (338 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:08:09 +00:00
Weblate
49f891f577 Translated using Weblate (Japanese)
Currently translated at 66.4% (336 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:05:58 +00:00
Weblate
25cf4ecc51 Translated using Weblate (Japanese)
Currently translated at 66.4% (336 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:05:48 +00:00
Weblate
e77f1dd68c Translated using Weblate (Japanese)
Currently translated at 66.2% (335 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:05:31 +00:00
Weblate
4cfece1bf5 Translated using Weblate (Japanese)
Currently translated at 66.2% (335 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 66.2% (335 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:05:19 +00:00
Weblate
6e5b348d82 Translated using Weblate (Japanese)
Currently translated at 65.8% (333 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 65.8% (333 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:01:40 +00:00
Weblate
d53c643de0 Translated using Weblate (Japanese)
Currently translated at 65.4% (331 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 65.4% (331 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 03:00:57 +00:00
Weblate
8c53d76819 Translated using Weblate (Japanese)
Currently translated at 64.4% (326 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 64.4% (326 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 02:57:50 +00:00
Weblate
5364833afb Translated using Weblate (Japanese)
Currently translated at 64.0% (324 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 64.0% (324 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 02:57:06 +00:00
Weblate
541585c0bb Translated using Weblate (Japanese)
Currently translated at 63.6% (322 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 63.6% (322 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 02:56:29 +00:00
Weblate
350a35f7f4 Translated using Weblate (Japanese)
Currently translated at 63.2% (320 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 63.2% (320 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 02:55:03 +00:00
Weblate
856f2584b9 Translated using Weblate (Japanese)
Currently translated at 62.6% (317 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 62.6% (317 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 62.6% (317 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: Weblate Translation Memory <noreply-mt-weblate-translation-memory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 02:49:52 +00:00
Weblate
c997f274cc Translated using Weblate (Japanese)
Currently translated at 62.0% (314 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 62.0% (314 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 02:49:21 +00:00
Weblate
e9689b6b52 Translated using Weblate (Japanese)
Currently translated at 61.4% (311 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 61.4% (311 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 02:48:57 +00:00
Weblate
3713816576 Translated using Weblate (Japanese)
Currently translated at 61.2% (310 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 61.2% (310 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 02:48:46 +00:00
Weblate
3529a95ebe Translated using Weblate (Japanese)
Currently translated at 60.6% (307 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 60.6% (307 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 02:46:56 +00:00
Weblate
fa066bc962 Translated using Weblate (Japanese)
Currently translated at 60.2% (305 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 60.2% (305 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 02:45:43 +00:00
Weblate
ba358790ea Translated using Weblate (Japanese)
Currently translated at 59.4% (301 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 59.4% (301 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 02:37:47 +00:00
Weblate
3aff39cdaf Translated using Weblate (Japanese)
Currently translated at 58.8% (298 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 58.8% (298 of 506 strings)

Translated using Weblate (English)

Currently translated at 100.0% (506 of 506 strings)

Co-authored-by: Matthew Kilgore <matthew@kilgore.dev>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: ななしぃ <weblate@nanasi-rasi.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/en/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
Translation: Homebox/Frontend
2025-08-19 02:30:52 +00:00
Weblate
877bb2ddbf Translated using Weblate (German)
Currently translated at 100.0% (506 of 506 strings)

Translated using Weblate (Italian)

Currently translated at 82.4% (417 of 506 strings)

Co-authored-by: Matteo Lombardi <matteolomba@protonmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/de/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/it/
Translation: Homebox/Frontend
2025-08-18 16:58:57 +00:00
Weblate
c8a48e4400 Translated using Weblate (Polish)
Currently translated at 100.0% (506 of 506 strings)

Translated using Weblate (German)

Currently translated at 99.8% (505 of 506 strings)

Translated using Weblate (German)

Currently translated at 99.8% (505 of 506 strings)

Translated using Weblate (Italian)

Currently translated at 82.4% (417 of 506 strings)

Translated using Weblate (Italian)

Currently translated at 82.4% (417 of 506 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (506 of 506 strings)

Co-authored-by: Krzysztof G. <mordret@o2.pl>
Co-authored-by: Mats <sysadminsmedia@mats-bueser.de>
Co-authored-by: Matteo Lombardi <matteolomba@protonmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: verhese <sean.verheyen1@telenet.be>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/de/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/it/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/nl/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/pl/
Translation: Homebox/Frontend
2025-08-18 11:34:12 +00:00
Weblate
1211105eb4 Translated using Weblate (Polish)
Currently translated at 100.0% (506 of 506 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/pl/
Translation: Homebox/Frontend
2025-08-17 17:43:08 +00:00
Matthew Kilgore
28ce0d29a4 Default postgres ssl_mode to fix #943 2025-08-17 08:58:57 -04:00
Matthew Kilgore
dbf8322ec6 Update dependencies 2025-08-16 21:20:19 -04:00
Matthew Kilgore
9f34f80a60 Update dependencies 2025-08-16 17:43:02 -04:00
Matthew Kilgore
175b93a62e Make sure all languages are part of core translations. 2025-08-16 17:40:16 -04:00
Matt
d41f313cff Fix Windows Paths (#917)
* In theory this should fix the issue with Windows paths

* Fix Windows path handling in file storage connections for non-default
2025-08-16 17:08:24 -04:00
Weblate
1439e20d93 Translated using Weblate (Danish)
Currently translated at 99.4% (501 of 504 strings)

Translated using Weblate (Danish)

Currently translated at 99.4% (501 of 504 strings)

Co-authored-by: LovelessCodes <hello@loveless.codes>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/da/
Translation: Homebox/Frontend
2025-08-11 23:58:41 +00:00
Weblate
17e3a6d0cf Translated using Weblate (Turkish)
Currently translated at 86.7% (437 of 504 strings)

Co-authored-by: Can Dikyol <candikyol@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/tr/
Translation: Homebox/Frontend
2025-08-10 21:58:40 +00:00
Weblate
1ed7734b2e Translated using Weblate (German)
Currently translated at 100.0% (504 of 504 strings)

Co-authored-by: Katos <katos@creatorswave.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/de/
Translation: Homebox/Frontend
2025-08-10 14:47:35 +00:00
Matias Godoy
362c0bb3e6 Fix accent-insensitive search for Postgres databases (#932) 2025-08-04 20:35:22 -04:00
Weblate
0d3151ae5c Translated using Weblate (Turkish)
Currently translated at 85.9% (433 of 504 strings)

Co-authored-by: Can Dikyol <candikyol@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/tr/
Translation: Homebox/Frontend
2025-08-04 16:17:42 +00:00
Weblate
b4e679e321 Translated using Weblate (Turkish)
Currently translated at 67.6% (341 of 504 strings)

Co-authored-by: Can Dikyol <candikyol@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/tr/
Translation: Homebox/Frontend
2025-08-04 12:43:43 +00:00
Weblate
de3b63639b Translated using Weblate (Portuguese (Portugal))
Currently translated at 96.0% (484 of 504 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/pt_PT/
Translation: Homebox/Frontend
2025-08-04 03:54:18 +00:00
Weblate
23ba40892a Translated using Weblate (Korean)
Currently translated at 6.9% (35 of 504 strings)

Co-authored-by: HAN, Sang-uk <nouveau.monde.1987@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ko/
Translation: Homebox/Frontend
2025-08-03 19:49:16 +00:00
Ahmed Al Hafoudh
624c1763ac Add external label service support to label maker (#913)
* Add external label service support to label maker

* Make external label service fetch to include user agent, limit response size and allow any image type

* Fix linting errors

* Fix "response body closed" closing the Body to soon
2025-08-01 12:02:40 -04:00
Weblate
75c2423fd5 Translated using Weblate (Italian)
Currently translated at 81.5% (411 of 504 strings)

Translated using Weblate (Italian)

Currently translated at 81.5% (411 of 504 strings)

Co-authored-by: Matteo Lombardi <matteolomba@protonmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/it/
Translation: Homebox/Frontend
2025-07-30 18:57:54 +00:00
Weblate
d4f2b52b6c Translated using Weblate (Vietnamese)
Currently translated at 19.4% (98 of 504 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (504 of 504 strings)

Co-authored-by: Ngô Tạ Đình Phong <thichcarot@outlook.com>
Co-authored-by: askolock <askolock@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ru/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/vi/
Translation: Homebox/Frontend
2025-07-28 15:00:41 +00:00
Weblate
028b1382ad Translated using Weblate (Russian)
Currently translated at 100.0% (504 of 504 strings)

Co-authored-by: akrstlv <zmilex@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ru/
Translation: Homebox/Frontend
2025-07-25 20:53:56 +00:00
Weblate
d8781950fa Translated using Weblate (Dutch)
Currently translated at 100.0% (504 of 504 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (504 of 504 strings)

Co-authored-by: Hannes Salen <hannes.salen@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/nl/
Translation: Homebox/Frontend
2025-07-24 15:00:41 +00:00
Weblate
8646360b8c Translated using Weblate (Spanish)
Currently translated at 100.0% (504 of 504 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (504 of 504 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: Ricardo González <notorius28@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/es/
Translation: Homebox/Frontend
2025-07-23 07:00:43 +00:00
Weblate
6ce83ea04c Translated using Weblate (German)
Currently translated at 100.0% (504 of 504 strings)

Translated using Weblate (German)

Currently translated at 100.0% (504 of 504 strings)

Co-authored-by: Christoph Auer <Christoph.Auer@pilsheim.de>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/de/
Translation: Homebox/Frontend
2025-07-21 12:11:39 +00:00
Weblate
ad356acc73 Translated using Weblate (German)
Currently translated at 98.8% (498 of 504 strings)

Translated using Weblate (German)

Currently translated at 98.8% (498 of 504 strings)

Co-authored-by: Christoph Auer <Christoph.Auer@pilsheim.de>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/de/
Translation: Homebox/Frontend
2025-07-21 07:02:54 +00:00
Weblate
863b84355d Translated using Weblate (German)
Currently translated at 98.4% (496 of 504 strings)

Translated using Weblate (German)

Currently translated at 98.4% (496 of 504 strings)

Co-authored-by: Christoph Auer <Christoph.Auer@pilsheim.de>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/de/
Translation: Homebox/Frontend
2025-07-21 07:02:19 +00:00
Weblate
959d9961f1 Translated using Weblate (German)
Currently translated at 97.6% (492 of 504 strings)

Translated using Weblate (German)

Currently translated at 97.6% (492 of 504 strings)

Co-authored-by: Christoph Auer <Christoph.Auer@pilsheim.de>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/de/
Translation: Homebox/Frontend
2025-07-21 07:01:42 +00:00
Weblate
c5b783bef7 Translated using Weblate (Hungarian)
Currently translated at 100.0% (504 of 504 strings)

Translated using Weblate (German)

Currently translated at 97.4% (491 of 504 strings)

Translated using Weblate (German)

Currently translated at 97.4% (491 of 504 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: Christoph Auer <Christoph.Auer@pilsheim.de>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/de/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-21 07:01:26 +00:00
Weblate
1d78b953dd Translated using Weblate (Hungarian)
Currently translated at 100.0% (504 of 504 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (504 of 504 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-21 05:16:36 +00:00
Weblate
44f5aaec57 Translated using Weblate (Hungarian)
Currently translated at 99.4% (501 of 504 strings)

Translated using Weblate (Hungarian)

Currently translated at 99.4% (501 of 504 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-21 05:15:55 +00:00
Weblate
4933446202 Translated using Weblate (Hungarian)
Currently translated at 99.2% (500 of 504 strings)

Translated using Weblate (Hungarian)

Currently translated at 99.2% (500 of 504 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-21 05:15:30 +00:00
Weblate
e1fbb99203 Translated using Weblate (Hungarian)
Currently translated at 98.8% (498 of 504 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (504 of 504 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (504 of 504 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: WilliamStark <yujinghao007@163.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/zh_Hans/
Translation: Homebox/Frontend
2025-07-21 05:14:55 +00:00
Weblate
4a9557fcb7 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.4% (501 of 504 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.4% (501 of 504 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: WilliamStark <yujinghao007@163.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/zh_Hans/
Translation: Homebox/Frontend
2025-07-21 02:07:44 +00:00
Weblate
5766277c16 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.2% (500 of 504 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.2% (500 of 504 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: WilliamStark <yujinghao007@163.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/zh_Hans/
Translation: Homebox/Frontend
2025-07-21 02:07:10 +00:00
Weblate
5374f31d69 Translated using Weblate (Vietnamese)
Currently translated at 14.8% (75 of 504 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (504 of 504 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (504 of 504 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (504 of 504 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 98.0% (494 of 504 strings)

Co-authored-by: Adam Havránek <adamhavra@seznam.cz>
Co-authored-by: Lucas Wilson <lucasws2020@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/cs/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/pl/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/vi/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/zh_Hans/
Translation: Homebox/Frontend
2025-07-21 02:06:03 +00:00
Balki
e82f5084d4 Fix Windows build and re-apply unix socket support (#906)
* Reapply "Support listening on unix sockets and systemd sockets (#878)"

This reverts commit 2f51ba419b.

* Fix windows build

Upgrade anyhttp to v0.5.2
2025-07-20 09:51:31 -04:00
Katos
bbd773fb3a Merge pull request #818 from crumbowl/feat/barcode
Add product fetching using barcodes
2025-07-20 10:59:44 +01:00
Crumb Owl
7129650efa ProductBarcode: properly check array boundaries 2025-07-19 23:06:44 +02:00
Crumb Owl
a57b83c52d ProductBarcode: various fix requested by Tonya
- fix many missing translations
- properly reset QR scanner when reopening
- add error message on BarcodeModal when no item is found
- fix icon size in item CreateModal
- remove useless closeDialog
2025-07-19 23:06:44 +02:00
Crumb Owl
bb5e36f0c4 ProductBarcode: final linting 2025-07-19 23:06:44 +02:00
Crumb Owl
bd44b36666 ProductBarcode: BarcodeModal: improve erroring 2025-07-19 23:06:43 +02:00
Crumb Owl
895063fa36 ProductBarcode: improve readability on CreateModal 2025-07-19 23:06:43 +02:00
Crumb Owl
aa7658b0d4 ProductBarcode: fix barcode value not updated + fix search button not reset properly 2025-07-19 23:06:43 +02:00
Crumb Owl
68f97f24c7 ProductBarcode: fix various remarks from Tonya 2025-07-19 23:06:43 +02:00
Crumb Owl
6555c9277a ProductBarcode: use json encoder from the project 2025-07-19 23:06:43 +02:00
Crumb Owl
b5d13380fe ProductBarcode: BarcodeModal: launch search on "Return" key 2025-07-19 23:06:43 +02:00
Crumb Owl
9271cdae4b ProductBarcode: architecture: move to strongly typed DialogID and parameters 2025-07-19 23:06:43 +02:00
Crumb Owl
18149a5c9a ProductBarcode: apply linting and fixes on frontend 2025-07-19 23:06:43 +02:00
Crumb Owl
68b6d58ab4 ProductBarcode: BarcodeModal: many fixes catched by linter 2025-07-19 23:06:43 +02:00
Crumb Owl
6d516f6de6 ProductBarcode: backend: properly define max length of a barcode 2025-07-19 23:06:43 +02:00
Crumb Owl
36d5ae1466 ProductBarcode: backend: improve verbosity for user 2025-07-19 23:06:43 +02:00
Crumb Owl
f37f609dff ProductBarcode: backend: prevent DoS with image download 2025-07-19 23:06:43 +02:00
Crumb Owl
a980d9f243 ProductBarcode: backend: remove API response verbosity 2025-07-19 23:06:43 +02:00
Crumb Owl
aac82c9236 ProductBarcode: backend: add timeout to external API calls 2025-07-19 23:06:43 +02:00
Crumb Owl
8dedfcca43 ProductBarcode: backend: fix error handling with http requests 2025-07-19 23:06:43 +02:00
Crumb Owl
f72fcb0800 ProductBarcode: backend: fix resource leak with defer 2025-07-19 23:06:43 +02:00
Crumb Owl
94e81809d3 ProductBarcode: backend: properly check barcodespider API response 2025-07-19 23:06:43 +02:00
Crumb Owl
e80e5744f7 ProductBarcode: backend: improve security of image fetching 2025-07-19 23:06:43 +02:00
Crumb Owl
402b8c429e ProductBarcode: improve error handling in BarcodeModal 2025-07-19 23:06:43 +02:00
Crumb Owl
d2919de8e8 ProductBarcode: add barcode shortcuts in item/Createmodal.vue 2025-07-19 23:06:43 +02:00
Crumb Owl
8a60729153 ProductBarcode: clean code, add error handling 2025-07-19 23:06:43 +02:00
Crumb Owl
4a4bf9a175 ProductBarcode: rename API call from getproductfromean to products/search-from-barcode 2025-07-19 23:06:43 +02:00
Crumb Owl
24923f2a83 ProductBarcode: refactoring Go method 2025-07-19 23:06:43 +02:00
Crumb Owl
66c2de22ed ProductBarcode: Go Linter fixing 2025-07-19 23:06:43 +02:00
Crumb Owl
c93fddae7f ProductBarcode: move backend code in dedicated source file 2025-07-19 23:06:43 +02:00
Crumb Owl
fb17b56f09 ProductBarcode: create a dedicated dialog for product selection 2025-07-19 23:06:43 +02:00
Crumb Owl
a3c13a8a74 ProductBarcode: return an array of BarcodeProduct instead of one 2025-07-19 23:06:38 +02:00
Crumb Owl
09f29d82f4 ProductBarcode: properly use of language system in frontend/Scanner.vue 2025-07-19 22:51:48 +02:00
Crumb Owl
dd94fd43ee ProductBarcode: improve UI of Barcode message in frontend/Scanner.vue 2025-07-19 22:51:48 +02:00
Crumb Owl
a85bdfef88 ProductBarcode: display barcode type in frontend/Scanner.vue 2025-07-19 22:51:48 +02:00
Crumb Owl
79baf6b5ef ProductBarcode: define Barcodespider API key using env variables 2025-07-19 22:51:48 +02:00
Crumb Owl
d691e908a4 ProductBarcode: add image downloading from remote product database
- Backend download images from the database
- Frontend retrieve the image as base64, no architecture change needed
2025-07-19 22:51:48 +02:00
Crumb Owl
ec8320bc42 ProductBarcode: update UPCItemDB parsing
- JSON response seems to have changed
2025-07-19 22:51:48 +02:00
Crumb Owl
6dbb243ba5 ProductBarcode: return more fields from DB (brand, model...)
- backend: change data structure returned to frontend
2025-07-19 22:51:48 +02:00
Crumb Owl
7c56bfb4ab ProductBarcode: fix error on pages/Scanner.vue when using a barcode 2025-07-19 22:51:48 +02:00
Crumb Owl
c3af4ac4ac ProductBarcode: add barcode processing in frontend 2025-07-19 22:51:48 +02:00
Crumb Owl
fc88df0ff0 ProductBarcode: allow passing parameters to Dialog 2025-07-19 22:51:48 +02:00
Crumb Owl
0e1e5ae3f0 ProductBarcode: add frontend API call utils 2025-07-19 22:51:48 +02:00
Crumb Owl
0ed69b75a1 ProductBarcode: add first backend API implementation 2025-07-19 22:51:48 +02:00
Crumb Owl
c666a8a8c1 ProductBarcode: add barcode detection to ScannerModal.vue 2025-07-19 22:51:48 +02:00
Weblate
6ef7045f62 Translated using Weblate (Polish)
Currently translated at 100.0% (492 of 492 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (492 of 492 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (492 of 492 strings)

Translated using Weblate (Albanian)

Currently translated at 19.1% (94 of 492 strings)

Translated using Weblate (French)

Currently translated at 99.3% (489 of 492 strings)

Translated using Weblate (Swedish)

Currently translated at 68.2% (336 of 492 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (492 of 492 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 97.3% (479 of 492 strings)

Translated using Weblate (Catalan)

Currently translated at 56.0% (276 of 492 strings)

Co-authored-by: Krzysztof G. <mordret@o2.pl>
Co-authored-by: Matthew Kilgore <matthew@kilgore.dev>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: Thomas J. Mazon de Oliveira <thomas.mazon@gmail.com>
Co-authored-by: Weblate Translation Memory <noreply-mt-weblate-translation-memory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ca/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/fr/
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/sq/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/sv/
Translation: Homebox/Frontend
2025-07-19 09:00:42 +00:00
Weblate
98ce90636d Translated using Weblate (Danish)
Currently translated at 99.7% (491 of 492 strings)

Translated using Weblate (Chinese (Simplified) (zh_MO))

Currently translated at 37.1% (183 of 492 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 72.5% (357 of 492 strings)

Translated using Weblate (German)

Currently translated at 99.3% (489 of 492 strings)

Translated using Weblate (Italian)

Currently translated at 81.9% (403 of 492 strings)

Co-authored-by: Matthew Kilgore <matthew@kilgore.dev>
Co-authored-by: Thomas J. Mazon de Oliveira <thomas.mazon@gmail.com>
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/it/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/pt_BR/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/zh_MO/
Translation: Homebox/Frontend
2025-07-17 19:00:41 +00:00
Weblate
86721c9b9a Translated using Weblate (Hungarian)
Currently translated at 100.0% (492 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (492 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-16 07:00:42 +00:00
Michael Manganiello
62f6121260 feat: Add plugin to set image sizes in Markdown (#901)
* feat: Add plugin to set image sizes in Markdown

Install the `@mdit/plugin-img-size` plugin [1] to allow setting image sizes
in Markdown content. This improves the image rendering capabilities for
Markdown blocks.

Before (no resizing possible):

```markdown
![logo](https://raw.githubusercontent.com/sysadminsmedia/homebox/refs/tags/v0.20.2/docs/public/lilbox.svg)
```

After (size specified):

```markdown
![logo =100x](https://raw.githubusercontent.com/sysadminsmedia/homebox/refs/tags/v0.20.2/docs/public/lilbox.svg)
```

[1] https://mdit-plugins.github.io/img-size.html

* Update @types/markdown-it to match markdown-it version
2025-07-16 05:58:24 +00:00
Matt
90bb6ed1fe Daily Analytics (#896)
* Send analytics daily

* Clean up error handling, add uptime to analytics

* Better analytics scheduling

* Even better logic for scheduling the analytics (hopefully)

* Some cleanup

* Switch to minutes for uptime, remove duplicate event on startup
2025-07-15 04:24:19 -04:00
Weblate
bd79ee3227 Translated using Weblate (Hungarian)
Currently translated at 99.5% (490 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 99.5% (490 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:45:22 +00:00
Weblate
c0e79cdb9e Translated using Weblate (Hungarian)
Currently translated at 99.3% (489 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 99.3% (489 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:45:01 +00:00
Weblate
5156792319 Translated using Weblate (Hungarian)
Currently translated at 99.1% (488 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 99.1% (488 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:44:30 +00:00
Weblate
8bbc39e416 Translated using Weblate (Hungarian)
Currently translated at 98.5% (485 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 98.5% (485 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:43:15 +00:00
Weblate
0beb430704 Translated using Weblate (Hungarian)
Currently translated at 98.3% (484 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 98.3% (484 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:41:46 +00:00
Weblate
0f7107f86d Translated using Weblate (Hungarian)
Currently translated at 97.7% (481 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 97.7% (481 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:36:35 +00:00
Weblate
115cda5c37 Translated using Weblate (Hungarian)
Currently translated at 97.1% (478 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 97.1% (478 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 97.1% (478 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: Weblate Translation Memory <noreply-mt-weblate-translation-memory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:36:07 +00:00
Weblate
a6c1c8c652 Translated using Weblate (Hungarian)
Currently translated at 96.3% (474 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 96.3% (474 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:35:42 +00:00
Weblate
c69c6a1518 Translated using Weblate (Hungarian)
Currently translated at 96.1% (473 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 96.1% (473 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:34:55 +00:00
Weblate
adaffa5ca8 Translated using Weblate (Hungarian)
Currently translated at 95.9% (472 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 95.9% (472 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:32:21 +00:00
Weblate
b410642dc6 Translated using Weblate (Hungarian)
Currently translated at 95.7% (471 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 95.7% (471 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:29:52 +00:00
Weblate
4bed1a3158 Translated using Weblate (Hungarian)
Currently translated at 93.6% (461 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 93.6% (461 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:27:05 +00:00
Weblate
9ff39bb402 Translated using Weblate (Hungarian)
Currently translated at 93.4% (460 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 93.4% (460 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:26:15 +00:00
Weblate
3ab250a045 Translated using Weblate (Hungarian)
Currently translated at 93.2% (459 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 93.2% (459 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:24:16 +00:00
Weblate
4147cff1db Translated using Weblate (Hungarian)
Currently translated at 92.6% (456 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 92.6% (456 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:21:24 +00:00
Weblate
dada2f0266 Translated using Weblate (Hungarian)
Currently translated at 92.2% (454 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 92.2% (454 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:21:01 +00:00
Weblate
e9e852c8a3 Translated using Weblate (Hungarian)
Currently translated at 92.0% (453 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 92.0% (453 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:20:50 +00:00
Weblate
7dda0f473a Translated using Weblate (Hungarian)
Currently translated at 89.6% (441 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 89.6% (441 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:17:06 +00:00
Weblate
2006b8056a Translated using Weblate (Hungarian)
Currently translated at 89.4% (440 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 89.4% (440 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:16:31 +00:00
Weblate
41f63456eb Translated using Weblate (Hungarian)
Currently translated at 89.2% (439 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 89.2% (439 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:15:54 +00:00
Weblate
fe177deff4 Translated using Weblate (Hungarian)
Currently translated at 89.0% (438 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 89.0% (438 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:15:38 +00:00
Weblate
d729a74b34 Translated using Weblate (Hungarian)
Currently translated at 88.6% (436 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 88.6% (436 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:15:04 +00:00
Weblate
6ab51e4767 Translated using Weblate (Hungarian)
Currently translated at 88.2% (434 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 88.2% (434 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:13:09 +00:00
Weblate
e080817e1a Translated using Weblate (Hungarian)
Currently translated at 87.3% (430 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 87.3% (430 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:12:49 +00:00
Weblate
31e6f0264d Translated using Weblate (Hungarian)
Currently translated at 85.7% (422 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 85.7% (422 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:09:38 +00:00
Weblate
8e98ded03f Translated using Weblate (Hungarian)
Currently translated at 85.5% (421 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 85.5% (421 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:09:26 +00:00
Weblate
8da030d415 Translated using Weblate (Hungarian)
Currently translated at 84.7% (417 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 84.7% (417 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:09:06 +00:00
Weblate
393342bc32 Translated using Weblate (Hungarian)
Currently translated at 84.5% (416 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 84.5% (416 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:08:45 +00:00
Weblate
9f331b87df Translated using Weblate (Hungarian)
Currently translated at 84.1% (414 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 84.1% (414 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:08:21 +00:00
Weblate
27efa00ee2 Translated using Weblate (Hungarian)
Currently translated at 83.5% (411 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 83.5% (411 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:07:57 +00:00
Weblate
1224a6e516 Translated using Weblate (Hungarian)
Currently translated at 81.9% (403 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 81.9% (403 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-15 06:06:58 +00:00
Weblate
988f9eee8c Translated using Weblate (Hungarian)
Currently translated at 80.8% (398 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-14 22:56:25 +00:00
Weblate
832b4a6484 Translated using Weblate (Hungarian)
Currently translated at 80.8% (398 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 80.8% (398 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-14 14:06:37 +00:00
Weblate
64298511ee Translated using Weblate (Hungarian)
Currently translated at 79.4% (391 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 79.4% (391 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-14 14:06:01 +00:00
Weblate
f4ed929e4a Translated using Weblate (Hungarian)
Currently translated at 78.4% (386 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 78.4% (386 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-14 14:04:13 +00:00
Weblate
b272c97694 Translated using Weblate (Hungarian)
Currently translated at 73.5% (362 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 73.5% (362 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translation: Homebox/Frontend
2025-07-14 14:01:43 +00:00
Weblate
3004d376ab Translated using Weblate (Slovak)
Currently translated at 100.0% (492 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 70.7% (348 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 70.7% (348 of 492 strings)

Co-authored-by: Adam Kleizer <adamkleizer@gmail.com>
Co-authored-by: Jose Riha <jose1711@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/sk/
Translation: Homebox/Frontend
2025-07-14 13:59:12 +00:00
Matthew Kilgore
8f440e2a64 Fix setup directory for Windows binary 2025-07-12 22:21:38 -04:00
Matthew Kilgore
017b05452a Merge remote-tracking branch 'origin/main' 2025-07-12 16:37:09 -04:00
Matthew Kilgore
6a1f2549df Cleanup main file after revert, add freebsd build 2025-07-12 16:37:01 -04:00
Matthew Kilgore
2f51ba419b Revert "Support listening on unix sockets and systemd sockets (#878)"
This reverts commit 850ed476
2025-07-12 16:33:29 -04:00
Matias Godoy
bcd77ee796 Make search accent-insensitive (#887)
* Make search accent-insensitive

* Efficiendy improvements and small fixes

* Fix tests to improve coverage

* Fix SQL compatibility issues
2025-07-12 16:16:55 -04:00
Matt
23cecfb2a5 Refactor main file, add support for postgres certificate authentication (#897)
* Refactor main file, add support for postgres certificate authentication

* Fix potential issues.

* Remove legacy linting ignore comment

* Minor cleanup, documentation update
2025-07-12 16:11:50 -04:00
Matthew Kilgore
f4c8dd5450 Prep docs for Cloudflare worker migration (Pages is apparently deprecated/no longer recommended) 2025-07-12 14:56:05 -04:00
Copilot
72033341b4 Fix photo display issue when adding additional attachments to items (#895)
* Initial plan

* Fix attachment display issue - prevent photo primary status loss when updating non-photo attachments

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-07-12 13:36:21 -04:00
Copilot
c2cfa10336 Fix nil pointer dereference panic in thumbnail subscription during shutdown (#892)
* Initial plan

* Fix nil pointer dereference in thumbnail subscription handling

Add nil check for msg after subscription.Receive() returns error to prevent
panic when accessing msg.Metadata. When an error occurs or msg is nil,
continue to next iteration instead of trying to process the message.

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-07-12 11:40:50 -04:00
Balki
850ed476d4 Support listening on unix sockets and systemd sockets (#878) 2025-07-12 09:58:16 -04:00
Ahmosys
adea83d421 fix(frontend/location): preserve parent location when using "Create and Add another" (#879)
* fix(frontend/location): preserve parent in "Create and Add another" modal flow

* fix: normalize line endings

* fix: preserve parent location state when modal closed
2025-07-12 00:08:41 +00:00
Ahmosys
d678c35c57 fix(frontend/scanner): close scanner modal after successful QR code scan (#889)
* fix(frontend/scanner): close scanner modal after successful QR code scan

* fix: linting errors
2025-07-10 17:00:08 -04:00
Matt
d3073b472d Fix rootless 2025-07-10 16:58:23 -04:00
Matt
b274f81dbb Fix broken docker actions 2025-07-10 16:56:50 -04:00
Matt
721e407600 Update docker-publish.yaml 2025-07-10 14:32:12 -04:00
Copilot
ca4aed7bd3 Fix GitHub Actions Docker workflow syntax errors for secrets access (#882)
* Initial plan

* Fix GitHub Actions Docker workflow syntax errors

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

* Fix GitHub Actions expression syntax for if conditions

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-07-10 14:29:30 -04:00
Weblate
746bd50f24 Translated using Weblate (Slovenian)
Currently translated at 100.0% (492 of 492 strings)

Co-authored-by: Murk <saso@workrum.net>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/sl/
Translation: Homebox/Frontend
2025-07-10 13:52:06 +00:00
Weblate
945a768691 Translated using Weblate (Danish)
Currently translated at 99.7% (491 of 492 strings)

Co-authored-by: Heine Olsen <olsen10051988@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/da/
Translation: Homebox/Frontend
2025-07-10 02:06:57 +00:00
Weblate
27237ae6d3 Translated using Weblate (Danish)
Currently translated at 95.9% (472 of 492 strings)

Co-authored-by: Heine Olsen <olsen10051988@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/da/
Translation: Homebox/Frontend
2025-07-10 00:18:26 +00:00
Ahmed Al Hafoudh
4463867cf0 Pass label param to print command template (#886) 2025-07-09 12:11:16 -04:00
Weblate
95e2fb6a15 Translated using Weblate (Norwegian Bokmål)
Currently translated at 99.3% (489 of 492 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 99.3% (489 of 492 strings)

Co-authored-by: Anders Øyvind Urke-Sætre <andersoyvind@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/nb_NO/
Translation: Homebox/Frontend
2025-07-09 12:35:25 +00:00
Copilot
e32dd0aaa5 Fix frontend duplicate tag creation in Label Selector (#861)
* Initial plan

* Fix frontend duplicate tag creation issue

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-07-09 03:48:46 +00:00
Weblate
ee5c43dc29 Translated using Weblate (French)
Currently translated at 99.3% (489 of 492 strings)

Co-authored-by: buzz <buzz.eclair@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/fr/
Translation: Homebox/Frontend
2025-07-08 20:00:40 +00:00
Matt
17c9685391 Better Copilot tooling 2025-07-07 11:46:41 -04:00
Copilot
fd41065250 Fix warranty section visibility when lifetime warranty is enabled (#875)
* Initial plan

* Fix warranty section visibility when lifetime warranty is enabled

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-07-07 11:24:26 -04:00
Weblate
f9b1327507 Translated using Weblate (Slovak)
Currently translated at 100.0% (492 of 492 strings)

Co-authored-by: Jose Riha <jose1711@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/sk/
Translation: Homebox/Frontend
2025-07-07 14:00:40 +00:00
mcarbonne
5ed0e5c000 fix ghcr repo + disable dockerhub if not provided (#870) 2025-07-06 21:43:07 -04:00
mcarbonne
ce1e58828a Add migration for old sqlite timestamps (#869)
* add migration for old sqlite timestamps

* format python file + add support for negative timezones
2025-07-06 21:42:19 -04:00
Matt
d74508e214 Create .README FIRST.md 2025-07-06 08:26:40 -04:00
Weblate
178e676521 Update translation files
Updated by "Remove blank strings" add-on in Weblate.

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/
Translation: Homebox/Frontend
2025-07-06 12:14:50 +00:00
Weblate
c215373458 Translated using Weblate (Vietnamese)
Currently translated at 6.0% (30 of 492 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (492 of 492 strings)

Co-authored-by: Ng. H. Duyên <huongduyen.work@gmail.com>
Co-authored-by: euforik <euforik22@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/sk/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/vi/
Translation: Homebox/Frontend
2025-07-05 15:00:40 +00:00
Copilot
82bceb2185 Fix HBOX_LOG_LEVEL environment variable being ignored due to backwards logic (#862)
Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2025-07-04 23:44:20 -04:00
Matt
be1f3c0ad3 Use aspect ratio when making thumbnails and fix rotation (#857) 2025-07-04 11:44:08 -04:00
Weblate
518d13ccbb Added translation using Weblate (Vietnamese)
Co-authored-by: Ng. H. Duyên <huongduyen.work@gmail.com>
2025-07-04 14:30:49 +00:00
Weblate
8bef7b236b Translated using Weblate (Czech)
Currently translated at 100.0% (492 of 492 strings)

Co-authored-by: Adam Havránek <adamhavra@seznam.cz>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/cs/
Translation: Homebox/Frontend
2025-07-04 03:20:53 +00:00
Weblate
e774e57bee Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (492 of 492 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (492 of 492 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (492 of 492 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: Weblate Translation Memory <noreply-mt-weblate-translation-memory@weblate.org>
Co-authored-by: xdjohn99 <jh24cd@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/zh_Hans/
Translation: Homebox/Frontend
2025-07-03 01:16:22 +00:00
Weblate
d6d0d6dc56 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 94.7% (466 of 492 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 94.7% (466 of 492 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: xdjohn99 <jh24cd@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/zh_Hans/
Translation: Homebox/Frontend
2025-07-02 23:53:49 +00:00
Weblate
7e0ea5fee5 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 94.3% (464 of 492 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 94.3% (464 of 492 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: xdjohn99 <jh24cd@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/zh_Hans/
Translation: Homebox/Frontend
2025-07-02 23:50:20 +00:00
Weblate
10dcc1c01d Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 92.0% (453 of 492 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 92.0% (453 of 492 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: xdjohn99 <jh24cd@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/zh_Hans/
Translation: Homebox/Frontend
2025-07-02 23:42:07 +00:00
Weblate
38c37111cf Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 86.5% (426 of 492 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 86.5% (426 of 492 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: xdjohn99 <jh24cd@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/zh_Hans/
Translation: Homebox/Frontend
2025-07-02 23:21:29 +00:00
Weblate
bf27d147dd Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 77.8% (383 of 492 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 77.8% (383 of 492 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: xdjohn99 <jh24cd@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/zh_Hans/
Translation: Homebox/Frontend
2025-07-02 23:01:12 +00:00
Weblate
000ccd6d38 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 76.4% (376 of 492 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 76.4% (376 of 492 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: xdjohn99 <jh24cd@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/zh_Hans/
Translation: Homebox/Frontend
2025-07-02 22:58:12 +00:00
Weblate
f6fc30e218 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 76.0% (374 of 492 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 76.0% (374 of 492 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: xdjohn99 <jh24cd@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/zh_Hans/
Translation: Homebox/Frontend
2025-07-02 22:57:51 +00:00
Weblate
b444774f9b Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 75.8% (373 of 492 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/zh_Hans/
Translation: Homebox/Frontend
2025-07-02 22:57:31 +00:00
Weblate
91373dceb8 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 75.6% (372 of 492 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 75.6% (372 of 492 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (492 of 492 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (492 of 492 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: Slydite4 <39199098+Slydite4@users.noreply.github.com>
Co-authored-by: xdjohn99 <jh24cd@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/es/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/zh_Hans/
Translation: Homebox/Frontend
2025-07-02 22:56:25 +00:00
Weblate
ceb1bf89a1 Translated using Weblate (Spanish)
Currently translated at 98.5% (485 of 492 strings)

Translated using Weblate (Spanish)

Currently translated at 98.5% (485 of 492 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: Slydite4 <39199098+Slydite4@users.noreply.github.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/es/
Translation: Homebox/Frontend
2025-07-02 20:12:09 +00:00
Weblate
eb1428d3ac Translated using Weblate (Spanish)
Currently translated at 98.3% (484 of 492 strings)

Translated using Weblate (Spanish)

Currently translated at 98.3% (484 of 492 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: Slydite4 <39199098+Slydite4@users.noreply.github.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/es/
Translation: Homebox/Frontend
2025-07-02 20:11:48 +00:00
Weblate
e4ac7633a5 Translated using Weblate (Norwegian Bokmål)
Currently translated at 98.9% (487 of 492 strings)

Translated using Weblate (Spanish)

Currently translated at 97.9% (482 of 492 strings)

Translated using Weblate (Spanish)

Currently translated at 97.9% (482 of 492 strings)

Translated using Weblate (Swedish)

Currently translated at 68.0% (335 of 492 strings)

Translated using Weblate (Swedish)

Currently translated at 68.0% (335 of 492 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (492 of 492 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (492 of 492 strings)

Co-authored-by: Hannes Salen <hannes.salen@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: Slydite4 <39199098+Slydite4@users.noreply.github.com>
Co-authored-by: networked47 <nic.walsh@gmail.com>
Co-authored-by: terhoy <terjeho@hotmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/es/
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/sv/
Translation: Homebox/Frontend
2025-07-02 20:10:53 +00:00
Matt
bd604f5867 Update .goreleaser.yaml 2025-07-01 20:36:33 -04:00
Matthew Kilgore
b6b939db40 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	frontend/locales/fr.json
#	frontend/locales/sk-SK.json
#	frontend/locales/sl.json
2025-07-01 19:14:07 -04:00
Matthew Kilgore
d3f56b1b95 Fix version links for all languages 2025-07-01 19:13:52 -04:00
Weblate
a258e1d2bc Translated using Weblate (Slovak)
Currently translated at 99.1% (488 of 492 strings)

Translated using Weblate (Slovenian)

Currently translated at 100.0% (492 of 492 strings)

Translated using Weblate (French)

Currently translated at 100.0% (492 of 492 strings)

Co-authored-by: Jose Riha <jose1711@gmail.com>
Co-authored-by: Murk <saso@workrum.net>
Co-authored-by: Telectroboy <telectroboy@hotmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/fr/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/sk/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/sl/
Translation: Homebox/Frontend
2025-07-01 23:13:32 +00:00
Stanley Jochman
5ca671a4ab fix: footer release link points to wrong link (#830) 2025-07-01 19:13:15 -04:00
Matthew Kilgore
ccdab8bac1 Fix attachments get method 2025-07-01 19:03:24 -04:00
Matt
0d2a6d6ac8 Revert weblate skipping 2025-07-01 10:09:40 -04:00
Matt
e159dd8a0b Merge commit from fork 2025-07-01 09:56:34 -04:00
Matt
b311a5c9ed Update CreateModal.vue 2025-07-01 08:53:12 -04:00
Matthew Kilgore
04c8e38ecf Escape file name for content-disposition. 2025-06-30 20:55:11 -04:00
Matthew Kilgore
1fd2f42282 Hopefully fixed ARMv7 Rootless build 2025-06-30 20:44:20 -04:00
Matthew Kilgore
d3cff18cc6 Ignore weblate for builds 2025-06-30 20:21:53 -04:00
Weblate
3e27c24fbd Translated using Weblate (Japanese)
Currently translated at 59.9% (295 of 492 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 96.5% (475 of 492 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 66.2% (326 of 492 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 74.5% (367 of 492 strings)

Translated using Weblate (German)

Currently translated at 97.7% (481 of 492 strings)

Translated using Weblate (English)

Currently translated at 100.0% (492 of 492 strings)

Co-authored-by: Matthew Kilgore <matthew@kilgore.dev>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/de/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/en/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ja/
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/zh_Hans/
Translation: Homebox/Frontend
2025-07-01 00:17:05 +00:00
Weblate
acd5acd4cf Translated using Weblate (Slovak)
Currently translated at 98.7% (486 of 492 strings)

Co-authored-by: Jose Riha <jose1711@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/sk/
Translation: Homebox/Frontend
2025-06-30 23:57:40 +00:00
Weblate
71dc5fcb23 Translated using Weblate (Slovak)
Currently translated at 98.5% (485 of 492 strings)

Co-authored-by: Jose Riha <jose1711@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/sk/
Translation: Homebox/Frontend
2025-06-30 20:45:18 +00:00
Weblate
2ff5f4ca0b Translated using Weblate (Slovak)
Currently translated at 91.0% (448 of 492 strings)

Translated using Weblate (Slovak)

Currently translated at 91.0% (448 of 492 strings)

Co-authored-by: Jose Riha <jose1711@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/sk/
Translation: Homebox/Frontend
2025-06-30 20:27:53 +00:00
Weblate
12831a40d0 Translated using Weblate (Slovak)
Currently translated at 88.6% (436 of 492 strings)

Translated using Weblate (Slovak)

Currently translated at 88.6% (436 of 492 strings)

Co-authored-by: Jose Riha <jose1711@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/sk/
Translation: Homebox/Frontend
2025-06-30 20:25:38 +00:00
Weblate
c966090889 Translated using Weblate (Slovak)
Currently translated at 87.3% (430 of 492 strings)

Translated using Weblate (Slovak)

Currently translated at 87.3% (430 of 492 strings)

Co-authored-by: Jose Riha <jose1711@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/sk/
Translation: Homebox/Frontend
2025-06-30 20:24:27 +00:00
Weblate
f03eb637a7 Translated using Weblate (Slovak)
Currently translated at 85.1% (419 of 492 strings)

Translated using Weblate (Slovak)

Currently translated at 85.1% (419 of 492 strings)

Co-authored-by: Jose Riha <jose1711@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/sk/
Translation: Homebox/Frontend
2025-06-30 20:22:40 +00:00
Weblate
49ea34f352 Translated using Weblate (Slovak)
Currently translated at 83.7% (412 of 492 strings)

Translated using Weblate (Slovak)

Currently translated at 83.7% (412 of 492 strings)

Co-authored-by: Jose Riha <jose1711@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/sk/
Translation: Homebox/Frontend
2025-06-30 20:21:36 +00:00
Weblate
85d91667eb Translated using Weblate (Slovak)
Currently translated at 81.3% (400 of 492 strings)

Translated using Weblate (Slovak)

Currently translated at 81.3% (400 of 492 strings)

Co-authored-by: Jose Riha <jose1711@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/sk/
Translation: Homebox/Frontend
2025-06-30 20:14:34 +00:00
Weblate
458554b6e1 Translated using Weblate (Slovak)
Currently translated at 78.2% (385 of 492 strings)

Translated using Weblate (Slovak)

Currently translated at 78.2% (385 of 492 strings)

Co-authored-by: Jose Riha <jose1711@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/sk/
Translation: Homebox/Frontend
2025-06-30 20:05:37 +00:00
Weblate
79ff5cedc6 Translated using Weblate (Slovak)
Currently translated at 77.6% (382 of 492 strings)

Translated using Weblate (Slovak)

Currently translated at 77.6% (382 of 492 strings)

Co-authored-by: Jose Riha <jose1711@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/sk/
Translation: Homebox/Frontend
2025-06-30 20:05:00 +00:00
Weblate
f0008abd04 Translated using Weblate (Slovak)
Currently translated at 77.4% (381 of 492 strings)

Translated using Weblate (Slovak)

Currently translated at 77.4% (381 of 492 strings)

Co-authored-by: Jose Riha <jose1711@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/sk/
Translation: Homebox/Frontend
2025-06-30 20:04:12 +00:00
Weblate
8a377b3e4d Translated using Weblate (Slovak)
Currently translated at 77.2% (380 of 492 strings)

Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/sk/
Translation: Homebox/Frontend
2025-06-30 20:03:44 +00:00
Weblate
e7d31722f7 Translated using Weblate (Slovak)
Currently translated at 77.0% (379 of 492 strings)

Translated using Weblate (Slovak)

Currently translated at 77.0% (379 of 492 strings)

Translated using Weblate (German)

Currently translated at 99.3% (489 of 492 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (492 of 492 strings)

Co-authored-by: Daniel Galle <smarthome@galle-fw.com>
Co-authored-by: Hannes Salen <hannes.salen@gmail.com>
Co-authored-by: Jose Riha <jose1711@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/de/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/nl/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/sk/
Translation: Homebox/Frontend
2025-06-30 17:00:40 +00:00
Matthew Kilgore
52b4506e12 Fix docker builds 2025-06-29 20:44:20 -04:00
Matthew Kilgore
cb9631c999 Try this to fix the build? (More like the original) 2025-06-29 20:28:35 -04:00
Matthew Kilgore
6e3186a9de Merge remote-tracking branch 'origin/main' 2025-06-29 20:08:54 -04:00
Matthew Kilgore
a67070f965 Hopefully fix build 2025-06-29 20:08:43 -04:00
132 changed files with 7343 additions and 9397 deletions

View File

@@ -29,6 +29,6 @@
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "node",
"features": {
"ghcr.io/devcontainers/features/go:1": "1.21"
"ghcr.io/devcontainers/features/go:1": "1.24"
}
}

40
.github/AGENTS.md vendored Normal file
View File

@@ -0,0 +1,40 @@
This is a Go based repository with a VueJS client for the frontend built with Vite and Nuxt, with ShadCN.
To make life easier, the use of a Taskfile is included for the majority of development commands.
Please follow these guidelines when contributing:
## Required Before Each Commit
- Generate Swagger Files: `task swag --force`
- Generate JS API Client: `task typescript-types --force`
- Lint Golang: `task go:lint`
- Lint frontend: `task ui:fix`
## Repository Structure
### Backend
- `backend/`: Contains the backend folders
- `backend/app`: Contains main app code including API endpoints
- `backend/internal/core`: Contains basic services such as currencies
- `backend/data`: Contains all information related to data, including `ent` schemas, repos, migrations, etc.
- `backend/data/migrations`: Contains migration data, the `sqlite3` sub-folder contains sqlite migrations, `postgres` sub-folder the postgres migrations, BOTH are REQUIRED.
- `backend/data/ent/schema`: Contains the actual `ent` data models.
- `backend/data/repo`: Contains the data repositories
- `backend/pkgs`: Contains general helper functions and services
### Frontend
- `frontend/`: Contains initial frontend files
- `frontend/components`: Contains the ShadCN components
- `frontend/locales`: Contains the i18n JSON for languages
- `frontend/pages`: Contains VueJS pages
- `frontend/test`: Contains Playwright setup
- `frontend/test/e2e`: Contains actual Playwright test files
### Docs
- `docs/`: Contains VitePress based documentation
## Key Guidelines
1. Follow best practices for the various programming languages
2. Maintain existing code structure and organization when possible
3. Use dependency injection when reasonable
4. Write tests for new functionality and after fixing bugs to validate they're fixed
5. Document changes to the `docs/` folder when appropriate

View File

@@ -1,6 +1,7 @@
name: Publish Release Binaries
on:
workflow_dispatch:
push:
tags: [ 'v*.*.*' ]
@@ -8,6 +9,10 @@ jobs:
goreleaser:
name: goreleaser
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -37,6 +42,7 @@ jobs:
go install github.com/sigstore/cosign/cmd/cosign@latest
- name: Run GoReleaser
if: startsWith(github.ref, 'refs/tags/')
uses: goreleaser/goreleaser-action@v5
with:
workdir: "backend"
@@ -45,3 +51,18 @@ jobs:
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COSIGN_PWD: ${{ secrets.COSIGN_PWD }}
COSIGN_YES: "true"
- name: Run GoReleaser No Release
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
uses: goreleaser/goreleaser-action@v5
with:
workdir: "backend"
distribution: goreleaser
version: "~> v2"
args: release --clean --snapshot --skip=publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COSIGN_PWD: ${{ secrets.COSIGN_PWD }}
COSIGN_YES: "true"

View File

@@ -0,0 +1,52 @@
name: "Copilot Setup Steps"
# Automatically run the setup steps when they are changed to allow for easy validation, and
# allow manual testing through the repository's "Actions" tab
on:
workflow_dispatch:
push:
paths:
- .github/workflows/copilot-setup-steps.yml
pull_request:
paths:
- .github/workflows/copilot-setup-steps.yml
jobs:
# The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot.
copilot-setup-steps:
runs-on: ubuntu-latest
# Set the permissions to the lowest permissions possible needed for your steps.
# Copilot will be given its own token for its operations.
permissions:
# If you want to clone the repository as part of your setup steps, for example to install dependencies, you'll need the `contents: read` permission. If you don't clone the repository in your setup steps, Copilot will do this for you automatically after the steps complete.
contents: read
# You can define any steps you want, and they will run before the agent starts.
# If you do not check out your code, Copilot will do this for you.
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
- uses: pnpm/action-setup@v3.0.0
with:
version: 9.12.2
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.24"
cache-dependency-path: backend/go.mod
- name: Install Task
uses: arduino/setup-task@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Perform setup
run: task setup

View File

@@ -0,0 +1,208 @@
name: Docker publish hardened
on:
schedule:
- cron: '00 0 * * *'
push:
branches: [ "main" ]
paths:
- 'backend/**'
- 'frontend/**'
- 'Dockerfile.hardened'
- '.dockerignore'
- '.github/workflows/docker-publish-hardened.yaml'
tags: [ 'v*.*.*' ]
pull_request:
branches: [ "main" ]
paths:
- 'backend/**'
- 'frontend/**'
- 'Dockerfile.hardened'
- '.dockerignore'
- '.github/workflows/docker-publish-hardened.yaml'
permissions:
contents: read # Access to repository contents
packages: write # Write access for pushing to GHCR
id-token: write # Required for OIDC authentication (if used)
attestations: write # Required for signing and attestation (if needed)
env:
DOCKERHUB_REPO: sysadminsmedia/homebox
GHCR_REPO: ghcr.io/${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
attestations: write
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
- linux/arm/v7
steps:
- name: Enable Debug Logs
run: echo "##[debug]Enabling debug logging"
env:
ACTIONS_RUNNER_DEBUG: true
ACTIONS_STEP_DEBUG: true
- name: Checkout repository
uses: actions/checkout@v4
- name: Prepare
run: |
echo "BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_ENV
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
branch=${{ github.event.pull_request.number || github.ref_name }}
echo "BRANCH=${branch//\//-}" >> $GITHUB_ENV
echo "DOCKERNAMES=${{ env.DOCKERHUB_REPO }},${{ env.GHCR_REPO }}" >> $GITHUB_ENV
if [[ "${{ github.event_name }}" != "schedule" ]] || [[ "${{ github.ref }}" != refs/tags/* ]]; then
echo "DOCKERNAMES=${{ env.GHCR_REPO }}" >> $GITHUB_ENV
fi
- name: Docker meta
id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
with:
images: |
name=${{ env.DOCKERHUB_REPO }},enable=${{ github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') }}
name=${{ env.GHCR_REPO }}
- name: Login to Docker Hub
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GHCR
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392
with:
image: ghcr.io/amitie10g/binfmt:latest
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
with:
driver-opts: |
image=ghcr.io/amitie10g/buildkit:master
- name: Build and push by digest
id: build
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
with:
context: . # Explicitly specify the build context
file: ./Dockerfile.hardened # Explicitly specify the Dockerfile
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,"name=${{ env.DOCKERNAMES }}",push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
cache-from: type=registry,ref=ghcr.io/sysadminsmedia/devcache:${{ env.PLATFORM_PAIR }}-${{ env.BRANCH }}-hardened
cache-to: type=registry,ref=ghcr.io/sysadminsmedia/devcache:${{ env.PLATFORM_PAIR }}-${{ env.BRANCH }}-hardened,mode=max,ignore-error=true
build-args: |
VERSION=${{ github.ref_name }}
COMMIT=${{ github.sha }}
BUILD_TIME=${{ env.BUILD_TIME }}
provenance: true
sbom: true
annotations: ${{ steps.meta.outputs.annotations }}
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
attestations: write
needs:
- build
steps:
- name: Download digests
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Login to Docker Hub
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GHCR
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
with:
driver-opts: |
image=ghcr.io/amitie10g/buildkit:master
- name: Docker meta
id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
with:
images: |
name=${{ env.DOCKERHUB_REPO }},enable=${{ github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/') }}
name=${{ env.GHCR_REPO }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=schedule,pattern=nightly
flavor: |
suffix=-hardened,onlatest=true
- name: Create manifest list and push GHCR
id: push-ghcr
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *)
- name: Create manifest list and push Dockerhub
id: push-dockerhub
working-directory: /tmp/digests
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.DOCKERHUB_REPO }}@sha256:%s ' *)

View File

@@ -8,7 +8,7 @@ on:
paths:
- 'backend/**'
- 'frontend/**'
- 'Dockerfile'
- 'Dockerfile.rootless'
- '.dockerignore'
- '.github/workflows/docker-publish-rootless.yaml'
ignore:
@@ -19,7 +19,7 @@ on:
paths:
- 'backend/**'
- 'frontend/**'
- 'Dockerfile'
- 'Dockerfile.rootless'
- '.dockerignore'
- '.github/workflows/docker-publish-rootless.yaml'
ignore:
@@ -33,7 +33,7 @@ permissions:
env:
DOCKERHUB_REPO: sysadminsmedia/homebox
GHCR_REPO: ghcr.io/sysadminsmedia/homebox
GHCR_REPO: ghcr.io/${{ github.repository }}
jobs:
build:
@@ -51,7 +51,6 @@ jobs:
- linux/amd64
- linux/arm64
- linux/arm/v7
- linux/riscv64
steps:
- name: Enable Debug Logs
@@ -84,7 +83,7 @@ jobs:
- name: Login to Docker Hub
uses: docker/login-action@v3
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -160,7 +159,7 @@ jobs:
- name: Login to Docker Hub
uses: docker/login-action@v3
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -205,7 +204,7 @@ jobs:
- name: Create manifest list and push Dockerhub
id: push-dockerhub
working-directory: /tmp/digests
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.DOCKERHUB_REPO }}@sha256:%s ' *)

View File

@@ -27,7 +27,7 @@ on:
env:
DOCKERHUB_REPO: sysadminsmedia/homebox
GHCR_REPO: ghcr.io/sysadminsmedia/homebox
GHCR_REPO: ghcr.io/${{ github.repository }}
permissions:
contents: read # Access to repository contents
@@ -51,7 +51,6 @@ jobs:
- linux/amd64
- linux/arm64
- linux/arm/v7
- linux/riscv64
steps:
- name: Checkout repository
@@ -79,7 +78,7 @@ jobs:
- name: Login to Docker Hub
uses: docker/login-action@v3
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -153,6 +152,7 @@ jobs:
- name: Login to Docker Hub
uses: docker/login-action@v3
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -195,8 +195,7 @@ jobs:
- name: Create manifest list and push Dockerhub
id: push-dockerhub
working-directory: /tmp/digests
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.DOCKERHUB_REPO }}@sha256:%s ' *)

View File

@@ -9,7 +9,10 @@ on:
paths:
- 'backend/**'
- 'frontend/**'
- '.github/workflows/**'
- '.github/workflows/partial-backend.yaml'
- '.github/workflows/partial-frontend.yaml'
- '.github/workflows/e2e-partial.yaml'
- '.github/workflows/pull-requests.yaml'
jobs:
backend-tests:

8
.vscode/launch.json vendored
View File

@@ -16,14 +16,12 @@
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceRoot}/backend/app/api/",
"program": "${workspaceFolder}/backend/app/api/",
"args": [],
"env": {
"HBOX_DEMO": "true",
"HBOX_LOG_LEVEL": "debug",
"HBOX_DEBUG_ENABLED": "true",
"HBOX_STORAGE_DATA": "${workspaceRoot}/backend/.data",
"HBOX_STORAGE_SQLITE_URL": "${workspaceRoot}/backend/.data/homebox.db?_fk=1&_time_format=sqlite"
"HBOX_DEBUG_ENABLED": "true"
},
"console": "integratedTerminal",
},
@@ -46,4 +44,4 @@
"console": "integratedTerminal",
}
]
}
}

View File

@@ -1,5 +1,5 @@
# Node dependencies stage
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/node:lts-alpine AS frontend-dependencies
FROM public.ecr.aws/docker/library/node:lts-alpine AS frontend-dependencies
WORKDIR /app
# Install pnpm globally (caching layer)
@@ -10,7 +10,7 @@ COPY frontend/package.json frontend/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# Build Nuxt (frontend) stage
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/node:lts-alpine AS frontend-builder
FROM public.ecr.aws/docker/library/node:lts-alpine AS frontend-builder
WORKDIR /app
# Install pnpm globally again (it can reuse the cache if not changed)
@@ -22,7 +22,7 @@ COPY --from=frontend-dependencies /app/node_modules ./node_modules
RUN pnpm build
# Go dependencies stage
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/golang:alpine AS builder-dependencies
FROM public.ecr.aws/docker/library/golang:alpine AS builder-dependencies
WORKDIR /go/src/app
# Copy go.mod and go.sum for better caching
@@ -30,7 +30,7 @@ COPY ./backend/go.mod ./backend/go.sum ./
RUN go mod download
# Build API stage
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/golang:alpine AS builder
FROM public.ecr.aws/docker/library/golang:alpine AS builder
ARG TARGETOS
ARG TARGETARCH
ARG BUILD_TIME
@@ -40,7 +40,8 @@ ARG VERSION
# Install necessary build tools
RUN apk update && \
apk upgrade && \
apk add --no-cache git build-base gcc g++
apk add --no-cache git build-base gcc g++ && \
if [ "$TARGETARCH" != "arm" ] || [ "$TARGETARCH" != "riscv64" ]; then apk --no-cache add libwebp libavif libheif libjxl; fi
WORKDIR /go/src/app
@@ -55,17 +56,17 @@ COPY --from=frontend-builder /app/.output/public ./app/api/static/public
# Use cache for Go build artifacts
RUN --mount=type=cache,target=/root/.cache/go-build \
if [ "$TARGETARCH" = "arm" ] || [ "$TARGETARCH" = "riscv64" ]; \
then CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
then echo "nodynamic" $TARGETOS $TARGETARCH; CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build \
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
-tags nodynamic -o /go/bin/api -v ./app/api/*.go; \
else \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
echo $TARGETOS $TARGETARCH; CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build \
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
-o /go/bin/api -v ./app/api/*.go; \
fi
# Production stage
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/alpine:latest
FROM public.ecr.aws/docker/library/alpine:latest
ENV HBOX_MODE=production
ENV HBOX_STORAGE_CONN_STRING=file:///?no_tmp_dir=true
ENV HBOX_STORAGE_PREFIX_PATH=data
@@ -73,7 +74,7 @@ ENV HBOX_DATABASE_SQLITE_PATH=/data/homebox.db?_pragma=busy_timeout=2000&_pragma
# Install necessary runtime dependencies
RUN apk --no-cache add ca-certificates wget && \
if [ "$TARGETARCH" != "arm" ] || [ "$TARGETARCH" != "riscv64" ]; then apk --no-cache add libwebp libavif; fi
if [ "$TARGETARCH" != "arm" ] || [ "$TARGETARCH" != "riscv64" ]; then apk --no-cache add libwebp libavif libheif libjxl; fi
# Create application directory and copy over built Go binary
RUN mkdir /app

136
Dockerfile.hardened Normal file
View File

@@ -0,0 +1,136 @@
# ---------------------------------------
# Node dependencies stage
# ---------------------------------------
FROM public.ecr.aws/docker/library/node:lts-alpine AS frontend-dependencies
WORKDIR /app
# Install pnpm globally (caching layer)
RUN npm install -g pnpm
# Copy package.json and lockfile to leverage caching
COPY frontend/package.json frontend/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# ---------------------------------------
# Build Nuxt (frontend) stage
# ---------------------------------------
FROM public.ecr.aws/docker/library/node:lts-alpine AS frontend-builder
WORKDIR /app
# Install pnpm globally again (it can reuse the cache if not changed)
RUN npm install -g pnpm
# Copy over source files and node_modules from dependencies stage
COPY frontend .
COPY --from=frontend-dependencies /app/node_modules ./node_modules
RUN pnpm build
# ---------------------------------------
# Go dependencies stage
# ---------------------------------------
FROM public.ecr.aws/docker/library/golang:alpine AS builder-dependencies
WORKDIR /go/src/app
# Copy go.mod and go.sum for better caching
COPY ./backend/go.mod ./backend/go.sum ./
RUN go mod download
# ---------------------------------------
# Build API + healthcheck stage
# ---------------------------------------
FROM public.ecr.aws/docker/library/golang:alpine AS builder
ARG TARGETOS
ARG TARGETARCH
ARG BUILD_TIME
ARG COMMIT
ARG VERSION
# Install necessary build tools
RUN apk update && \
apk upgrade && \
apk add --no-cache git build-base gcc g++
WORKDIR /go/src/app
# Copy Go modules (from dependencies stage) and source code
COPY --from=builder-dependencies /go/pkg/mod /go/pkg/mod
COPY ./backend .
# Clear old public files and copy new ones from frontend build
RUN rm -rf ./app/api/public
COPY --from=frontend-builder /app/.output/public ./app/api/static/public
# Use cache for Go build artifacts to build Homebox API
RUN --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build \
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
-tags nodynamic -o /go/bin/api -v ./app/api/*.go
RUN chmod +x /go/bin/api
RUN mkdir /app
RUN mkdir /data
# ---------- Build static healthcheck helper ----------
# A small Go program that GETs the status URL and exits 0 on 2xx.
RUN cat > /tmp/healthcheck.go <<'EOF'
package main
import (
"fmt"
"net/http"
"os"
"time"
)
func main() {
url := "http://127.0.0.1:7745/api/v1/status"
if len(os.Args) > 1 { url = os.Args[1] }
c := &http.Client{ Timeout: 3 * time.Second }
resp, err := c.Get(url)
if err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) }
resp.Body.Close()
if resp.StatusCode/100 != 2 {
fmt.Fprintln(os.Stderr, "unexpected status:", resp.StatusCode)
os.Exit(1)
}
}
EOF
RUN --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
go build -ldflags "-s -w" -o /go/bin/hc /tmp/healthcheck.go
# ---------------------------------------
# Production stage
# ---------------------------------------
FROM gcr.io/distroless/static:nonroot
ENV HBOX_MODE=production
ENV HBOX_STORAGE_CONN_STRING=file:///?no_tmp_dir=true
ENV HBOX_STORAGE_PREFIX_PATH=data
ENV HBOX_DATABASE_SQLITE_PATH=/data/homebox.db?_pragma=busy_timeout=2000&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite
# Create application directory and copy over built Go binary and assets
COPY --from=builder --chown=65532:65532 /app /app
COPY --from=builder --chown=65532:65532 --chmod=755 /go/bin/api /app
COPY --from=builder --chown=65532:65532 /data /data
# Copy the healthcheck helper
COPY --from=builder --chown=65532:65532 --chmod=755 /go/bin/hc /app/healthcheck
# Labels and configuration for the final image
LABEL Name=homebox Version=0.0.1
LABEL org.opencontainers.image.source="https://github.com/sysadminsmedia/homebox"
# Expose necessary ports for Homebox
EXPOSE 7745
WORKDIR /app
# Persist volume for data
VOLUME [ "/data" ]
# Entrypoint and CMD
USER 65532
ENTRYPOINT [ "/app/api" ]
CMD [ "/data/config.yml" ]
# JSON exec-form healthcheck (no shell, no wget)
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD ["/app/healthcheck", "http://127.0.0.1:7745/api/v1/status"]

View File

@@ -1,5 +1,5 @@
# Node dependencies stage
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/node:lts-alpine AS frontend-dependencies
FROM public.ecr.aws/docker/library/node:lts-alpine AS frontend-dependencies
WORKDIR /app
# Install pnpm globally (caching layer)
@@ -10,7 +10,7 @@ COPY frontend/package.json frontend/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# Build Nuxt (frontend) stage
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/node:lts-alpine AS frontend-builder
FROM public.ecr.aws/docker/library/node:lts-alpine AS frontend-builder
WORKDIR /app
# Install pnpm globally again (it can reuse the cache if not changed)
@@ -22,7 +22,7 @@ COPY --from=frontend-dependencies /app/node_modules ./node_modules
RUN pnpm build
# Go dependencies stage
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/golang:alpine AS builder-dependencies
FROM public.ecr.aws/docker/library/golang:alpine AS builder-dependencies
WORKDIR /go/src/app
# Copy go.mod and go.sum for better caching
@@ -30,7 +30,9 @@ COPY ./backend/go.mod ./backend/go.sum ./
RUN go mod download
# Build API stage
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/golang:alpine AS builder
FROM public.ecr.aws/docker/library/golang:alpine AS builder
ARG TARGETOS
ARG TARGETARCH
ARG BUILD_TIME
ARG COMMIT
ARG VERSION
@@ -38,7 +40,8 @@ ARG VERSION
# Install necessary build tools
RUN apk update && \
apk upgrade && \
apk add --no-cache git build-base gcc g++
apk add --no-cache git build-base gcc g++ && \
if [ "$TARGETARCH" != "arm" ] || [ "$TARGETARCH" != "riscv64" ]; then apk --no-cache add libwebp libavif libheif libjxl; fi
WORKDIR /go/src/app
@@ -53,11 +56,11 @@ COPY --from=frontend-builder /app/.output/public ./app/api/static/public
# Use cache for Go build artifacts
RUN --mount=type=cache,target=/root/.cache/go-build \
if [ "$TARGETARCH" = "arm" ] || [ "$TARGETARCH" = "riscv64" ]; \
then CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
then echo "nodynamic" $TARGETOS $TARGETARCH; CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build \
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
-tags nodynamic -o /go/bin/api -v ./app/api/*.go; \
else \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
echo $TARGETOS $TARGETARCH; CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build \
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
-o /go/bin/api -v ./app/api/*.go; \
fi
@@ -65,7 +68,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
RUN mkdir /data
# Production stage
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/alpine:latest
FROM public.ecr.aws/docker/library/alpine:latest
ENV HBOX_MODE=production
ENV HBOX_STORAGE_CONN_STRING=file:///?no_tmp_dir=true
ENV HBOX_STORAGE_PREFIX_PATH=data
@@ -73,7 +76,7 @@ ENV HBOX_DATABASE_SQLITE_PATH=/data/homebox.db?_pragma=busy_timeout=2000&_pragma
# Install necessary runtime dependencies
RUN apk --no-cache add ca-certificates wget && \
if [ "$TARGETARCH" != "arm" ] || [ "$TARGETARCH" != "riscv64" ]; then apk --no-cache add libwebp libavif; fi
if [ "$TARGETARCH" != "arm" ] || [ "$TARGETARCH" != "riscv64" ]; then apk --no-cache add libwebp libavif libheif libjxl; fi
# Create a nonroot user with UID/GID 65532
RUN addgroup -g 65532 nonroot && adduser -u 65532 -G nonroot -S nonroot

View File

@@ -14,6 +14,7 @@ builds:
- linux
- windows
- darwin
- freebsd
goarch:
- amd64
- "386"
@@ -25,22 +26,24 @@ builds:
goarch: arm
- goos: windows
goarch: "386"
- goos: freebsd
goarch: arm
- goos: freebsd
goarch: "386"
tags:
- >-
{{- if eq .Arch "riscv64" }}nodynamic
{{- else if eq .Arch "arm" }}nodynamic
{{- else if eq .Arch "386" }}nodynamic
{{- else if eq .Os "freebsd" }}nodynamic
{{ end }}
sboms:
- disable: false
artifacts: any
signs:
- cmd: cosign
stdin: "{{ .Env.COSIGN_PWD }}"
args:
- "sign-blob"
- "--key=cosign.key"
- "--output-certificate=${certificate}"
- "--output-signature=${signature}"
- "${artifact}"
- "--yes" # needed on cosign 2.0.0+

View File

@@ -254,6 +254,25 @@ func (ctrl *V1Controller) HandleItemPatch() errchain.HandlerFunc {
return adapters.ActionID("id", fn, http.StatusOK)
}
// HandleItemDuplicate godocs
//
// @Summary Duplicate Item
// @Tags Items
// @Produce json
// @Param id path string true "Item ID"
// @Param payload body repo.DuplicateOptions true "Duplicate Options"
// @Success 201 {object} repo.ItemOut
// @Router /v1/items/{id}/duplicate [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleItemDuplicate() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID, options repo.DuplicateOptions) (repo.ItemOut, error) {
ctx := services.NewContext(r.Context())
return ctrl.svc.Items.Duplicate(ctx, ctx.GID, ID, options)
}
return adapters.ActionID("id", fn, http.StatusCreated)
}
// HandleGetAllCustomFieldNames godocs
//
// @Summary Get All Custom Field Names

View File

@@ -3,6 +3,7 @@ package v1
import (
"errors"
"net/http"
"net/url"
"path/filepath"
"strconv"
"strings"
@@ -174,7 +175,7 @@ func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r
ctx := services.NewContext(r.Context())
switch r.Method {
case http.MethodGet:
doc, err := ctrl.svc.Items.AttachmentPath(r.Context(), attachmentID)
doc, err := ctrl.svc.Items.AttachmentPath(r.Context(), ctx.GID, attachmentID)
if err != nil {
log.Err(err).Msg("failed to get attachment path")
return validate.NewRequestError(err, http.StatusInternalServerError)
@@ -203,7 +204,9 @@ func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r
}
}(bucket)
w.Header().Set("Content-Disposition", "attachment; filename="+doc.Title)
// Set the Content-Disposition header for RFC6266 compliance
disposition := "inline; filename*=UTF-8''" + url.QueryEscape(doc.Title)
w.Header().Set("Content-Disposition", disposition)
http.ServeContent(w, r, doc.Title, doc.CreatedAt, file)
return nil
@@ -227,9 +230,9 @@ func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r
}
attachment.ID = attachmentID
val, err := ctrl.svc.Items.AttachmentUpdate(ctx, ID, &attachment)
val, err := ctrl.svc.Items.AttachmentUpdate(ctx, ctx.GID, ID, &attachment)
if err != nil {
log.Err(err).Msg("failed to delete attachment")
log.Err(err).Msg("failed to update attachment")
return validate.NewRequestError(err, http.StatusInternalServerError)
}

View File

@@ -29,7 +29,7 @@ func generateOrPrint(ctrl *V1Controller, w http.ResponseWriter, r *http.Request,
_, err = w.Write([]byte("Printed!"))
return err
} else {
return labelmaker.GenerateLabel(w, &params)
return labelmaker.GenerateLabel(w, &params, ctrl.config)
}
}

View File

@@ -0,0 +1,332 @@
package v1
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/hay-kot/httpkit/errchain"
"github.com/hay-kot/httpkit/server"
"github.com/rs/zerolog/log"
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
"github.com/sysadminsmedia/homebox/backend/internal/web/adapters"
)
type UPCITEMDBResponse struct {
Code string `json:"code"`
Total int `json:"total"`
Offset int `json:"offset"`
Items []struct {
Ean string `json:"ean"`
Title string `json:"title"`
Description string `json:"description"`
Upc string `json:"upc"`
Brand string `json:"brand"`
Model string `json:"model"`
Color string `json:"color"`
Size string `json:"size"`
Dimension string `json:"dimension"`
Weight string `json:"weight"`
Category string `json:"category"`
LowestRecordedPrice float64 `json:"lowest_recorded_price"`
HighestRecordedPrice float64 `json:"highest_recorded_price"`
Images []string `json:"images"`
Offers []struct {
Merchant string `json:"merchant"`
Domain string `json:"domain"`
Title string `json:"title"`
Currency string `json:"currency"`
ListPrice string `json:"list_price"`
Price float64 `json:"price"`
Shipping string `json:"shipping"`
Condition string `json:"condition"`
Availability string `json:"availability"`
Link string `json:"link"`
UpdatedT int `json:"updated_t"`
} `json:"offers"`
Asin string `json:"asin"`
Elid string `json:"elid"`
} `json:"items"`
}
type BARCODESPIDER_COMResponse struct {
ItemResponse struct {
Code int `json:"code"`
Status string `json:"status"`
Message string `json:"message"`
} `json:"item_response"`
ItemAttributes struct {
Title string `json:"title"`
Upc string `json:"upc"`
Ean string `json:"ean"`
ParentCategory string `json:"parent_category"`
Category string `json:"category"`
Brand string `json:"brand"`
Model string `json:"model"`
Mpn string `json:"mpn"`
Manufacturer string `json:"manufacturer"`
Publisher string `json:"publisher"`
Asin string `json:"asin"`
Color string `json:"color"`
Size string `json:"size"`
Weight string `json:"weight"`
Image string `json:"image"`
IsAdult string `json:"is_adult"`
Description string `json:"description"`
} `json:"item_attributes"`
Stores []struct {
StoreName string `json:"store_name"`
Title string `json:"title"`
Image string `json:"image"`
Price string `json:"price"`
Currency string `json:"currency"`
Link string `json:"link"`
Updated string `json:"updated"`
} `json:"Stores"`
}
// HandleGenerateQRCode godoc
//
// @Summary Search EAN from Barcode
// @Tags Items
// @Produce json
// @Param data query string false "barcode to be searched"
// @Success 200 {object} []repo.BarcodeProduct
// @Router /v1/products/search-from-barcode [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleProductSearchFromBarcode(conf config.BarcodeAPIConf) errchain.HandlerFunc {
type query struct {
// 80 characters is the longest non-2D barcode length (GS1-128)
EAN string `schema:"productEAN" validate:"required,max=80"`
}
return func(w http.ResponseWriter, r *http.Request) error {
q, err := adapters.DecodeQuery[query](r)
if err != nil {
return err
}
const TIMEOUT_SEC = 10
log.Info().Msg("Processing barcode lookup request on: " + q.EAN)
// Search on UPCITEMDB
var products []repo.BarcodeProduct
// www.ean-search.org/: not free
// Example code: dewalt 5035048748428
upcitemdb := func(iEan string) ([]repo.BarcodeProduct, error) {
client := &http.Client{Timeout: TIMEOUT_SEC * time.Second}
resp, err := client.Get("https://api.upcitemdb.com/prod/trial/lookup?upc=" + iEan)
if err != nil {
return nil, err
}
defer func() {
err = errors.Join(err, resp.Body.Close())
}()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API returned status code: %d", resp.StatusCode)
}
// We Read the response body on the line below.
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// Uncomment the following string for debug
// sb := string(body)
// log.Debug().Msg("Response: " + sb)
var result UPCITEMDBResponse
if err := json.Unmarshal(body, &result); err != nil { // Parse []byte to go struct pointer
log.Error().Msg("Can not unmarshal JSON")
}
var res []repo.BarcodeProduct
for _, it := range result.Items {
var p repo.BarcodeProduct
p.SearchEngineName = "upcitemdb.com"
p.Barcode = iEan
p.Item.Description = it.Description
p.Item.Name = it.Title
p.Manufacturer = it.Brand
p.ModelNumber = it.Model
if len(it.Images) != 0 {
p.ImageURL = it.Images[0]
}
res = append(res, p)
}
return res, nil
}
ps, err := upcitemdb(q.EAN)
if err != nil {
log.Error().Msg("Can not retrieve product from upcitemdb.com" + err.Error())
}
// Barcode spider implementation
barcodespider := func(tokenAPI string, iEan string) ([]repo.BarcodeProduct, error) {
if len(tokenAPI) == 0 {
return nil, errors.New("no api token configured for barcodespider. " +
"Please define the api token in environment variable HBOX_BARCODE_TOKEN_BARCODESPIDER")
}
req, err := http.NewRequest(
"GET", "https://api.barcodespider.com/v1/lookup?upc="+iEan, nil)
if err != nil {
return nil, err
}
req.Header.Add("token", tokenAPI)
client := &http.Client{Timeout: TIMEOUT_SEC * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
// defer the call to Body.Close(). We also check the error code, and merge
// it with the other error in this code to avoid error overiding.
defer func() {
err = errors.Join(err, resp.Body.Close())
}()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("barcodespider API returned status code: %d", resp.StatusCode)
}
// We Read the response body on the line below.
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// Uncomment the following string for debug
// sb := string(body)
// log.Debug().Msg("Response: " + sb)
var result BARCODESPIDER_COMResponse
if err := json.Unmarshal(body, &result); err != nil { // Parse []byte to go struct pointer
log.Error().Msg("Can not unmarshal JSON")
}
// TODO: check 200 code on HTTP response.
var p repo.BarcodeProduct
p.Barcode = iEan
p.SearchEngineName = "barcodespider.com"
p.Item.Name = result.ItemAttributes.Title
p.Item.Description = result.ItemAttributes.Description
p.Manufacturer = result.ItemAttributes.Brand
p.ModelNumber = result.ItemAttributes.Model
p.ImageURL = result.ItemAttributes.Image
var res []repo.BarcodeProduct
res = append(res, p)
return res, nil
}
ps2, err := barcodespider(conf.TokenBarcodespider, q.EAN)
if err != nil {
log.Error().Msg("Can not retrieve product from barcodespider.com: " + err.Error())
}
// Merge everything.
products = append(products, ps...)
products = append(products, ps2...)
// Retrieve images if possible
for i := range products {
p := &products[i]
if len(p.ImageURL) == 0 {
continue
}
// Validate URL is HTTPS
u, err := url.Parse(p.ImageURL)
if err != nil || u.Scheme != "https" {
log.Warn().Msg("Skipping non-HTTPS image URL: " + p.ImageURL)
continue
}
client := &http.Client{Timeout: TIMEOUT_SEC * time.Second}
res, err := client.Get(p.ImageURL)
if err != nil {
log.Warn().Msg("Cannot fetch image for URL: " + p.ImageURL + ": " + err.Error())
}
defer func() {
err = errors.Join(err, res.Body.Close())
}()
// Validate response
if res.StatusCode != http.StatusOK {
continue
}
// Check content type
contentType := res.Header.Get("Content-Type")
if !strings.HasPrefix(contentType, "image/") {
continue
}
// Limit image size to 8MB
limitedReader := io.LimitReader(res.Body, 8*1024*1024)
// Read data of image
bytes, err := io.ReadAll(limitedReader)
if err != nil {
log.Warn().Msg(err.Error())
continue
}
// Convert to Base64
var base64Encoding string
// Determine the content type of the image file
mimeType := http.DetectContentType(bytes)
// Prepend the appropriate URI scheme header depending
// on the MIME type
switch mimeType {
case "image/jpeg":
base64Encoding += "data:image/jpeg;base64,"
case "image/png":
base64Encoding += "data:image/png;base64,"
default:
continue
}
// Append the base64 encoded output
base64Encoding += base64.StdEncoding.EncodeToString(bytes)
p.ImageBase64 = base64Encoding
}
if len(products) != 0 {
return server.JSON(w, http.StatusOK, products)
}
return server.JSON(w, http.StatusNoContent, nil)
}
}

View File

@@ -19,6 +19,9 @@ func (a *app) setupLogger() {
level, err := zerolog.ParseLevel(a.conf.Log.Level)
if err != nil {
log.Error().Err(err).Str("level", a.conf.Log.Level).Msg("invalid log level, falling back to info")
zerolog.SetGlobalLevel(zerolog.InfoLevel)
} else {
zerolog.SetGlobalLevel(level)
}
}

View File

@@ -1,21 +1,17 @@
package main
import (
"bytes"
"context"
"errors"
"fmt"
"github.com/google/uuid"
"github.com/sysadminsmedia/homebox/backend/pkgs/utils"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/pressly/goose/v3"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/pressly/goose/v3"
"github.com/sysadminsmedia/homebox/backend/internal/sys/analytics"
"github.com/hay-kot/httpkit/errchain"
"github.com/hay-kot/httpkit/graceful"
@@ -28,16 +24,15 @@ import (
"github.com/sysadminsmedia/homebox/backend/internal/data/ent"
"github.com/sysadminsmedia/homebox/backend/internal/data/migrations"
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
"github.com/sysadminsmedia/homebox/backend/internal/sys/analytics"
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
"github.com/sysadminsmedia/homebox/backend/internal/web/mid"
"go.balki.me/anyhttp"
_ "github.com/lib/pq"
_ "github.com/sysadminsmedia/homebox/backend/internal/data/migrations/postgres"
_ "github.com/sysadminsmedia/homebox/backend/internal/data/migrations/sqlite3"
_ "github.com/sysadminsmedia/homebox/backend/pkgs/cgofreesqlite"
"gocloud.dev/pubsub"
_ "gocloud.dev/pubsub/awssnssqs"
_ "gocloud.dev/pubsub/azuresb"
_ "gocloud.dev/pubsub/gcppubsub"
@@ -102,79 +97,56 @@ func main() {
}
}
//nolint:gocyclo
func run(cfg *config.Config) error {
app := new(cfg)
app.setupLogger()
if cfg.Options.AllowAnalytics {
analytics.Send(version, build())
}
// =========================================================================
// Initialize Database & Repos
if strings.HasPrefix(cfg.Storage.ConnString, "file:///./") {
raw := strings.TrimPrefix(cfg.Storage.ConnString, "file:///./")
clean := filepath.Clean(raw)
absBase, err := filepath.Abs(clean)
if err != nil {
log.Fatal().Err(err).Msg("failed to get absolute path for storage connection string")
}
// Construct and validate the full storage path
storageDir := filepath.Join(absBase, cfg.Storage.PrefixPath)
if !strings.HasPrefix(storageDir, absBase+string(os.PathSeparator)) && storageDir != absBase {
log.Fatal().
Str("path", storageDir).
Msg("invalid storage path: you tried to use a prefix that is not a subdirectory of the base path")
}
// Create with more restrictive permissions
if err := os.MkdirAll(storageDir, 0o750); err != nil {
log.Fatal().
Err(err).
Msg("failed to create data directory")
}
err := setupStorageDir(cfg)
if err != nil {
return err
}
if strings.ToLower(cfg.Database.Driver) == "postgres" {
if !validatePostgresSSLMode(cfg.Database.SslMode) {
log.Fatal().Str("sslmode", cfg.Database.SslMode).Msg("invalid sslmode")
log.Error().Str("sslmode", cfg.Database.SslMode).Msg("invalid sslmode")
return fmt.Errorf("invalid sslmode: %s", cfg.Database.SslMode)
}
}
// Set up the database URL based on the driver because for some reason a common URL format is not used
databaseURL := ""
switch strings.ToLower(cfg.Database.Driver) {
case "sqlite3":
databaseURL = cfg.Database.SqlitePath
// Create directory for SQLite database if it doesn't exist
dbFilePath := strings.Split(cfg.Database.SqlitePath, "?")[0] // Remove query parameters
dbDir := filepath.Dir(dbFilePath)
if err := os.MkdirAll(dbDir, 0o755); err != nil {
log.Fatal().Err(err).Str("path", dbDir).Msg("failed to create SQLite database directory")
}
case "postgres":
databaseURL = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", cfg.Database.Host, cfg.Database.Port, cfg.Database.Username, cfg.Database.Password, cfg.Database.Database, cfg.Database.SslMode)
default:
log.Fatal().Str("driver", cfg.Database.Driver).Msg("unsupported database driver")
databaseURL, err := setupDatabaseURL(cfg)
if err != nil {
return err
}
c, err := ent.Open(strings.ToLower(cfg.Database.Driver), databaseURL)
if err != nil {
log.Fatal().
log.Error().
Err(err).
Str("driver", strings.ToLower(cfg.Database.Driver)).
Str("host", cfg.Database.Host).
Str("port", cfg.Database.Port).
Str("database", cfg.Database.Database).
Msg("failed opening connection to {driver} database at {host}:{port}/{database}")
return fmt.Errorf("failed opening connection to %s database at %s:%s/%s: %w",
strings.ToLower(cfg.Database.Driver),
cfg.Database.Host,
cfg.Database.Port,
cfg.Database.Database,
err,
)
}
goose.SetBaseFS(migrations.Migrations(strings.ToLower(cfg.Database.Driver)))
migrationsFs, err := migrations.Migrations(strings.ToLower(cfg.Database.Driver))
if err != nil {
return fmt.Errorf("failed to get migrations for %s: %w", strings.ToLower(cfg.Database.Driver), err)
}
goose.SetBaseFS(migrationsFs)
err = goose.SetDialect(strings.ToLower(cfg.Database.Driver))
if err != nil {
log.Fatal().Str("driver", cfg.Database.Driver).Msg("unsupported database driver")
log.Error().Str("driver", cfg.Database.Driver).Msg("unsupported database driver")
return fmt.Errorf("unsupported database driver: %s", cfg.Database.Driver)
}
@@ -184,25 +156,9 @@ func run(cfg *config.Config) error {
return err
}
collectFuncs := []currencies.CollectorFunc{
currencies.CollectDefaults(),
}
if cfg.Options.CurrencyConfig != "" {
log.Info().
Str("path", cfg.Options.CurrencyConfig).
Msg("loading currency config file")
content, err := os.ReadFile(cfg.Options.CurrencyConfig)
if err != nil {
log.Error().
Err(err).
Str("path", cfg.Options.CurrencyConfig).
Msg("failed to read currency config file")
return err
}
collectFuncs = append(collectFuncs, currencies.CollectJSON(bytes.NewReader(content)))
collectFuncs, err := loadCurrencies(cfg)
if err != nil {
return err
}
currencies, err := currencies.CollectionCurrencies(collectFuncs...)
@@ -256,154 +212,52 @@ func run(cfg *config.Config) error {
_ = httpserver.Shutdown(context.Background())
}()
listener, addrType, addrCfg, err := anyhttp.GetListener(cfg.Web.Host)
if err == nil {
switch addrType {
case anyhttp.SystemdFD:
sysdCfg := addrCfg.(*anyhttp.SysdConfig)
if sysdCfg.IdleTimeout != nil {
log.Error().Msg("idle timeout not yet supported. Please remove and try again")
return errors.New("idle timeout not yet supported. Please remove and try again")
}
fallthrough
case anyhttp.UnixSocket:
log.Info().Msgf("Server is running on %s", cfg.Web.Host)
return httpserver.Serve(listener)
}
} else {
log.Debug().Msgf("anyhttp error: %v", err)
}
log.Info().Msgf("Server is running on %s:%s", cfg.Web.Host, cfg.Web.Port)
return httpserver.ListenAndServe()
})
// =========================================================================
// Start Reoccurring Tasks
registerRecurringTasks(app, cfg, runner)
runner.AddFunc("eventbus", app.bus.Run)
runner.AddFunc("seed_database", func(ctx context.Context) error {
// TODO: Remove through external API that does setup
if cfg.Demo {
log.Info().Msg("Running in demo mode, creating demo data")
err := app.SetupDemo()
if err != nil {
log.Fatal().Msg(err.Error())
}
}
return nil
})
runner.AddPlugin(NewTask("purge-tokens", time.Duration(24)*time.Hour, func(ctx context.Context) {
_, err := app.repos.AuthTokens.PurgeExpiredTokens(ctx)
if err != nil {
log.Error().
Err(err).
Msg("failed to purge expired tokens")
}
}))
runner.AddPlugin(NewTask("purge-invitations", time.Duration(24)*time.Hour, func(ctx context.Context) {
_, err := app.repos.Groups.InvitationPurge(ctx)
if err != nil {
log.Error().
Err(err).
Msg("failed to purge expired invitations")
}
}))
runner.AddPlugin(NewTask("send-notifications", time.Duration(1)*time.Hour, func(ctx context.Context) {
now := time.Now()
if now.Hour() == 8 {
fmt.Println("run notifiers")
err := app.services.BackgroundService.SendNotifiersToday(context.Background())
if err != nil {
log.Error().
Err(err).
Msg("failed to send notifiers")
}
}
}))
go runner.AddFunc("create-thumbnails-subscription", func(ctx context.Context) error {
pubsubString, err := utils.GenerateSubPubConn(cfg.Database.PubSubConnString, "thumbnails")
if err != nil {
log.Error().Err(err).Msg("failed to generate pubsub connection string")
return err
}
topic, err := pubsub.OpenTopic(ctx, pubsubString)
if err != nil {
return err
}
defer func(topic *pubsub.Topic, ctx context.Context) {
err := topic.Shutdown(ctx)
if err != nil {
log.Err(err).Msg("fail to shutdown pubsub topic")
}
}(topic, ctx)
subscription, err := pubsub.OpenSubscription(ctx, pubsubString)
if err != nil {
log.Err(err).Msg("failed to open pubsub topic")
return err
}
defer func(topic *pubsub.Subscription, ctx context.Context) {
err := topic.Shutdown(ctx)
if err != nil {
log.Err(err).Msg("fail to shutdown pubsub topic")
}
}(subscription, ctx)
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
msg, err := subscription.Receive(ctx)
log.Debug().Msg("received thumbnail generation request from pubsub topic")
if err != nil {
log.Err(err).Msg("failed to receive message from pubsub topic")
// Send analytics if enabled at around midnight UTC
if cfg.Options.AllowAnalytics {
analyticsTime := time.Second
runner.AddPlugin(NewTask("send-analytics", analyticsTime, func(ctx context.Context) {
for {
now := time.Now().UTC()
nextMidnight := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, time.UTC)
dur := time.Until(nextMidnight)
analyticsTime = dur
select {
case <-ctx.Done():
return
case <-time.After(dur):
log.Debug().Msg("running send analytics")
err := analytics.Send(version, build())
if err != nil {
log.Error().Err(err).Msg("failed to send analytics")
}
}
groupId, err := uuid.Parse(msg.Metadata["group_id"])
if err != nil {
log.Error().
Err(err).
Str("group_id", msg.Metadata["group_id"]).
Msg("failed to parse group ID from message metadata")
}
attachmentId, err := uuid.Parse(msg.Metadata["attachment_id"])
if err != nil {
log.Error().
Err(err).
Str("attachment_id", msg.Metadata["attachment_id"]).
Msg("failed to parse attachment ID from message metadata")
}
err = app.repos.Attachments.CreateThumbnail(ctx, groupId, attachmentId, msg.Metadata["title"], msg.Metadata["path"])
if err != nil {
log.Err(err).Msg("failed to create thumbnail")
}
msg.Ack()
}
}
})
if cfg.Options.GithubReleaseCheck {
runner.AddPlugin(NewTask("get-latest-github-release", time.Hour, func(ctx context.Context) {
log.Debug().Msg("running get latest github release")
err := app.services.BackgroundService.GetLatestGithubRelease(context.Background())
if err != nil {
log.Error().
Err(err).
Msg("failed to get latest github release")
}
}))
}
if cfg.Debug.Enabled {
runner.AddFunc("debug", func(ctx context.Context) error {
debugserver := http.Server{
Addr: fmt.Sprintf("%s:%s", cfg.Web.Host, cfg.Debug.Port),
Handler: app.debugRouter(),
ReadTimeout: cfg.Web.ReadTimeout,
WriteTimeout: cfg.Web.WriteTimeout,
IdleTimeout: cfg.Web.IdleTimeout,
}
go func() {
<-ctx.Done()
_ = debugserver.Shutdown(context.Background())
}()
log.Info().Msgf("Debug server is running on %s:%s", cfg.Web.Host, cfg.Debug.Port)
return debugserver.ListenAndServe()
})
// Print the configuration to the console
cfg.Print()
}
return runner.Start(context.Background())
}

View File

@@ -0,0 +1,151 @@
package main
import (
"context"
"fmt"
"net/http"
"time"
"github.com/google/uuid"
"github.com/hay-kot/httpkit/graceful"
"github.com/rs/zerolog/log"
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
"github.com/sysadminsmedia/homebox/backend/pkgs/utils"
"gocloud.dev/pubsub"
)
func registerRecurringTasks(app *app, cfg *config.Config, runner *graceful.Runner) {
runner.AddFunc("eventbus", app.bus.Run)
runner.AddFunc("seed_database", func(ctx context.Context) error {
if cfg.Demo {
log.Info().Msg("Running in demo mode, creating demo data")
err := app.SetupDemo()
if err != nil {
log.Error().Err(err).Msg("failed to setup demo data")
return fmt.Errorf("failed to setup demo data: %w", err)
}
}
return nil
})
runner.AddPlugin(NewTask("purge-tokens", 24*time.Hour, func(ctx context.Context) {
_, err := app.repos.AuthTokens.PurgeExpiredTokens(ctx)
if err != nil {
log.Error().Err(err).Msg("failed to purge expired tokens")
}
}))
runner.AddPlugin(NewTask("purge-invitations", 24*time.Hour, func(ctx context.Context) {
_, err := app.repos.Groups.InvitationPurge(ctx)
if err != nil {
log.Error().Err(err).Msg("failed to purge expired invitations")
}
}))
runner.AddPlugin(NewTask("send-notifications", time.Hour, func(ctx context.Context) {
now := time.Now()
if now.Hour() == 8 {
fmt.Println("run notifiers")
err := app.services.BackgroundService.SendNotifiersToday(context.Background())
if err != nil {
log.Error().Err(err).Msg("failed to send notifiers")
}
}
}))
if cfg.Thumbnail.Enabled {
runner.AddFunc("create-thumbnails-subscription", func(ctx context.Context) error {
pubsubString, err := utils.GenerateSubPubConn(cfg.Database.PubSubConnString, "thumbnails")
if err != nil {
log.Error().Err(err).Msg("failed to generate pubsub connection string")
return err
}
topic, err := pubsub.OpenTopic(ctx, pubsubString)
if err != nil {
return err
}
defer func(topic *pubsub.Topic, ctx context.Context) {
err := topic.Shutdown(ctx)
if err != nil {
log.Err(err).Msg("fail to shutdown pubsub topic")
}
}(topic, ctx)
subscription, err := pubsub.OpenSubscription(ctx, pubsubString)
if err != nil {
log.Err(err).Msg("failed to open pubsub topic")
return err
}
defer func(topic *pubsub.Subscription, ctx context.Context) {
err := topic.Shutdown(ctx)
if err != nil {
log.Err(err).Msg("fail to shutdown pubsub topic")
}
}(subscription, ctx)
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
msg, err := subscription.Receive(ctx)
log.Debug().Msg("received thumbnail generation request from pubsub topic")
if err != nil {
log.Err(err).Msg("failed to receive message from pubsub topic")
continue
}
if msg == nil {
log.Warn().Msg("received nil message from pubsub topic")
continue
}
groupId, err := uuid.Parse(msg.Metadata["group_id"])
if err != nil {
log.Error().Err(err).Str("group_id", msg.Metadata["group_id"]).Msg("failed to parse group ID from message metadata")
}
attachmentId, err := uuid.Parse(msg.Metadata["attachment_id"])
if err != nil {
log.Error().Err(err).Str("attachment_id", msg.Metadata["attachment_id"]).Msg("failed to parse attachment ID from message metadata")
}
err = app.repos.Attachments.CreateThumbnail(ctx, groupId, attachmentId, msg.Metadata["title"], msg.Metadata["path"])
if err != nil {
log.Err(err).Msg("failed to create thumbnail")
}
msg.Ack()
}
}
})
}
if cfg.Options.GithubReleaseCheck {
runner.AddPlugin(NewTask("get-latest-github-release", time.Hour, func(ctx context.Context) {
log.Debug().Msg("running get latest github release")
err := app.services.BackgroundService.GetLatestGithubRelease(context.Background())
if err != nil {
log.Error().Err(err).Msg("failed to get latest github release")
}
}))
}
if cfg.Debug.Enabled {
runner.AddFunc("debug", func(ctx context.Context) error {
debugserver := http.Server{
Addr: fmt.Sprintf("%s:%s", cfg.Web.Host, cfg.Debug.Port),
Handler: app.debugRouter(),
ReadTimeout: cfg.Web.ReadTimeout,
WriteTimeout: cfg.Web.WriteTimeout,
IdleTimeout: cfg.Web.IdleTimeout,
}
go func() {
<-ctx.Done()
_ = debugserver.Shutdown(context.Background())
}()
log.Info().Msgf("Debug server is running on %s:%s", cfg.Web.Host, cfg.Debug.Port)
return debugserver.ListenAndServe()
})
// Print the configuration to the console
cfg.Print()
}
}

View File

@@ -129,6 +129,7 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
r.Put("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemUpdate(), userMW...))
r.Patch("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemPatch(), userMW...))
r.Delete("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemDelete(), userMW...))
r.Post("/items/{id}/duplicate", chain.ToHandlerFunc(v1Ctrl.HandleItemDuplicate(), userMW...))
r.Post("/items/{id}/attachments", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentCreate(), userMW...))
r.Put("/items/{id}/attachments/{attachment_id}", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentUpdate(), userMW...))
@@ -157,6 +158,8 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
a.mwRoles(RoleModeOr, authroles.RoleUser.String(), authroles.RoleAttachments.String()),
}
r.Get("/products/search-from-barcode", chain.ToHandlerFunc(v1Ctrl.HandleProductSearchFromBarcode(a.conf.Barcode), userMW...))
r.Get("/qrcode", chain.ToHandlerFunc(v1Ctrl.HandleGenerateQRCode(), assetMW...))
r.Get(
"/items/{id}/attachments/{attachment_id}",

103
backend/app/api/setup.go Normal file
View File

@@ -0,0 +1,103 @@
package main
import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/rs/zerolog/log"
"github.com/sysadminsmedia/homebox/backend/internal/core/currencies"
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
)
// setupStorageDir handles the creation and validation of the storage directory.
func setupStorageDir(cfg *config.Config) error {
if strings.HasPrefix(cfg.Storage.ConnString, "file:///./") {
raw := strings.TrimPrefix(cfg.Storage.ConnString, "file:///./")
clean := filepath.Clean(raw)
absBase, err := filepath.Abs(clean)
if err != nil {
log.Error().Err(err).Msg("failed to get absolute path for storage connection string")
return fmt.Errorf("failed to get absolute path for storage connection string: %w", err)
}
absBase = strings.ReplaceAll(absBase, "\\", "/")
storageDir := filepath.Join(absBase, cfg.Storage.PrefixPath)
storageDir = strings.ReplaceAll(storageDir, "\\", "/")
if !strings.HasPrefix(storageDir, absBase+"/") && storageDir != absBase {
log.Error().Str("path", storageDir).Msg("invalid storage path: you tried to use a prefix that is not a subdirectory of the base path")
return fmt.Errorf("invalid storage path: you tried to use a prefix that is not a subdirectory of the base path")
}
if err := os.MkdirAll(storageDir, 0o750); err != nil {
log.Error().Err(err).Msg("failed to create data directory")
return fmt.Errorf("failed to create data directory: %w", err)
}
}
return nil
}
// setupDatabaseURL returns the database URL and ensures any required directories exist.
func setupDatabaseURL(cfg *config.Config) (string, error) {
databaseURL := ""
switch strings.ToLower(cfg.Database.Driver) {
case "sqlite3":
databaseURL = cfg.Database.SqlitePath
dbFilePath := strings.Split(cfg.Database.SqlitePath, "?")[0]
dbDir := filepath.Dir(dbFilePath)
if err := os.MkdirAll(dbDir, 0o755); err != nil {
log.Error().Err(err).Str("path", dbDir).Msg("failed to create SQLite database directory")
return "", fmt.Errorf("failed to create SQLite database directory: %w", err)
}
case "postgres":
databaseURL = fmt.Sprintf("host=%s port=%s dbname=%s sslmode=%s", cfg.Database.Host, cfg.Database.Port, cfg.Database.Database, cfg.Database.SslMode)
if cfg.Database.Username != "" {
databaseURL += fmt.Sprintf(" user=%s", cfg.Database.Username)
}
if cfg.Database.Password != "" {
databaseURL += fmt.Sprintf(" password=%s", cfg.Database.Password)
}
if cfg.Database.SslRootCert != "" {
if _, err := os.Stat(cfg.Database.SslRootCert); err != nil {
log.Error().Err(err).Str("path", cfg.Database.SslRootCert).Msg("SSL root certificate file is not accessible")
return "", fmt.Errorf("SSL root certificate file is not accessible: %w", err)
}
databaseURL += fmt.Sprintf(" sslrootcert=%s", cfg.Database.SslRootCert)
}
if cfg.Database.SslCert != "" {
if _, err := os.Stat(cfg.Database.SslCert); err != nil {
log.Error().Err(err).Str("path", cfg.Database.SslCert).Msg("SSL certificate file is not accessible")
return "", fmt.Errorf("SSL certificate file is not accessible: %w", err)
}
databaseURL += fmt.Sprintf(" sslcert=%s", cfg.Database.SslCert)
}
if cfg.Database.SslKey != "" {
if _, err := os.Stat(cfg.Database.SslKey); err != nil {
log.Error().Err(err).Str("path", cfg.Database.SslKey).Msg("SSL key file is not accessible")
return "", fmt.Errorf("SSL key file is not accessible: %w", err)
}
databaseURL += fmt.Sprintf(" sslkey=%s", cfg.Database.SslKey)
}
default:
log.Error().Str("driver", cfg.Database.Driver).Msg("unsupported database driver")
return "", fmt.Errorf("unsupported database driver: %s", cfg.Database.Driver)
}
return databaseURL, nil
}
// loadCurrencies loads currency data from config if provided.
func loadCurrencies(cfg *config.Config) ([]currencies.CollectorFunc, error) {
collectFuncs := []currencies.CollectorFunc{
currencies.CollectDefaults(),
}
if cfg.Options.CurrencyConfig != "" {
log.Info().Str("path", cfg.Options.CurrencyConfig).Msg("loading currency config file")
content, err := os.ReadFile(cfg.Options.CurrencyConfig)
if err != nil {
log.Error().Err(err).Str("path", cfg.Options.CurrencyConfig).Msg("failed to read currency config file")
return nil, err
}
collectFuncs = append(collectFuncs, currencies.CollectJSON(bytes.NewReader(content)))
}
return collectFuncs, nil
}

View File

@@ -943,6 +943,48 @@ const docTemplate = `{
}
}
},
"/v1/items/{id}/duplicate": {
"post": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Items"
],
"summary": "Duplicate Item",
"parameters": [
{
"type": "string",
"description": "Item ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Duplicate Options",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/repo.DuplicateOptions"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/repo.ItemOut"
}
}
}
}
},
"/v1/items/{id}/maintenance": {
"get": {
"security": [
@@ -1811,6 +1853,41 @@ const docTemplate = `{
}
}
},
"/v1/products/search-from-barcode": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Items"
],
"summary": "Search EAN from Barcode",
"parameters": [
{
"type": "string",
"description": "barcode to be searched",
"name": "data",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.BarcodeProduct"
}
}
}
}
}
},
"/v1/qrcode": {
"get": {
"security": [
@@ -3063,6 +3140,54 @@ const docTemplate = `{
"TypeTime"
]
},
"repo.BarcodeProduct": {
"type": "object",
"properties": {
"barcode": {
"type": "string"
},
"imageBase64": {
"type": "string"
},
"imageURL": {
"type": "string"
},
"item": {
"$ref": "#/definitions/repo.ItemCreate"
},
"manufacturer": {
"type": "string"
},
"modelNumber": {
"description": "Identifications",
"type": "string"
},
"notes": {
"description": "Extras",
"type": "string"
},
"search_engine_name": {
"type": "string"
}
}
},
"repo.DuplicateOptions": {
"type": "object",
"properties": {
"copyAttachments": {
"type": "boolean"
},
"copyCustomFields": {
"type": "boolean"
},
"copyMaintenance": {
"type": "boolean"
},
"copyPrefix": {
"type": "string"
}
}
},
"repo.Group": {
"type": "object",
"properties": {
@@ -3573,7 +3698,7 @@ const docTemplate = `{
},
"description": {
"type": "string",
"maxLength": 255
"maxLength": 1000
},
"name": {
"type": "string",

View File

@@ -941,6 +941,48 @@
}
}
},
"/v1/items/{id}/duplicate": {
"post": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Items"
],
"summary": "Duplicate Item",
"parameters": [
{
"type": "string",
"description": "Item ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Duplicate Options",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/repo.DuplicateOptions"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/repo.ItemOut"
}
}
}
}
},
"/v1/items/{id}/maintenance": {
"get": {
"security": [
@@ -1809,6 +1851,41 @@
}
}
},
"/v1/products/search-from-barcode": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Items"
],
"summary": "Search EAN from Barcode",
"parameters": [
{
"type": "string",
"description": "barcode to be searched",
"name": "data",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.BarcodeProduct"
}
}
}
}
}
},
"/v1/qrcode": {
"get": {
"security": [
@@ -3061,6 +3138,54 @@
"TypeTime"
]
},
"repo.BarcodeProduct": {
"type": "object",
"properties": {
"barcode": {
"type": "string"
},
"imageBase64": {
"type": "string"
},
"imageURL": {
"type": "string"
},
"item": {
"$ref": "#/definitions/repo.ItemCreate"
},
"manufacturer": {
"type": "string"
},
"modelNumber": {
"description": "Identifications",
"type": "string"
},
"notes": {
"description": "Extras",
"type": "string"
},
"search_engine_name": {
"type": "string"
}
}
},
"repo.DuplicateOptions": {
"type": "object",
"properties": {
"copyAttachments": {
"type": "boolean"
},
"copyCustomFields": {
"type": "boolean"
},
"copyMaintenance": {
"type": "boolean"
},
"copyPrefix": {
"type": "string"
}
}
},
"repo.Group": {
"type": "object",
"properties": {
@@ -3571,7 +3696,7 @@
},
"description": {
"type": "string",
"maxLength": 255
"maxLength": 1000
},
"name": {
"type": "string",

View File

@@ -646,6 +646,38 @@ definitions:
- TypeNumber
- TypeBoolean
- TypeTime
repo.BarcodeProduct:
properties:
barcode:
type: string
imageBase64:
type: string
imageURL:
type: string
item:
$ref: '#/definitions/repo.ItemCreate'
manufacturer:
type: string
modelNumber:
description: Identifications
type: string
notes:
description: Extras
type: string
search_engine_name:
type: string
type: object
repo.DuplicateOptions:
properties:
copyAttachments:
type: boolean
copyCustomFields:
type: boolean
copyMaintenance:
type: boolean
copyPrefix:
type: string
type: object
repo.Group:
properties:
createdAt:
@@ -991,7 +1023,7 @@ definitions:
color:
type: string
description:
maxLength: 255
maxLength: 1000
type: string
name:
maxLength: 255
@@ -1947,6 +1979,32 @@ paths:
summary: Update Item Attachment
tags:
- Items Attachments
/v1/items/{id}/duplicate:
post:
parameters:
- description: Item ID
in: path
name: id
required: true
type: string
- description: Duplicate Options
in: body
name: payload
required: true
schema:
$ref: '#/definitions/repo.DuplicateOptions'
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/repo.ItemOut'
security:
- Bearer: []
summary: Duplicate Item
tags:
- Items
/v1/items/{id}/maintenance:
get:
parameters:
@@ -2543,6 +2601,27 @@ paths:
summary: Test Notifier
tags:
- Notifiers
/v1/products/search-from-barcode:
get:
parameters:
- description: barcode to be searched
in: query
name: data
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/repo.BarcodeProduct'
type: array
security:
- Bearer: []
summary: Search EAN from Barcode
tags:
- Items
/v1/qrcode:
get:
parameters:

View File

@@ -1,102 +1,104 @@
module github.com/sysadminsmedia/homebox/backend
go 1.24
go 1.24.0
toolchain go1.24.3
require (
entgo.io/ent v0.14.4
entgo.io/ent v0.14.5
github.com/ardanlabs/conf/v3 v3.8.0
github.com/containrrr/shoutrrr v0.8.0
github.com/evanoberholster/imagemeta v0.3.1
github.com/gen2brain/avif v0.4.4
github.com/gen2brain/heic v0.4.5
github.com/gen2brain/jpegxl v0.4.5
github.com/gen2brain/webp v0.5.5
github.com/go-chi/chi/v5 v5.2.2
github.com/go-playground/validator/v10 v10.26.0
github.com/go-playground/validator/v10 v10.27.0
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/google/uuid v1.6.0
github.com/gorilla/schema v1.4.1
github.com/hay-kot/httpkit v0.0.11
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.28
github.com/olahol/melody v1.2.1
github.com/mattn/go-sqlite3 v1.14.32
github.com/olahol/melody v1.3.0
github.com/pkg/errors v0.9.1
github.com/pressly/goose/v3 v3.24.3
github.com/rs/zerolog v1.34.0
github.com/shirou/gopsutil/v4 v4.25.5
github.com/shirou/gopsutil/v4 v4.25.7
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/stretchr/testify v1.10.0
github.com/swaggo/http-swagger/v2 v2.0.2
github.com/swaggo/swag v1.16.4
github.com/swaggo/swag v1.16.6
github.com/yeqown/go-qrcode/v2 v2.2.5
github.com/yeqown/go-qrcode/writer/standard v1.3.0
github.com/zeebo/blake3 v0.2.4
gocloud.dev v0.41.0
gocloud.dev/pubsub/kafkapubsub v0.41.0
gocloud.dev/pubsub/natspubsub v0.41.0
gocloud.dev/pubsub/rabbitpubsub v0.41.0
golang.org/x/crypto v0.39.0
golang.org/x/image v0.28.0
modernc.org/sqlite v1.37.1
go.balki.me/anyhttp v0.5.2
gocloud.dev v0.43.0
gocloud.dev/pubsub/kafkapubsub v0.43.0
gocloud.dev/pubsub/natspubsub v0.43.0
gocloud.dev/pubsub/rabbitpubsub v0.43.0
golang.org/x/crypto v0.41.0
golang.org/x/image v0.30.0
golang.org/x/text v0.28.0
modernc.org/sqlite v1.38.2
)
require (
ariga.io/atlas v0.31.1-0.20250212144724-069be8033e83 // indirect
cel.dev/expr v0.22.1 // indirect
cloud.google.com/go v0.120.0 // indirect
cloud.google.com/go/auth v0.15.0 // indirect
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9 // indirect
cel.dev/expr v0.24.0 // indirect
cloud.google.com/go v0.121.4 // indirect
cloud.google.com/go/auth v0.16.4 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
cloud.google.com/go/iam v1.4.2 // indirect
cloud.google.com/go/monitoring v1.24.1 // indirect
cloud.google.com/go/pubsub v1.48.0 // indirect
cloud.google.com/go/storage v1.51.0 // indirect
cloud.google.com/go/compute/metadata v0.8.0 // indirect
cloud.google.com/go/iam v1.5.2 // indirect
cloud.google.com/go/monitoring v1.24.2 // indirect
cloud.google.com/go/pubsub v1.49.0 // indirect
cloud.google.com/go/storage v1.55.0 // indirect
github.com/Azure/azure-amqp-common-go/v3 v3.2.3 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.8.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.9.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 // indirect
github.com/Azure/go-amqp v1.4.0 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect
github.com/IBM/sarama v1.45.1 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect
github.com/IBM/sarama v1.45.2 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/agext/levenshtein v1.2.1 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/aws/aws-sdk-go v1.55.6 // indirect
github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
github.com/aws/aws-sdk-go-v2/config v1.29.12 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.65 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
github.com/aws/aws-sdk-go v1.55.7 // indirect
github.com/aws/aws-sdk-go-v2 v1.36.5 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect
github.com/aws/aws-sdk-go-v2/config v1.29.17 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.70 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sns v1.34.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.25.2 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect
github.com/aws/smithy-go v1.22.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sns v1.34.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect
github.com/aws/smithy-go v1.22.4 // indirect
github.com/bmatcuk/doublestar v1.3.4 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/eapache/go-resiliency v1.7.0 // indirect
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect
@@ -104,33 +106,33 @@ require (
github.com/ebitengine/purego v0.8.4 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/fatih/color v1.15.0 // indirect
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.8 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/go-jose/go-jose/v4 v4.1.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/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.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/spec v0.20.6 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-openapi/jsonpointer v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang-jwt/jwt/v5 v5.2.3 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/wire v0.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/hcl/v2 v2.13.0 // indirect
github.com/hashicorp/hcl/v2 v2.18.1 // indirect
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
github.com/jcmturner/gofork v1.7.6 // indirect
@@ -139,66 +141,67 @@ require (
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.0.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
github.com/nats-io/nats.go v1.40.1 // indirect
github.com/nats-io/nkeys v0.4.10 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/nats-io/nats.go v1.44.0 // indirect
github.com/nats-io/nkeys v0.4.11 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/rabbitmq/amqp091-go v1.10.0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/swaggo/files/v2 v2.0.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
github.com/swaggo/files/v2 v2.0.2 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/tinylib/msgp v1.3.0 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/yeqown/reedsolomon v1.0.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zclconf/go-cty v1.14.4 // indirect
github.com/zclconf/go-cty-yaml v1.1.0 // indirect
go.opencensus.io v0.24.0 // indirect
github.com/zeebo/errs v1.4.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.37.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/oauth2 v0.28.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.33.0 // indirect
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.36.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/api v0.228.0 // indirect
google.golang.org/genproto v0.0.0-20250324211829-b45e905df463 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
google.golang.org/grpc v1.71.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
google.golang.org/api v0.247.0 // indirect
google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a // indirect
google.golang.org/grpc v1.74.2 // indirect
google.golang.org/protobuf v1.36.7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.65.7 // indirect
modernc.org/libc v1.66.7 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)

View File

@@ -1,48 +1,47 @@
ariga.io/atlas v0.31.1-0.20250212144724-069be8033e83 h1:nX4HXncwIdvQ8/8sIUIf1nyCkK8qdBaHQ7EtzPpuiGE=
ariga.io/atlas v0.31.1-0.20250212144724-069be8033e83/go.mod h1:Oe1xWPuu5q9LzyrWfbZmEZxFYeu4BHTyzfjeW2aZp/w=
cel.dev/expr v0.22.1 h1:xoFEsNh972Yzey8N9TCPx2nDvMN7TMhQEzxLuj/iRrI=
cel.dev/expr v0.22.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA=
cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q=
cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps=
cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8=
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9 h1:E0wvcUXTkgyN4wy4LGtNzMNGMytJN8afmIWXJVMi4cc=
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9/go.mod h1:Oe1xWPuu5q9LzyrWfbZmEZxFYeu4BHTyzfjeW2aZp/w=
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go v0.121.4 h1:cVvUiY0sX0xwyxPwdSU2KsF9knOVmtRyAMt8xou0iTs=
cloud.google.com/go v0.121.4/go.mod h1:XEBchUiHFJbz4lKBZwYBDHV/rSyfFktk737TLDU089s=
cloud.google.com/go/auth v0.16.4 h1:fXOAIQmkApVvcIn7Pc2+5J8QTMVbUGLscnSVNl11su8=
cloud.google.com/go/auth v0.16.4/go.mod h1:j10ncYwjX/g3cdX7GpEzsdM+d+ZNsXAbb6qXA7p1Y5M=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/iam v1.4.2 h1:4AckGYAYsowXeHzsn/LCKWIwSWLkdb0eGjH8wWkd27Q=
cloud.google.com/go/iam v1.4.2/go.mod h1:REGlrt8vSlh4dfCJfSEcNjLGq75wW75c5aU3FLOYq34=
cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA=
cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
cloud.google.com/go/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.6 h1:XJNDo5MUfMM05xK3ewpbSdmt7R2Zw+aQEMbdQR65Rbw=
cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw=
cloud.google.com/go/monitoring v1.24.1 h1:vKiypZVFD/5a3BbQMvI4gZdl8445ITzXFh257XBgrS0=
cloud.google.com/go/monitoring v1.24.1/go.mod h1:Z05d1/vn9NaujqY2voG6pVQXoJGbp+r3laV+LySt9K0=
cloud.google.com/go/pubsub v1.48.0 h1:ntFpQVrr10Wj/GXSOpxGmexGynldv/bFp25H0jy8aOs=
cloud.google.com/go/pubsub v1.48.0/go.mod h1:AAtyjyIT/+zaY1ERKFJbefOvkUxRDNp3nD6TdfdqUZk=
cloud.google.com/go/storage v1.51.0 h1:ZVZ11zCiD7b3k+cH5lQs/qcNaoSz3U9I0jgwVzqDlCw=
cloud.google.com/go/storage v1.51.0/go.mod h1:YEJfu/Ki3i5oHC/7jyTgsGZwdQ8P9hqMqvpi5kRKGgc=
cloud.google.com/go/trace v1.11.5 h1:CALS1loyxJMnRiCwZSpdf8ac7iCsjreMxFD2WGxzzHU=
cloud.google.com/go/trace v1.11.5/go.mod h1:TwblCcqNInriu5/qzaeYEIH7wzUcchSdeY2l5wL3Eec=
entgo.io/ent v0.14.4 h1:/DhDraSLXIkBhyiVoJeSshr4ZYi7femzhj6/TckzZuI=
entgo.io/ent v0.14.4/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM=
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM=
cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=
cloud.google.com/go/pubsub v1.49.0 h1:5054IkbslnrMCgA2MAEPcsN3Ky+AyMpEZcii/DoySPo=
cloud.google.com/go/pubsub v1.49.0/go.mod h1:K1FswTWP+C1tI/nfi3HQecoVeFvL4HUOB1tdaNXKhUY=
cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2znf0=
cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY=
cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=
cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
entgo.io/ent v0.14.5 h1:Rj2WOYJtCkWyFo6a+5wB3EfBRP0rnx1fMk6gGA0UUe4=
entgo.io/ent v0.14.5/go.mod h1:zTzLmWtPvGpmSwtkaayM2cm5m819NdM7z7tYPq3vN0U=
github.com/Azure/azure-amqp-common-go/v3 v3.2.3 h1:uDF62mbd9bypXWi19V1bN5NZEO84JqgmI5G73ibAmrk=
github.com/Azure/azure-amqp-common-go/v3 v3.2.3/go.mod h1:7rPmbSfszeovxGfc5fSAXE4ehlXQZHpMja2OtxC2Tas=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 h1:DSDNVxqkoXJiko6x8a90zidoYqnYYa6c1MTzDKzKkTo=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1/go.mod h1:zGqV2R4Cr/k8Uye5w+dgQ06WJtEcbQG/8J7BB6hnCr4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 h1:Wc1ml6QlJs2BHQ/9Bqu1jiyggbsSjramq2oUmp5WeIo=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.8.0 h1:JNgM3Tz592fUHU2vgwgvOgKxo5s9Ki0y2wicBeckn70=
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.8.0/go.mod h1:6vUKmzY17h6dpn9ZLAhM4R/rcrltBeq52qZIkUR7Oro=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 h1:UXT0o77lXQrikd1kgwIPQOUect7EoR/+sbP4wQKdzxM=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0/go.mod h1:cTvi54pg19DoT07ekoeMgE/taAwNtCShVeZqA+Iv2xI=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.9.1 h1:CRZwf68N55u7ZZo3Xx2ynuqEA6k5GZfwsEUkU8qsAPk=
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.9.1/go.mod h1:NydgUaroiShkgOcb+X6OUdS3RalWBrvDNtOyFHJtsZY=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.0 h1:LR0kAX9ykz8G4YgLCaRDVJ3+n43R8MneB5dTy2konZo=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.0/go.mod h1:DWAciXemNf++PQJLeXUB4HHH5OpsAh12HZnu2wXE1jA=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 h1:lhZdRq7TIx0GJQvSyX2Si406vrYsov2FXGp/RnSEtcs=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1/go.mod h1:8cl44BDmi+effbARHMQjgOKA2AYvcohNm7KEt42mSV8=
github.com/Azure/go-amqp v0.17.0/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fwmw9Zlg=
github.com/Azure/go-amqp v1.4.0 h1:Xj3caqi4comOF/L1Uc5iuBxR/pB6KumejC01YQOqOR4=
github.com/Azure/go-amqp v1.4.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE=
@@ -60,91 +59,85 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0=
github.com/IBM/sarama v1.45.1 h1:nY30XqYpqyXOXSNoe2XCgjj9jklGM1Ye94ierUb1jQ0=
github.com/IBM/sarama v1.45.1/go.mod h1:qifDhA3VWSrQ1TjSMyxDl3nYL3oX2C83u+G6L79sq4w=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo=
github.com/IBM/sarama v1.45.2 h1:8m8LcMCu3REcwpa7fCP6v2fuPuzVwXDAM2DOv3CBrKw=
github.com/IBM/sarama v1.45.2/go.mod h1:ppaoTcVdGv186/z6MEKsMm70A5fwJfRTpstI37kVn3Y=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
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.8.0 h1:Mvv2wZJz8tIl705m5BU3ZRCP1V6TKY6qebA8i4sykrY=
github.com/ardanlabs/conf/v3 v3.8.0/go.mod h1:XlL9P0quWP4m1weOVFmlezabinbZLI05niDof/+Ochk=
github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk=
github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14=
github.com/aws/aws-sdk-go-v2/config v1.29.12 h1:Y/2a+jLPrPbHpFkpAAYkVEtJmxORlXoo5k2g1fa2sUo=
github.com/aws/aws-sdk-go-v2/config v1.29.12/go.mod h1:xse1YTjmORlb/6fhkWi8qJh3cvZi4JoVNhc+NbJt4kI=
github.com/aws/aws-sdk-go-v2/credentials v1.17.65 h1:q+nV2yYegofO/SUXruT+pn4KxkxmaQ++1B/QedcKBFM=
github.com/aws/aws-sdk-go-v2/credentials v1.17.65/go.mod h1:4zyjAuGOdikpNYiSGpsGz8hLGmUzlY8pc8r9QQ/RXYQ=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69 h1:6VFPH/Zi9xYFMJKPQOX5URYkQoXRWeJ7V/7Y6ZDYoms=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69/go.mod h1:GJj8mmO6YT6EqgduWocwhMoxTLFitkhIrK+owzrYL2I=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go-v2 v1.36.5 h1:0OF9RiEMEdDdZEMqF9MRjevyxAQcf6gY+E7vwBILFj0=
github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY=
github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0=
github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8=
github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0=
github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 h1:cTXRdLkpBanlDwISl+5chq5ui1d1YWg4PWMR9c3kXyw=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84/go.mod h1:kwSy5X7tfIHN39uucmjQVs2LvDdXEjQucgQQEqCggEo=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR6aqlgJaiaexz/aNvdCktW/kAM=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 h1:lguz0bmOoGzozP9XfRJR1QIayEYo+2vP/No3OfLF0pU=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 h1:jIiopHEV22b4yQP2q36Y0OmwLbsxNWdWwfZRR5QRRO4=
github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc=
github.com/aws/aws-sdk-go-v2/service/sns v1.34.2 h1:PajtbJ/5bEo6iUAIGMYnK8ljqg2F1h4mMCGh1acjN30=
github.com/aws/aws-sdk-go-v2/service/sns v1.34.2/go.mod h1:PJtxxMdj747j8DeZENRTTYAz/lx/pADn/U0k7YNNiUY=
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.3 h1:j5BchjfDoS7K26vPdyJlyxBIIBGDflq3qjjJKBDlbcI=
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.3/go.mod h1:Bar4MrRxeqdn6XIh8JGfiXuFRmyrrsZNTJotxEJmWW0=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.2 h1:pdgODsAhGo4dvzC3JAG5Ce0PX8kWXrTZGx+jxADD+5E=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.2/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0 h1:90uX0veLKcdHVfvxhkWUQSCi5VabtwMLFutYiRke4oo=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k=
github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36/go.mod h1:gDhdAV6wL3PmPqBhiPbnlS447GoWs8HTTOYef9/9Inw=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 h1:nAP2GYbfh8dd2zGZqFRSMlq+/F6cMPBUuCsGAMkN074=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4/go.mod h1:LT10DsiGjLWh4GbjInf9LQejkYEhBgBCjLG5+lvk4EE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 h1:qcLWgdhq45sDM9na4cvXax9dyLitn8EYBRl8Ak4XtG4=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17/go.mod h1:M+jkjBFZ2J6DJrjMv2+vkBbuht6kxJYtJiwoVgX4p4U=
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 h1:0reDqfEN+tB+sozj2r92Bep8MEwBZgtAXTND1Kk9OXg=
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU=
github.com/aws/aws-sdk-go-v2/service/sns v1.34.7 h1:OBuZE9Wt8h2imuRktu+WfjiTGrnYdCIJg8IX92aalHE=
github.com/aws/aws-sdk-go-v2/service/sns v1.34.7/go.mod h1:4WYoZAhHt+dWYpoOQUgkUKfuQbE6Gg/hW4oXE0pKS9U=
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8 h1:80dpSqWMwx2dAm30Ib7J6ucz1ZHfiv5OCRwN/EnCOXQ=
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8/go.mod h1:IzNt/udsXlETCdvBOL0nmyMe2t9cGmXmZgsdoZGYYhI=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E=
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0=
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w=
github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw=
github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k=
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec=
github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
@@ -158,20 +151,18 @@ github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/evanoberholster/imagemeta v0.3.1 h1:E4GUjXcvlVMjP9joN25+bBNf3Al3MTTfMqCrDOCW+LE=
github.com/evanoberholster/imagemeta v0.3.1/go.mod h1:V0vtDJmjTqvwAYO8r+u33NRVIMXQb0qSqEfImoKEiXM=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
@@ -179,8 +170,8 @@ github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzP
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gen2brain/avif v0.4.4 h1:Ga/ss7qcWWQm2bxFpnjYjhJsNfZrWs5RsyklgFjKRSE=
github.com/gen2brain/avif v0.4.4/go.mod h1:/XCaJcjZraQwKVhpu9aEd9aLOssYOawLvhMBtmHVGqk=
github.com/gen2brain/heic v0.4.5 h1:Cq3hPu6wwlTJNv2t48ro3oWje54h82Q5pALeCBNgaSk=
@@ -191,33 +182,33 @@ github.com/gen2brain/webp v0.5.5 h1:MvQR75yIPU/9nSqYT5h13k4URaJK3gf9tgz/ksRbyEg=
github.com/gen2brain/webp v0.5.5/go.mod h1:xOSMzp4aROt2KFW++9qcK/RBTOVC2S9tJG66ip/9Oc0=
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI=
github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ=
github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/jsonpointer v0.21.2 h1:AqQaNADVwq/VnkCmQg6ogE+M3FOsKTytwges0JdwVuA=
github.com/go-openapi/jsonpointer v0.21.2/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-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=
@@ -225,37 +216,18 @@ github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3a
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ=
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
@@ -270,21 +242,20 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI=
github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA=
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
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/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -293,8 +264,8 @@ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/hcl/v2 v2.13.0 h1:0Apadu1w6M11dyGFxWnmhhcMjkbAiKCv7G1r/2QgCNc=
github.com/hashicorp/hcl/v2 v2.13.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0=
github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZYSo=
github.com/hashicorp/hcl/v2 v2.18.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
github.com/hay-kot/httpkit v0.0.11 h1:ZdB2uqsFBSDpfUoClGK5c5orjBjQkEVSXh7fZX5FKEk=
github.com/hay-kot/httpkit v0.0.11/go.mod h1:0kZdk5/swzdfqfg2c6pBWimcgeJ9PTyO97EbHnYl2Sw=
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
@@ -319,17 +290,14 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs=
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw=
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
@@ -340,47 +308,44 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-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.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
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=
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/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.40.1 h1:MLjDkdsbGUeCMKFyCFoLnNn/HDTqcgVa3EQm+pMNDPk=
github.com/nats-io/nats.go v1.40.1/go.mod h1:wV73x0FSI/orHPSYoyMeJB+KajMDoWyXmFaRrrYaaTo=
github.com/nats-io/nkeys v0.4.10 h1:glmRrpCmYLHByYcePvnTBEAwawwapjCPMjy2huw20wc=
github.com/nats-io/nkeys v0.4.10/go.mod h1:OjRrnIKnWBFl+s4YK5ChQfvHP2fxqZexrKJoVVyWB3U=
github.com/nats-io/nats.go v1.44.0 h1:ECKVrDLdh/kDPV1g0gAQ+2+m2KprqZK5O/eJAyAnH2M=
github.com/nats-io/nats.go v1.44.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/olahol/melody v1.2.1 h1:xdwRkzHxf+B0w4TKbGpUSSkV516ZucQZJIWLztOWICQ=
github.com/olahol/melody v1.2.1/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/olahol/melody v1.3.0 h1:n7UlKiQnxVrgxKoM0d7usZiN+Z0y2lVENtYLgKtXS6s=
github.com/olahol/melody v1.3.0/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4=
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
@@ -389,19 +354,19 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/pressly/goose/v3 v3.24.3 h1:DSWWNwwggVUsYZ0X2VitiAa9sKuqtBfe+Jr9zFGwWlM=
github.com/pressly/goose/v3 v3.24.3/go.mod h1:v9zYL4xdViLHCUUJh/mhjnm6JrK7Eul8AS93IxiZM4E=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -409,18 +374,16 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc=
github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM=
github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U=
github.com/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.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -432,18 +395,20 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU=
github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=
github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg=
github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ=
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
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.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/yeqown/go-qrcode/v2 v2.2.5 h1:HCOe2bSjkhZyYoyyNaXNzh4DJZll6inVJQQw+8228Zk=
github.com/yeqown/go-qrcode/v2 v2.2.5/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw=
github.com/yeqown/go-qrcode/writer/standard v1.3.0 h1:chdyhEfRtUPgQtuPeaWVGQ/TQx4rE1PqeoW3U+53t34=
@@ -461,73 +426,65 @@ github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.balki.me/anyhttp v0.5.2 h1:et4tCDXLeXpWfMNvRKG7ojfrnlr3du7cEaG966MLSpA=
go.balki.me/anyhttp v0.5.2/go.mod h1:JhfekOIjgVODoVqUCficjpIgmB3wwlB7jhN0eN2EZ/s=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA=
go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/contrib/detectors/gcp v1.37.0 h1:B+WbN9RPsvobe6q4vP6KgM8/9plR/HNjgGBrfcOlweA=
go.opentelemetry.io/contrib/detectors/gcp v1.37.0/go.mod h1:K5zQ3TT7p2ru9Qkzk0bKtCql0RGkPj9pRjpXgZJZ+rU=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0 h1:6VjV6Et+1Hd2iLZEPtdV7vie80Yyqf7oikJLjQ/myi0=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0/go.mod h1:u8hcp8ji5gaM/RfcOo8z9NMnf1pVLfVY7lBY2VOGuUU=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
gocloud.dev v0.41.0 h1:qBKd9jZkBKEghYbP/uThpomhedK5s2Gy6Lz7h/zYYrM=
gocloud.dev v0.41.0/go.mod h1:IetpBcWLUwroOOxKr90lhsZ8vWxeSkuszBnW62sbcf0=
gocloud.dev/pubsub/kafkapubsub v0.41.0 h1:Ft6YB77ejqk++VjW51UP39RH/WDAMtv6ed3+PHMxBzg=
gocloud.dev/pubsub/kafkapubsub v0.41.0/go.mod h1:kJf4c6b+4yJk6nXmv33yXKblbrgWmrYCzI5QEsr27G0=
gocloud.dev/pubsub/natspubsub v0.41.0 h1:UxNb0DiAzdnyHut6jcCG7u6lsB/hzxTyZ/RHWeCUJ4Q=
gocloud.dev/pubsub/natspubsub v0.41.0/go.mod h1:uCBKjwvIcuNuf3+ft4wUI9hPHHKQvroxq9ZPB/410ac=
gocloud.dev/pubsub/rabbitpubsub v0.41.0 h1:RutvHbacZxlFr0t3wlr+kz63j53UOfHY3PJR8NKN1EI=
gocloud.dev/pubsub/rabbitpubsub v0.41.0/go.mod h1:s7oQXOlQ2FOj8XmYMv5Ocgs1t+8hIXfsKaWGgECM9SQ=
gocloud.dev v0.43.0 h1:aW3eq4RMyehbJ54PMsh4hsp7iX8cO/98ZRzJJOzN/5M=
gocloud.dev v0.43.0/go.mod h1:eD8rkg7LhKUHrzkEdLTZ+Ty/vgPHPCd+yMQdfelQVu4=
gocloud.dev/pubsub/kafkapubsub v0.43.0 h1:Kgwi0na69W3RgxEffEkdrMhox6A3Q0gajoJtjHGVr/s=
gocloud.dev/pubsub/kafkapubsub v0.43.0/go.mod h1:uKI0CXuj7HJ/YnnOLQ3VkDnuUnkz+q/d+tRzmfhmOOU=
gocloud.dev/pubsub/natspubsub v0.43.0 h1:k35tFoaorvD9Fa26zVEEzyXiMOEyXNHc0pBOmRYvQI0=
gocloud.dev/pubsub/natspubsub v0.43.0/go.mod h1:xJn8TO8pGYieDn6AsRFsYfhQW8cnC+xGmG9APGNxkpQ=
gocloud.dev/pubsub/rabbitpubsub v0.43.0 h1:6nNZFSlJ1dk2GujL8PFltfLz3vC6IbrpjGS4FTduo1s=
gocloud.dev/pubsub/rabbitpubsub v0.43.0/go.mod h1:sEaueAGat+OASRoB3QDkghCtibKttgg7X6zsPTm1pl0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
@@ -535,25 +492,20 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -564,11 +516,10 @@ 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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -582,60 +533,35 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/api v0.228.0 h1:X2DJ/uoWGnY5obVjewbp8icSL5U4FzuCfy9OjbLSnLs=
google.golang.org/api v0.228.0/go.mod h1:wNvRS1Pbe8r4+IfBIniV8fwCpGwTrYa+kMUDiC5z5a4=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20250324211829-b45e905df463 h1:qEFnJI6AnfZk0NNe8YTyXQh5i//Zxi4gBHwRgp76qpw=
google.golang.org/genproto v0.0.0-20250324211829-b45e905df463/go.mod h1:SqIx1NV9hcvqdLHo7uNZDS5lrUJybQ3evo3+z/WBfA0=
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/api v0.247.0 h1:tSd/e0QrUlLsrwMKmkbQhYVa109qIintOls2Wh6bngc=
google.golang.org/api v0.247.0/go.mod h1:r1qZOPmxXffXg6xS5uhx16Fa/UFY8QU/K4bfKrnvovM=
google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 h1:Nt6z9UHqSlIdIGJdz6KhTIs2VRx/iOsA5iE8bmQNcxs=
google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79/go.mod h1:kTmlBHMPqR5uCZPBvwa2B18mvubkjyY3CRLI0c6fj0s=
google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79 h1:iOye66xuaAK0WnkPuhQPUFy8eJcmwUXqGGP3om6IxX8=
google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79/go.mod h1:HKJDgKsFUnv5VAGeQjz8kxcgDP0HoE0iZNp0OdZNlhE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a h1:tPE/Kp+x9dMSwUm/uM0JKK0IfdiJkwAbSMSeZBXXJXc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -643,21 +569,20 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/cc/v4 v4.26.3 h1:yEN8dzrkRFnn4PUUKXLYIqVf2PJYAEjMTFjO3BDGc3I=
modernc.org/cc/v4 v4.26.3/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8=
modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/fileutil v1.3.15 h1:rJAXTP6ilMW/1+kzDiqmBlHLWszheUFXIyGQIAvjJpY=
modernc.org/fileutil v1.3.15/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00=
modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.7 h1:rjhZ8OSCybKWxS1CJr0hikpEi6Vg+944Ouyrd+bQsoY=
modernc.org/libc v1.66.7/go.mod h1:ln6tbWX0NH+mzApEoDRvilBvAWFt1HX7AUA4VDdVDPM=
modernc.org/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=
@@ -666,8 +591,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.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs=
modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g=
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
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

@@ -38,6 +38,10 @@ func (svc *ItemService) Create(ctx Context, item repo.ItemCreate) (repo.ItemOut,
return svc.repo.Items.Create(ctx, ctx.GID, item)
}
func (svc *ItemService) Duplicate(ctx Context, gid, id uuid.UUID, options repo.DuplicateOptions) (repo.ItemOut, error) {
return svc.repo.Items.Duplicate(ctx, gid, id, options)
}
func (svc *ItemService) EnsureAssetID(ctx context.Context, gid uuid.UUID) (int, error) {
items, err := svc.repo.Items.GetAllZeroAssetID(ctx, gid)
if err != nil {

View File

@@ -10,8 +10,8 @@ import (
"io"
)
func (svc *ItemService) AttachmentPath(ctx context.Context, attachmentID uuid.UUID) (*ent.Attachment, error) {
attachment, err := svc.repo.Attachments.Get(ctx, attachmentID)
func (svc *ItemService) AttachmentPath(ctx context.Context, gid uuid.UUID, attachmentID uuid.UUID) (*ent.Attachment, error) {
attachment, err := svc.repo.Attachments.Get(ctx, gid, attachmentID)
if err != nil {
return nil, err
}
@@ -19,16 +19,16 @@ func (svc *ItemService) AttachmentPath(ctx context.Context, attachmentID uuid.UU
return attachment, nil
}
func (svc *ItemService) AttachmentUpdate(ctx Context, itemID uuid.UUID, data *repo.ItemAttachmentUpdate) (repo.ItemOut, error) {
func (svc *ItemService) AttachmentUpdate(ctx Context, gid uuid.UUID, itemID uuid.UUID, data *repo.ItemAttachmentUpdate) (repo.ItemOut, error) {
// Update Attachment
attachment, err := svc.repo.Attachments.Update(ctx, data.ID, data)
attachment, err := svc.repo.Attachments.Update(ctx, gid, data.ID, data)
if err != nil {
return repo.ItemOut{}, err
}
// Update Document
attDoc := attachment
_, err = svc.repo.Attachments.Rename(ctx, attDoc.ID, data.Title)
_, err = svc.repo.Attachments.Rename(ctx, gid, attDoc.ID, data.Title)
if err != nil {
return repo.ItemOut{}, err
}
@@ -57,7 +57,7 @@ func (svc *ItemService) AttachmentAdd(ctx Context, itemID uuid.UUID, filename st
func (svc *ItemService) AttachmentDelete(ctx context.Context, gid uuid.UUID, id uuid.UUID, attachmentID uuid.UUID) error {
// Delete the attachment
err := svc.repo.Attachments.Delete(ctx, attachmentID)
err := svc.repo.Attachments.Delete(ctx, gid, id, attachmentID)
if err != nil {
return err
}

View File

@@ -0,0 +1,120 @@
package ent
import (
"entgo.io/ent/dialect/sql"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/item"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/predicate"
"github.com/sysadminsmedia/homebox/backend/pkgs/textutils"
)
// AccentInsensitiveContains creates a predicate that performs accent-insensitive text search.
// It normalizes both the database field value and the search value for comparison.
func AccentInsensitiveContains(field string, searchValue string) predicate.Item {
if searchValue == "" {
return predicate.Item(func(s *sql.Selector) {
// Return a predicate that never matches if search is empty
s.Where(sql.False())
})
}
// Normalize the search value
normalizedSearch := textutils.NormalizeSearchQuery(searchValue)
return predicate.Item(func(s *sql.Selector) {
dialect := s.Dialect()
switch dialect {
case "sqlite3":
// For SQLite, we'll create a custom normalization function using REPLACE
// to handle common accented characters
normalizeFunc := buildSQLiteNormalizeExpression(s.C(field))
s.Where(sql.ExprP(
"LOWER("+normalizeFunc+") LIKE ?",
"%"+normalizedSearch+"%",
))
case "postgres":
// For PostgreSQL, use REPLACE-based normalization to avoid unaccent dependency
normalizeFunc := buildGenericNormalizeExpression(s.C(field))
// Use sql.P() for proper PostgreSQL parameter binding ($1, $2, etc.)
s.Where(sql.P(func(b *sql.Builder) {
b.WriteString("LOWER(")
b.WriteString(normalizeFunc)
b.WriteString(") LIKE ")
b.Arg("%" + normalizedSearch + "%")
}))
default:
// Default fallback using REPLACE for common accented characters
normalizeFunc := buildGenericNormalizeExpression(s.C(field))
s.Where(sql.ExprP(
"LOWER("+normalizeFunc+") LIKE ?",
"%"+normalizedSearch+"%",
))
}
})
}
// buildSQLiteNormalizeExpression creates a SQLite expression to normalize accented characters
func buildSQLiteNormalizeExpression(fieldExpr string) string {
return buildGenericNormalizeExpression(fieldExpr)
}
// buildGenericNormalizeExpression creates a database-agnostic expression to normalize common accented characters
func buildGenericNormalizeExpression(fieldExpr string) string {
// Chain REPLACE functions to handle the most common accented characters
// Focused on the most frequently used accents in Spanish, French, and Portuguese
// Ordered by frequency of use for better performance
normalized := fieldExpr
// Most common accented characters ordered by frequency
commonAccents := []struct {
from, to string
}{
// Spanish - most common
{"á", "a"}, {"é", "e"}, {"í", "i"}, {"ó", "o"}, {"ú", "u"}, {"ñ", "n"},
{"Á", "A"}, {"É", "E"}, {"Í", "I"}, {"Ó", "O"}, {"Ú", "U"}, {"Ñ", "N"},
// French - most common
{"è", "e"}, {"ê", "e"}, {"à", "a"}, {"ç", "c"},
{"È", "E"}, {"Ê", "E"}, {"À", "A"}, {"Ç", "C"},
// German umlauts and Portuguese - common
{"ä", "a"}, {"ö", "o"}, {"ü", "u"}, {"ã", "a"}, {"õ", "o"},
{"Ä", "A"}, {"Ö", "O"}, {"Ü", "U"}, {"Ã", "A"}, {"Õ", "O"},
}
for _, accent := range commonAccents {
normalized = "REPLACE(" + normalized + ", '" + accent.from + "', '" + accent.to + "')"
}
return normalized
}
// ItemNameAccentInsensitiveContains creates an accent-insensitive search predicate for the item name field.
func ItemNameAccentInsensitiveContains(value string) predicate.Item {
return AccentInsensitiveContains(item.FieldName, value)
}
// ItemDescriptionAccentInsensitiveContains creates an accent-insensitive search predicate for the item description field.
func ItemDescriptionAccentInsensitiveContains(value string) predicate.Item {
return AccentInsensitiveContains(item.FieldDescription, value)
}
// ItemSerialNumberAccentInsensitiveContains creates an accent-insensitive search predicate for the item serial number field.
func ItemSerialNumberAccentInsensitiveContains(value string) predicate.Item {
return AccentInsensitiveContains(item.FieldSerialNumber, value)
}
// ItemModelNumberAccentInsensitiveContains creates an accent-insensitive search predicate for the item model number field.
func ItemModelNumberAccentInsensitiveContains(value string) predicate.Item {
return AccentInsensitiveContains(item.FieldModelNumber, value)
}
// ItemManufacturerAccentInsensitiveContains creates an accent-insensitive search predicate for the item manufacturer field.
func ItemManufacturerAccentInsensitiveContains(value string) predicate.Item {
return AccentInsensitiveContains(item.FieldManufacturer, value)
}
// ItemNotesAccentInsensitiveContains creates an accent-insensitive search predicate for the item notes field.
func ItemNotesAccentInsensitiveContains(value string) predicate.Item {
return AccentInsensitiveContains(item.FieldNotes, value)
}

View File

@@ -0,0 +1,147 @@
package ent
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestBuildGenericNormalizeExpression(t *testing.T) {
tests := []struct {
name string
field string
expected string
}{
{
name: "Simple field name",
field: "name",
expected: "name", // Should be wrapped in many REPLACE functions
},
{
name: "Complex field name",
field: "description",
expected: "description",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := buildGenericNormalizeExpression(tt.field)
// Should contain the original field
assert.Contains(t, result, tt.field)
// Should contain REPLACE functions for accent normalization
assert.Contains(t, result, "REPLACE(")
// Should handle common accented characters
assert.Contains(t, result, "'á'", "Should handle Spanish á")
assert.Contains(t, result, "'é'", "Should handle Spanish é")
assert.Contains(t, result, "'ñ'", "Should handle Spanish ñ")
assert.Contains(t, result, "'ü'", "Should handle German ü")
// Should handle uppercase accents too
assert.Contains(t, result, "'Á'", "Should handle uppercase Spanish Á")
assert.Contains(t, result, "'É'", "Should handle uppercase Spanish É")
})
}
}
func TestSQLiteNormalizeExpression(t *testing.T) {
result := buildSQLiteNormalizeExpression("test_field")
// Should contain the field name and REPLACE functions
assert.Contains(t, result, "test_field")
assert.Contains(t, result, "REPLACE(")
// Check for some specific accent replacements (order doesn't matter)
assert.Contains(t, result, "'á'", "Should handle Spanish á")
assert.Contains(t, result, "'ó'", "Should handle Spanish ó")
}
func TestAccentInsensitivePredicateCreation(t *testing.T) {
tests := []struct {
name string
field string
searchValue string
description string
}{
{
name: "Normal search value",
field: "name",
searchValue: "electronica",
description: "Should create predicate for normal search",
},
{
name: "Accented search value",
field: "description",
searchValue: "electrónica",
description: "Should create predicate for accented search",
},
{
name: "Empty search value",
field: "name",
searchValue: "",
description: "Should handle empty search gracefully",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
predicate := AccentInsensitiveContains(tt.field, tt.searchValue)
assert.NotNil(t, predicate, tt.description)
})
}
}
func TestSpecificItemPredicates(t *testing.T) {
tests := []struct {
name string
predicateFunc func(string) interface{}
searchValue string
description string
}{
{
name: "ItemNameAccentInsensitiveContains",
predicateFunc: func(val string) interface{} { return ItemNameAccentInsensitiveContains(val) },
searchValue: "electronica",
description: "Should create accent-insensitive name search predicate",
},
{
name: "ItemDescriptionAccentInsensitiveContains",
predicateFunc: func(val string) interface{} { return ItemDescriptionAccentInsensitiveContains(val) },
searchValue: "descripcion",
description: "Should create accent-insensitive description search predicate",
},
{
name: "ItemManufacturerAccentInsensitiveContains",
predicateFunc: func(val string) interface{} { return ItemManufacturerAccentInsensitiveContains(val) },
searchValue: "compañia",
description: "Should create accent-insensitive manufacturer search predicate",
},
{
name: "ItemSerialNumberAccentInsensitiveContains",
predicateFunc: func(val string) interface{} { return ItemSerialNumberAccentInsensitiveContains(val) },
searchValue: "sn123",
description: "Should create accent-insensitive serial number search predicate",
},
{
name: "ItemModelNumberAccentInsensitiveContains",
predicateFunc: func(val string) interface{} { return ItemModelNumberAccentInsensitiveContains(val) },
searchValue: "model456",
description: "Should create accent-insensitive model number search predicate",
},
{
name: "ItemNotesAccentInsensitiveContains",
predicateFunc: func(val string) interface{} { return ItemNotesAccentInsensitiveContains(val) },
searchValue: "notas importantes",
description: "Should create accent-insensitive notes search predicate",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
predicate := tt.predicateFunc(tt.searchValue)
assert.NotNil(t, predicate, tt.description)
})
}
}

View File

@@ -3,6 +3,8 @@ package migrations
import (
"embed"
"fmt"
"github.com/rs/zerolog/log"
)
@@ -17,15 +19,16 @@ var sqliteFiles embed.FS
// migration files in the binary at build time. The function takes a string
// parameter "dialect" which specifies the SQL dialect to use. It returns an
// embedded file system containing the migration files for the specified dialect.
func Migrations(dialect string) embed.FS {
func Migrations(dialect string) (embed.FS, error) {
switch dialect {
case "postgres":
return postgresFiles
return postgresFiles, nil
case "sqlite3":
return sqliteFiles
return sqliteFiles, nil
default:
log.Fatal().Str("dialect", dialect).Msg("unknown sql dialect")
log.Error().Str("dialect", dialect).Msg("unknown sql dialect")
return embed.FS{}, fmt.Errorf("unknown sql dialect: %s", dialect)
}
// This should never get hit, but just in case
return sqliteFiles
return sqliteFiles, nil
}

View File

@@ -0,0 +1,126 @@
-- +goose Up
-- GENERATED with 20250706190000_generate_migration.py
-- Migrating auth_tokens/created_at
update auth_tokens set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
update auth_tokens set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
-- Migrating auth_tokens/updated_at
update auth_tokens set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
update auth_tokens set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
-- Migrating auth_tokens/expires_at
update auth_tokens set expires_at = substr(expires_at,1, instr(expires_at, ' +')-1) || substr(expires_at, instr(expires_at, ' +')+1,3) || ':' || substr(expires_at, instr(expires_at, ' +')+4,2) where expires_at like '% +%';
update auth_tokens set expires_at = substr(expires_at,1, instr(expires_at, ' -')-1) || substr(expires_at, instr(expires_at, ' -')+1,3) || ':' || substr(expires_at, instr(expires_at, ' -')+4,2) where expires_at like '% -%';
-- Migrating groups/created_at
update groups set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
update groups set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
-- Migrating groups/updated_at
update groups set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
update groups set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
-- Migrating group_invitation_tokens/created_at
update group_invitation_tokens set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
update group_invitation_tokens set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
-- Migrating group_invitation_tokens/updated_at
update group_invitation_tokens set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
update group_invitation_tokens set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
-- Migrating group_invitation_tokens/expires_at
update group_invitation_tokens set expires_at = substr(expires_at,1, instr(expires_at, ' +')-1) || substr(expires_at, instr(expires_at, ' +')+1,3) || ':' || substr(expires_at, instr(expires_at, ' +')+4,2) where expires_at like '% +%';
update group_invitation_tokens set expires_at = substr(expires_at,1, instr(expires_at, ' -')-1) || substr(expires_at, instr(expires_at, ' -')+1,3) || ':' || substr(expires_at, instr(expires_at, ' -')+4,2) where expires_at like '% -%';
-- Migrating item_fields/created_at
update item_fields set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
update item_fields set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
-- Migrating item_fields/updated_at
update item_fields set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
update item_fields set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
-- Migrating item_fields/time_value
update item_fields set time_value = substr(time_value,1, instr(time_value, ' +')-1) || substr(time_value, instr(time_value, ' +')+1,3) || ':' || substr(time_value, instr(time_value, ' +')+4,2) where time_value like '% +%';
update item_fields set time_value = substr(time_value,1, instr(time_value, ' -')-1) || substr(time_value, instr(time_value, ' -')+1,3) || ':' || substr(time_value, instr(time_value, ' -')+4,2) where time_value like '% -%';
-- Migrating labels/created_at
update labels set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
update labels set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
-- Migrating labels/updated_at
update labels set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
update labels set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
-- Migrating locations/created_at
update locations set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
update locations set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
-- Migrating locations/updated_at
update locations set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
update locations set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
-- Migrating maintenance_entries/created_at
update maintenance_entries set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
update maintenance_entries set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
-- Migrating maintenance_entries/updated_at
update maintenance_entries set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
update maintenance_entries set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
-- Migrating maintenance_entries/date
update maintenance_entries set date = substr(date,1, instr(date, ' +')-1) || substr(date, instr(date, ' +')+1,3) || ':' || substr(date, instr(date, ' +')+4,2) where date like '% +%';
update maintenance_entries set date = substr(date,1, instr(date, ' -')-1) || substr(date, instr(date, ' -')+1,3) || ':' || substr(date, instr(date, ' -')+4,2) where date like '% -%';
-- Migrating maintenance_entries/scheduled_date
update maintenance_entries set scheduled_date = substr(scheduled_date,1, instr(scheduled_date, ' +')-1) || substr(scheduled_date, instr(scheduled_date, ' +')+1,3) || ':' || substr(scheduled_date, instr(scheduled_date, ' +')+4,2) where scheduled_date like '% +%';
update maintenance_entries set scheduled_date = substr(scheduled_date,1, instr(scheduled_date, ' -')-1) || substr(scheduled_date, instr(scheduled_date, ' -')+1,3) || ':' || substr(scheduled_date, instr(scheduled_date, ' -')+4,2) where scheduled_date like '% -%';
-- Migrating notifiers/created_at
update notifiers set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
update notifiers set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
-- Migrating notifiers/updated_at
update notifiers set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
update notifiers set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
-- Migrating users/created_at
update users set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
update users set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
-- Migrating users/updated_at
update users set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
update users set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
-- Migrating users/activated_on
update users set activated_on = substr(activated_on,1, instr(activated_on, ' +')-1) || substr(activated_on, instr(activated_on, ' +')+1,3) || ':' || substr(activated_on, instr(activated_on, ' +')+4,2) where activated_on like '% +%';
update users set activated_on = substr(activated_on,1, instr(activated_on, ' -')-1) || substr(activated_on, instr(activated_on, ' -')+1,3) || ':' || substr(activated_on, instr(activated_on, ' -')+4,2) where activated_on like '% -%';
-- Migrating items/created_at
update items set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
update items set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
-- Migrating items/updated_at
update items set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
update items set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';
-- Migrating items/warranty_expires
update items set warranty_expires = substr(warranty_expires,1, instr(warranty_expires, ' +')-1) || substr(warranty_expires, instr(warranty_expires, ' +')+1,3) || ':' || substr(warranty_expires, instr(warranty_expires, ' +')+4,2) where warranty_expires like '% +%';
update items set warranty_expires = substr(warranty_expires,1, instr(warranty_expires, ' -')-1) || substr(warranty_expires, instr(warranty_expires, ' -')+1,3) || ':' || substr(warranty_expires, instr(warranty_expires, ' -')+4,2) where warranty_expires like '% -%';
-- Migrating items/purchase_time
update items set purchase_time = substr(purchase_time,1, instr(purchase_time, ' +')-1) || substr(purchase_time, instr(purchase_time, ' +')+1,3) || ':' || substr(purchase_time, instr(purchase_time, ' +')+4,2) where purchase_time like '% +%';
update items set purchase_time = substr(purchase_time,1, instr(purchase_time, ' -')-1) || substr(purchase_time, instr(purchase_time, ' -')+1,3) || ':' || substr(purchase_time, instr(purchase_time, ' -')+4,2) where purchase_time like '% -%';
-- Migrating items/sold_time
update items set sold_time = substr(sold_time,1, instr(sold_time, ' +')-1) || substr(sold_time, instr(sold_time, ' +')+1,3) || ':' || substr(sold_time, instr(sold_time, ' +')+4,2) where sold_time like '% +%';
update items set sold_time = substr(sold_time,1, instr(sold_time, ' -')-1) || substr(sold_time, instr(sold_time, ' -')+1,3) || ':' || substr(sold_time, instr(sold_time, ' -')+4,2) where sold_time like '% -%';
-- Migrating attachments/created_at
update attachments set created_at = substr(created_at,1, instr(created_at, ' +')-1) || substr(created_at, instr(created_at, ' +')+1,3) || ':' || substr(created_at, instr(created_at, ' +')+4,2) where created_at like '% +%';
update attachments set created_at = substr(created_at,1, instr(created_at, ' -')-1) || substr(created_at, instr(created_at, ' -')+1,3) || ':' || substr(created_at, instr(created_at, ' -')+4,2) where created_at like '% -%';
-- Migrating attachments/updated_at
update attachments set updated_at = substr(updated_at,1, instr(updated_at, ' +')-1) || substr(updated_at, instr(updated_at, ' +')+1,3) || ':' || substr(updated_at, instr(updated_at, ' +')+4,2) where updated_at like '% +%';
update attachments set updated_at = substr(updated_at,1, instr(updated_at, ' -')-1) || substr(updated_at, instr(updated_at, ' -')+1,3) || ':' || substr(updated_at, instr(updated_at, ' -')+4,2) where updated_at like '% -%';

View File

@@ -0,0 +1,61 @@
#!/usr/bin/env python
import os
# Extract fields with
""" WITH tables AS (
SELECT name AS table_name
FROM sqlite_master
WHERE type = 'table'
AND name NOT LIKE 'sqlite_%'
)
SELECT
'["' || t.table_name || '", "' || c.name || '"],' AS table_column
FROM tables t
JOIN pragma_table_info(t.table_name) c
WHERE c.name like'%date%'; """
fields = [["auth_tokens", "created_at"],
["auth_tokens", "updated_at"],
["auth_tokens", "expires_at"],
["groups", "created_at"],
["groups", "updated_at"],
["group_invitation_tokens", "created_at"],
["group_invitation_tokens", "updated_at"],
["group_invitation_tokens", "expires_at"],
["item_fields", "created_at"],
["item_fields", "updated_at"],
["item_fields", "time_value"],
["labels", "created_at"],
["labels", "updated_at"],
["locations", "created_at"],
["locations", "updated_at"],
["maintenance_entries", "created_at"],
["maintenance_entries", "updated_at"],
["maintenance_entries", "date"],
["maintenance_entries", "scheduled_date"],
["notifiers", "created_at"],
["notifiers", "updated_at"],
["users", "created_at"],
["users", "updated_at"],
["users", "activated_on"],
["items", "created_at"],
["items", "updated_at"],
["items", "warranty_expires"],
["items", "purchase_time"],
["items", "sold_time"],
["attachments", "created_at"],
["attachments", "updated_at"]]
def generate_migration(table_name, field_name):
return f"""update {table_name} set {field_name} = substr({field_name},1, instr({field_name}, ' +')-1) || substr({field_name}, instr({field_name}, ' +')+1,3) || ':' || substr({field_name}, instr({field_name}, ' +')+4,2) where {field_name} like '% +%';\n""" + \
f"""update {table_name} set {field_name} = substr({field_name},1, instr({field_name}, ' -')-1) || substr({field_name}, instr({field_name}, ' -')+1,3) || ':' || substr({field_name}, instr({field_name}, ' -')+4,2) where {field_name} like '% -%';"""
print("-- +goose Up")
print(f"-- GENERATED with {os.path.basename(__file__)}")
for table, column in fields:
print(f"-- Migrating {table}/{column}")
print(generate_migration(table, column))
print()

View File

@@ -5,25 +5,27 @@ import (
"context"
"crypto/md5"
"fmt"
"github.com/rs/zerolog/log"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/group"
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
"github.com/sysadminsmedia/homebox/backend/pkgs/utils"
"github.com/zeebo/blake3"
"github.com/gen2brain/avif"
"github.com/gen2brain/heic"
"github.com/gen2brain/jpegxl"
"github.com/gen2brain/webp"
"golang.org/x/image/draw"
"image"
"io"
"io/fs"
"net/http"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/evanoberholster/imagemeta"
"github.com/gen2brain/avif"
"github.com/gen2brain/heic"
"github.com/gen2brain/jpegxl"
"github.com/gen2brain/webp"
"github.com/rs/zerolog/log"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/group"
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
"github.com/sysadminsmedia/homebox/backend/pkgs/utils"
"github.com/zeebo/blake3"
"golang.org/x/image/draw"
"github.com/google/uuid"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/attachment"
@@ -100,13 +102,30 @@ func (r *AttachmentRepo) path(gid uuid.UUID, hash string) string {
}
func (r *AttachmentRepo) GetConnString() string {
// Handle the default case for file storage
// which is file:///./ meaning relative to the current working directory
if strings.HasPrefix(r.storage.ConnString, "file:///./") {
dir, err := filepath.Abs(strings.TrimPrefix(r.storage.ConnString, "file:///./"))
if runtime.GOOS == "windows" {
dir = fmt.Sprintf("/%s", dir)
}
if err != nil {
log.Err(err).Msg("failed to get absolute path for attachment directory")
return r.storage.ConnString
}
return fmt.Sprintf("file://%s?no_tmp_dir=true", dir)
return strings.ReplaceAll(fmt.Sprintf("file://%s?no_tmp_dir=true", dir), "\\", "/")
} else if strings.HasPrefix(r.storage.ConnString, "file://") {
// Handle the case for file storage with an absolute path
// Convert Windows paths to a format compatible with fileblob
// e.g. file:///C:/path/to/file becomes file:///C/path
dir := strings.TrimPrefix(strings.ReplaceAll(r.storage.ConnString, "\\", "/"), "file://")
if runtime.GOOS == "windows" {
// Remove the colon from the drive letter (in case the user adds it)
dir = strings.ReplaceAll(dir, ":", "")
// Ensure the path starts with a slash for Windows compatibility
dir = fmt.Sprintf("/%s", dir)
}
return fmt.Sprintf("file://%s", dir)
}
return r.storage.ConnString
}
@@ -256,16 +275,46 @@ func (r *AttachmentRepo) Create(ctx context.Context, itemID uuid.UUID, doc ItemC
return attachmentDb, nil
}
func (r *AttachmentRepo) Get(ctx context.Context, id uuid.UUID) (*ent.Attachment, error) {
return r.db.Attachment.
Query().
Where(attachment.ID(id)).
WithItem().
WithThumbnail().
Only(ctx)
func (r *AttachmentRepo) Get(ctx context.Context, gid uuid.UUID, id uuid.UUID) (*ent.Attachment, error) {
first, err := r.db.Attachment.Query().Where(attachment.ID(id)).Only(ctx)
if err != nil {
return nil, err
}
if first.Type == attachment.TypeThumbnail {
// If the attachment is a thumbnail, get the parent attachment and check if it belongs to the specified group
return r.db.Attachment.
Query().
Where(attachment.ID(id),
attachment.HasThumbnailWith(attachment.HasItemWith(item.HasGroupWith(group.ID(gid)))),
).
WithItem().
WithThumbnail().
Only(ctx)
} else {
// For regular attachments, check if the attachment's item belongs to the specified group
return r.db.Attachment.
Query().
Where(attachment.ID(id),
attachment.HasItemWith(item.HasGroupWith(group.ID(gid))),
).
WithItem().
WithThumbnail().
Only(ctx)
}
}
func (r *AttachmentRepo) Update(ctx context.Context, id uuid.UUID, data *ItemAttachmentUpdate) (*ent.Attachment, error) {
func (r *AttachmentRepo) Update(ctx context.Context, gid uuid.UUID, id uuid.UUID, data *ItemAttachmentUpdate) (*ent.Attachment, error) {
// Validate that the attachment belongs to the specified group
_, err := r.db.Attachment.Query().
Where(
attachment.ID(id),
attachment.HasItemWith(item.HasGroupWith(group.ID(gid))),
).
Only(ctx)
if err != nil {
return nil, err
}
// TODO: execute within Tx
typ := attachment.Type(data.Type)
@@ -289,25 +338,34 @@ func (r *AttachmentRepo) Update(ctx context.Context, id uuid.UUID, data *ItemAtt
return nil, err
}
// Ensure all other attachments are not primary
err = r.db.Attachment.Update().
Where(
attachment.HasItemWith(item.ID(attachmentItem.ID)),
attachment.IDNEQ(updatedAttachment.ID),
).
SetPrimary(false).
Exec(ctx)
if err != nil {
return nil, err
// Only remove primary status from other photo attachments when setting a new photo as primary
if typ == attachment.TypePhoto && data.Primary {
err = r.db.Attachment.Update().
Where(
attachment.HasItemWith(item.ID(attachmentItem.ID)),
attachment.IDNEQ(updatedAttachment.ID),
attachment.TypeEQ(attachment.TypePhoto),
).
SetPrimary(false).
Exec(ctx)
if err != nil {
return nil, err
}
}
return r.Get(ctx, updatedAttachment.ID)
return r.Get(ctx, gid, updatedAttachment.ID)
}
func (r *AttachmentRepo) Delete(ctx context.Context, id uuid.UUID) error {
doc, error := r.db.Attachment.Get(ctx, id)
if error != nil {
return error
func (r *AttachmentRepo) Delete(ctx context.Context, gid uuid.UUID, itemId uuid.UUID, id uuid.UUID) error {
// Validate that the attachment belongs to the specified group
doc, err := r.db.Attachment.Query().
Where(
attachment.ID(id),
attachment.HasItemWith(item.HasGroupWith(group.ID(gid))),
).
Only(ctx)
if err != nil {
return err
}
all, err := r.db.Attachment.Query().Where(attachment.Path(doc.Path)).All(ctx)
@@ -358,7 +416,18 @@ func (r *AttachmentRepo) Delete(ctx context.Context, id uuid.UUID) error {
return r.db.Attachment.DeleteOneID(id).Exec(ctx)
}
func (r *AttachmentRepo) Rename(ctx context.Context, id uuid.UUID, title string) (*ent.Attachment, error) {
func (r *AttachmentRepo) Rename(ctx context.Context, gid uuid.UUID, id uuid.UUID, title string) (*ent.Attachment, error) {
// Validate that the attachment belongs to the specified group
_, err := r.db.Attachment.Query().
Where(
attachment.ID(id),
attachment.HasItemWith(item.HasGroupWith(group.ID(gid))),
).
Only(ctx)
if err != nil {
return nil, err
}
return r.db.Attachment.UpdateOneID(id).SetTitle(title).Save(ctx)
}
@@ -455,10 +524,13 @@ func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmen
contentType := http.DetectContentType(contentBytes[:min(512, len(contentBytes))])
if contentType == "application/octet-stream" {
if strings.HasSuffix(title, ".heic") || strings.HasSuffix(title, ".heif") {
switch {
case strings.HasSuffix(title, ".heic") || strings.HasSuffix(title, ".heif"):
contentType = "image/heic"
} else if strings.HasSuffix(title, ".avif") {
case strings.HasSuffix(title, ".avif"):
contentType = "image/avif"
case strings.HasSuffix(title, ".jxl"):
contentType = "image/jxl"
}
}
@@ -475,10 +547,18 @@ func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmen
}
return err
}
dst := image.NewRGBA(image.Rect(0, 0, r.thumbnail.Width, r.thumbnail.Height))
draw.ApproxBiLinear.Scale(dst, dst.Rect, img, img.Bounds(), draw.Over, nil)
buf := new(bytes.Buffer)
err = webp.Encode(buf, dst, webp.Options{Quality: 80, Lossless: false})
log.Debug().Msg("reading original file orientation")
imageMeta, err := imagemeta.Decode(bytes.NewReader(contentBytes))
if err != nil {
log.Err(err).Msg("failed to decode original file content")
err := tx.Rollback()
if err != nil {
return err
}
return err
}
orientation := uint16(imageMeta.Orientation)
thumbnailPath, err := r.processThumbnailFromImage(ctx, groupId, img, title, orientation)
if err != nil {
err := tx.Rollback()
if err != nil {
@@ -486,22 +566,7 @@ func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmen
}
return err
}
contentBytes := buf.Bytes()
log.Debug().Msg("uploading thumbnail file")
thumbnailFile, err := r.UploadFile(ctx, tx.Group.GetX(ctx, groupId), ItemCreateAttachment{
Title: fmt.Sprintf("%s-thumb", title),
Content: bytes.NewReader(contentBytes),
})
if err != nil {
log.Err(err).Msg("failed to upload thumbnail file")
err := tx.Rollback()
if err != nil {
return err
}
return err
}
log.Debug().Msg("setting thumbnail file path in attachment")
att.SetPath(thumbnailFile)
att.SetPath(thumbnailPath)
case contentType == "image/webp":
log.Debug().Msg("creating thumbnail for webp file")
img, err := webp.Decode(bytes.NewReader(contentBytes))
@@ -513,10 +578,18 @@ func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmen
}
return err
}
dst := image.NewRGBA(image.Rect(0, 0, r.thumbnail.Width, r.thumbnail.Height))
draw.ApproxBiLinear.Scale(dst, dst.Rect, img, img.Bounds(), draw.Over, nil)
buf := new(bytes.Buffer)
err = webp.Encode(buf, dst, webp.Options{Quality: 80, Lossless: false})
log.Debug().Msg("reading original file orientation")
imageMeta, err := imagemeta.Decode(bytes.NewReader(contentBytes))
if err != nil {
log.Err(err).Msg("failed to decode original file content")
err := tx.Rollback()
if err != nil {
return err
}
return err
}
orientation := uint16(imageMeta.Orientation)
thumbnailPath, err := r.processThumbnailFromImage(ctx, groupId, img, title, orientation)
if err != nil {
err := tx.Rollback()
if err != nil {
@@ -524,22 +597,7 @@ func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmen
}
return err
}
contentBytes := buf.Bytes()
log.Debug().Msg("uploading thumbnail file")
thumbnailFile, err := r.UploadFile(ctx, tx.Group.GetX(ctx, groupId), ItemCreateAttachment{
Title: fmt.Sprintf("%s-thumb", title),
Content: bytes.NewReader(contentBytes),
})
if err != nil {
log.Err(err).Msg("failed to upload thumbnail file")
err := tx.Rollback()
if err != nil {
return err
}
return err
}
log.Debug().Msg("setting thumbnail file path in attachment")
att.SetPath(thumbnailFile)
att.SetPath(thumbnailPath)
case contentType == "image/avif":
log.Debug().Msg("creating thumbnail for avif file")
img, err := avif.Decode(bytes.NewReader(contentBytes))
@@ -551,10 +609,7 @@ func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmen
}
return err
}
dst := image.NewRGBA(image.Rect(0, 0, r.thumbnail.Width, r.thumbnail.Height))
draw.ApproxBiLinear.Scale(dst, dst.Rect, img, img.Bounds(), draw.Over, nil)
buf := new(bytes.Buffer)
err = webp.Encode(buf, dst, webp.Options{Quality: 80, Lossless: false})
thumbnailPath, err := r.processThumbnailFromImage(ctx, groupId, img, title, uint16(1))
if err != nil {
err := tx.Rollback()
if err != nil {
@@ -562,22 +617,7 @@ func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmen
}
return err
}
contentBytes := buf.Bytes()
log.Debug().Msg("uploading thumbnail file")
thumbnailFile, err := r.UploadFile(ctx, tx.Group.GetX(ctx, groupId), ItemCreateAttachment{
Title: fmt.Sprintf("%s-thumb", title),
Content: bytes.NewReader(contentBytes),
})
if err != nil {
log.Err(err).Msg("failed to upload thumbnail file")
err := tx.Rollback()
if err != nil {
return err
}
return err
}
log.Debug().Msg("setting thumbnail file path in attachment")
att.SetPath(thumbnailFile)
att.SetPath(thumbnailPath)
case contentType == "image/heic" || contentType == "image/heif":
log.Debug().Msg("creating thumbnail for heic file")
img, err := heic.Decode(bytes.NewReader(contentBytes))
@@ -589,10 +629,18 @@ func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmen
}
return err
}
dst := image.NewRGBA(image.Rect(0, 0, r.thumbnail.Width, r.thumbnail.Height))
draw.ApproxBiLinear.Scale(dst, dst.Rect, img, img.Bounds(), draw.Over, nil)
buf := new(bytes.Buffer)
err = webp.Encode(buf, dst, webp.Options{Quality: 80, Lossless: false})
log.Debug().Msg("reading original file orientation")
imageMeta, err := imagemeta.Decode(bytes.NewReader(contentBytes))
if err != nil {
log.Err(err).Msg("failed to decode original file content")
err := tx.Rollback()
if err != nil {
return err
}
return err
}
orientation := uint16(imageMeta.Orientation)
thumbnailPath, err := r.processThumbnailFromImage(ctx, groupId, img, title, orientation)
if err != nil {
err := tx.Rollback()
if err != nil {
@@ -600,22 +648,7 @@ func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmen
}
return err
}
contentBytes := buf.Bytes()
log.Debug().Msg("uploading thumbnail file")
thumbnailFile, err := r.UploadFile(ctx, tx.Group.GetX(ctx, groupId), ItemCreateAttachment{
Title: fmt.Sprintf("%s-thumb", title),
Content: bytes.NewReader(contentBytes),
})
if err != nil {
log.Err(err).Msg("failed to upload thumbnail file")
err := tx.Rollback()
if err != nil {
return err
}
return err
}
log.Debug().Msg("setting thumbnail file path in attachment")
att.SetPath(thumbnailFile)
att.SetPath(thumbnailPath)
case contentType == "image/jxl":
log.Debug().Msg("creating thumbnail for jpegxl file")
img, err := jpegxl.Decode(bytes.NewReader(contentBytes))
@@ -627,10 +660,7 @@ func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmen
}
return err
}
dst := image.NewRGBA(image.Rect(0, 0, r.thumbnail.Width, r.thumbnail.Height))
draw.ApproxBiLinear.Scale(dst, dst.Rect, img, img.Bounds(), draw.Over, nil)
buf := new(bytes.Buffer)
err = webp.Encode(buf, dst, webp.Options{Quality: 80, Lossless: false})
thumbnailPath, err := r.processThumbnailFromImage(ctx, groupId, img, title, uint16(1))
if err != nil {
err := tx.Rollback()
if err != nil {
@@ -638,22 +668,7 @@ func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmen
}
return err
}
contentBytes := buf.Bytes()
log.Debug().Msg("uploading thumbnail file")
thumbnailFile, err := r.UploadFile(ctx, tx.Group.GetX(ctx, groupId), ItemCreateAttachment{
Title: fmt.Sprintf("%s-thumb", title),
Content: bytes.NewReader(contentBytes),
})
if err != nil {
log.Err(err).Msg("failed to upload thumbnail file")
err := tx.Rollback()
if err != nil {
return err
}
return err
}
log.Debug().Msg("setting thumbnail file path in attachment")
att.SetPath(thumbnailFile)
att.SetPath(thumbnailPath)
default:
return fmt.Errorf("file type %s is not supported for thumbnail creation or document thumnails disabled", title)
}
@@ -784,3 +799,73 @@ func isImageFile(mimetype string) bool {
// Check file extension for image types
return strings.Contains(mimetype, "image/jpeg") || strings.Contains(mimetype, "image/png") || strings.Contains(mimetype, "image/gif")
}
// calculateThumbnailDimensions calculates new dimensions that preserve aspect ratio
// while fitting within the configured maximum width and height
func calculateThumbnailDimensions(origWidth, origHeight, maxWidth, maxHeight int) (int, int) {
if origWidth <= maxWidth && origHeight <= maxHeight {
return origWidth, origHeight
}
// Calculate scaling factors for both dimensions
scaleX := float64(maxWidth) / float64(origWidth)
scaleY := float64(maxHeight) / float64(origHeight)
// Use the smaller scaling factor to ensure both dimensions fit
scale := scaleX
if scaleY < scaleX {
scale = scaleY
}
newWidth := int(float64(origWidth) * scale)
newHeight := int(float64(origHeight) * scale)
// Ensure we don't get zero dimensions
if newWidth < 1 {
newWidth = 1
}
if newHeight < 1 {
newHeight = 1
}
return newWidth, newHeight
}
// processThumbnailFromImage handles the common thumbnail processing logic after image decoding
// Returns the thumbnail file path or an error
func (r *AttachmentRepo) processThumbnailFromImage(ctx context.Context, groupId uuid.UUID, img image.Image, title string, orientation uint16) (string, error) {
bounds := img.Bounds()
// Apply EXIF orientation if needed
if orientation > 1 {
img = utils.ApplyOrientation(img, orientation)
bounds = img.Bounds()
}
newWidth, newHeight := calculateThumbnailDimensions(bounds.Dx(), bounds.Dy(), r.thumbnail.Width, r.thumbnail.Height)
dst := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))
draw.ApproxBiLinear.Scale(dst, dst.Rect, img, img.Bounds(), draw.Over, nil)
buf := new(bytes.Buffer)
err := webp.Encode(buf, dst, webp.Options{Quality: 80, Lossless: false})
if err != nil {
return "", err
}
contentBytes := buf.Bytes()
log.Debug().Msg("uploading thumbnail file")
// Get the group for uploading the thumbnail
group, err := r.db.Group.Get(ctx, groupId)
if err != nil {
return "", err
}
thumbnailFile, err := r.UploadFile(ctx, group, ItemCreateAttachment{
Title: fmt.Sprintf("%s-thumb", title),
Content: bytes.NewReader(contentBytes),
})
if err != nil {
log.Err(err).Msg("failed to upload thumbnail file")
return "", err
}
return thumbnailFile, nil
}

View File

@@ -18,7 +18,7 @@ func TestAttachmentRepo_Create(t *testing.T) {
ids := []uuid.UUID{item.ID}
t.Cleanup(func() {
for _, id := range ids {
_ = tRepos.Attachments.Delete(context.Background(), id)
_ = tRepos.Attachments.Delete(context.Background(), tGroup.ID, item.ID, id)
}
})
@@ -69,7 +69,7 @@ func TestAttachmentRepo_Create(t *testing.T) {
assert.Equal(t, tt.want.Type, got.Type)
withItems, err := tRepos.Attachments.Get(tt.args.ctx, got.ID)
withItems, err := tRepos.Attachments.Get(tt.args.ctx, tGroup.ID, got.ID)
require.NoError(t, err)
assert.Equal(t, tt.args.itemID, withItems.Edges.Item.ID)
@@ -86,17 +86,17 @@ func useAttachments(t *testing.T, n int) []*ent.Attachment {
ids := make([]uuid.UUID, 0, n)
t.Cleanup(func() {
for _, id := range ids {
_ = tRepos.Attachments.Delete(context.Background(), id)
_ = tRepos.Attachments.Delete(context.Background(), tGroup.ID, item.ID, id)
}
})
attachments := make([]*ent.Attachment, n)
for i := 0; i < n; i++ {
attachment, err := tRepos.Attachments.Create(context.Background(), item.ID, ItemCreateAttachment{Title: "Test", Content: strings.NewReader("Test String")}, attachment.TypePhoto, true)
attach, err := tRepos.Attachments.Create(context.Background(), item.ID, ItemCreateAttachment{Title: "Test", Content: strings.NewReader("Test String")}, attachment.TypePhoto, true)
require.NoError(t, err)
attachments[i] = attachment
attachments[i] = attach
ids = append(ids, attachment.ID)
ids = append(ids, attach.ID)
}
return attachments
@@ -107,13 +107,13 @@ func TestAttachmentRepo_Update(t *testing.T) {
for _, typ := range []attachment.Type{"photo", "manual", "warranty", "attachment"} {
t.Run(string(typ), func(t *testing.T) {
_, err := tRepos.Attachments.Update(context.Background(), entity.ID, &ItemAttachmentUpdate{
_, err := tRepos.Attachments.Update(context.Background(), tGroup.ID, entity.ID, &ItemAttachmentUpdate{
Type: string(typ),
})
require.NoError(t, err)
updated, err := tRepos.Attachments.Get(context.Background(), entity.ID)
updated, err := tRepos.Attachments.Get(context.Background(), tGroup.ID, entity.ID)
require.NoError(t, err)
assert.Equal(t, typ, updated.Type)
})
@@ -122,11 +122,12 @@ func TestAttachmentRepo_Update(t *testing.T) {
func TestAttachmentRepo_Delete(t *testing.T) {
entity := useAttachments(t, 1)[0]
item := useItems(t, 1)[0]
err := tRepos.Attachments.Delete(context.Background(), entity.ID)
err := tRepos.Attachments.Delete(context.Background(), tGroup.ID, item.ID, entity.ID)
require.NoError(t, err)
_, err = tRepos.Attachments.Get(context.Background(), entity.ID)
_, err = tRepos.Attachments.Get(context.Background(), tGroup.ID, entity.ID)
require.Error(t, err)
}
@@ -135,13 +136,13 @@ func TestAttachmentRepo_EnsureSinglePrimaryAttachment(t *testing.T) {
attachments := useAttachments(t, 2)
setAndVerifyPrimary := func(primaryAttachmentID, nonPrimaryAttachmentID uuid.UUID) {
primaryAttachment, err := tRepos.Attachments.Update(ctx, primaryAttachmentID, &ItemAttachmentUpdate{
primaryAttachment, err := tRepos.Attachments.Update(ctx, tGroup.ID, primaryAttachmentID, &ItemAttachmentUpdate{
Type: attachment.TypePhoto.String(),
Primary: true,
})
require.NoError(t, err)
nonPrimaryAttachment, err := tRepos.Attachments.Get(ctx, nonPrimaryAttachmentID)
nonPrimaryAttachment, err := tRepos.Attachments.Get(ctx, tGroup.ID, nonPrimaryAttachmentID)
require.NoError(t, err)
assert.True(t, primaryAttachment.Primary)
@@ -151,3 +152,132 @@ func TestAttachmentRepo_EnsureSinglePrimaryAttachment(t *testing.T) {
setAndVerifyPrimary(attachments[0].ID, attachments[1].ID)
setAndVerifyPrimary(attachments[1].ID, attachments[0].ID)
}
func TestAttachmentRepo_UpdateNonPhotoDoesNotAffectPrimaryPhoto(t *testing.T) {
ctx := context.Background()
item := useItems(t, 1)[0]
// Create a photo attachment that will be primary
photoAttachment, err := tRepos.Attachments.Create(ctx, item.ID, ItemCreateAttachment{Title: "Test Photo", Content: strings.NewReader("Photo content")}, attachment.TypePhoto, true)
require.NoError(t, err)
// Create a manual attachment (non-photo)
manualAttachment, err := tRepos.Attachments.Create(ctx, item.ID, ItemCreateAttachment{Title: "Test Manual", Content: strings.NewReader("Manual content")}, attachment.TypeManual, false)
require.NoError(t, err)
// Cleanup
t.Cleanup(func() {
_ = tRepos.Attachments.Delete(ctx, tGroup.ID, item.ID, photoAttachment.ID)
_ = tRepos.Attachments.Delete(ctx, tGroup.ID, item.ID, manualAttachment.ID)
})
// Verify photo is primary initially
photoAttachment, err = tRepos.Attachments.Get(ctx, tGroup.ID, photoAttachment.ID)
require.NoError(t, err)
assert.True(t, photoAttachment.Primary)
// Update the manual attachment (this should NOT affect the photo's primary status)
_, err = tRepos.Attachments.Update(ctx, tGroup.ID, manualAttachment.ID, &ItemAttachmentUpdate{
Type: attachment.TypeManual.String(),
Title: "Updated Manual",
Primary: false, // This should have no effect since it's not a photo
})
require.NoError(t, err)
// Verify photo is still primary after updating the manual
photoAttachment, err = tRepos.Attachments.Get(ctx, tGroup.ID, photoAttachment.ID)
require.NoError(t, err)
assert.True(t, photoAttachment.Primary, "Photo attachment should remain primary after updating non-photo attachment")
// Verify manual attachment is not primary
manualAttachment, err = tRepos.Attachments.Get(ctx, tGroup.ID, manualAttachment.ID)
require.NoError(t, err)
assert.False(t, manualAttachment.Primary)
}
func TestAttachmentRepo_AddingPDFAfterPhotoKeepsPhotoAsPrimary(t *testing.T) {
ctx := context.Background()
item := useItems(t, 1)[0]
// Step 1: Upload a photo first (this should become primary since it's the first photo)
photoAttachment, err := tRepos.Attachments.Create(ctx, item.ID, ItemCreateAttachment{Title: "Item Photo", Content: strings.NewReader("Photo content")}, attachment.TypePhoto, false)
require.NoError(t, err)
// Cleanup
t.Cleanup(func() {
_ = tRepos.Attachments.Delete(ctx, tGroup.ID, item.ID, photoAttachment.ID)
})
// Verify photo becomes primary automatically (since it's the first photo)
photoAttachment, err = tRepos.Attachments.Get(ctx, tGroup.ID, photoAttachment.ID)
require.NoError(t, err)
assert.True(t, photoAttachment.Primary, "First photo should automatically become primary")
// Step 2: Add a PDF receipt (this should NOT affect the photo's primary status)
pdfAttachment, err := tRepos.Attachments.Create(ctx, item.ID, ItemCreateAttachment{Title: "Receipt PDF", Content: strings.NewReader("PDF content")}, attachment.TypeReceipt, false)
require.NoError(t, err)
// Add to cleanup
t.Cleanup(func() {
_ = tRepos.Attachments.Delete(ctx, tGroup.ID, item.ID, pdfAttachment.ID)
})
// Step 3: Verify photo is still primary after adding PDF
photoAttachment, err = tRepos.Attachments.Get(ctx, tGroup.ID, photoAttachment.ID)
require.NoError(t, err)
assert.True(t, photoAttachment.Primary, "Photo should remain primary after adding PDF attachment")
// Verify PDF is not primary
pdfAttachment, err = tRepos.Attachments.Get(ctx, tGroup.ID, pdfAttachment.ID)
require.NoError(t, err)
assert.False(t, pdfAttachment.Primary)
// Step 4: Test the actual item summary mapping (this is what determines the card display)
updatedItem, err := tRepos.Items.GetOne(ctx, item.ID)
require.NoError(t, err)
// The item should have the photo's ID as the imageId
assert.NotNil(t, updatedItem.ImageID, "Item should have an imageId")
assert.Equal(t, photoAttachment.ID, *updatedItem.ImageID, "Item's imageId should match the photo attachment ID")
}
func TestAttachmentRepo_SettingPhotoPrimaryStillWorks(t *testing.T) {
ctx := context.Background()
item := useItems(t, 1)[0]
// Create two photo attachments
photo1, err := tRepos.Attachments.Create(ctx, item.ID, ItemCreateAttachment{Title: "Photo 1", Content: strings.NewReader("Photo 1 content")}, attachment.TypePhoto, false)
require.NoError(t, err)
photo2, err := tRepos.Attachments.Create(ctx, item.ID, ItemCreateAttachment{Title: "Photo 2", Content: strings.NewReader("Photo 2 content")}, attachment.TypePhoto, false)
require.NoError(t, err)
// Cleanup
t.Cleanup(func() {
_ = tRepos.Attachments.Delete(ctx, tGroup.ID, item.ID, photo1.ID)
_ = tRepos.Attachments.Delete(ctx, tGroup.ID, item.ID, photo2.ID)
})
// First photo should be primary (since it was created first)
photo1, err = tRepos.Attachments.Get(ctx, tGroup.ID, photo1.ID)
require.NoError(t, err)
assert.True(t, photo1.Primary)
photo2, err = tRepos.Attachments.Get(ctx, tGroup.ID, photo2.ID)
require.NoError(t, err)
assert.False(t, photo2.Primary)
// Now set photo2 as primary (this should work and remove primary from photo1)
photo2, err = tRepos.Attachments.Update(ctx, tGroup.ID, photo2.ID, &ItemAttachmentUpdate{
Type: attachment.TypePhoto.String(),
Title: "Photo 2",
Primary: true,
})
require.NoError(t, err)
assert.True(t, photo2.Primary)
// Verify photo1 is no longer primary
photo1, err = tRepos.Attachments.Get(ctx, tGroup.ID, photo1.ID)
require.NoError(t, err)
assert.False(t, photo1.Primary, "Photo 1 should no longer be primary after setting Photo 2 as primary")
}

View File

@@ -6,6 +6,7 @@ import (
"time"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
"github.com/sysadminsmedia/homebox/backend/internal/core/services/reporting/eventbus"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/attachment"
@@ -14,6 +15,7 @@ import (
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/itemfield"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/label"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/location"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/maintenanceentry"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/predicate"
"github.com/sysadminsmedia/homebox/backend/internal/data/types"
)
@@ -46,6 +48,13 @@ type (
OrderBy string `json:"orderBy"`
}
DuplicateOptions struct {
CopyMaintenance bool `json:"copyMaintenance"`
CopyAttachments bool `json:"copyAttachments"`
CopyCustomFields bool `json:"copyCustomFields"`
CopyPrefix string `json:"copyPrefix"`
}
ItemField struct {
ID uuid.UUID `json:"id,omitempty"`
Type string `json:"type"`
@@ -360,14 +369,25 @@ func (e *ItemsRepository) QueryByGroup(ctx context.Context, gid uuid.UUID, q Ite
}
if q.Search != "" {
// Use accent-insensitive search predicates that normalize both
// the search query and database field values during comparison.
// For queries without accents, the traditional search is more efficient.
qb.Where(
item.Or(
// Regular case-insensitive search (fastest)
item.NameContainsFold(q.Search),
item.DescriptionContainsFold(q.Search),
item.SerialNumberContainsFold(q.Search),
item.ModelNumberContainsFold(q.Search),
item.ManufacturerContainsFold(q.Search),
item.NotesContainsFold(q.Search),
// Accent-insensitive search using custom predicates
ent.ItemNameAccentInsensitiveContains(q.Search),
ent.ItemDescriptionAccentInsensitiveContains(q.Search),
ent.ItemSerialNumberAccentInsensitiveContains(q.Search),
ent.ItemModelNumberAccentInsensitiveContains(q.Search),
ent.ItemManufacturerAccentInsensitiveContains(q.Search),
ent.ItemNotesAccentInsensitiveContains(q.Search),
),
)
}
@@ -993,3 +1013,164 @@ func (e *ItemsRepository) SetPrimaryPhotos(ctx context.Context, gid uuid.UUID) (
return updated, nil
}
// Duplicate creates a copy of an item with configurable options for what data to copy.
// The new item will have the next available asset ID and a customizable prefix in the name.
func (e *ItemsRepository) Duplicate(ctx context.Context, gid, id uuid.UUID, options DuplicateOptions) (ItemOut, error) {
tx, err := e.db.Tx(ctx)
if err != nil {
return ItemOut{}, err
}
committed := false
defer func() {
if !committed {
if err := tx.Rollback(); err != nil {
log.Warn().Err(err).Msg("failed to rollback transaction during item duplication")
}
}
}()
// Get the original item with all its data
originalItem, err := e.getOne(ctx, item.ID(id), item.HasGroupWith(group.ID(gid)))
if err != nil {
return ItemOut{}, err
}
nextAssetID, err := e.GetHighestAssetID(ctx, gid)
if err != nil {
return ItemOut{}, err
}
nextAssetID++
// Set default copy prefix if not provided
if options.CopyPrefix == "" {
options.CopyPrefix = "Copy of "
}
// Create the new item directly in the transaction
newItemID := uuid.New()
itemBuilder := tx.Item.Create().
SetID(newItemID).
SetName(options.CopyPrefix + originalItem.Name).
SetDescription(originalItem.Description).
SetQuantity(originalItem.Quantity).
SetLocationID(originalItem.Location.ID).
SetGroupID(gid).
SetAssetID(int(nextAssetID)).
SetSerialNumber(originalItem.SerialNumber).
SetModelNumber(originalItem.ModelNumber).
SetManufacturer(originalItem.Manufacturer).
SetLifetimeWarranty(originalItem.LifetimeWarranty).
SetWarrantyExpires(originalItem.WarrantyExpires.Time()).
SetWarrantyDetails(originalItem.WarrantyDetails).
SetPurchaseTime(originalItem.PurchaseTime.Time()).
SetPurchaseFrom(originalItem.PurchaseFrom).
SetPurchasePrice(originalItem.PurchasePrice).
SetSoldTime(originalItem.SoldTime.Time()).
SetSoldTo(originalItem.SoldTo).
SetSoldPrice(originalItem.SoldPrice).
SetSoldNotes(originalItem.SoldNotes).
SetNotes(originalItem.Notes).
SetInsured(originalItem.Insured).
SetArchived(originalItem.Archived).
SetSyncChildItemsLocations(originalItem.SyncChildItemsLocations)
if originalItem.Parent != nil {
itemBuilder.SetParentID(originalItem.Parent.ID)
}
// Add labels
if len(originalItem.Labels) > 0 {
labelIDs := make([]uuid.UUID, len(originalItem.Labels))
for i, label := range originalItem.Labels {
labelIDs[i] = label.ID
}
itemBuilder.AddLabelIDs(labelIDs...)
}
_, err = itemBuilder.Save(ctx)
if err != nil {
return ItemOut{}, err
}
// Copy custom fields if requested
if options.CopyCustomFields {
for _, field := range originalItem.Fields {
_, err = tx.ItemField.Create().
SetItemID(newItemID).
SetType(itemfield.Type(field.Type)).
SetName(field.Name).
SetTextValue(field.TextValue).
SetNumberValue(field.NumberValue).
SetBooleanValue(field.BooleanValue).
Save(ctx)
if err != nil {
log.Warn().Err(err).Str("field_name", field.Name).Msg("failed to copy custom field during duplication")
continue
}
}
}
// Copy attachments if requested
if options.CopyAttachments {
for _, att := range originalItem.Attachments {
// Get the original attachment file
originalAttachment, err := tx.Attachment.Query().
Where(attachment.ID(att.ID)).
Only(ctx)
if err != nil {
// Log error but continue to copy other attachments
log.Warn().Err(err).Str("attachment_id", att.ID.String()).Msg("failed to find attachment during duplication")
continue
}
// Create a copy of the attachment with the same file path
// Since files are stored with hash-based paths, this is safe
_, err = tx.Attachment.Create().
SetItemID(newItemID).
SetType(originalAttachment.Type).
SetTitle(originalAttachment.Title).
SetPath(originalAttachment.Path).
SetMimeType(originalAttachment.MimeType).
SetPrimary(originalAttachment.Primary).
Save(ctx)
if err != nil {
log.Warn().Err(err).Str("original_attachment_id", att.ID.String()).Msg("failed to copy attachment during duplication")
continue
}
}
}
// Copy maintenance entries if requested
if options.CopyMaintenance {
maintenanceEntries, err := tx.MaintenanceEntry.Query().
Where(maintenanceentry.HasItemWith(item.ID(id))).
All(ctx)
if err == nil {
for _, entry := range maintenanceEntries {
_, err = tx.MaintenanceEntry.Create().
SetItemID(newItemID).
SetDate(entry.Date).
SetScheduledDate(entry.ScheduledDate).
SetName(entry.Name).
SetDescription(entry.Description).
SetCost(entry.Cost).
Save(ctx)
if err != nil {
log.Warn().Err(err).Str("maintenance_entry_id", entry.ID.String()).Msg("failed to copy maintenance entry during duplication")
continue
}
}
}
}
if err := tx.Commit(); err != nil {
return ItemOut{}, err
}
committed = true
e.publishMutationEvent(gid)
// Get the final item with all copied data
return e.GetOne(ctx, newItemID)
}

View File

@@ -0,0 +1,213 @@
package repo
import (
"testing"
"github.com/sysadminsmedia/homebox/backend/pkgs/textutils"
"github.com/stretchr/testify/assert"
)
func TestItemsRepository_AccentInsensitiveSearch(t *testing.T) {
// Test cases for accent-insensitive search
testCases := []struct {
name string
itemName string
searchQuery string
shouldMatch bool
description string
}{
{
name: "Spanish accented item, search without accents",
itemName: "electrónica",
searchQuery: "electronica",
shouldMatch: true,
description: "Should find 'electrónica' when searching for 'electronica'",
},
{
name: "Spanish accented item, search with accents",
itemName: "electrónica",
searchQuery: "electrónica",
shouldMatch: true,
description: "Should find 'electrónica' when searching for 'electrónica'",
},
{
name: "Non-accented item, search with accents",
itemName: "electronica",
searchQuery: "electrónica",
shouldMatch: true,
description: "Should find 'electronica' when searching for 'electrónica' (bidirectional search)",
},
{
name: "Spanish item with tilde, search without accents",
itemName: "café",
searchQuery: "cafe",
shouldMatch: true,
description: "Should find 'café' when searching for 'cafe'",
},
{
name: "Spanish item without tilde, search with accents",
itemName: "cafe",
searchQuery: "café",
shouldMatch: true,
description: "Should find 'cafe' when searching for 'café' (bidirectional)",
},
{
name: "French accented item, search without accents",
itemName: "pére",
searchQuery: "pere",
shouldMatch: true,
description: "Should find 'pére' when searching for 'pere'",
},
{
name: "French: père without accent, search with accents",
itemName: "pere",
searchQuery: "père",
shouldMatch: true,
description: "Should find 'pere' when searching for 'père' (bidirectional)",
},
{
name: "Mixed case with accents",
itemName: "Electrónica",
searchQuery: "ELECTRONICA",
shouldMatch: true,
description: "Should find 'Electrónica' when searching for 'ELECTRONICA' (case insensitive)",
},
{
name: "Bidirectional: Non-accented item, search with different accents",
itemName: "cafe",
searchQuery: "café",
shouldMatch: true,
description: "Should find 'cafe' when searching for 'café' (bidirectional)",
},
{
name: "Bidirectional: Item with accent, search with different accent",
itemName: "résumé",
searchQuery: "resume",
shouldMatch: true,
description: "Should find 'résumé' when searching for 'resume' (bidirectional)",
},
{
name: "Bidirectional: Spanish ñ to n",
itemName: "espanol",
searchQuery: "español",
shouldMatch: true,
description: "Should find 'espanol' when searching for 'español' (bidirectional ñ)",
},
{
name: "French: français with accent, search without",
itemName: "français",
searchQuery: "francais",
shouldMatch: true,
description: "Should find 'français' when searching for 'francais'",
},
{
name: "French: français without accent, search with",
itemName: "francais",
searchQuery: "français",
shouldMatch: true,
description: "Should find 'francais' when searching for 'français' (bidirectional)",
},
{
name: "French: été with accent, search without",
itemName: "été",
searchQuery: "ete",
shouldMatch: true,
description: "Should find 'été' when searching for 'ete'",
},
{
name: "French: été without accent, search with",
itemName: "ete",
searchQuery: "été",
shouldMatch: true,
description: "Should find 'ete' when searching for 'été' (bidirectional)",
},
{
name: "French: hôtel with accent, search without",
itemName: "hôtel",
searchQuery: "hotel",
shouldMatch: true,
description: "Should find 'hôtel' when searching for 'hotel'",
},
{
name: "French: hôtel without accent, search with",
itemName: "hotel",
searchQuery: "hôtel",
shouldMatch: true,
description: "Should find 'hotel' when searching for 'hôtel' (bidirectional)",
},
{
name: "French: naïve with accent, search without",
itemName: "naïve",
searchQuery: "naive",
shouldMatch: true,
description: "Should find 'naïve' when searching for 'naive'",
},
{
name: "French: naïve without accent, search with",
itemName: "naive",
searchQuery: "naïve",
shouldMatch: true,
description: "Should find 'naive' when searching for 'naïve' (bidirectional)",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Test the normalization logic used in the repository
normalizedSearch := textutils.NormalizeSearchQuery(tc.searchQuery)
// This simulates what happens in the repository
// The original search would find exact matches (case-insensitive)
// The normalized search would find accent-insensitive matches
// Test that our normalization works as expected
if tc.shouldMatch {
// If it should match, then either the original query should match
// or the normalized query should match when applied to the stored data
assert.NotEqual(t, "", normalizedSearch, "Normalized search should not be empty")
// The key insight is that we're searching with both the original and normalized queries
// So "electrónica" will be found when searching for "electronica" because:
// 1. Original search: "electronica" doesn't match "electrónica"
// 2. Normalized search: "electronica" matches the normalized version
t.Logf("✓ %s: Item '%s' should be found with search '%s' (normalized: '%s')",
tc.description, tc.itemName, tc.searchQuery, normalizedSearch)
} else {
t.Logf("✗ %s: Item '%s' should NOT be found with search '%s' (normalized: '%s')",
tc.description, tc.itemName, tc.searchQuery, normalizedSearch)
}
})
}
}
func TestNormalizeSearchQueryIntegration(t *testing.T) {
// Test that the normalization function works correctly
testCases := []struct {
input string
expected string
}{
{"electrónica", "electronica"},
{"café", "cafe"},
{"ELECTRÓNICA", "electronica"},
{"Café París", "cafe paris"},
{"hello world", "hello world"},
// French accented words
{"père", "pere"},
{"français", "francais"},
{"été", "ete"},
{"hôtel", "hotel"},
{"naïve", "naive"},
{"PÈRE", "pere"},
{"FRANÇAIS", "francais"},
{"ÉTÉ", "ete"},
{"HÔTEL", "hotel"},
{"NAÏVE", "naive"},
}
for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
result := textutils.NormalizeSearchQuery(tc.input)
assert.Equal(t, tc.expected, result, "Normalization should work correctly")
})
}
}

View File

@@ -20,14 +20,14 @@ type LabelRepository struct {
type (
LabelCreate struct {
Name string `json:"name" validate:"required,min=1,max=255"`
Description string `json:"description" validate:"max=255"`
Description string `json:"description" validate:"max=1000"`
Color string `json:"color"`
}
LabelUpdate struct {
ID uuid.UUID `json:"id"`
Name string `json:"name" validate:"required,min=1,max=255"`
Description string `json:"description" validate:"max=255"`
Description string `json:"description" validate:"max=1000"`
Color string `json:"color"`
}

View File

@@ -0,0 +1,18 @@
package repo
type BarcodeProduct struct {
SearchEngineName string `json:"search_engine_name"`
// Identifications
ModelNumber string `json:"modelNumber"`
Manufacturer string `json:"manufacturer"`
// Extras
Country string `json:"notes"`
Barcode string `json:"barcode"`
ImageURL string `json:"imageURL"`
ImageBase64 string `json:"imageBase64"`
Item ItemCreate `json:"item"`
}

View File

@@ -11,6 +11,8 @@ import (
"github.com/rs/zerolog/log"
)
var startTime = time.Now()
type Data struct {
Domain string `json:"domain"`
Name string `json:"name"`
@@ -18,7 +20,7 @@ type Data struct {
Props map[string]interface{} `json:"props"`
}
func Send(version, buildInfo string) {
func Send(version, buildInfo string) error {
hostData, _ := host.Info()
analytics := Data{
Domain: "homebox.software",
@@ -32,22 +34,23 @@ func Send(version, buildInfo string) {
"platform_version": hostData.PlatformVersion,
"kernel_arch": hostData.KernelArch,
"virt_type": hostData.VirtualizationSystem,
"uptime_min": time.Since(startTime).Minutes(),
},
}
jsonBody, err := json.Marshal(analytics)
if err != nil {
log.Error().Err(err).Msg("failed to marshal analytics data")
return
return err
}
bodyReader := bytes.NewReader(jsonBody)
req, err := http.NewRequest("POST", "https://a.sysadmins.zone/api/event", bodyReader)
if err != nil {
log.Error().Err(err).Msg("failed to create analytics request")
return
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "Homebox/"+version+"/"+buildInfo+" (https://homebox.software)")
req.Header.Set("User-Agent", "Homebox/"+version+"/(https://homebox.software)")
client := &http.Client{
Timeout: 10 * time.Second,
@@ -56,7 +59,7 @@ func Send(version, buildInfo string) {
res, err := client.Do(req)
if err != nil {
log.Error().Err(err).Msg("failed to send analytics request")
return
return err
}
defer func() {
@@ -65,4 +68,5 @@ func Send(version, buildInfo string) {
log.Error().Err(err).Msg("failed to close response body")
}
}()
return nil
}

View File

@@ -29,6 +29,7 @@ type Config struct {
Options Options `yaml:"options"`
LabelMaker LabelMakerConf `yaml:"labelmaker"`
Thumbnail Thumbnail `yaml:"thumbnail"`
Barcode BarcodeAPIConf `yaml:"barcode"`
}
type Options struct {
@@ -60,14 +61,20 @@ type WebConfig struct {
}
type LabelMakerConf struct {
Width int64 `yaml:"width" conf:"default:526"`
Height int64 `yaml:"height" conf:"default:200"`
Padding int64 `yaml:"padding" conf:"default:32"`
Margin int64 `yaml:"margin" conf:"default:32"`
FontSize float64 `yaml:"font_size" conf:"default:32.0"`
PrintCommand *string `yaml:"string"`
AdditionalInformation *string `yaml:"string"`
DynamicLength bool `yaml:"bool" conf:"default:true"`
Width int64 `yaml:"width" conf:"default:526"`
Height int64 `yaml:"height" conf:"default:200"`
Padding int64 `yaml:"padding" conf:"default:32"`
Margin int64 `yaml:"margin" conf:"default:32"`
FontSize float64 `yaml:"font_size" conf:"default:32.0"`
PrintCommand *string `yaml:"string"`
AdditionalInformation *string `yaml:"string"`
DynamicLength bool `yaml:"bool" conf:"default:true"`
LabelServiceUrl *string `yaml:"label_service_url"`
LabelServiceTimeout *time.Duration `yaml:"label_service_timeout"`
}
type BarcodeAPIConf struct {
TokenBarcodespider string `yaml:"token_barcodespider"`
}
// New parses the CLI/Config file and returns a Config struct. If the file argument is an empty string, the

View File

@@ -17,7 +17,10 @@ type Database struct {
Host string `yaml:"host"`
Port string `yaml:"port"`
Database string `yaml:"database"`
SslMode string `yaml:"ssl_mode"`
SslMode string `yaml:"ssl_mode" conf:"default:prefer"`
SslRootCert string `yaml:"ssl_rootcert"`
SslCert string `yaml:"ssl_cert"`
SslKey string `yaml:"ssl_key"`
SqlitePath string `yaml:"sqlite_path" conf:"default:./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite"`
PubSubConnString string `yaml:"pubsub_conn_string" conf:"default:mem://{{ .Topic }}"`
}

View File

@@ -9,6 +9,8 @@ import (
"image/png"
"io"
"log"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
@@ -138,11 +140,18 @@ func wrapText(text string, face font.Face, maxWidth int, maxHeight int, lineHeig
return wrappedLines, ""
}
func GenerateLabel(w io.Writer, params *GenerateParameters) error {
func GenerateLabel(w io.Writer, params *GenerateParameters, cfg *config.Config) error {
if err := params.Validate(); err != nil {
return err
}
// If LabelServiceUrl is configured, fetch the label from the URL instead of generating it
if cfg != nil && cfg.LabelMaker.LabelServiceUrl != nil && *cfg.LabelMaker.LabelServiceUrl != "" {
log.Printf("LabelServiceUrl configured: %s", *cfg.LabelMaker.LabelServiceUrl)
return fetchLabelFromURL(w, *cfg.LabelMaker.LabelServiceUrl, params, cfg)
}
bodyText := params.DescriptionText
if params.AdditionalInformation != nil {
bodyText = bodyText + "\n" + *params.AdditionalInformation
@@ -218,7 +227,7 @@ func GenerateLabel(w io.Writer, params *GenerateParameters) error {
// Create the actual image with calculated height
bounds := image.Rect(0, 0, params.Width, requiredHeight)
img := image.NewRGBA(bounds)
draw.Draw(img, bounds, &image.Uniform{color.White}, image.Point{}, draw.Src)
draw.Draw(img, bounds, &image.Uniform{C: color.White}, image.Point{}, draw.Src)
// Draw QR code onto the image
draw.Draw(img,
@@ -279,6 +288,98 @@ func createContext(font *truetype.Font, size float64, img *image.RGBA, dpi float
return c
}
// fetchLabelFromURL fetches an image from the specified URL and writes it to the writer
func fetchLabelFromURL(w io.Writer, serviceURL string, params *GenerateParameters, cfg *config.Config) error {
// Parse the base URL
baseURL, err := url.Parse(serviceURL)
if err != nil {
return fmt.Errorf("failed to parse service URL %s: %w", serviceURL, err)
}
// Build query parameters with the same attributes passed to print command
query := url.Values{}
query.Set("Width", fmt.Sprintf("%d", params.Width))
query.Set("Height", fmt.Sprintf("%d", params.Height))
query.Set("QrSize", fmt.Sprintf("%d", params.QrSize))
query.Set("Margin", fmt.Sprintf("%d", params.Margin))
query.Set("ComponentPadding", fmt.Sprintf("%d", params.ComponentPadding))
query.Set("TitleText", params.TitleText)
query.Set("TitleFontSize", fmt.Sprintf("%f", params.TitleFontSize))
query.Set("DescriptionText", params.DescriptionText)
query.Set("DescriptionFontSize", fmt.Sprintf("%f", params.DescriptionFontSize))
query.Set("Dpi", fmt.Sprintf("%f", params.Dpi))
query.Set("URL", params.URL)
query.Set("DynamicLength", fmt.Sprintf("%t", params.DynamicLength))
// Add AdditionalInformation if it exists
if params.AdditionalInformation != nil {
query.Set("AdditionalInformation", *params.AdditionalInformation)
}
// Set the query parameters
baseURL.RawQuery = query.Encode()
finalServiceURL := baseURL.String()
log.Printf("Fetching label from URL: %s", finalServiceURL)
// Use configured timeout or default to 30 seconds
timeout := 30 * time.Second
if cfg != nil && cfg.LabelMaker.LabelServiceTimeout != nil {
timeout = *cfg.LabelMaker.LabelServiceTimeout
}
// Create HTTP client with configurable timeout
client := &http.Client{
Timeout: timeout,
}
// Create HTTP request with custom headers
req, err := http.NewRequest("GET", finalServiceURL, nil)
if err != nil {
return fmt.Errorf("failed to create request for URL %s: %w", finalServiceURL, err)
}
// Set custom headers
req.Header.Set("User-Agent", "Homebox-LabelMaker/1.0")
req.Header.Set("Accept", "image/*")
// Make HTTP request to the label service
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to fetch label from URL %s: %w", finalServiceURL, err)
}
// Check if the response status is OK
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("label service returned status %d for URL %s", resp.StatusCode, finalServiceURL)
}
// Check if the response is an image
contentType := resp.Header.Get("Content-Type")
if !strings.HasPrefix(contentType, "image/") {
return fmt.Errorf("label service returned invalid content type %s, expected image/*", contentType)
}
// Set default max response size (10MB)
maxResponseSize := int64(10 << 20)
if cfg != nil {
maxResponseSize = cfg.Web.MaxUploadSize << 20
}
limitedReader := io.LimitReader(resp.Body, maxResponseSize)
// Copy the response body to the writer
_, err = io.Copy(w, limitedReader)
if err != nil {
return fmt.Errorf("failed to write fetched label data: %w", err)
}
if err := resp.Body.Close(); err != nil {
log.Printf("failed to close response body: %v", err)
}
return nil
}
func PrintLabel(cfg *config.Config, params *GenerateParameters) error {
tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("label-%d.png", time.Now().UnixNano()))
f, err := os.OpenFile(tmpFile, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
@@ -292,7 +393,7 @@ func PrintLabel(cfg *config.Config, params *GenerateParameters) error {
}
}()
err = GenerateLabel(f, params)
err = GenerateLabel(f, params, cfg)
if err != nil {
return err
}
@@ -303,8 +404,27 @@ func PrintLabel(cfg *config.Config, params *GenerateParameters) error {
commandTemplate := template.Must(template.New("command").Parse(*cfg.LabelMaker.PrintCommand))
builder := &strings.Builder{}
additionalInformation := func() string {
if params.AdditionalInformation != nil {
return *params.AdditionalInformation
}
return ""
}()
if err := commandTemplate.Execute(builder, map[string]string{
"FileName": f.Name(),
"FileName": f.Name(),
"Width": fmt.Sprintf("%d", params.Width),
"Height": fmt.Sprintf("%d", params.Height),
"QrSize": fmt.Sprintf("%d", params.QrSize),
"Margin": fmt.Sprintf("%d", params.Margin),
"ComponentPadding": fmt.Sprintf("%d", params.ComponentPadding),
"TitleText": params.TitleText,
"TitleFontSize": fmt.Sprintf("%f", params.TitleFontSize),
"DescriptionText": params.DescriptionText,
"DescriptionFontSize": fmt.Sprintf("%f", params.DescriptionFontSize),
"AdditionalInformation": additionalInformation,
"Dpi": fmt.Sprintf("%f", params.Dpi),
"URL": params.URL,
"DynamicLength": fmt.Sprintf("%t", params.DynamicLength),
}); err != nil {
return err
}

View File

@@ -0,0 +1,40 @@
package textutils
import (
"strings"
"unicode"
"golang.org/x/text/runes"
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
)
// RemoveAccents removes accents from text by normalizing Unicode characters
// and removing diacritical marks. This allows for accent-insensitive search.
//
// Example:
// - "electrónica" becomes "electronica"
// - "café" becomes "cafe"
// - "père" becomes "pere"
func RemoveAccents(text string) string {
// Create a transformer that:
// 1. Normalizes to NFD (canonical decomposition)
// 2. Removes diacritical marks (combining characters)
// 3. Normalizes back to NFC (canonical composition)
t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
result, _, err := transform.String(t, text)
if err != nil {
// If transformation fails, return the original text
return text
}
return result
}
// NormalizeSearchQuery normalizes a search query for accent-insensitive matching.
// This function removes accents and converts to lowercase for consistent search behavior.
func NormalizeSearchQuery(query string) string {
normalized := RemoveAccents(query)
return strings.ToLower(normalized)
}

View File

@@ -0,0 +1,152 @@
package textutils
import (
"strings"
"testing"
)
func TestRemoveAccents(t *testing.T) {
testCases := []struct {
name string
input string
expected string
}{
{
name: "Spanish accented characters",
input: "electrónica",
expected: "electronica",
},
{
name: "Spanish accented characters with tilde",
input: "café",
expected: "cafe",
},
{
name: "French accented characters",
input: "père",
expected: "pere",
},
{
name: "German umlauts",
input: "Björk",
expected: "Bjork",
},
{
name: "Mixed accented characters",
input: "résumé",
expected: "resume",
},
{
name: "Portuguese accented characters",
input: "João",
expected: "Joao",
},
{
name: "No accents",
input: "hello world",
expected: "hello world",
},
{
name: "Empty string",
input: "",
expected: "",
},
{
name: "Numbers and symbols",
input: "123!@#",
expected: "123!@#",
},
{
name: "Multiple accents in one word",
input: "été",
expected: "ete",
},
{
name: "Complex Unicode characters",
input: "français",
expected: "francais",
},
{
name: "Unicode diacritics",
input: "naïve",
expected: "naive",
},
{
name: "Unicode combining characters",
input: "e\u0301", // e with combining acute accent
expected: "e",
},
{
name: "Very long string with accents",
input: strings.Repeat("café", 1000),
expected: strings.Repeat("cafe", 1000),
},
{
name: "All French accents",
input: "àâäéèêëïîôöùûüÿç",
expected: "aaaeeeeiioouuuyc",
},
{
name: "All Spanish accents",
input: "áéíóúñüÁÉÍÓÚÑÜ",
expected: "aeiounuAEIOUNU",
},
{
name: "All German umlauts",
input: "äöüÄÖÜß",
expected: "aouAOUß",
},
{
name: "Mixed languages",
input: "Français café España niño",
expected: "Francais cafe Espana nino",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := RemoveAccents(tc.input)
if result != tc.expected {
t.Errorf("RemoveAccents(%q) = %q, expected %q", tc.input, result, tc.expected)
}
})
}
}
func TestNormalizeSearchQuery(t *testing.T) {
testCases := []struct {
name string
input string
expected string
}{
{
name: "Uppercase with accents",
input: "ELECTRÓNICA",
expected: "electronica",
},
{
name: "Mixed case with accents",
input: "Electrónica",
expected: "electronica",
},
{
name: "Multiple words with accents",
input: "Café París",
expected: "cafe paris",
},
{
name: "No accents mixed case",
input: "Hello World",
expected: "hello world",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := NormalizeSearchQuery(tc.input)
if result != tc.expected {
t.Errorf("NormalizeSearchQuery(%q) = %q, expected %q", tc.input, result, tc.expected)
}
})
}
}

View File

@@ -0,0 +1,86 @@
package utils
import "image"
// flipHorizontal will flip the image horizontally. There is a limit of 10000 pixels in either dimension to prevent excessive memory usage.
func flipHorizontal(img image.Image) image.Image {
b := img.Bounds()
if b.Dx() > 10000 || b.Dy() > 10000 {
return img
}
dst := image.NewRGBA(b)
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
dst.Set(b.Max.X-1-(x-b.Min.X), y, img.At(x, y))
}
}
return dst
}
// flipVertical will flip the image vertically. There is a limit of 10000 pixels in either dimension to prevent excessive memory usage.
func flipVertical(img image.Image) image.Image {
b := img.Bounds()
if b.Dx() > 10000 || b.Dy() > 10000 {
return img
}
dst := image.NewRGBA(b)
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
dst.Set(x, b.Max.Y-1-(y-b.Min.Y), img.At(x, y))
}
}
return dst
}
// rotate90 will rotate the image 90 degrees clockwise. There is a limit of 10000 pixels in either dimension to prevent excessive memory usage.
func rotate90(img image.Image) image.Image {
b := img.Bounds()
if b.Dx() > 10000 || b.Dy() > 10000 {
return img
}
dst := image.NewRGBA(image.Rect(0, 0, b.Dy(), b.Dx()))
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
dst.Set(b.Max.Y-1-y, x, img.At(x, y))
}
}
return dst
}
func rotate180(img image.Image) image.Image {
return rotate90(rotate90(img))
}
func rotate270(img image.Image) image.Image {
return rotate90(rotate180(img))
}
// Applies EXIF orientation using only stdlib
func ApplyOrientation(img image.Image, orientation uint16) image.Image {
if img == nil {
return nil
}
if orientation < 1 || orientation > 8 {
return img // No orientation or invalid orientation
}
switch orientation {
case 1:
return img // No rotation needed
case 2:
return flipHorizontal(img)
case 3:
return rotate180(img)
case 4:
return flipVertical(img)
case 5:
return rotate90(flipHorizontal(img))
case 6:
return rotate90(img)
case 7:
return rotate270(flipHorizontal(img))
case 8:
return rotate270(img)
default:
return img
}
}

View File

@@ -3,7 +3,7 @@ services:
image: homebox
build:
context: .
dockerfile: ./Dockerfile
dockerfile: ./Dockerfile.hardened
args:
- COMMIT=head
- BUILD_TIME=0001-01-01T00:00:00Z
@@ -12,7 +12,6 @@ services:
- linux/amd64
- linux/arm64
- linux/arm/v7
- linux/riscv64
environment:
- HBOX_DEBUG=true
- HBOX_LOGGER_LEVEL=-1

View File

@@ -941,6 +941,48 @@
}
}
},
"/v1/items/{id}/duplicate": {
"post": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Items"
],
"summary": "Duplicate Item",
"parameters": [
{
"type": "string",
"description": "Item ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Duplicate Options",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/repo.DuplicateOptions"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/repo.ItemOut"
}
}
}
}
},
"/v1/items/{id}/maintenance": {
"get": {
"security": [
@@ -1809,6 +1851,41 @@
}
}
},
"/v1/products/search-from-barcode": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Items"
],
"summary": "Search EAN from Barcode",
"parameters": [
{
"type": "string",
"description": "barcode to be searched",
"name": "data",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.BarcodeProduct"
}
}
}
}
}
},
"/v1/qrcode": {
"get": {
"security": [
@@ -3061,6 +3138,54 @@
"TypeTime"
]
},
"repo.BarcodeProduct": {
"type": "object",
"properties": {
"barcode": {
"type": "string"
},
"imageBase64": {
"type": "string"
},
"imageURL": {
"type": "string"
},
"item": {
"$ref": "#/definitions/repo.ItemCreate"
},
"manufacturer": {
"type": "string"
},
"modelNumber": {
"description": "Identifications",
"type": "string"
},
"notes": {
"description": "Extras",
"type": "string"
},
"search_engine_name": {
"type": "string"
}
}
},
"repo.DuplicateOptions": {
"type": "object",
"properties": {
"copyAttachments": {
"type": "boolean"
},
"copyCustomFields": {
"type": "boolean"
},
"copyMaintenance": {
"type": "boolean"
},
"copyPrefix": {
"type": "string"
}
}
},
"repo.Group": {
"type": "object",
"properties": {
@@ -3571,7 +3696,7 @@
},
"description": {
"type": "string",
"maxLength": 255
"maxLength": 1000
},
"name": {
"type": "string",

View File

@@ -646,6 +646,38 @@ definitions:
- TypeNumber
- TypeBoolean
- TypeTime
repo.BarcodeProduct:
properties:
barcode:
type: string
imageBase64:
type: string
imageURL:
type: string
item:
$ref: '#/definitions/repo.ItemCreate'
manufacturer:
type: string
modelNumber:
description: Identifications
type: string
notes:
description: Extras
type: string
search_engine_name:
type: string
type: object
repo.DuplicateOptions:
properties:
copyAttachments:
type: boolean
copyCustomFields:
type: boolean
copyMaintenance:
type: boolean
copyPrefix:
type: string
type: object
repo.Group:
properties:
createdAt:
@@ -991,7 +1023,7 @@ definitions:
color:
type: string
description:
maxLength: 255
maxLength: 1000
type: string
name:
maxLength: 255
@@ -1947,6 +1979,32 @@ paths:
summary: Update Item Attachment
tags:
- Items Attachments
/v1/items/{id}/duplicate:
post:
parameters:
- description: Item ID
in: path
name: id
required: true
type: string
- description: Duplicate Options
in: body
name: payload
required: true
schema:
$ref: '#/definitions/repo.DuplicateOptions'
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/repo.ItemOut'
security:
- Bearer: []
summary: Duplicate Item
tags:
- Items
/v1/items/{id}/maintenance:
get:
parameters:
@@ -2543,6 +2601,27 @@ paths:
summary: Test Notifier
tags:
- Notifiers
/v1/products/search-from-barcode:
get:
parameters:
- description: barcode to be searched
in: query
name: data
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/repo.BarcodeProduct'
type: array
security:
- Bearer: []
summary: Search EAN from Barcode
tags:
- Items
/v1/qrcode:
get:
parameters:

View File

@@ -11,7 +11,7 @@ aside: false
|-----------------------------------------|----------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| HBOX_MODE | `production` | application mode used for runtime behavior can be one of: `development`, `production` |
| HBOX_WEB_PORT | 7745 | port to run the web server on, if you're using docker do not change this |
| HBOX_WEB_HOST | | host to run the web server on, if you're using docker do not change this |
| HBOX_WEB_HOST | | host to run the web server on, if you're using docker do not change this. see below for examples |
| HBOX_OPTIONS_ALLOW_REGISTRATION | true | allow users to register themselves |
| HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID | true | auto-increments the asset_id field for new items |
| HBOX_OPTIONS_CURRENCY_CONFIG | | json configuration file containing additional currencie |
@@ -35,10 +35,13 @@ aside: false
| HBOX_DATABASE_SQLITE_PATH | ./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1 | sets the directory path for Sqlite |
| HBOX_DATABASE_HOST | | sets the hostname for a postgres database |
| HBOX_DATABASE_PORT | | sets the port for a postgres database |
| HBOX_DATABASE_USERNAME | | sets the username for a postgres connection |
| HBOX_DATABASE_PASSWORD | | sets the password for a postgres connection |
| HBOX_DATABASE_USERNAME | | sets the username for a postgres connection (optional if using cert auth) |
| HBOX_DATABASE_PASSWORD | | sets the password for a postgres connection (optional if using cert auth) |
| HBOX_DATABASE_DATABASE | | sets the database for a postgres connection |
| HBOX_DATABASE_SSL_MODE | | sets the sslmode for a postgres connection |
| HBOX_DATABASE_SSL_CERT | | sets the sslcert for a postgres connection (should be a path) |
| HBOX_DATABASE_SSL_KEY | | sets the sslkey for a postgres connection (should be a path) |
| HBOX_DATABASE_SSL_ROOTCERT | | sets the sslrootcert for a postgres connection (should be a path) |
| HBOX_OPTIONS_CHECK_GITHUB_RELEASE | true | check for new github releases |
| HBOX_LABEL_MAKER_WIDTH | 526 | width for generated labels in pixels |
| HBOX_LABEL_MAKER_HEIGHT | 200 | height for generated labels in pixels |
@@ -51,6 +54,84 @@ aside: false
| HBOX_THUMBNAIL_WIDTH | 500 | width for generated thumbnails in pixels |
| HBOX_THUMBNAIL_HEIGHT | 500 | height for generated thumbnails in pixels |
### HBOX_WEB_HOST examples
| Value | Notes |
|-----------------------------|------------------------------------------------------------|
| 0.0.0.0 | Visible all interfaces (default behaviour) |
| 127.0.0.1 | Only visible on same host |
| 100.64.0.1 | Only visible on a specific interface (e.g., VPN in a VPS). |
| unix?path=/run/homebox.sock | Listen on unix socket at specified path |
| sysd?name=homebox.socket | Listen on systemd socket |
For unix and systemd socket address syntax and available options, see the [anyhttp address-syntax documentation](https://pkg.go.dev/go.balki.me/anyhttp#readme-address-syntax).
#### Private network example
Below example starts homebox in an isolated network. The process cannot make
any external requests (including check for newer release) and thus more secure.
```bash
sudo systemd-run --property=PrivateNetwork=yes --uid $UID --pty --same-dir --wait --collect homebox --web-host "unix?path=/run/user/$UID/homebox.sock"
Running as unit: run-p74482-i74483.service
Press ^] three times within 1s to disconnect TTY.
2025/07/11 22:33:29 goose: no migrations to run. current version: 20250706190000
10:33PM INF ../../../go/src/app/app/api/handlers/v1/v1_ctrl_auth.go:98 > registering auth provider name=local
10:33PM INF ../../../go/src/app/app/api/main.go:275 > Server is running on unix?path=/run/user/1000/homebox.sock
10:33PM ERR ../../../go/src/app/app/api/main.go:403 > failed to get latest github release error="failed to make latest version request: Get \"https://api.github.com/repos/sysadminsmedia/homebox/releases/l
atest\": dial tcp: lookup api.github.com on [::1]:53: read udp [::1]:50951->[::1]:53: read: connection refused"
10:33PM INF ../../../go/src/app/internal/web/mid/logger.go:36 > request received method=GET path=/ rid=hname/PoXyRgt6ol-000001
10:33PM INF ../../../go/src/app/internal/web/mid/logger.go:41 > request finished method=GET path=/ rid=hname/PoXyRgt6ol-000001 status=0
```
#### Systemd socket example
In the example below, Homebox listens on a systemd socket securely so that only
the webserver (Caddy) can access it. Other processes/containers on the host
cannot connect to Homebox directly, bypassing the webserver.
File: homebox.socket
```systemd
# /usr/local/lib/systemd/system/homebox.socket
[Unit]
Description=Homebox socket
[Socket]
ListenStream=/run/homebox.sock
SocketGroup=caddy
SocketMode=0660
[Install]
WantedBy=sockets.target
```
File: homebox.service
```systemd
# /usr/local/lib/systemd/system/homebox.service
[Unit]
Description=Homebox
After=network.target
Documentation=https://homebox.software
[Service]
DynamicUser=yes
StateDirectory=homebox
Environment=HBOX_WEB_HOST=sysd?name=homebox.socket
WorkingDirectory=/var/lib/homebox
ExecStart=/usr/local/bin/homebox
NoNewPrivileges=yes
CapabilityBoundingSet=
RestrictNamespaces=true
SystemCallFilter=@system-service
```
Usage:
```bash
systemctl start homebox.socket
```
::: warning Security Considerations
For postgreSQL in production:

4
docs/public/_headers Normal file
View File

@@ -0,0 +1,4 @@
/*
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Content-Security-Policy: default-src 'self'; script-src 'report-sample' 'unsafe-inline' 'self' https://a.sysadmins.zone/js/embed.host.js https://static.cloudflareinsights.com/beacon.min.js/vcd15cbe7772f49c399c6a5babf22c1241717689176015 https://unpkg.com/@stoplight/elements/web-components.min.js; style-src 'report-sample' 'unsafe-inline' 'self' https://unpkg.com; object-src 'none'; base-uri 'self'; connect-src 'self' https://raw.githubusercontent.com; font-src 'self'; frame-src 'self' https://a.sysadmins.zone; img-src 'self' data: http://translate.sysadminsmedia.com; manifest-src 'self'; media-src 'self'; worker-src 'none';

7
docs/wrangler.toml Normal file
View File

@@ -0,0 +1,7 @@
name = "homebox-docs"
compatibility_date = "2025-07-12"
preview_urls = true
[assets]
directory = ".vitepress/dist"
not_found_handling = "single-page-application"

View File

@@ -2,7 +2,10 @@
<Dialog v-if="isDesktop" :dialog-id="dialogId">
<DialogScrollContent>
<DialogHeader>
<DialogTitle>{{ title }}</DialogTitle>
<div class="mr-4 flex place-items-center justify-between">
<DialogTitle>{{ title }}</DialogTitle>
<slot name="header-actions" />
</div>
</DialogHeader>
<slot />
@@ -29,6 +32,9 @@
<DrawerHeader>
<DrawerTitle>{{ title }}</DrawerTitle>
</DrawerHeader>
<div class="flex justify-center">
<slot name="header-actions" />
</div>
<div class="m-2 overflow-y-auto p-2">
<slot />
@@ -39,13 +45,14 @@
<script setup lang="ts">
import { useMediaQuery } from "@vueuse/core";
import type { DialogID } from "@/components/ui/dialog-provider/utils";
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from "@/components/ui/drawer";
import { Dialog, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
const isDesktop = useMediaQuery("(min-width: 768px)");
defineProps<{
dialogId: string;
dialogId: DialogID;
title: string;
}>();
</script>

View File

@@ -1,5 +1,5 @@
<template>
<Dialog dialog-id="import">
<Dialog :dialog-id="DialogID.Import">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ $t("components.app.import_dialog.title") }}</DialogTitle>
@@ -38,6 +38,7 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { DialogID } from "@/components/ui/dialog-provider/utils";
import { toast } from "@/components/ui/sonner";
import {
Dialog,

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { DialogID, type NoParamDialogIDs, type OptionalDialogIDs } from "@/components/ui/dialog-provider/utils";
import {
CommandDialog,
CommandInput,
@@ -14,7 +15,7 @@
export type QuickMenuAction =
| { text: string; href: string; type: "navigate" }
| { text: string; dialogId: string; shortcut: string; type: "create" };
| { text: string; dialogId: NoParamDialogIDs | OptionalDialogIDs; shortcut: string; type: "create" };
const props = defineProps({
actions: {
@@ -27,11 +28,11 @@
const { t } = useI18n();
const { closeDialog, openDialog } = useDialog();
useDialogHotkey("quick-menu", { code: "Backquote", ctrl: true });
useDialogHotkey(DialogID.QuickMenu, { code: "Backquote", ctrl: true });
</script>
<template>
<CommandDialog dialog-id="quick-menu">
<CommandDialog :dialog-id="DialogID.QuickMenu">
<CommandInput
:placeholder="t('components.quick_menu.shortcut_hint')"
@keydown="
@@ -39,12 +40,12 @@
const item = props.actions.filter(item => 'shortcut' in item).find(item => item.shortcut === e.key);
if (item) {
e.preventDefault();
openDialog(item.dialogId);
openDialog(item.dialogId as NoParamDialogIDs);
}
// if esc is pressed, close the dialog
if (e.key === 'Escape') {
e.preventDefault();
closeDialog('quick-menu');
closeDialog(DialogID.QuickMenu);
}
}
"
@@ -60,7 +61,7 @@
@select="
e => {
e.preventDefault();
openDialog(create.dialogId);
openDialog(create.dialogId as NoParamDialogIDs);
}
"
>
@@ -76,7 +77,7 @@
:value="`global.navigate_${i + 1}`"
@select="
() => {
closeDialog('quick-menu');
closeDialog(DialogID.QuickMenu);
navigateTo(navigate.href);
}
"
@@ -87,8 +88,8 @@
value="scanner"
@select="
() => {
closeDialog('quick-menu');
openDialog('scanner');
closeDialog(DialogID.QuickMenu);
openDialog(DialogID.Scanner);
}
"
>

View File

@@ -1,5 +1,5 @@
<template>
<Dialog dialog-id="scanner">
<Dialog :dialog-id="DialogID.Scanner">
<DialogScrollContent>
<DialogHeader>
<DialogTitle>{{ t("scanner.title") }}</DialogTitle>
@@ -13,6 +13,25 @@
<MdiAlertCircleOutline class="text-destructive" />
<span class="text-sm font-medium">{{ errorMessage }}</span>
</div>
<div
v-if="detectedBarcode"
class="mb-5 flex flex-col items-center gap-2 rounded-md border border-accent-foreground bg-accent p-4 text-accent-foreground"
role="alert"
>
<div class="flex">
<MdiBarcode class="mr-2" />
<span class="flex-1 text-center text-sm font-medium">
{{ detectedBarcodeType }} {{ $t("scanner.barcode_detected_message") }}:
<strong>{{ detectedBarcode }}</strong>
</span>
</div>
<ButtonGroup>
<Button :disabled="loading" type="submit" @click="handleButtonClick">
{{ $t("scanner.barcode_fetch_data") }}
</Button>
</ButtonGroup>
</div>
<!-- eslint-disable-next-line tailwindcss/no-custom-classname -->
<video ref="video" class="aspect-video w-full rounded-lg bg-muted shadow" poster="data:image/gif,AAAA"></video>
<div class="mt-4">
@@ -34,16 +53,19 @@
<script setup lang="ts">
import { ref, watch, computed } from "vue";
import { BrowserMultiFormatReader, NotFoundException } from "@zxing/library";
import { BrowserMultiFormatReader, NotFoundException, BarcodeFormat } from "@zxing/library";
import { useI18n } from "vue-i18n";
import { DialogID } from "@/components/ui/dialog-provider/utils";
import { Dialog, DialogHeader, DialogTitle, DialogScrollContent } from "@/components/ui/dialog";
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import MdiBarcode from "~icons/mdi/barcode";
import MdiAlertCircleOutline from "~icons/mdi/alert-circle-outline";
import { useDialog } from "@/components/ui/dialog-provider";
const { t } = useI18n();
const { activeDialog } = useDialog();
const open = computed(() => activeDialog.value === "scanner");
const { activeDialog, openDialog, closeDialog } = useDialog();
const open = computed(() => activeDialog && activeDialog.value === DialogID.Scanner);
const sources = ref<MediaDeviceInfo[]>([]);
const selectedSource = ref<string | null>(null);
@@ -51,6 +73,8 @@
const video = ref<HTMLVideoElement>();
const codeReader = new BrowserMultiFormatReader();
const errorMessage = ref<string | null>(null);
const detectedBarcode = ref<string>("");
const detectedBarcodeType = ref<string>("");
const handleError = (error: unknown) => {
console.error("Scanner error:", error);
@@ -68,6 +92,10 @@
}
};
const handleButtonClick = () => {
openDialog(DialogID.ProductImport, { params: { barcode: detectedBarcode.value } });
};
const startScanner = async () => {
errorMessage.value = null;
if (!(navigator && navigator.mediaDevices && "enumerateDevices" in navigator.mediaDevices)) {
@@ -109,6 +137,7 @@
watch(open, async isOpen => {
if (isOpen) {
detectedBarcode.value = "";
await startScanner();
} else {
stopScanner();
@@ -129,10 +158,27 @@
throw new Error(t("scanner.invalid_url"));
}
const sanitizedPath = url.pathname.replace(/[^a-zA-Z0-9-_/]/g, "");
closeDialog(DialogID.Scanner);
navigateTo(sanitizedPath);
} catch (err) {
// Check if it's a barcode for a new element
const bcfmt = result.getBarcodeFormat();
switch (bcfmt) {
case BarcodeFormat.EAN_13:
case BarcodeFormat.UPC_A:
case BarcodeFormat.UPC_E:
case BarcodeFormat.UPC_EAN_EXTENSION:
console.info("Barcode detected");
detectedBarcode.value = result.getText();
detectedBarcodeType.value = BarcodeFormat[bcfmt].replaceAll("_", "-");
break;
default:
handleError(err);
}
loading.value = false;
handleError(err);
}
}
if (err && !(err instanceof NotFoundException)) {
@@ -149,9 +195,3 @@
stopScanner();
});
</script>
<style scoped>
video {
object-fit: cover;
}
</style>

View File

@@ -0,0 +1,45 @@
<script setup lang="ts">
import { themes } from "~~/lib/data/themes";
import { useTheme } from "~/composables/use-theme";
const { setTheme } = useTheme();
</script>
<template>
<div class="homebox grid grid-cols-1 gap-4 font-sans sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
<div
v-for="theme in themes"
:key="theme.value"
:class="'theme-' + theme.value"
class="overflow-hidden rounded-lg border outline-2 outline-offset-2"
:data-theme="theme.value"
:data-set-theme="theme.value"
data-act-class="outline"
@click="setTheme(theme.value)"
>
<div :data-theme="theme.value" class="w-full cursor-pointer bg-background-accent text-foreground">
<div class="grid grid-cols-5 grid-rows-3">
<div class="col-start-1 row-start-1 bg-background"></div>
<div class="col-start-1 row-start-2 bg-sidebar"></div>
<div class="col-start-1 row-start-3 bg-background-accent"></div>
<div class="col-span-4 col-start-2 row-span-3 row-start-1 flex flex-col gap-1 bg-background p-2">
<div class="font-bold">{{ theme.label }}</div>
<div class="flex flex-wrap gap-1">
<div class="flex size-5 items-center justify-center rounded bg-primary lg:size-6">
<div class="text-sm font-bold text-primary-foreground">A</div>
</div>
<div class="flex size-5 items-center justify-center rounded bg-secondary lg:size-6">
<div class="text-sm font-bold text-secondary-foreground">A</div>
</div>
<div class="flex size-5 items-center justify-center rounded bg-accent lg:size-6">
<div class="text-sm font-bold text-accent-foreground">A</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped></style>

View File

@@ -15,6 +15,7 @@
import "@vuepic/vue-datepicker/dist/main.css";
import * as datelib from "~/lib/datelib/datelib";
import { Label } from "@/components/ui/label";
import { darkThemes } from "~/lib/data/themes";
const emit = defineEmits(["update:modelValue", "update:text"]);
@@ -34,7 +35,7 @@
},
});
const isDark = useIsDark();
const isDark = useIsThemeInList(darkThemes);
const formatDate = (date: Date | string | number) => fmtDate(date, "human", "date");

View File

@@ -0,0 +1,256 @@
<template>
<Dialog :dialog-id="DialogID.ProductImport">
<DialogContent :class="'w-full md:max-w-xl lg:max-w-4xl'">
<DialogHeader>
<DialogTitle>{{ $t("components.item.product_import.title") }}</DialogTitle>
</DialogHeader>
<div
v-if="errorMessage"
class="flex items-center gap-2 rounded-md border border-destructive bg-destructive/10 p-4 text-destructive"
role="alert"
>
<MdiAlertCircleOutline class="text-destructive" />
<span class="text-sm font-medium">{{ errorMessage }}</span>
</div>
<div class="flex items-center gap-3">
<FormTextField
v-model="barcode"
:disabled="searching"
class="w-[30%]"
:label="$t('components.item.product_import.barcode')"
@keyup.enter="retrieveProductInfo(barcode)"
/>
<Button
:variant="searching ? 'destructive' : 'default'"
class="mt-auto h-10"
@click="retrieveProductInfo(barcode)"
>
<MdiLoading v-if="searching" class="animate-spin" />
<div v-if="!searching" class="relative mx-2">
<div class="absolute inset-0 flex items-center justify-center">
<MdiBarcode class="size-5 group-hover:hidden" />
</div>
</div>
{{ searching ? $t("global.cancel") : $t("components.item.product_import.search_item") }}
</Button>
</div>
<Separator />
<BaseCard>
<Table class="w-full">
<TableHeader>
<TableRow>
<TableHead
v-for="h in headers"
:key="h.value"
class="text-no-transform bg-secondary text-sm text-secondary-foreground hover:bg-secondary/90"
>
<div
class="flex items-center gap-1"
:class="{
'justify-center': h.align === 'center',
}"
>
<template v-if="typeof h === 'string'">{{ h }}</template>
<template v-else>{{ $t(h.text) }}</template>
</div>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow
v-for="(p, index) in products"
:key="index"
class="cursor-pointer"
:class="{ selected: selectedRow === index }"
@click="selectProduct(index)"
>
<TableCell
v-for="h in headers"
:key="h.value"
:class="{
'text-center': h.align === 'center',
}"
>
<template v-if="h.type === 'name'">
<div class="flex items-center space-x-4">
<img :src="p.imageBase64" class="w-16 rounded object-fill shadow-sm" alt="Product's photo" />
<span class="text-sm font-medium">
{{ p.item.name }}
</span>
</div>
</template>
<template v-else-if="h.type === 'url'">
<NuxtLink class="underline" :to="'https://' + extractValue(p, h.value)" target="_blank">{{
extractValue(p, h.value)
}}</NuxtLink>
</template>
<slot v-else :name="cell(h)">
{{ extractValue(p, h.value) }}
</slot>
</TableCell>
</TableRow>
</TableBody>
</Table>
</BaseCard>
<DialogFooter>
<Button type="import" :disabled="selectedRow === -1" @click="createItem"> Import selected </Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { DialogID } from "@/components/ui/dialog-provider/utils";
import { Button } from "~/components/ui/button";
import type { BarcodeProduct } from "~~/lib/api/types/data-contracts";
import { useDialog } from "~/components/ui/dialog-provider";
import MdiAlertCircleOutline from "~icons/mdi/alert-circle-outline";
import MdiBarcode from "~icons/mdi/barcode";
import MdiLoading from "~icons/mdi/loading";
import type { TableData } from "~/components/Item/View/Table.types";
const { openDialog, registerOpenDialogCallback } = useDialog();
const { t } = useI18n();
const searching = ref(false);
const barcode = ref<string>("");
const products = ref<BarcodeProduct[] | null>(null);
const selectedRow = ref(-1);
const errorMessage = ref<string | null>(null);
type BarcodeTableHeader = {
text: string;
value: string;
align?: "left" | "center" | "right";
type?: "name" | "url";
};
const defaultHeaders = [
{
text: "items.name",
value: "name",
align: "center",
type: "name",
},
{ text: "items.manufacturer", value: "manufacturer", align: "center" },
{ text: "items.model_number", value: "modelNumber", align: "center" },
{ text: "components.item.product_import.db_source", value: "search_engine_name", align: "center", type: "url" },
] satisfies BarcodeTableHeader[];
// Need for later filtering
const headers = defaultHeaders;
onMounted(() => {
const cleanup = registerOpenDialogCallback(DialogID.ProductImport, params => {
selectedRow.value = -1;
searching.value = false;
errorMessage.value = null;
if (params?.barcode) {
// Reset if the barcode is different
if (params.barcode !== barcode.value) {
barcode.value = params.barcode;
retrieveProductInfo(barcode.value).then(() => {
console.log("Processing finished");
});
}
} else {
barcode.value = "";
products.value = null;
}
});
onUnmounted(cleanup);
});
const api = useUserApi();
function createItem() {
if (
products.value !== null &&
products.value.length > 0 &&
selectedRow.value >= 0 &&
selectedRow.value < products.value.length
) {
const p = products.value![selectedRow.value];
openDialog(DialogID.CreateItem, {
params: { product: p },
});
}
}
async function retrieveProductInfo(barcode: string) {
errorMessage.value = null;
if (!barcode || barcode.trim().length === 0 || !/^[0-9]+$/.test(barcode)) {
errorMessage.value = t("components.item.product_import.error_invalid_barcode");
console.error(errorMessage.value);
return;
}
products.value = null;
searching.value = true;
try {
const result = await api.products.searchFromBarcode(barcode.trim());
if (result.error) {
errorMessage.value = t("errors.api_failure") + result.error;
console.error(errorMessage.value);
} else {
if (result.data === undefined || result.data.length === undefined || result.data.length === 0) {
errorMessage.value = t("components.item.product_import.error_not_found");
}
products.value = result.data;
}
} catch (error) {
errorMessage.value = t("components.item.product_import.error_exception") + error;
console.error(errorMessage.value);
} finally {
searching.value = false;
}
}
function extractValue(data: TableData, value: string) {
const parts = value.split(".");
let current = data;
for (const part of parts) {
current = current[part];
}
return current;
}
function cell(h: BarcodeTableHeader) {
return `cell-${h.value.replace(".", "_")}`;
}
function selectProduct(index: number) {
// Unselect if already selected
if (selectedRow.value === index) {
selectedRow.value = -1;
return;
}
selectedRow.value = index;
}
</script>
<style>
tr.selected {
background-color: hsl(var(--primary));
color: hsl(var(--background));
}
tr:hover.selected {
background-color: hsl(var(--primary));
}
</style>

View File

@@ -1,5 +1,34 @@
<template>
<BaseModal dialog-id="create-item" :title="$t('components.item.create_modal.title')">
<BaseModal :dialog-id="DialogID.CreateItem" :title="$t('components.item.create_modal.title')">
<template #header-actions>
<div class="flex">
<TooltipProvider :delay-duration="0">
<ButtonGroup>
<Tooltip>
<TooltipTrigger>
<Button variant="outline" :disabled="loading" size="icon" data-pos="start" @click="openQrScannerPage()">
<MdiBarcodeScan class="size-5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ $t("components.item.create_modal.product_tooltip_scan_barcode") }}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
<Button variant="outline" :disabled="loading" size="icon" data-pos="end" @click="openBarcodeDialog()">
<MdiBarcode class="size-5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ $t("components.item.create_modal.product_tooltip_input_barcode") }}</p>
</TooltipContent>
</Tooltip>
</ButtonGroup>
</TooltipProvider>
</div>
</template>
<form class="flex flex-col gap-2" @submit.prevent="create()">
<LocationSelector v-model="form.location" />
<ItemSelector
@@ -41,7 +70,6 @@
class="absolute left-0 top-0 size-full cursor-pointer opacity-0"
type="file"
accept="image/png,image/jpeg,image/gif,image/avif,image/webp;capture=camera"
capture="environment"
multiple
@change="previewImage"
/>
@@ -141,6 +169,7 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { DialogID } from "@/components/ui/dialog-provider/utils";
import { toast } from "@/components/ui/sonner";
import { Button, ButtonGroup } from "~/components/ui/button";
import BaseModal from "@/components/App/CreateModal.vue";
@@ -149,6 +178,8 @@
import type { ItemCreate, LocationOut } from "~~/lib/api/types/data-contracts";
import { useLabelStore } from "~~/stores/labels";
import { useLocationStore } from "~~/stores/locations";
import MdiBarcode from "~icons/mdi/barcode";
import MdiBarcodeScan from "~icons/mdi/barcode-scan";
import MdiPackageVariant from "~icons/mdi/package-variant";
import MdiPackageVariantClosed from "~icons/mdi/package-variant-closed";
import MdiDelete from "~icons/mdi/delete";
@@ -168,9 +199,9 @@
}
const { t } = useI18n();
const { activeDialog, closeDialog } = useDialog();
const { openDialog, closeDialog, registerOpenDialogCallback } = useDialog();
useDialogHotkey("create-item", { code: "Digit1", shift: true });
useDialogHotkey(DialogID.CreateItem, { code: "Digit1", shift: true });
const api = useUserApi();
@@ -268,55 +299,69 @@
}
}
watch(
() => activeDialog.value,
async active => {
if (active === "create-item") {
// needed since URL will be cleared in the next step => ParentId Selection should stay though
subItemCreate.value = subItemCreateParam.value === "y";
let parentItemLocationId = null;
onMounted(() => {
const cleanup = registerOpenDialogCallback(DialogID.CreateItem, async params => {
// needed since URL will be cleared in the next step => ParentId Selection should stay though
subItemCreate.value = subItemCreateParam.value === "y";
let parentItemLocationId = null;
if (subItemCreate.value && itemId.value) {
const itemIdRead = typeof itemId.value === "string" ? (itemId.value as string) : itemId.value[0];
const { data, error } = await api.items.get(itemIdRead);
if (error || !data) {
toast.error(t("components.item.create_modal.toast.failed_load_parent"));
console.error("Parent item fetch error:", error);
}
if (data) {
parent.value = data;
}
if (data.location) {
const { location } = data;
parentItemLocationId = location.id;
}
// clear URL Parameter (subItemCreate) since intention was communicated and received
const currentQuery = { ...route.query };
delete currentQuery.subItemCreate;
await router.push({ query: currentQuery });
} else {
// since Input is hidden in this case, make sure no accidental parent information is sent out
parent.value = {};
form.parentId = null;
if (subItemCreate.value && itemId.value) {
const itemIdRead = typeof itemId.value === "string" ? (itemId.value as string) : itemId.value[0];
const { data, error } = await api.items.get(itemIdRead);
if (error || !data) {
toast.error(t("components.item.create_modal.toast.failed_load_parent"));
console.error("Parent item fetch error:", error);
}
const locId = locationId.value ? locationId.value : parentItemLocationId;
if (locId) {
const found = locations.value.find(l => l.id === locId);
if (found) {
form.location = found;
}
if (data) {
parent.value = data;
}
if (labelId.value) {
form.labels = labels.value.filter(l => l.id === labelId.value).map(l => l.id);
if (data.location) {
const { location } = data;
parentItemLocationId = location.id;
}
// clear URL Parameter (subItemCreate) since intention was communicated and received
const currentQuery = { ...route.query };
delete currentQuery.subItemCreate;
await router.push({ query: currentQuery });
} else {
// since Input is hidden in this case, make sure no accidental parent information is sent out
parent.value = {};
form.parentId = null;
}
const locId = locationId.value ? locationId.value : parentItemLocationId;
if (locId) {
const found = locations.value.find(l => l.id === locId);
if (found) {
form.location = found;
}
}
}
);
if (params?.product) {
form.name = params.product.item.name;
form.description = params.product.item.description;
if (params.product.imageURL) {
form.photos.push({
photoName: "product_view.jpg",
fileBase64: params.product.imageBase64,
primary: form.photos.length === 0,
file: dataURLtoFile(params.product.imageBase64, "product_view.jpg"),
});
}
}
if (labelId.value) {
form.labels = labels.value.filter(l => l.id === labelId.value).map(l => l.id);
}
});
onUnmounted(cleanup);
});
async function create(close = true) {
if (!form.location?.id) {
@@ -387,7 +432,7 @@
loading.value = false;
if (close) {
closeDialog("create-item");
closeDialog(DialogID.CreateItem);
navigateTo(`/item/${data.id}`);
}
}
@@ -466,4 +511,12 @@
offScreenCanvas.height = 0;
}
}
function openQrScannerPage() {
openDialog(DialogID.Scanner);
}
function openBarcodeDialog() {
openDialog(DialogID.ProductImport);
}
</script>

View File

@@ -0,0 +1,83 @@
<script setup lang="ts">
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import type { DuplicateSettings } from "~/composables/use-preferences";
type Props = {
modelValue: DuplicateSettings;
};
type Emits = {
(e: "update:modelValue", value: DuplicateSettings): void;
};
const props = defineProps<Props>();
const enableCustomPrefix = ref(props.modelValue.copyPrefixOverride !== null);
const prefix = ref(props.modelValue.copyPrefixOverride ?? "");
const emit = defineEmits<Emits>();
const settings = computed({
get: () => props.modelValue,
set: value => emit("update:modelValue", value),
});
</script>
<template>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-3">
<div class="flex items-center gap-2">
<Switch id="copy-maintenance" v-model="settings.copyMaintenance" />
<Label for="copy-maintenance">
{{ $t("items.duplicate.copy_maintenance") }}
</Label>
</div>
<div class="flex items-center gap-2">
<Switch id="copy-attachments" v-model="settings.copyAttachments" />
<Label for="copy-attachments">
{{ $t("items.duplicate.copy_attachments") }}
</Label>
</div>
<div class="flex items-center gap-2">
<Switch id="copy-custom-fields" v-model="settings.copyCustomFields" />
<Label for="copy-custom-fields">
{{ $t("items.duplicate.copy_custom_fields") }}
</Label>
</div>
<div class="flex items-center gap-2">
<Switch
id="copy-prefix"
v-model="enableCustomPrefix"
@update:model-value="
v => {
settings.copyPrefixOverride = v ? prefix : null;
}
"
/>
<Label for="copy-prefix">{{ $t("items.duplicate.enable_custom_prefix") }}</Label>
</div>
<div class="flex flex-col gap-2">
<Label for="copy-prefix" :class="{ 'opacity-50': !enableCustomPrefix }">
{{ $t("items.duplicate.custom_prefix") }}
</Label>
<Input
id="copy-prefix"
v-model="prefix"
:disabled="!enableCustomPrefix"
:placeholder="$t('items.duplicate.prefix')"
class="w-full"
@input="settings.copyPrefixOverride = prefix"
/>
<p class="text-sm text-muted-foreground">
{{ $t("items.duplicate.prefix_instructions") }}
</p>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,99 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { buttonVariants, Button } from "@/components/ui/button";
import { useDialog } from "@/components/ui/dialog-provider";
import { DialogID } from "~/components/ui/dialog-provider/utils";
import { useConfirm } from "@/composables/use-confirm";
import { toast } from "@/components/ui/sonner";
import MdiClose from "~icons/mdi/close";
import MdiDownload from "~icons/mdi/download";
import MdiDelete from "~icons/mdi/delete";
const { t } = useI18n();
const confirm = useConfirm();
const { closeDialog, registerOpenDialogCallback } = useDialog();
const api = useUserApi();
const image = reactive<{
attachmentId: string;
itemId: string;
originalSrc: string;
originalType?: string;
thumbnailSrc?: string;
}>({
attachmentId: "",
itemId: "",
originalSrc: "",
});
onMounted(() => {
const cleanup = registerOpenDialogCallback(DialogID.ItemImage, params => {
image.attachmentId = params.attachmentId;
image.itemId = params.itemId;
if (params.type === "preloaded") {
image.originalSrc = params.originalSrc;
image.originalType = params.originalType;
image.thumbnailSrc = params.thumbnailSrc;
} else if (params.type === "attachment") {
image.originalSrc = api.authURL(`/items/${params.itemId}/attachments/${params.attachmentId}`);
image.originalType = params.mimeType;
image.thumbnailSrc = params.thumbnailId
? api.authURL(`/items/${params.itemId}/attachments/${params.thumbnailId}`)
: image.originalSrc;
}
});
onUnmounted(cleanup);
});
async function deleteAttachment() {
const confirmed = await confirm.open(t("items.delete_attachment_confirm"));
if (confirmed.isCanceled) {
return;
}
const { error } = await api.items.attachments.delete(image.itemId, image.attachmentId);
if (error) {
toast.error(t("items.toast.failed_delete_attachment"));
return;
}
closeDialog(DialogID.ItemImage, {
action: "delete",
id: image.attachmentId,
});
toast.success(t("items.toast.attachment_deleted"));
}
</script>
<template>
<Dialog :dialog-id="DialogID.ItemImage">
<DialogContent class="w-auto border-transparent bg-transparent p-0" disable-close>
<picture>
<source :srcset="image.originalSrc" :type="image.originalType" />
<img :src="image.thumbnailSrc" alt="attachment image" />
</picture>
<Button variant="destructive" size="icon" class="absolute right-[84px] top-1" @click="deleteAttachment">
<MdiDelete />
</Button>
<a :class="buttonVariants({ size: 'icon' })" :href="image.originalSrc" download class="absolute right-11 top-1">
<MdiDownload />
</a>
<Button
size="icon"
class="absolute right-1 top-1"
@click="
closeDialog(DialogID.ItemImage);
image.originalSrc = '';
"
>
<MdiClose />
</Button>
</DialogContent>
</Dialog>
</template>

View File

@@ -1,5 +1,5 @@
<template>
<Dialog dialog-id="item-table-settings">
<Dialog :dialog-id="DialogID.ItemTableSettings">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ $t("components.item.view.table.table_settings") }}</DialogTitle>
@@ -41,7 +41,7 @@
</div>
<DialogFooter>
<Button @click="closeDialog('item-table-settings')"> {{ $t("global.save") }} </Button>
<Button @click="closeDialog(DialogID.ItemTableSettings)"> {{ $t("global.save") }} </Button>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -123,7 +123,7 @@
hidden: disableControls,
}"
>
<Button class="size-10 p-0" variant="outline" @click="openDialog('item-table-settings')">
<Button class="size-10 p-0" variant="outline" @click="openDialog(DialogID.ItemTableSettings)">
<MdiTableCog />
</Button>
<Pagination
@@ -174,6 +174,7 @@
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { useDialog } from "@/components/ui/dialog-provider";
import { DialogID } from "~/components/ui/dialog-provider/utils";
const { openDialog, closeDialog } = useDialog();

View File

@@ -42,6 +42,6 @@
<MdiArrowUp class="hidden group-hover/label-chip:block" />
</div>
</div>
{{ label.name.length > 20 ? `${label.name.substring(0, 20)}...` : label.name }}
{{ label.name }}
</NuxtLink>
</template>

View File

@@ -1,5 +1,5 @@
<template>
<BaseModal dialog-id="create-label" :title="$t('components.label.create_modal.title')">
<BaseModal :dialog-id="DialogID.CreateLabel" :title="$t('components.label.create_modal.title')">
<form class="flex flex-col gap-2" @submit.prevent="create()">
<FormTextField
v-model="form.name"
@@ -12,7 +12,7 @@
<FormTextArea
v-model="form.description"
:label="$t('components.label.create_modal.label_description')"
:max-length="255"
:max-length="1000"
/>
<ColorSelector v-model="form.color" :label="$t('components.label.create_modal.label_color')" :show-hex="true" />
<div class="mt-4 flex flex-row-reverse">
@@ -29,6 +29,7 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { DialogID } from "@/components/ui/dialog-provider/utils";
import { toast } from "@/components/ui/sonner";
import BaseModal from "@/components/App/CreateModal.vue";
import { useDialog, useDialogHotkey } from "~/components/ui/dialog-provider";
@@ -38,7 +39,7 @@
const { closeDialog } = useDialog();
useDialogHotkey("create-label", { code: "Digit2", shift: true });
useDialogHotkey(DialogID.CreateLabel, { code: "Digit2", shift: true });
const loading = ref(false);
const focused = ref(false);
@@ -85,7 +86,7 @@
reset();
if (close) {
closeDialog("create-label");
closeDialog(DialogID.CreateLabel);
navigateTo(`/label/${data.id}`);
}
}

View File

@@ -7,16 +7,16 @@
<TagsInput
v-model="modelValue"
class="w-full gap-0 px-0"
:display-value="v => shortenedLabels.find(l => l.id === v)?.name ?? 'Loading...'"
:display-value="v => props.labels.find(l => l.id === v)?.name ?? 'Loading...'"
>
<div class="flex flex-wrap items-center gap-2 px-3">
<TagsInputItem v-for="item in modelValue" :key="item" :value="item">
<div class="flex flex-wrap items-center gap-2 overflow-hidden px-3">
<TagsInputItem v-for="item in modelValue" :key="item" :value="item" class="h-auto overflow-hidden text-wrap">
<span
v-if="shortenedLabels.find(l => l.id === item)?.color"
class="ml-2 inline-block size-4 rounded-full"
:style="{ backgroundColor: shortenedLabels.find(l => l.id === item)?.color }"
v-if="props.labels.find(l => l.id === item)?.color"
class="ml-2 size-4 shrink-0 rounded-full"
:style="{ backgroundColor: props.labels.find(l => l.id === item)?.color }"
/>
<TagsInputItemText />
<TagsInputItemText class="py-0.5" />
<TagsInputItemDelete />
</TagsInputItem>
</div>
@@ -61,9 +61,9 @@
"
>
<span
class="mr-2 inline-block size-4 rounded-full align-middle"
:class="{ border: shortenedLabels.find(l => l.id === label.value)?.color }"
:style="{ backgroundColor: shortenedLabels.find(l => l.id === label.value)?.color }"
class="mr-2 size-4 shrink-0 rounded-full align-middle"
:class="{ border: props.labels.find(l => l.id === label.value)?.color }"
:style="{ backgroundColor: props.labels.find(l => l.id === label.value)?.color }"
/>
{{ label.label }}
</CommandItem>
@@ -114,24 +114,23 @@
const open = ref(false);
const searchTerm = ref("");
const shortenedLabels = computed(() => {
return props.labels.map(l => ({
...l,
name: l.name.length > 20 ? `${l.name.substring(0, 20)}...` : l.name,
}));
});
const filteredLabels = computed(() => {
const filtered = fuzzysort
.go(searchTerm.value, shortenedLabels.value, { key: "name", all: true })
.go(searchTerm.value, props.labels, { key: "name", all: true })
.map(l => ({
value: l.obj.id,
label: l.obj.name,
}))
.filter(i => !modelValue.value.includes(i.value));
// Only show "Create" option if search term is not empty and no exact match exists
if (searchTerm.value.trim() !== "") {
filtered.push({ value: "create-item", label: `${t("global.create")} ${searchTerm.value}` });
const trimmedSearchTerm = searchTerm.value.trim();
const hasExactMatch = props.labels.some(label => label.name.toLowerCase() === trimmedSearchTerm.toLowerCase());
if (!hasExactMatch) {
filtered.push({ value: "create-item", label: `${t("global.create")} ${searchTerm.value}` });
}
}
return filtered;

View File

@@ -1,5 +1,5 @@
<template>
<BaseModal dialog-id="create-location" :title="$t('components.location.create_modal.title')">
<BaseModal :dialog-id="DialogID.CreateLocation" :title="$t('components.location.create_modal.title')">
<form class="flex flex-col gap-2" @submit.prevent="create()">
<LocationSelector v-model="form.parent" />
<FormTextField
@@ -31,6 +31,7 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { DialogID } from "@/components/ui/dialog-provider/utils";
import { toast } from "@/components/ui/sonner";
import { Button, ButtonGroup } from "~/components/ui/button";
import BaseModal from "@/components/App/CreateModal.vue";
@@ -41,7 +42,7 @@
const { activeDialog, closeDialog } = useDialog();
useDialogHotkey("create-location", { code: "Digit3", shift: true });
useDialogHotkey(DialogID.CreateLocation, { code: "Digit3", shift: true });
const loading = ref(false);
const focused = ref(false);
@@ -54,19 +55,11 @@
watch(
() => activeDialog.value,
active => {
if (active === "create-location") {
// useTimeoutFn(() => {
// focused.value = true;
// }, 50);
if (active && active === DialogID.CreateLocation) {
if (locationId.value) {
const found = locations.value.find(l => l.id === locationId.value);
if (found) {
form.parent = found;
}
form.parent = found || null;
}
} else {
// focused.value = false;
}
}
);
@@ -74,7 +67,6 @@
function reset() {
form.name = "";
form.description = "";
form.parent = null;
focused.value = false;
loading.value = false;
}
@@ -118,10 +110,11 @@
if (data) {
toast.success(t("components.location.create_modal.toast.create_success"));
}
reset();
if (close) {
closeDialog("create-location");
closeDialog(DialogID.CreateLocation);
navigateTo(`/location/${data.id}`);
}
}

View File

@@ -1,5 +1,5 @@
<template>
<Dialog dialog-id="edit-maintenance">
<Dialog :dialog-id="DialogID.EditMaintenance">
<DialogContent>
<DialogHeader>
<DialogTitle>
@@ -27,6 +27,7 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { DialogID } from "@/components/ui/dialog-provider/utils";
import { toast } from "@/components/ui/sonner";
import type { MaintenanceEntry, MaintenanceEntryWithDetails } from "~~/lib/api/types/data-contracts";
import MdiPost from "~icons/mdi/post";
@@ -77,7 +78,7 @@
return;
}
closeDialog("edit-maintenance");
closeDialog(DialogID.EditMaintenance);
emit("changed");
}
@@ -99,7 +100,7 @@
return;
}
closeDialog("edit-maintenance");
closeDialog(DialogID.EditMaintenance);
emit("changed");
}
@@ -111,7 +112,7 @@
entry.description = "";
entry.cost = "";
entry.itemId = itemId;
openDialog("edit-maintenance");
openDialog(DialogID.EditMaintenance);
};
const openUpdateModal = (maintenanceEntry: MaintenanceEntry | MaintenanceEntryWithDetails) => {
@@ -122,7 +123,7 @@
entry.description = maintenanceEntry.description;
entry.cost = maintenanceEntry.cost;
entry.itemId = null;
openDialog("edit-maintenance");
openDialog(DialogID.EditMaintenance);
};
const confirm = useConfirm();
@@ -164,7 +165,7 @@
entry.description = maintenanceEntry.description;
entry.cost = maintenanceEntry.cost;
entry.itemId = itemId;
openDialog("edit-maintenance");
openDialog(DialogID.EditMaintenance);
}
defineExpose({ openCreateModal, openUpdateModal, deleteEntry, complete, duplicate });

View File

@@ -2,6 +2,7 @@
import { useI18n } from "vue-i18n";
import { route } from "../../lib/api/base";
import PageQRCode from "./PageQRCode.vue";
import { DialogID } from "@/components/ui/dialog-provider/utils";
import { toast } from "@/components/ui/sonner";
import MdiLoading from "~icons/mdi/loading";
import MdiPrinterPos from "~icons/mdi/printer-pos";
@@ -63,7 +64,7 @@
}
toast.success(t("components.global.label_maker.toast.print_success"));
closeDialog("print-label");
closeDialog(DialogID.PrintLabel);
serverPrinting.value = false;
}
@@ -93,7 +94,7 @@
<template>
<div>
<Dialog dialog-id="print-label">
<Dialog :dialog-id="DialogID.PrintLabel">
<DialogContent>
<DialogHeader>
<DialogTitle>
@@ -137,7 +138,7 @@
<Tooltip>
<TooltipTrigger as-child>
<Button size="icon" @click="openDialog('print-label')">
<Button size="icon" @click="openDialog(DialogID.PrintLabel)">
<MdiPrinterPos name="mdi-printer-pos" />
</Button>
</TooltipTrigger>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import MarkdownIt from "markdown-it";
import { imgSize } from "@mdit/plugin-img-size";
import DOMPurify from "dompurify";
type Props = {
@@ -14,7 +15,7 @@
html: true,
linkify: true,
typographer: true,
});
}).use(imgSize);
const raw = computed(() => {
const html = md.render(props.source || "").replace(/\n$/, ""); // remove trailing newline

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { DialogID } from "@/components/ui/dialog-provider/utils";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { route } from "~/lib/api/base";
import MdiQrcode from "~icons/mdi/qrcode";
@@ -16,7 +17,7 @@
</script>
<template>
<Dialog dialog-id="page-qr-code">
<Dialog :dialog-id="DialogID.PageQRCode">
<DialogContent>
<DialogHeader>
<DialogTitle>
@@ -29,7 +30,7 @@
<Tooltip>
<TooltipTrigger as-child>
<Button size="icon" @click="openDialog('page-qr-code')">
<Button size="icon" @click="openDialog(DialogID.PageQRCode)">
<MdiQrcode name="mdi-qrcode" />
</Button>
</TooltipTrigger>

View File

@@ -25,13 +25,13 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<template>
<AlertDialogPortal>
<AlertDialogOverlay
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
class="fixed inset-0 z-[60] bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
/>
<AlertDialogContent
v-bind="forwarded"
:class="
cn(
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
'fixed left-1/2 top-1/2 z-[60] grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
props.class,
)
"

View File

@@ -3,8 +3,9 @@ import type { DialogRootEmits, DialogRootProps } from 'reka-ui'
import { Dialog, DialogContent } from '@/components/ui/dialog'
import { useForwardPropsEmits } from 'reka-ui'
import Command from './Command.vue'
import type { DialogID } from '@/components/ui/dialog-provider/utils';
const props = defineProps<DialogRootProps & { dialogId: string }>();
const props = defineProps<DialogRootProps & { dialogId: DialogID }>();
const emits = defineEmits<DialogRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)

View File

@@ -1,40 +1,73 @@
<!-- DialogProvider.vue -->
<script setup lang="ts">
import { ref, reactive, computed } from "vue";
import { provideDialogContext } from "./utils";
import { ref, reactive, computed } from 'vue';
import {
provideDialogContext,
type DialogID,
type DialogParamsMap,
} from './utils';
const activeDialog = ref<string | null>(null);
const activeDialog = ref<DialogID | null>(null);
const activeAlerts = reactive<string[]>([]);
const openDialogCallbacks = new Map<DialogID, (params: any) => void>();
const openDialog = (dialogId: string) => {
if (activeAlerts.length > 0) return;
activeDialog.value = dialogId;
// onClose for the currently-open dialog (only one dialog can be active)
let activeOnCloseCallback: ((result?: any) => void) | undefined;
const registerOpenDialogCallback = <T extends DialogID>(
dialogId: T,
callback: (params?: T extends keyof DialogParamsMap ? DialogParamsMap[T] : undefined) => void
) => {
openDialogCallbacks.set(dialogId, callback as (params: any) => void);
return () => {
openDialogCallbacks.delete(dialogId);
};
};
const closeDialog = (dialogId?: string) => {
if (dialogId) {
if (activeDialog.value === dialogId) {
activeDialog.value = null;
}
} else {
activeDialog.value = null;
const openDialog = <T extends DialogID>(dialogId: T, options?: any) => {
if (activeAlerts.length > 0) return;
activeDialog.value = dialogId;
activeOnCloseCallback = options?.onClose;
const openCallback = openDialogCallbacks.get(dialogId);
if (openCallback) {
openCallback(options?.params);
}
};
function closeDialog(dialogId?: DialogID, result?: any) {
// No dialogId passed -> close current active dialog without result
if (!dialogId) {
if (activeDialog.value) {
// call onClose (if any) with no result
activeOnCloseCallback?.(undefined);
activeOnCloseCallback = undefined;
}
activeDialog.value = null;
return;
}
// dialogId passed -> if it's the active dialog, call onClose with result
if (activeDialog.value && activeDialog.value === dialogId) {
activeOnCloseCallback?.(result);
activeOnCloseCallback = undefined;
activeDialog.value = null;
}
}
const addAlert = (alertId: string) => {
activeAlerts.push(alertId);
};
const removeAlert = (alertId: string) => {
const index = activeAlerts.indexOf(alertId);
if (index !== -1) {
activeAlerts.splice(index, 1);
}
if (index !== -1) activeAlerts.splice(index, 1);
};
// Provide context to child components
provideDialogContext({
activeDialog: computed(() => activeDialog.value),
registerOpenDialogCallback,
openDialog,
closeDialog,
activeAlerts: computed(() => activeAlerts),

View File

@@ -1,56 +1,188 @@
import type { ComputedRef } from "vue";
import { createContext } from "reka-ui";
import { useMagicKeys, useActiveElement } from "@vueuse/core";
import { computed, type ComputedRef } from 'vue';
import { createContext } from 'reka-ui';
import { useMagicKeys, useActiveElement } from '@vueuse/core';
import type { BarcodeProduct } from '~~/lib/api/types/data-contracts';
export enum DialogID {
AttachmentEdit = 'attachment-edit',
ChangePassword = 'changePassword',
CreateItem = 'create-item',
CreateLocation = 'create-location',
CreateLabel = 'create-label',
CreateNotifier = 'create-notifier',
DuplicateSettings = 'duplicate-settings',
DuplicateTemporarySettings = 'duplicate-temporary-settings',
EditMaintenance = 'edit-maintenance',
Import = 'import',
ItemImage = 'item-image',
ItemTableSettings = 'item-table-settings',
PrintLabel = 'print-label',
ProductImport = 'product-import',
QuickMenu = 'quick-menu',
Scanner = 'scanner',
PageQRCode = 'page-qr-code',
UpdateLabel = 'update-label',
UpdateLocation = 'update-location',
}
/**
* - Keys present without ? => params required
* - Keys present with ? => params optional
* - Keys not present => no params allowed
*/
export type DialogParamsMap = {
[DialogID.ItemImage]:
| ({
type: 'preloaded';
originalSrc: string;
originalType?: string;
thumbnailSrc?: string;
}
| {
type: 'attachment';
mimeType: string;
thumbnailId?: string;
}) & {
itemId: string;
attachmentId: string;
};
[DialogID.CreateItem]?: { product?: BarcodeProduct };
[DialogID.ProductImport]?: { barcode?: string };
};
/**
* Defines the payload type for a dialog's onClose callback.
*/
export type DialogResultMap = {
[DialogID.ItemImage]?: { action: 'delete', id: string };
};
/** Helpers to split IDs by requirement */
type OptionalKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];
type RequiredKeys<T> = Exclude<keyof T, OptionalKeys<T>>;
type SpecifiedDialogIDs = keyof DialogParamsMap;
export type NoParamDialogIDs = Exclude<DialogID, SpecifiedDialogIDs>;
export type RequiredDialogIDs = RequiredKeys<DialogParamsMap>;
export type OptionalDialogIDs = OptionalKeys<DialogParamsMap>;
type ParamsOf<T extends DialogID> = T extends SpecifiedDialogIDs
? DialogParamsMap[T]
: never;
type ResultOf<T extends DialogID> = T extends keyof DialogResultMap
? DialogResultMap[T]
: void;
type OpenDialog = {
// Dialogs with no parameters
<T extends NoParamDialogIDs>(
dialogId: T,
options?: { onClose?: (result?: ResultOf<T>) => void; params?: never }
): void;
// Dialogs with required parameters
<T extends RequiredDialogIDs>(
dialogId: T,
options: { params: ParamsOf<T>; onClose?: (result?: ResultOf<T>) => void }
): void;
// Dialogs with optional parameters
<T extends OptionalDialogIDs>(
dialogId: T,
options?: { params?: ParamsOf<T>; onClose?: (result?: ResultOf<T>) => void }
): void;
};
type CloseDialog = {
// Close the currently active dialog, no ID specified. No result payload.
(): void;
// Close a specific dialog that has a defined result type.
<T extends keyof DialogResultMap>(dialogId: T, result?: ResultOf<T>): void;
// Close a specific dialog that has NO defined result type.
<T extends Exclude<DialogID, keyof DialogResultMap>>(
dialogId: T,
result?: never
): void;
};
type OpenCallback = {
<T extends NoParamDialogIDs>(dialogId: T, cb: () => void): () => void;
<T extends RequiredDialogIDs>(
dialogId: T,
cb: (params: ParamsOf<T>) => void
): () => void;
<T extends OptionalDialogIDs>(
dialogId: T,
cb: (params?: ParamsOf<T>) => void
): () => void;
};
export const [useDialog, provideDialogContext] = createContext<{
activeDialog: ComputedRef<string | null>;
activeDialog: ComputedRef<DialogID | null>;
activeAlerts: ComputedRef<string[]>;
openDialog: (dialogId: string) => void;
closeDialog: (dialogId?: string) => void;
registerOpenDialogCallback: OpenCallback;
openDialog: OpenDialog;
closeDialog: CloseDialog;
addAlert: (alertId: string) => void;
removeAlert: (alertId: string) => void;
}>("DialogProvider");
}>('DialogProvider');
export const useDialogHotkey = (
dialogId: string,
key: {
shift?: boolean;
ctrl?: boolean;
code: string;
}
) => {
/**
* Hotkey helper:
* - No/optional params: pass dialogId + key
* - Required params: pass dialogId + key + getParams()
*/
type HotkeyKey = {
shift?: boolean;
ctrl?: boolean;
code: string;
};
export function useDialogHotkey<T extends NoParamDialogIDs | OptionalDialogIDs>(
dialogId: T,
key: HotkeyKey
): void;
export function useDialogHotkey<T extends RequiredDialogIDs>(
dialogId: T,
key: HotkeyKey,
getParams: () => ParamsOf<T>
): void;
export function useDialogHotkey(
dialogId: DialogID,
key: HotkeyKey,
getParams?: () => unknown
) {
const { openDialog } = useDialog();
const activeElement = useActiveElement();
const notUsingInput = computed(
() => activeElement.value?.tagName !== "INPUT" && activeElement.value?.tagName !== "TEXTAREA"
() =>
activeElement.value?.tagName !== 'INPUT' &&
activeElement.value?.tagName !== 'TEXTAREA'
);
useMagicKeys({
passive: false,
onEventFired: event => {
// console.log({
// event,
// notUsingInput: notUsingInput.value,
// eventType: event.type,
// keyCode: event.code,
// matchingKeyCode: key.code === event.code,
// shift: event.shiftKey,
// matchingShift: key.shift === undefined || event.shiftKey === key.shift,
// ctrl: event.ctrlKey,
// matchingCtrl: key.ctrl === undefined || event.ctrlKey === key.ctrl,
// });
onEventFired: (event) => {
if (
notUsingInput.value &&
event.type === "keydown" &&
event.type === 'keydown' &&
event.code === key.code &&
(key.shift === undefined || event.shiftKey === key.shift) &&
(key.ctrl === undefined || event.ctrlKey === key.ctrl)
) {
openDialog(dialogId);
if (getParams) {
openDialog(dialogId as RequiredDialogIDs, {
params: getParams() as never,
});
} else {
openDialog(dialogId as NoParamDialogIDs);
}
event.preventDefault();
}
},
});
};
}

View File

@@ -1,15 +1,15 @@
<script setup lang="ts">
import { DialogRoot, type DialogRootEmits, type DialogRootProps, useForwardPropsEmits } from "reka-ui";
import { useDialog } from "../dialog-provider/utils";
import { useDialog, type DialogID } from "@/components/ui/dialog-provider/utils";
const props = defineProps<DialogRootProps & { dialogId: string }>();
const props = defineProps<DialogRootProps & { dialogId: DialogID }>();
const emits = defineEmits<DialogRootEmits>();
const { closeDialog, activeDialog } = useDialog();
const isOpen = computed(() => activeDialog.value === props.dialogId);
const isOpen = computed(() => (activeDialog.value && activeDialog.value === props.dialogId));
const onOpenChange = (open: boolean) => {
if (!open) closeDialog(props.dialogId);
if (!open) closeDialog(props.dialogId as any);
};
const forwarded = useForwardPropsEmits(props, emits);

View File

@@ -2,19 +2,19 @@
import type { DrawerRootEmits, DrawerRootProps } from "vaul-vue";
import { useForwardPropsEmits } from "reka-ui";
import { DrawerRoot } from "vaul-vue";
import { useDialog } from "../dialog-provider/utils";
import { DialogID, useDialog } from "@/components/ui/dialog-provider/utils";
const props = withDefaults(defineProps<DrawerRootProps & { dialogId: string }>(), {
shouldScaleBackground: true,
}) as DrawerRootProps & { dialogId: string };
}) as DrawerRootProps & { dialogId: DialogID };
const emits = defineEmits<DrawerRootEmits>();
const { closeDialog, activeDialog } = useDialog();
const isOpen = computed(() => activeDialog.value === props.dialogId);
const isOpen = computed(() => activeDialog.value !== null && activeDialog.value === props.dialogId);
const onOpenChange = (open: boolean) => {
if (!open) closeDialog(props.dialogId);
if (!open) closeDialog(props.dialogId as any);
};
const forwarded = useForwardPropsEmits(props, emits);

View File

@@ -4,6 +4,13 @@ import type { DaisyTheme } from "~~/lib/data/themes";
export type ViewType = "table" | "card" | "tree";
export type DuplicateSettings = {
copyMaintenance: boolean;
copyAttachments: boolean;
copyCustomFields: boolean;
copyPrefixOverride: string | null;
};
export type LocationViewPreferences = {
showDetails: boolean;
showEmpty: boolean;
@@ -15,6 +22,7 @@ export type LocationViewPreferences = {
displayLegacyHeader: boolean;
language?: string;
overrideFormatLocale?: string;
duplicateSettings: DuplicateSettings;
};
/**
@@ -34,6 +42,12 @@ export function useViewPreferences(): Ref<LocationViewPreferences> {
displayLegacyHeader: false,
language: null,
overrideFormatLocale: null,
duplicateSettings: {
copyMaintenance: false,
copyAttachments: true,
copyCustomFields: true,
copyPrefixOverride: null,
},
},
{ mergeDefaults: true }
);

View File

@@ -1,5 +1,5 @@
import type { ComputedRef } from "vue";
import type { DaisyTheme } from "~~/lib/data/themes";
import { type DaisyTheme } from "~~/lib/data/themes";
export interface UseTheme {
theme: ComputedRef<DaisyTheme>;
@@ -42,27 +42,11 @@ export function useTheme(): UseTheme {
return { theme, setTheme };
}
export function useIsDark() {
export function useIsThemeInList(list: DaisyTheme[]) {
const theme = useTheme();
const darkthemes = [
"synthwave",
"retro",
"cyberpunk",
"valentine",
"halloween",
"forest",
"aqua",
"black",
"luxury",
"dracula",
"business",
"night",
"coffee",
];
return computed(() => {
return darkthemes.includes(theme.theme.value);
return list.includes(theme.theme.value);
});
}

View File

@@ -10,6 +10,7 @@
<ItemCreateModal />
<LabelCreateModal />
<LocationCreateModal />
<ItemBarcodeModal />
<AppQuickMenuModal :actions="quickMenuActions" />
<AppScannerModal />
<SidebarProvider :default-open="sidebarState">
@@ -41,7 +42,7 @@
v-for="btn in dropdown"
:key="btn.id"
class="group cursor-pointer text-lg"
@click="openDialog(btn.dialogId)"
@click="openDialog(btn.dialogId as NoParamDialogIDs)"
>
{{ btn.name.value }}
<Shortcut
@@ -78,7 +79,7 @@
'text-nowrap': typeof locale === 'string' && locale.startsWith('zh-'),
}"
:tooltip="$t('menu.scanner')"
@click.prevent="openDialog('scanner')"
@click.prevent="openDialog(DialogID.Scanner)"
>
<MdiQrcodeScan />
<span>{{ $t("menu.scanner") }}</span>
@@ -209,6 +210,7 @@
import { Input } from "~/components/ui/input";
import { Button } from "~/components/ui/button";
import { toast } from "@/components/ui/sonner";
import { DialogID, type NoParamDialogIDs, type OptionalDialogIDs } from "~/components/ui/dialog-provider/utils";
const { t, locale } = useI18n();
const username = computed(() => authCtx.user?.name || "User");
@@ -249,7 +251,7 @@
navigator.mediaDevices
.getUserMedia({ video: true })
.then(() => {
openDialog("scanner");
openDialog(DialogID.Scanner);
})
.catch(err => {
console.error(err);
@@ -263,24 +265,31 @@
// Preload currency format
useFormatCurrency();
const dropdown = [
type DropdownItem = {
id: number;
name: ComputedRef<string>;
shortcut: string;
dialogId: NoParamDialogIDs | OptionalDialogIDs;
};
const dropdown: DropdownItem[] = [
{
id: 0,
name: computed(() => t("menu.create_item")),
shortcut: "Shift+1",
dialogId: "create-item",
dialogId: DialogID.CreateItem,
},
{
id: 1,
name: computed(() => t("menu.create_location")),
shortcut: "Shift+3",
dialogId: "create-location",
dialogId: DialogID.CreateLocation,
},
{
id: 2,
name: computed(() => t("menu.create_label")),
shortcut: "Shift+2",
dialogId: "create-label",
dialogId: DialogID.CreateLabel,
},
];
@@ -334,7 +343,7 @@
const quickMenuActions = reactive([
...dropdown.map(v => ({
text: computed(() => v.name.value),
dialogId: v.dialogId,
dialogId: v.dialogId as NoParamDialogIDs,
shortcut: v.shortcut.split("+")[1],
type: "create" as const,
})),

View File

@@ -153,6 +153,26 @@ export class ItemsApi extends BaseAPI {
return resp;
}
duplicate(
id: string,
options: {
copyMaintenance?: boolean;
copyAttachments?: boolean;
copyCustomFields?: boolean;
copyPrefix?: string;
} = {}
) {
return this.http.post<typeof options, ItemOut>({
url: route(`/items/${id}/duplicate`),
body: {
copyMaintenance: options.copyMaintenance,
copyAttachments: options.copyAttachments,
copyCustomFields: options.copyCustomFields,
copyPrefix: options.copyPrefix,
},
});
}
import(file: File | Blob) {
const formData = new FormData();
formData.append("csv", file);

View File

@@ -0,0 +1,8 @@
import { BaseAPI, route } from "../base";
import type { BarcodeProduct } from "../types/data-contracts";
export class ProductAPI extends BaseAPI {
searchFromBarcode(productEAN: string) {
return this.http.get<BarcodeProduct[]>({ url: route(`/products/search-from-barcode`, { productEAN }) });
}
}

View File

@@ -451,6 +451,19 @@ export interface EntUserEdges {
notifiers: EntNotifier[];
}
export interface BarcodeProduct {
barcode: string;
imageBase64: string;
imageURL: string;
item: ItemCreate;
manufacturer: string;
/** Identifications */
modelNumber: string;
/** Extras */
notes: string;
search_engine_name: string;
}
export interface Group {
createdAt: Date | string;
currency: string;
@@ -631,7 +644,7 @@ export interface ItemUpdate {
export interface LabelCreate {
color: string;
/** @maxLength 255 */
/** @maxLength 1000 */
description: string;
/**
* @minLength 1

View File

@@ -10,6 +10,7 @@ import { AssetsApi } from "./classes/assets";
import { ReportsAPI } from "./classes/reports";
import { NotifiersAPI } from "./classes/notifiers";
import { MaintenanceAPI } from "./classes/maintenance";
import { ProductAPI } from "./classes/product";
import type { Requests } from "~~/lib/requests";
export class UserClient extends BaseAPI {
@@ -24,6 +25,7 @@ export class UserClient extends BaseAPI {
assets: AssetsApi;
reports: ReportsAPI;
notifiers: NotifiersAPI;
products: ProductAPI;
constructor(requests: Requests, attachmentToken: string) {
super(requests, attachmentToken);
@@ -39,6 +41,7 @@ export class UserClient extends BaseAPI {
this.assets = new AssetsApi(requests);
this.reports = new ReportsAPI(requests);
this.notifiers = new NotifiersAPI(requests);
this.products = new ProductAPI(requests);
Object.freeze(this);
}

View File

@@ -153,3 +153,19 @@ export const themes: ThemeOption[] = [
value: "winter",
},
];
export const darkThemes: DaisyTheme[] = [
"synthwave",
"retro",
"cyberpunk",
"valentine",
"halloween",
"forest",
"aqua",
"black",
"luxury",
"dracula",
"business",
"night",
"coffee",
];

View File

@@ -0,0 +1,8 @@
# DO NOT EDIT FILES HERE DIRECTLY UNLESS ADDING KEYS!
Please do not edit the files here directly unless your adding new translation keys to the en.json file.
All translations should be added according to [our documentation](https://homebox.software/en/contribute/get-started.html#translations) through our [weblate instance](https://translate.sysadminsmedia.com).
This helps ensure that our translations are consistent and effective.
PRs that modify these files directly will be closed without merge.

View File

@@ -1,20 +1,9 @@
{
"components": {
"app": {
"create_modal": {
"createAndAddAnother": "",
"enter": "",
"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.",
"description": "",
"title": "Importuj CSV fajl",
"toast": {
"import_failed": "",
"import_success": "",
"please_select_file": ""
}
"title": "Importuj CSV fajl"
},
"outdated": {
"current_version": "Trenutna verzija",
@@ -24,11 +13,6 @@
"new_version_available_link": "Klikni ovdje za pregled bilješke o izdanju"
}
},
"form": {
"password": {
"toggle_show": ""
}
},
"global": {
"copy_text": {
"documentation": "dokumentacija",
@@ -49,9 +33,6 @@
"minute": "minuta",
"minutes": "minuta",
"months": "mjeseci",
"next-month": "",
"next-week": "",
"next-year": "",
"second": "sekunda",
"seconds": "sekundi",
"tomorrow": "sutra",
@@ -59,553 +40,79 @@
"weeks": "sedmica/e",
"years": "godine/a",
"yesterday": "jučer"
},
"label_maker": {
"browser_print": "",
"confirm_description": "",
"download": "",
"print": "",
"server_print": "",
"titles": "",
"toast": {
"load_status_failed": "",
"print_failed": "",
"print_success": ""
}
},
"page_qr_code": {
"page_url": "",
"qr_tooltip": ""
},
"password_score": {
"password_strength": ""
}
},
"item": {
"attachments_list": {
"download": "",
"open_new_tab": ""
},
"create_modal": {
"delete_photo": "",
"item_description": "",
"item_name": "",
"item_photo": "",
"item_quantity": "",
"parent_item": "",
"rotate_photo": "",
"set_as_primary_photo": "",
"title": "",
"toast": {
"already_creating": "",
"create_failed": "",
"create_success": "",
"failed_load_parent": "",
"no_canvas_support": "",
"please_select_location": "",
"rotate_failed": "",
"rotate_process_failed": "",
"some_photos_failed": "",
"upload_failed": "",
"upload_success": "",
"uploading_photos": ""
},
"upload_photos": "",
"uploaded": ""
},
"selector": {
"no_results": "",
"placeholder": "",
"search_placeholder": ""
},
"view": {
"selectable": {
"card": "Karta",
"items": "Predmeti",
"no_items": "",
"table": "Tabela"
},
"table": {
"headers": "Zaglavlje",
"page": "Stranica",
"rows_per_page": "",
"table_settings": "",
"view_item": ""
"page": "Stranica"
}
}
},
"label": {
"create_modal": {
"label_description": "",
"label_name": "",
"title": "",
"toast": {
"already_creating": "",
"create_failed": "",
"create_success": "",
"label_name_too_long": ""
}
},
"selector": {
"select_labels": ""
}
},
"location": {
"create_modal": {
"location_description": "",
"location_name": "",
"title": "",
"toast": {
"already_creating": "",
"create_failed": "",
"create_success": ""
}
},
"selector": {
"no_location_found": "",
"parent_location": "",
"search_location": "",
"select_location": ""
},
"tree": {
"no_locations": ""
}
},
"quick_menu": {
"no_results": "",
"shortcut_hint": ""
}
},
"global": {
"add": "Dodaj",
"archived": "Arhivirano",
"build": "",
"cancel": "Poništi",
"confirm": "Potvrdi",
"create": "Kreiraj",
"create_and_add": "",
"create_subitem": "",
"created": "Kreirano",
"delete": "Obriši",
"delete_confirm": "",
"demo_instance": "",
"details": "Detalji",
"duplicate": "Dupliciraj",
"edit": "Izmjeni",
"email": "Email",
"follow_dev": "",
"footer": {
"api_link": "",
"version_link": ""
},
"github": "",
"insured": "Osigurano",
"items": "Predmeti",
"join_discord": "",
"labels": "Etikete",
"loading": "",
"locations": "Lokacije",
"maintenance": "Održavanje",
"name": "Ime",
"navigate": "Navigiraj",
"password": "Šifra",
"quantity": "Količina",
"read_docs": "",
"return_home": "",
"save": "Sačuvaj",
"search": "Pretraži",
"sign_out": "",
"submit": "",
"unknown": "",
"update": "",
"updating": "",
"value": "",
"version": "",
"welcome": ""
},
"home": {
"labels": "",
"quick_statistics": "",
"recently_added": "",
"storage_locations": "",
"total_items": "",
"total_labels": "",
"total_locations": "",
"total_value": ""
},
"index": {
"disabled_registration": "",
"dont_join_group": "",
"joining_group": "",
"login": "",
"register": "",
"remember_me": "",
"set_email": "",
"set_name": "",
"set_password": "",
"tagline": "",
"title": "",
"toast": {
"invalid_email": "",
"invalid_email_password": "",
"login_success": "",
"problem_registering": "",
"user_registered": ""
}
},
"items": {
"add": "",
"advanced": "",
"archived": "",
"asset_id": "",
"associated_with_multiple": "",
"attachment": "",
"attachments": "",
"changes_persisted_immediately": "",
"created_at": "",
"custom_fields": "",
"delete_attachment_confirm": "",
"delete_item_confirm": "",
"description": "",
"details": "",
"drag_and_drop": "",
"edit": {
"edit_attachment_dialog": {
"attachment_title": "",
"attachment_type": "",
"primary_photo": "",
"primary_photo_sub": "",
"select_type": "",
"title": ""
}
},
"edit_details": "",
"field_selector": "",
"field_value": "",
"first": "",
"include_archive": "",
"insured": "",
"invalid_asset_id": "",
"last": "",
"lifetime_warranty": "",
"location": "",
"manual": "",
"manuals": "",
"manufacturer": "",
"model_number": "",
"name": "",
"negate_labels": "",
"next_page": "",
"no_attachments": "",
"no_results": "",
"notes": "",
"only_with_photo": "",
"only_without_photo": "",
"options": "",
"order_by": "",
"pages": "",
"parent_item": "",
"photo": "",
"photos": "",
"prev_page": "",
"purchase_date": "",
"purchase_details": "",
"purchase_price": "",
"purchased_from": "",
"quantity": "",
"query_id": "",
"receipt": "",
"receipts": "",
"reset_search": "",
"results": "",
"select_field": "",
"serial_number": "",
"show_advanced_view_options": "",
"sold_at": "",
"sold_details": "",
"sold_price": "",
"sold_to": "",
"sync_child_locations": "",
"tip_1": "",
"tip_2": "",
"tip_3": "",
"tips": "",
"tips_sub": "",
"toast": {
"asset_not_found": "",
"attachment_deleted": "",
"attachment_updated": "",
"attachment_uploaded": "",
"child_items_location_no_longer_synced": "",
"child_items_location_synced": "",
"child_location_desync": "",
"error_loading_parent_data": "",
"failed_adjust_quantity": "",
"failed_delete_attachment": "",
"failed_delete_item": "",
"failed_duplicate_item": "",
"failed_load_asset": "",
"failed_load_item": "",
"failed_load_items": "",
"failed_save": "",
"failed_save_no_location": "",
"failed_search_items": "",
"failed_update_attachment": "",
"failed_upload_attachment": "",
"item_deleted": "",
"item_saved": "",
"quantity_cannot_negative": "",
"sync_child_location": ""
},
"updated_at": "",
"warranty": "",
"warranty_details": "",
"warranty_expires": ""
},
"labels": {
"label_delete_confirm": "",
"no_results": "",
"toast": {
"failed_delete_label": "",
"failed_load_label": "",
"failed_update_label": "",
"label_deleted": "",
"label_updated": ""
},
"update_label": ""
},
"languages": {
"ca": "",
"cs-CZ": "",
"da-DK": "",
"de": "",
"en": "",
"es": "",
"fi-FI": "",
"fr": "",
"hu": "",
"id-ID": "",
"it": "",
"ja-JP": "",
"ko-KR": "",
"lb-LU": "",
"lt-LT": "",
"nb-NO": "",
"nl": "",
"pl": "",
"pt-BR": "",
"pt-PT": "",
"ro-RO": "",
"ru": "",
"sk-SK": "",
"sl": "",
"sq-AL": "",
"sv": "",
"ta-IN": "",
"th-TH": "",
"tr": "",
"uk-UA": "",
"zh-CN": "",
"zh-HK": "",
"zh-MO": "",
"zh-TW": ""
},
"locations": {
"child_locations": "",
"collapse_tree": "",
"expand_tree": "",
"location_items_delete_confirm": "",
"no_results": "",
"toast": {
"failed_delete_location": "",
"failed_load_location": "",
"failed_update_location": "",
"location_deleted": "",
"location_updated": ""
},
"update_location": ""
"search": "Pretraži"
},
"maintenance": {
"filter": {
"both": "",
"completed": "",
"scheduled": ""
},
"list": {
"complete": "",
"create_first": "",
"delete": "Obriši",
"duplicate": "Dupliciraj",
"edit": "Izmjeni",
"new": ""
"edit": "Izmjeni"
},
"modal": {
"completed_date": "",
"cost": "",
"delete_confirmation": "",
"edit_action": "",
"edit_title": "",
"entry_name": "",
"new_action": "",
"new_title": "",
"notes": "Bilješke",
"scheduled_date": ""
},
"monthly_average": "",
"toast": {
"failed_to_create": "",
"failed_to_delete": "",
"failed_to_update": ""
},
"total_cost": "",
"total_entries": ""
"notes": "Bilješke"
}
},
"menu": {
"create_item": "",
"create_label": "",
"create_location": "",
"home": "",
"locations": "",
"maintenance": "",
"profile": "Profil",
"scanner": "",
"search": "",
"tools": ""
"profile": "Profil"
},
"profile": {
"active": "",
"change_password": "",
"currency_format": "",
"current_password": "",
"delete_account": "",
"delete_account_confirm": "",
"delete_account_sub": "",
"delete_notifier_confirm": "",
"display_legacy_header": "",
"enabled": "Uključeno",
"example": "Primjer",
"gen_invite": "Generiši link pozivnicu",
"group_settings": "Postavke grupe",
"group_settings_sub": "",
"inactive": "",
"language": "",
"new_password": "",
"no_notifiers": "",
"no_override": "",
"notifier_modal": "",
"notifiers": "",
"notifiers_sub": "",
"override_locale": "",
"test": "Test",
"theme_settings": "",
"theme_settings_sub": "",
"toast": {
"account_deleted": "",
"failed_change_password": "",
"failed_create_notifier": "",
"failed_delete_account": "",
"failed_delete_notifier": "",
"failed_get_currencies": "",
"failed_test_notifier": "",
"failed_update_group": "",
"failed_update_notifier": "",
"group_updated": "",
"notifier_test_success": "",
"password_changed": ""
},
"update_group": "",
"update_language": "",
"url": "",
"user_profile": "",
"user_profile_sub": ""
},
"reports": {
"label_generator": {
"asset_end": "",
"asset_start": "",
"base_url": "",
"bordered_labels": "",
"generate_page": "",
"input_placeholder": "",
"instruction_1": "",
"instruction_2": "",
"instruction_3": "",
"label_height": "",
"label_width": "",
"measure_type": "",
"page_bottom_padding": "",
"page_height": "",
"page_left_padding": "",
"page_right_padding": "",
"page_top_padding": "",
"page_width": "",
"qr_code_example": "",
"tip_1": "",
"tip_2": "",
"tip_3": "",
"tips": "",
"title": "",
"toast": {
"page_too_small_card": ""
}
}
"test": "Test"
},
"scanner": {
"error": "",
"invalid_url": "",
"no_sources": "",
"permission_denied": "",
"select_video_source": "",
"title": "Skener",
"unsupported": ""
"title": "Skener"
},
"tools": {
"actions": "",
"actions_set": {
"ensure_ids": "",
"ensure_ids_button": "",
"ensure_ids_confirm": "",
"ensure_ids_sub": "",
"ensure_import_refs": "",
"ensure_import_refs_button": "",
"ensure_import_refs_sub": "",
"set_primary_photo": "",
"set_primary_photo_button": "",
"set_primary_photo_confirm": "",
"set_primary_photo_sub": "",
"zero_datetimes": "",
"zero_datetimes_button": "",
"zero_datetimes_confirm": "",
"zero_datetimes_sub": ""
},
"actions_sub": "",
"import_export": "Imortovanje/Eksportovanje",
"import_export_set": {
"export": "Izvoz inventara",
"export_button": "Izvoz inventara",
"export_sub": "",
"import": "",
"import_button": "",
"import_ref_confirm": "",
"import_sub": ""
"export_button": "Izvoz inventara"
},
"import_export_sub": "",
"reports": "Izvještaji",
"reports_set": {
"asset_labels": "",
"asset_labels_button": "",
"asset_labels_sub": "",
"bill_of_materials": "Popis materijala",
"bill_of_materials_button": "Generiši popis materijala",
"bill_of_materials_sub": ""
},
"reports_sub": "",
"toast": {
"asset_success": "",
"failed_ensure_ids": "",
"failed_ensure_import_refs": "",
"failed_set_primary_photos": "",
"failed_zero_datetimes": ""
"bill_of_materials_button": "Generiši popis materijala"
}
}
}

View File

@@ -3,8 +3,7 @@
"app": {
"create_modal": {
"createAndAddAnother": "Utilitza {shiftKey} + {enterKey} per crear i afegir-ne un altre.",
"enter": "Entrar",
"shift": ""
"enter": "Entrar"
},
"import_dialog": {
"change_warning": "El comportament de les importacions amb import_refs existents ha canviat. Si hi ha un import_refs al fitxer CSV, \nl'article s'actualitzarà amb els valors del fitxer CSV.",
@@ -24,18 +23,7 @@
"new_version_available_link": "Fes clic aquí per veure les notes de la versió"
}
},
"form": {
"password": {
"toggle_show": ""
}
},
"global": {
"copy_text": {
"documentation": "",
"failed_to_copy": "",
"https_required": "",
"learn_more": ""
},
"date_time": {
"ago": "fa {0}",
"days": "dies",
@@ -61,62 +49,24 @@
"yesterday": "ahir"
},
"label_maker": {
"browser_print": "",
"confirm_description": "",
"download": "",
"print": "",
"server_print": "Imprimeix al servidor",
"titles": "Etiquetes",
"toast": {
"load_status_failed": "Fallat en carregar l'estat",
"print_failed": "",
"print_success": ""
"load_status_failed": "Fallat en carregar l'estat"
}
},
"page_qr_code": {
"page_url": "URL de la pàgina",
"qr_tooltip": ""
"page_url": "URL de la pàgina"
},
"password_score": {
"password_strength": "Força de la contrasenya"
}
},
"item": {
"attachments_list": {
"download": "",
"open_new_tab": ""
},
"create_modal": {
"delete_photo": "",
"item_description": "Descripció de l'article",
"item_name": "Nom de l'article",
"item_photo": "",
"item_quantity": "",
"parent_item": "",
"rotate_photo": "",
"set_as_primary_photo": "",
"title": "Crea un article",
"toast": {
"already_creating": "",
"create_failed": "",
"create_success": "",
"failed_load_parent": "",
"no_canvas_support": "",
"please_select_location": "",
"rotate_failed": "",
"rotate_process_failed": "",
"some_photos_failed": "",
"upload_failed": "",
"upload_success": "",
"uploading_photos": ""
},
"upload_photos": "",
"uploaded": ""
},
"selector": {
"no_results": "",
"placeholder": "",
"search_placeholder": ""
"title": "Crea un article"
},
"view": {
"selectable": {
@@ -126,11 +76,8 @@
"table": "Taula"
},
"table": {
"headers": "",
"page": "Pàgina",
"rows_per_page": "Files per pàgina",
"table_settings": "",
"view_item": ""
"rows_per_page": "Files per pàgina"
}
}
},
@@ -138,87 +85,50 @@
"create_modal": {
"label_description": "Descripció de l'etiqueta",
"label_name": "Nom de l'etiqueta",
"title": "Crea una etiqueta",
"toast": {
"already_creating": "",
"create_failed": "",
"create_success": "",
"label_name_too_long": ""
}
},
"selector": {
"select_labels": ""
"title": "Crea una etiqueta"
}
},
"location": {
"create_modal": {
"location_description": "Descripció de la ubicació",
"location_name": "Nom de la ubicació",
"title": "Crea una ubicació",
"toast": {
"already_creating": "",
"create_failed": "",
"create_success": ""
}
"title": "Crea una ubicació"
},
"selector": {
"no_location_found": "",
"parent_location": "Ubicació pare",
"search_location": "",
"select_location": ""
"parent_location": "Ubicació pare"
},
"tree": {
"no_locations": "No hi ha ubicacions disponibles. Afegiu ubicacions amb el botó\n `<`span class=\"link-primary\"`>`Crea`<`/span`>` a la barra de navegació."
"no_locations": "No hi ha ubicacions disponibles. Afegiu ubicacions amb el botó\n '<span class=\"link-primary\">'Crea'</span>' a la barra de navegació."
}
},
"quick_menu": {
"no_results": "",
"shortcut_hint": ""
}
},
"global": {
"add": "Afegeix",
"archived": "",
"build": "Construcció: { build }",
"cancel": "",
"confirm": "Confirma",
"create": "Crea",
"create_and_add": "Crea i afegeix-ne un altre",
"create_subitem": "",
"created": "Creat",
"delete": "Esborra",
"delete_confirm": "",
"demo_instance": "",
"details": "Detalls",
"duplicate": "Duplica",
"edit": "Edita",
"email": "Correu electrònic",
"follow_dev": "Segueix al desenvolupador",
"footer": {
"api_link": "",
"version_link": ""
},
"github": "Projecte de GitHub",
"insured": "",
"items": "Articles",
"join_discord": "Uniu-vos a Discord",
"labels": "Etiquetes",
"loading": "",
"locations": "Ubicacions",
"maintenance": "Manteniment",
"name": "Nom",
"navigate": "",
"password": "Contrasenya",
"quantity": "",
"read_docs": "Llegiu la documentació",
"return_home": "",
"save": "Desa",
"search": "Cerca",
"sign_out": "Tanca la sessió",
"submit": "Envia",
"unknown": "",
"update": "Actualitza",
"updating": "",
"value": "Valor",
"version": "Versió {version}",
"welcome": "Us donem la benvinguda, { username }"
@@ -243,49 +153,27 @@
"set_email": "Quin és el seu correu electrònic?",
"set_name": "Com us dieu?",
"set_password": "Definiu la contrasenya",
"tagline": "Feu el seguiment, organitzeu i gestioneu les vostres coses.",
"title": "",
"toast": {
"invalid_email": "",
"invalid_email_password": "",
"login_success": "",
"problem_registering": "",
"user_registered": ""
}
"tagline": "Feu el seguiment, organitzeu i gestioneu les vostres coses."
},
"items": {
"add": "Afegeix",
"advanced": "Mode avançat",
"archived": "Arxivat",
"asset_id": "ID de l'actiu",
"associated_with_multiple": "",
"attachment": "Adjunt",
"attachments": "Documents adjunts",
"changes_persisted_immediately": "Els canvis als fitxers adjunts es desaran immediatament",
"created_at": "Creat a",
"custom_fields": "Camps personalitzats",
"delete_attachment_confirm": "",
"delete_item_confirm": "",
"description": "Descripció",
"details": "Detalls",
"drag_and_drop": "Arrossegueu i deixeu anar fitxers aquí o feu clic per seleccionar fitxers",
"edit": {
"edit_attachment_dialog": {
"attachment_title": "",
"attachment_type": "",
"primary_photo": "",
"primary_photo_sub": "",
"select_type": "",
"title": ""
}
},
"edit_details": "Edita els detalls",
"field_selector": "Selector del camp",
"field_value": "Valor del camp",
"first": "Primer",
"include_archive": "Inclou els articles arxivats",
"insured": "Assegurat",
"invalid_asset_id": "",
"last": "Últim",
"lifetime_warranty": "Garantia de per vida",
"location": "Ubicació",
@@ -296,11 +184,8 @@
"name": "Nom",
"negate_labels": "Nega les etiquetes seleccionades",
"next_page": "Pàgina següent",
"no_attachments": "",
"no_results": "No s'ha trobat cap element",
"notes": "Notes",
"only_with_photo": "",
"only_without_photo": "",
"options": "Opcions",
"order_by": "Ordena per",
"pages": "Pàgina { page } de { totalPages }",
@@ -318,88 +203,42 @@
"receipts": "Factures",
"reset_search": "Reinicia la cerca",
"results": "{ total } resultats",
"select_field": "",
"serial_number": "Número de sèrie",
"show_advanced_view_options": "Mostra les opcions avançades de visualització",
"sold_at": "Venut a (lloc)",
"sold_details": "Detalls de la venda",
"sold_price": "Preu de venda",
"sold_to": "Venut a",
"sync_child_locations": "",
"tip_1": "Els filtres d'ubicació i etiquetes utilitzen l'operació «O». Si se'n selecciona més d'un, \nnomés se'n requerirà un per a coincidència.",
"tip_2": "Les cerques amb el prefix «#» sol·licitaran un ID d'un actiu (per exemple, «#000-001»)",
"tip_3": "Els filtres de camp utilitzen l'operació «O». Si se'n selecciona més d'un, \nnomés se'n requerirà un per a coincidència.",
"tips": "Consells",
"tips_sub": "Consells de cerca",
"toast": {
"asset_not_found": "",
"attachment_deleted": "",
"attachment_updated": "",
"attachment_uploaded": "",
"child_items_location_no_longer_synced": "",
"child_items_location_synced": "",
"child_location_desync": "",
"error_loading_parent_data": "",
"failed_adjust_quantity": "",
"failed_delete_attachment": "",
"failed_delete_item": "",
"failed_duplicate_item": "",
"failed_load_asset": "",
"failed_load_item": "",
"failed_load_items": "",
"failed_save": "",
"failed_save_no_location": "",
"failed_search_items": "",
"failed_update_attachment": "",
"failed_upload_attachment": "",
"item_deleted": "",
"item_saved": "",
"quantity_cannot_negative": "",
"sync_child_location": ""
},
"updated_at": "Actualitzat a",
"warranty": "Garantia",
"warranty_details": "Detalls de la garantia",
"warranty_expires": "La garantia caduca"
},
"labels": {
"label_delete_confirm": "",
"no_results": "No s'han trobat etiquetes",
"toast": {
"failed_delete_label": "",
"failed_load_label": "",
"failed_update_label": "",
"label_deleted": "",
"label_updated": ""
},
"update_label": "Actualitza l'etiqueta"
},
"languages": {
"ca": "Català",
"cs-CZ": "",
"de": "Alemany",
"en": "Anglès",
"es": "Castellà",
"fi-FI": "",
"fr": "Francès",
"hu": "Hongarès",
"id-ID": "",
"it": "Italià",
"ja-JP": "Japonès",
"ko-KR": "",
"lb-LU": "",
"lt-LT": "",
"nb-NO": "",
"nl": "Neerlandès",
"pl": "Polonès",
"pt-BR": "Portuguès (Brasil)",
"pt-PT": "Portuguès (Portugal)",
"ru": "Rus",
"sl": "Eslovè",
"sq-AL": "",
"sv": "Suec",
"ta-IN": "",
"th-TH": "",
"tr": "Turc",
"uk-UA": "Ucraïnès",
"zh-CN": "Xinès (simplificat)",
@@ -413,16 +252,7 @@
"locations": {
"child_locations": "Ubicacions filles",
"collapse_tree": "Col·lapsa l'arbre",
"expand_tree": "",
"location_items_delete_confirm": "",
"no_results": "No s'han trobat ubicacions",
"toast": {
"failed_delete_location": "",
"failed_load_location": "",
"failed_update_location": "",
"location_deleted": "",
"location_updated": ""
},
"update_location": "Actualitza ubicació"
},
"maintenance": {
@@ -468,7 +298,6 @@
"locations": "Ubicacions",
"maintenance": "Manteniment",
"profile": "Perfil",
"scanner": "",
"search": "Cerca",
"tools": "Eines"
},
@@ -478,10 +307,7 @@
"currency_format": "Format de moneda",
"current_password": "Contrasenya actual",
"delete_account": "Suprimeix el compte",
"delete_account_confirm": "",
"delete_account_sub": "Elimina el compte i totes les dades associades. Aquesta acció no es pot desfer.",
"delete_notifier_confirm": "",
"display_legacy_header": "",
"enabled": "Habilitat",
"example": "Exemple",
"gen_invite": "Genera un enllaç d'invitació",
@@ -491,121 +317,44 @@
"language": "Idioma",
"new_password": "Contrasenya nova",
"no_notifiers": "No hi ha notificadors configurats",
"no_override": "",
"notifier_modal": "{ type, select, true {Edita} false {Crea} other {Altres}} Notificació",
"notifiers": "Notificadors",
"notifiers_sub": "Rebeu notificacions per als pròxims recordatoris de manteniment",
"override_locale": "",
"test": "Prova",
"theme_settings": "Configuracions del tema",
"theme_settings_sub": "La configuració del tema s'emmagatzema a l'emmagatzematge local del navegador. Podeu canviar el tema en qualsevol moment. \nSi teniu problemes per definir el tema, proveu d'actualitzar el navegador.",
"toast": {
"account_deleted": "",
"failed_change_password": "",
"failed_create_notifier": "",
"failed_delete_account": "",
"failed_delete_notifier": "",
"failed_get_currencies": "",
"failed_test_notifier": "",
"failed_update_group": "",
"failed_update_notifier": "",
"group_updated": "",
"notifier_test_success": "",
"password_changed": ""
},
"update_group": "Actualitza el grup",
"update_language": "Actualitza l'idioma",
"url": "URL",
"user_profile": "Perfil d'usuari",
"user_profile_sub": "Convida usuaris i gestiona el compte."
},
"reports": {
"label_generator": {
"asset_end": "",
"asset_start": "",
"base_url": "",
"bordered_labels": "",
"generate_page": "",
"input_placeholder": "",
"instruction_1": "",
"instruction_2": "",
"instruction_3": "",
"label_height": "",
"label_width": "",
"measure_type": "",
"page_bottom_padding": "",
"page_height": "",
"page_left_padding": "",
"page_right_padding": "",
"page_top_padding": "",
"page_width": "",
"qr_code_example": "",
"tip_1": "",
"tip_2": "",
"tip_3": "",
"tips": "",
"title": "",
"toast": {
"page_too_small_card": ""
}
}
},
"scanner": {
"error": "",
"invalid_url": "",
"no_sources": "",
"permission_denied": "",
"select_video_source": "",
"title": "",
"unsupported": ""
},
"tools": {
"actions": "Accions d'inventari",
"actions_set": {
"ensure_ids": "Assegura els identificadors de recursos",
"ensure_ids_button": "Assegura els identificadors de recursos",
"ensure_ids_confirm": "",
"ensure_ids_sub": "",
"ensure_import_refs": "Assegureu-vos d'importar les referències",
"ensure_import_refs_button": "Assegureu-vos d'importar les referències",
"ensure_import_refs_sub": "",
"set_primary_photo": "Defineix la foto principal",
"set_primary_photo_button": "Defineix la foto principal",
"set_primary_photo_confirm": "",
"set_primary_photo_sub": "",
"zero_datetimes": "",
"zero_datetimes_button": "",
"zero_datetimes_confirm": "",
"zero_datetimes_sub": ""
"set_primary_photo_button": "Defineix la foto principal"
},
"actions_sub": "",
"import_export": "Importa / Exporta",
"import_export_set": {
"export": "Exporta inventari",
"export_button": "Exporta inventari",
"export_sub": "Exporta el format CSV estàndard per a Homebox. S'exportaran tots els articles de l'inventari.",
"import": "Importa inventari",
"import_button": "Importa inventari",
"import_ref_confirm": "",
"import_sub": ""
"import_button": "Importa inventari"
},
"import_export_sub": "Importa i exporta l'inventari amb un fitxer CSV. És útil per a migracions d'inventari a una nova instància de Homebox.",
"reports": "Informes",
"reports_set": {
"asset_labels": "Etiquetes d'identificador de recurs",
"asset_labels_button": "Generador d'etiquetes",
"asset_labels_sub": "",
"bill_of_materials": "Llista de materials",
"bill_of_materials_button": "Genera llista de materials",
"bill_of_materials_sub": ""
"bill_of_materials_button": "Genera llista de materials"
},
"reports_sub": "Genera informes per a l'inventari",
"toast": {
"asset_success": "",
"failed_ensure_ids": "",
"failed_ensure_import_refs": "",
"failed_set_primary_photos": "",
"failed_zero_datetimes": ""
}
"reports_sub": "Genera informes per a l'inventari."
}
}

View File

@@ -100,6 +100,8 @@
"item_photo": "Fotografie položky 📷",
"item_quantity": "Množství položek",
"parent_item": "Nadřazená položka",
"product_tooltip_input_barcode": "Automatické vyplnění ručně zadaným čárovým kódem",
"product_tooltip_scan_barcode": "Automatické vyplnění čárovým kódem z 📷",
"rotate_photo": "Otočit fotku",
"set_as_primary_photo": "Nastavit jako { isPrimary, select, true {non-} false {} other {}}primární fotku",
"title": "Vytvořit položku",
@@ -115,15 +117,24 @@
"some_photos_failed": "{count, plural, =0 {Žádná fotka k nahrání.} =1 {Nepodařilo se nahrát 1 fotografii.} other {Některé fotografie se nepodařilo nahrát.}}",
"upload_failed": "Nepodařilo se nahrát fotku: { photoName }",
"upload_success": "{count, plural, =0 {Nejsou nahrané žádné fotky.} =1 {Fotka byla úspěšně nahrána.} other {Všechny fotky byly úspěšně nahrány.}}",
"uploading_photos": "{count, plural, =0 {Žádné fotky k nahrání} =1 {Nahrávání 1 fotky...} other {Nahrávání {count} fotek...}}"
"uploading_photos": "{count, plural, =0 {Žádné fotky k nahrání} =1 {Nahrávání 1 fotky} other {Nahrávání {count} fotek}}"
},
"upload_photos": "Nahrát fotografie",
"uploaded": "Fotka byla nahrána"
},
"product_import": {
"barcode": "Čárový kód produktu",
"db_source": "Zdroj DB",
"error_exception": "Při načítání čárového kódu položky došlo k výjimce: ",
"error_invalid_barcode": "Byl zadán neplatný čárový kód",
"error_not_found": "Žádný produkt s daným čárovým kódem nebyl nalezen.",
"search_item": "Vyhledat produkt",
"title": "Importovat produkt"
},
"selector": {
"no_results": "Nebyly nalezeny žádné výsledky",
"placeholder": "Vyberte...",
"search_placeholder": "Pište pro vyhledávání..."
"placeholder": "Vyberte",
"search_placeholder": "Pište pro vyhledávání"
},
"view": {
"selectable": {
@@ -176,7 +187,7 @@
"select_location": "Vybrat lokaci"
},
"tree": {
"no_locations": "Nejsou dostupné žádné lokace. Přidejte nové lokace\npomocí tlačítka `<`span class=\"link-primary\"`>`Vytvořit`<`/span`>` na navigační liště."
"no_locations": "Nejsou dostupné žádné lokace. Přidejte nové lokace\npomocí tlačítka <span class=\"link-primary\">Vytvořit</span> na navigační liště."
}
},
"quick_menu": {
@@ -184,6 +195,9 @@
"shortcut_hint": "Pomocí číselných tlačítek rychle vyberte akci."
}
},
"errors": {
"api_failure": "Volání backendového API selhalo: "
},
"global": {
"add": "Přidat",
"archived": "Archivované",
@@ -204,14 +218,14 @@
"follow_dev": "Sledovat vývojáře",
"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/v'{ version }\" target=\"_blank\"> Verze: { version } Sestavení: { build } '</a>'"
"version_link": "'<'a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/{ version }\" target=\"_blank\"'>' Verze: { version } Sestavení: { build } '</a>'"
},
"github": "GitHub projekt",
"insured": "Pojištěné",
"items": "Položky",
"join_discord": "Připojte se na Discord",
"labels": "Štítky",
"loading": "Načítání...",
"loading": "Načítání",
"locations": "Lokality",
"maintenance": "Údržba",
"name": "Jméno",
@@ -559,6 +573,8 @@
}
},
"scanner": {
"barcode_detected_message": "byl zjištěn čárový kód produktu",
"barcode_fetch_data": "Načíst produktová data",
"error": "Při skenování došlo k chybě",
"invalid_url": "Neplatná adresa URL čárového kódu",
"no_sources": "Nejsou k dispozici žádné zdroje videa",

View File

@@ -2,18 +2,18 @@
"components": {
"app": {
"create_modal": {
"createAndAddAnother": "",
"enter": "",
"shift": ""
"createAndAddAnother": "Brug {shiftKey} + {enterKey} til at oprette og tilføje en ny.",
"enter": "Indtast",
"shift": "Flytte"
},
"import_dialog": {
"change_warning": "Adfærd for imports med eksisterende import_refs har ændret sig. Hvis en import_ref er tilstede i CSV filen,\nvil genstanden blive opdateret med værdierne fra CSV filen.",
"description": "Importer en CSV fil som indeholder dine genstande, etiketter, og lokationer. Se dokumentation for mere information vedrørende\nden korrekte format.",
"title": "Importer CSV Fil",
"toast": {
"import_failed": "",
"import_success": "",
"please_select_file": ""
"import_failed": "Importen mislykkedes. Prøv igen senere.",
"import_success": "Importen er gennemført!",
"please_select_file": "Vælg venligst en fil, der skal importeres."
}
},
"outdated": {
@@ -24,9 +24,16 @@
"new_version_available_link": "Klik her for at læse udgivelsesnoterne"
}
},
"color_selector": {
"clear": "Reset farve",
"color": "Farve",
"no_color": "Ingen farve",
"no_color_selected": "Ingen farve valgt",
"randomize": "Tilfældig farve"
},
"form": {
"password": {
"toggle_show": ""
"toggle_show": "Slå adgangskode til/fra Vis"
}
},
"global": {
@@ -68,9 +75,9 @@
"server_print": "Print på Server",
"titles": "Labels",
"toast": {
"load_status_failed": "",
"print_failed": "",
"print_success": ""
"load_status_failed": "Status kunne ikke indlæses",
"print_failed": "Kunne ikke udskrive etiketten",
"print_success": "Etiket udskrevet"
}
},
"page_qr_code": {
@@ -83,40 +90,49 @@
},
"item": {
"attachments_list": {
"download": "",
"open_new_tab": ""
"download": "Download",
"open_new_tab": "Åbn i ny fane"
},
"create_modal": {
"delete_photo": "",
"delete_photo": "Slet billede",
"item_description": "Genstandsbeskrivelse",
"item_name": "Genstandsnavn",
"item_photo": "Vare Foto 📷",
"item_quantity": "",
"parent_item": "",
"rotate_photo": "",
"set_as_primary_photo": "",
"item_quantity": "Vare Antal",
"parent_item": "Overordnet element",
"product_tooltip_scan_barcode": "Fyld automatisk med stregkode fra 📷",
"rotate_photo": "Roter foto",
"set_as_primary_photo": "Sæt som { isPrimary, select, true {non-} false {} other {}}primært foto",
"title": "Opret genstand",
"toast": {
"already_creating": "",
"create_failed": "",
"create_success": "",
"failed_load_parent": "",
"no_canvas_support": "",
"please_select_location": "",
"rotate_failed": "",
"rotate_process_failed": "",
"some_photos_failed": "",
"upload_failed": "",
"upload_success": "",
"uploading_photos": ""
"already_creating": "Opretter allerede et element",
"create_failed": "Kunne ikke oprette elementet",
"create_success": "Element oprettet",
"failed_load_parent": "Kunne ikke indlæse overordnet element - vælg venligst manuelt",
"no_canvas_support": "Din browser understøtter ikke canvas-handlinger",
"please_select_location": "Vælg venligst en placering.",
"rotate_failed": "Kunne ikke rotere billedet: { error }",
"rotate_process_failed": "Kunne ikke behandle roteret billede",
"some_photos_failed": "{count, plural, =0 {Ingen billeder at uploade.} =1 {1 billede kunne ikke uploades.} other {Nogle billeder kunne ikke uploades.}}",
"upload_failed": "Kunne ikke uploade billede: { photoName }",
"upload_success": "{count, plural, =0 {Ingen billeder uploadet.} =1 {Foto uploadet.} other {Alle billeder uploadet.}}",
"uploading_photos": "{count, plural, =0 {Ingen billeder at uploade} =1 {Uploader 1 billede…} other {Uploader {count} billeder…}}"
},
"upload_photos": "Upload Billeder",
"uploaded": ""
"uploaded": "Uploadet billede"
},
"product_import": {
"barcode": "Produkts stregkode",
"db_source": "DB kilde",
"error_invalid_barcode": "Ugyldig stregkode angivet",
"error_not_found": "Intet produkt fundet med angivet stregkode.",
"search_item": "Søg produkt",
"title": "Importér produkt"
},
"selector": {
"no_results": "",
"placeholder": "",
"search_placeholder": ""
"no_results": "Ingen resultater fundet",
"placeholder": "Vælg…",
"search_placeholder": "Skriv for at søge…"
},
"view": {
"selectable": {
@@ -126,24 +142,25 @@
"table": "Tabel"
},
"table": {
"headers": "",
"headers": "Overskrifter",
"page": "Side",
"rows_per_page": "Rækker per side",
"table_settings": "Tabel Indstillinger",
"view_item": ""
"view_item": "Se vare"
}
}
},
"label": {
"create_modal": {
"label_color": "Etiketfarve",
"label_description": "Etiketbeskrivelse",
"label_name": "Etiketnavn",
"title": "Opret label",
"toast": {
"already_creating": "",
"create_failed": "",
"create_success": "",
"label_name_too_long": ""
"already_creating": "Allerede oprettet en etiket",
"create_failed": "Kunne ikke oprette etiket",
"create_success": "Etiket oprettet",
"label_name_too_long": "Etiketnavnet må ikke være længere end 50 tegn"
}
},
"selector": {
@@ -156,69 +173,72 @@
"location_name": "Lokationsnavn",
"title": "Opret lokation",
"toast": {
"already_creating": "",
"create_failed": "",
"create_success": ""
"already_creating": "Allerede oprettet en lokation",
"create_failed": "Kunne ikke oprette placering",
"create_success": "Placering oprettet"
}
},
"selector": {
"no_location_found": "",
"no_location_found": "Ingen placering fundet",
"parent_location": "Forældrelokation",
"search_location": "",
"select_location": ""
"search_location": "Søg efter placeringer",
"select_location": "Vælg en placering"
},
"tree": {
"no_locations": "Ingen tilgængelige lokationer. Opret nye lokationer gennem\n`<`span class=\"link-primary\">`Opret`<`/span`>` knappen i navigationslinjen."
"no_locations": "Ingen tilgængelige placeringer. Tilføj nye placeringer via knappen \n'<span class=\"link-primary\">'Opret'</span>' på navigationslinjen."
}
},
"quick_menu": {
"no_results": "",
"no_results": "Ingen resultater fundet.",
"shortcut_hint": "Brug de numeriske taster til hurtigt at vælge en handling."
}
},
"errors": {
"api_failure": "Backend API kald fejlede: "
},
"global": {
"add": "Tilføj",
"archived": "",
"archived": "Arkiveret",
"build": "Build: { build }",
"cancel": "",
"cancel": "Ophæv",
"confirm": "Bekræft",
"create": "Opret",
"create_and_add": "Opret og tilføj ny",
"create_subitem": "",
"create_subitem": "Opret underelement",
"created": "Oprettet",
"delete": "Slet",
"delete_confirm": "",
"demo_instance": "",
"delete_confirm": "Er du sikker på, at du vil slette dette element? ",
"demo_instance": "Dette er en demo-instans",
"details": "Detaljer",
"duplicate": "Dupliker",
"edit": "Rediger",
"email": "Email",
"follow_dev": "Følg udvikleren",
"footer": {
"api_link": "",
"version_link": ""
"api_link": "'<a href=\"https://homebox.software/en/api/\" target=\"_blank\">'API'</a>'",
"version_link": "'<'a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/{ version }\" target=\"_blank\"'>' Version: { version } Build: { build } '</a>'"
},
"github": "GitHub projekt",
"insured": "",
"insured": "Forsikret",
"items": "Genstande",
"join_discord": "Deltag i vores Discord",
"labels": "Etiketter",
"loading": "",
"loading": "Indlæser…",
"locations": "Lokationer",
"maintenance": "Opretholdelse",
"name": "Navn",
"navigate": "Naviger",
"password": "Adgangskode",
"quantity": "",
"quantity": "Mængde",
"read_docs": "Læs Docs",
"return_home": "",
"return_home": "Vend hjem",
"save": "Gem",
"search": "Søg",
"sign_out": "Log ud",
"submit": "Indsend",
"unknown": "",
"unknown": "Ukendt",
"update": "Opdater",
"updating": "",
"updating": "Opdaterer",
"value": "Værdi",
"version": "Version: { version }",
"welcome": "Velkommen, { username }"
@@ -244,13 +264,13 @@
"set_name": "Hvad hedder du?",
"set_password": "Opret din adgangskode",
"tagline": "Følg, Organiser, og Håndter dine Ting.",
"title": "",
"title": "Organiser og Tag dine ting",
"toast": {
"invalid_email": "",
"invalid_email_password": "",
"login_success": "",
"problem_registering": "",
"user_registered": ""
"invalid_email": "Ugyldig e-mailadresse",
"invalid_email_password": "Ugyldig e-mail eller adgangskode",
"login_success": "Logget ind",
"problem_registering": "Problem med at registrere bruger",
"user_registered": "Bruger registreret"
}
},
"items": {
@@ -258,25 +278,25 @@
"advanced": "Avanceret",
"archived": "Arkiveret",
"asset_id": "Aktiv-id",
"associated_with_multiple": "",
"associated_with_multiple": "Dette aktiv-id er knyttet til flere varer",
"attachment": "Vedhæftning",
"attachments": "Vedhæftninger",
"changes_persisted_immediately": "Ændringer af vedhæftede filer gemmes med det samme",
"created_at": "Oprettet den",
"custom_fields": "Brugerdefinerede felter",
"delete_attachment_confirm": "",
"delete_item_confirm": "",
"delete_attachment_confirm": "Er du sikker på, at du vil slette denne vedhæftede fil?",
"delete_item_confirm": "Er du sikker på, at du vil slette dette element?",
"description": "Beskrivelse",
"details": "Detaljer",
"drag_and_drop": "Træk og slip filer her, eller klik for at vælge filer",
"edit": {
"edit_attachment_dialog": {
"attachment_title": "",
"attachment_type": "",
"primary_photo": "",
"primary_photo_sub": "",
"select_type": "",
"title": ""
"attachment_title": "Titel på vedhæftet fil",
"attachment_type": "Vedhæftningstype",
"primary_photo": "Primært foto",
"primary_photo_sub": "Denne mulighed er kun tilgængelig for fotos. Kun ét foto kan være primært. Hvis du vælger denne mulighed, vil det aktuelle primære foto, hvis der er et, blive fravalgt.",
"select_type": "Vælg en type",
"title": "Rediger vedhæftet fil"
}
},
"edit_details": "Rediger detaljer",
@@ -285,7 +305,7 @@
"first": "Første",
"include_archive": "Medtag arkiverede elementer",
"insured": "Forsikret",
"invalid_asset_id": "",
"invalid_asset_id": "Ugyldigt aktiv-ID",
"last": "Sidst",
"lifetime_warranty": "livstidsgaranti",
"location": "Lokalitet",
@@ -296,7 +316,7 @@
"name": "Navn",
"negate_labels": "Ophæv valgte etiketter",
"next_page": "Næste side",
"no_attachments": "",
"no_attachments": "Ingen vedhæftede filer fundet",
"no_results": "Ingen elementer fundet",
"notes": "Noter",
"only_with_photo": "Kun elementer med foto",
@@ -318,44 +338,44 @@
"receipts": "Kvitteringer",
"reset_search": "Nulstil Søgning",
"results": "{ total } Wyniki",
"select_field": "",
"select_field": "Vælg et felt",
"serial_number": "Serienummer",
"show_advanced_view_options": "vis avancerede indstillinger",
"sold_at": "Solgt D.",
"sold_details": "Salgs detaljer",
"sold_price": "Solgt pris",
"sold_to": "Sold til",
"sync_child_locations": "",
"sync_child_locations": "Synkroniser placeringer af underordnede elementer",
"tip_1": "Placerings- og etiketfiltre bruger betjeningen 'ELLER'. Hvis mere end én er valgt, kræves der kun én\n til et match.",
"tip_2": "Søgninger med præfikset '#'' vil forespørge efter et aktiv-id (eksempel '#000-001')",
"tip_3": "Feltfiltre bruger handlingen 'ELLER'. Hvis mere end én er valgt, kræves der kun én til en\n kamp.",
"tips": "Tips",
"tips_sub": "Søgetips",
"toast": {
"asset_not_found": "",
"attachment_deleted": "",
"attachment_updated": "",
"attachment_uploaded": "",
"child_items_location_no_longer_synced": "",
"child_items_location_synced": "",
"child_location_desync": "",
"error_loading_parent_data": "",
"failed_adjust_quantity": "",
"failed_delete_attachment": "",
"failed_delete_item": "",
"failed_duplicate_item": "",
"failed_load_asset": "",
"failed_load_item": "",
"failed_load_items": "",
"failed_save": "",
"failed_save_no_location": "",
"failed_search_items": "",
"failed_update_attachment": "",
"failed_upload_attachment": "",
"item_deleted": "",
"item_saved": "",
"quantity_cannot_negative": "",
"sync_child_location": ""
"asset_not_found": "Aktivet blev ikke fundet",
"attachment_deleted": "Vedhæftet fil slettet",
"attachment_updated": "Vedhæftet fil opdateret",
"attachment_uploaded": "Vedhæftet fil uploadet",
"child_items_location_no_longer_synced": "Placeringen af underelementer vil ikke længere blive synkroniseret med dette element.",
"child_items_location_synced": "Placeringerne af underordnede elementer er blevet synkroniseret med dette element",
"child_location_desync": "Ændring af placering vil afsynkronisere den fra forælderens placering",
"error_loading_parent_data": "Noget gik galt under indlæsning af overordnede data",
"failed_adjust_quantity": "Kunne ikke justere mængden",
"failed_delete_attachment": "Kunne ikke slette vedhæftet fil",
"failed_delete_item": "Kunne ikke slette elementet",
"failed_duplicate_item": "Kunne ikke duplikere elementet",
"failed_load_asset": "Kunne ikke indlæse aktiv",
"failed_load_item": "Kunne ikke indlæse elementet",
"failed_load_items": "Kunne ikke indlæse elementer",
"failed_save": "Kunne ikke gemme elementet",
"failed_save_no_location": "Kunne ikke gemme elementet: ingen placering valgt",
"failed_search_items": "Kunne ikke søge efter elementer",
"failed_update_attachment": "Kunne ikke opdatere vedhæftet fil",
"failed_upload_attachment": "Kunne ikke uploade vedhæftet fil",
"item_deleted": "Element slettet",
"item_saved": "Element gemt",
"quantity_cannot_negative": "Mængden må ikke være negativ",
"sync_child_location": "Den valgte forælder synkroniserer sine børns placeringer med sine egne. Placeringen er blevet opdateret."
},
"updated_at": "Opdateret d.",
"warranty": "Garanti",
@@ -363,32 +383,32 @@
"warranty_expires": "Garantien udløber"
},
"labels": {
"label_delete_confirm": "",
"label_delete_confirm": "Er du sikker på, at du vil slette denne etiket? Denne handling kan ikke fortrydes.",
"no_results": "Ingen etiketter fundet",
"toast": {
"failed_delete_label": "",
"failed_load_label": "",
"failed_update_label": "",
"label_deleted": "",
"label_updated": ""
"failed_delete_label": "Etiketten kunne ikke slettes",
"failed_load_label": "Etiketten kunne ikke indlæses",
"failed_update_label": "Etiketten kunne ikke opdateres",
"label_deleted": "Etiket slettet",
"label_updated": "Etiket opdateret"
},
"update_label": "Opdater etiket"
},
"languages": {
"ca": "Catalansk",
"cs-CZ": "",
"cs-CZ": "Tjekkisk",
"de": "Tysk",
"en": "Engelsk",
"es": "Spansk",
"fi-FI": "",
"fi-FI": "Finsk",
"fr": "Fransk",
"hu": "Ungarsk",
"id-ID": "",
"id-ID": "Indonesisk",
"it": "Italiensk",
"ja-JP": "Japansk",
"ko-KR": "",
"lb-LU": "",
"lt-LT": "",
"ko-KR": "Koreansk",
"lb-LU": "Luxembourgsk (Luxembourg)",
"lt-LT": "Litauisk (Litauen)",
"nb-NO": "Norsk",
"nl": "Hollandsk",
"pl": "Polsk",
@@ -396,7 +416,7 @@
"pt-PT": "Portugisisk (Portugal)",
"ru": "Russisk",
"sl": "Slovensk",
"sq-AL": "",
"sq-AL": "Albansk",
"sv": "Svensk",
"ta-IN": "Tamilsk",
"th-TH": "Thailandsk",
@@ -413,15 +433,15 @@
"locations": {
"child_locations": "Underordnede placeringer",
"collapse_tree": "Kollaps træ",
"expand_tree": "",
"location_items_delete_confirm": "",
"expand_tree": "Udvid træ",
"location_items_delete_confirm": "Er du sikker på, at du vil slette denne placering og alle dens elementer? Denne handling kan ikke fortrydes.",
"no_results": "Ingen placeringer fundet",
"toast": {
"failed_delete_location": "",
"failed_load_location": "",
"failed_update_location": "",
"location_deleted": "",
"location_updated": ""
"failed_delete_location": "Kunne ikke slette placeringen",
"failed_load_location": "Placeringen kunne ikke indlæses",
"failed_update_location": "Kunne ikke opdatere placeringen",
"location_deleted": "Placering slettet",
"location_updated": "Placering opdateret"
},
"update_location": "Opdatér sted"
},
@@ -478,10 +498,10 @@
"currency_format": "Valuta format",
"current_password": "Aktuel adgangskode",
"delete_account": "Slet Konto",
"delete_account_confirm": "",
"delete_account_confirm": "Er du sikker på, at du vil slette din konto? Hvis du er det sidste medlem i din gruppe, vil alle dine data blive slettet. Denne handling kan ikke fortrydes.",
"delete_account_sub": "Slet din konto og alle dens tilknyttede data. Dette kan ikke laves om.",
"delete_notifier_confirm": "",
"display_legacy_header": "",
"delete_notifier_confirm": "Er du sikker på, at du vil slette denne underretter?",
"display_legacy_header": "{ currentValue, select, true {Deaktiver Legacy Header} false {Aktiver Legacy Header} other {Ikke ramt}}",
"enabled": "Aktiveret",
"example": "Eksempel",
"gen_invite": "Generer invitationslink",
@@ -491,27 +511,27 @@
"language": "Sprog",
"new_password": "Ny Adgangskode",
"no_notifiers": "Ingen notifikationer konfiguret",
"no_override": "",
"no_override": "Ingen tilsidesættelse",
"notifier_modal": "{ type, select, true {Rediger} false {Opret} other {Andet}} Meddeler",
"notifiers": "Meddelere",
"notifiers_sub": "Få notifikationer om kommende vedligeholdelsespåmindelser",
"override_locale": "",
"override_locale": "Tilsidesæt dato og valutasprog",
"test": "Test",
"theme_settings": "Temaindstillinger",
"theme_settings_sub": "Temaindstillinger gemmes i din browsers lokale lager. Du kan til enhver tid ændre temaet. Hvis du har\n problemer med at indstille dit tema, kan du prøve at opdatere din browser.",
"toast": {
"account_deleted": "",
"failed_change_password": "",
"failed_create_notifier": "",
"failed_delete_account": "",
"failed_delete_notifier": "",
"failed_get_currencies": "",
"failed_test_notifier": "",
"failed_update_group": "",
"failed_update_notifier": "",
"group_updated": "",
"notifier_test_success": "",
"password_changed": ""
"account_deleted": "Din konto er blevet slettet.",
"failed_change_password": "Kunne ikke ændre adgangskode.",
"failed_create_notifier": "Kunne ikke oprette underretteren.",
"failed_delete_account": "Kunne ikke slette din konto.",
"failed_delete_notifier": "Kunne ikke slette underretteren.",
"failed_get_currencies": "Kunne ikke hente valutaer",
"failed_test_notifier": "Kunne ikke teste underretteren.",
"failed_update_group": "Gruppen kunne ikke opdateres",
"failed_update_notifier": "Kunne ikke opdatere underretteren.",
"group_updated": "Gruppen er opdateret",
"notifier_test_success": "Underretter-testen er gennemført.",
"password_changed": "Adgangskoden er ændret."
},
"update_group": "Opdatér Gruppe",
"update_language": "Opdater sprogfil",
@@ -521,61 +541,67 @@
},
"reports": {
"label_generator": {
"asset_end": "",
"asset_start": "",
"base_url": "",
"bordered_labels": "",
"generate_page": "",
"input_placeholder": "",
"instruction_1": "",
"instruction_2": "",
"instruction_3": "",
"label_height": "",
"label_width": "",
"measure_type": "",
"page_bottom_padding": "",
"page_height": "",
"page_left_padding": "",
"page_right_padding": "",
"page_top_padding": "",
"page_width": "",
"qr_code_example": "",
"tip_1": "",
"tip_2": "",
"tip_3": "",
"tips": "",
"title": "",
"asset_end": "Aktivets slut",
"asset_start": "Aktivets start",
"base_url": "Base URL",
"bordered_labels": "Etiketter med kant",
"generate_page": "Generer side",
"input_placeholder": "Skriv her",
"instruction_1": "Homebox Label Generator er et værktøj, der hjælper dig med at udskrive etiketter til dit Homebox-lager. Disse er beregnet til\nat være etiketter, der kan udskrives på forhånd, så du kan udskrive mange etiketter og have dem klar til påsætning.",
"instruction_2": "Disse etiketter fungerer derfor ved at udskrive en URL, QR-kode og AssetID-oplysninger på en etiket. Hvis du har deaktiveret\nAssetID'er i dine Homebox-indstillinger, kan du stadig bruge dette værktøj, men AssetID'erne vil ikke referere til nogen varer.",
"instruction_3": "Denne funktion er i de tidlige udviklingsfaser og kan ændres i fremtidige udgivelser. Hvis du har feedback, bedes\ndu give den i '<a href=\"https://github.com/sysadminsmedia/homebox/discussions/53\">'GitHub-diskussionen'</a>'",
"label_height": "Etikethøjde",
"label_width": "Etiketbredde",
"measure_type": "Målingstype",
"page_bottom_padding": "Sidebundspolstring",
"page_height": "Sidehøjde",
"page_left_padding": "Venstre sidepolstring",
"page_right_padding": "Højre sidepolstring",
"page_top_padding": "Sidetoppolstring",
"page_width": "Sidebredde",
"qr_code_example": "Eksempel på QR-kode",
"tip_1": "Standardindstillingerne her er konfigureret for\n'<a href=\"https://www.avery.com/templates/5260\">'Avery 5260 etiketark'</a>'. Hvis du bruger et andet ark,\nskal du justere indstillingerne, så de passer til dit ark.",
"tip_2": "Hvis du tilpasser dit ark, er dimensionerne i tommer. Da jeg byggede 5260-arket, opdagede jeg, at de\ndimensioner, der blev brugt i deres skabelon, ikke matchede det, der var nødvendigt for at udskrive i felterne.\n'<b>'Vær forberedt på nogle forsøg og fejl.'</b>'",
"tip_3": "Ved printning sørg for at:\n'<ol><li>'Indstil margenerne til 0 eller Ingen'</li><li>'Indstil skaleringen til 100%'</li><li>'Deaktiver dobbeltsidet udskrivning'</li><li>'Udskriv en testside, før du udskriver flere sider'</li></ol>'",
"tips": "Tips",
"title": "Etiketgenerator",
"toast": {
"page_too_small_card": ""
"page_too_small_card": "Sidestørrelsen er for lille til kortstørrelsen"
}
}
},
"scanner": {
"barcode_detected_message": "produkt stregkode opdaget",
"barcode_fetch_data": "Hent produktdata",
"error": "Der skete en fejl under skanningen",
"invalid_url": "Ugyldig stregkode-URL",
"no_sources": "Ingen videokilder er tilgængelig",
"permission_denied": "",
"permission_denied": "Kameratilladelse nægtet, Tillad venligst adgang til kameraet i dine browserindstillinger",
"select_video_source": "Vælg en videokilde",
"title": "",
"title": "Skanner",
"unsupported": "Media Stream API understøttes ikke uden HTTPS"
},
"tools": {
"actions": "Handlinger på lagerbeholdning",
"actions_set": {
"create_missing_thumbnails": "Opret manglende miniaturebilleder",
"create_missing_thumbnails_button": "Opret miniaturebilleder",
"create_missing_thumbnails_confirm": "Er du sikker på, at du vil oprette manglende miniaturebilleder? Dette kan tage et stykke tid og kan ikke sættes på pause.",
"create_missing_thumbnails_sub": "Opretter miniaturebilleder for alle vedhæftede filer, der understøttes af den aktuelle konfiguration. Dette er nyttigt for vedhæftede filer, der blev uploadet før v0.20.0-udgivelsen af Homebox. Dette overskriver ikke eksisterende miniaturebilleder, men opretter kun nye for vedhæftede filer, der ikke har et miniaturebillede. Bemærk, at miniaturebillederne oprettes i baggrunden og kan tage et stykke tid at fuldføre.",
"ensure_ids": "Sørg for aktiv-id'er",
"ensure_ids_button": "Sørg for aktiv-id'er",
"ensure_ids_confirm": "",
"ensure_ids_confirm": "Er du sikker på, at du vil sikre dig, at alle aktiver har et ID? Dette kan tage et stykke tid og kan ikke fortrydes.",
"ensure_ids_sub": "Sikrer, at alle varer på lageret har et gyldigt asset_id felt. Dette gøres ved at finde det højeste aktuelle aktiv_id felt i databasen og anvende den næste værdi på hvert element, der har et ikke sat aktiv_id felt. Dette gøres i rækkefølge efter feltet opret_den.",
"ensure_import_refs": "Sørg for importreferencer",
"ensure_import_refs_button": "Sørg for importreferencer",
"ensure_import_refs_sub": "Sikrer, at alle varer på lageret har et gyldigt import_ref felt. Dette gøres ved tilfældigt at generere en streng på 8 tegn for hvert element, der har et uindstillet import_ref felt.",
"set_primary_photo": "Indstil primært foto",
"set_primary_photo_button": "Indstil primært foto",
"set_primary_photo_confirm": "",
"set_primary_photo_confirm": "Er du sikker på, at du vil indstille primære billeder? Dette kan tage et stykke tid og kan ikke fortrydes.",
"set_primary_photo_sub": "I version v0.10.0 af Homebox blev det primære billedfelt tilføjet til vedhæftede filer af typen foto. Denne handling indstiller det primære billedfelt til det første billede i matrixen for vedhæftede filer i databasen, hvis det ikke allerede er angivet. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/pull/576\">'Se GitHub PR #576'</a>'",
"zero_datetimes": "Nul Vare Dato Tider",
"zero_datetimes_button": "Nul Varedato Tider",
"zero_datetimes_confirm": "",
"zero_datetimes_confirm": "Er du sikker på, at du vil nulstille alle dato- og klokkeslætsværdier? Dette kan tage et stykke tid og kan ikke fortrydes.",
"zero_datetimes_sub": "Nulstiller klokkeslætsværdien for alle dato- og klokkeslætsfelter i lageret til begyndelsen af datoen. Dette er for at rette en fejl, der blev introduceret tidligt i udviklingen af webstedet, der forårsagede, at tidsværdien blev gemt med tiden, hvilket forårsagede problemer med datofelter, der viste nøjagtige værdier. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/issues/236\" target=\"_blank\">'Se Github-udgave #236 for flere detaljer.'</a>'"
},
"actions_sub": "Anvend flere handlinger på din beholdning på én gang. Det er uigenkaldelige handlinger. '<b>'Vær forsigtig.'</b>'",
@@ -586,7 +612,7 @@
"export_sub": "Eksporterer standard CSV-formatet til Homebox. Dette vil eksportere alle varer i dit lager.",
"import": "Importeret beholdning",
"import_button": "Importer beholdning",
"import_ref_confirm": "",
"import_ref_confirm": "Er du sikker på, at du vil sikre dig, at alle aktiver har en import_ref? Dette kan tage et stykke tid og kan ikke fortrydes.",
"import_sub": "Importerer standard CSV-formatet til Homebox. Uden en '<code>'HB.import_ref'</code>'-kolonne vil dette '<b>'ikke'</b>' overskrive eksisterende genstande i dit lager, kun tilføje nye genstande. Rækker med kolonnen \"<code>HB.import_ref\"</code> flettes ind i eksisterende elementer med samme import_ref, hvis der findes en."
},
"import_export_sub": "Importér og eksporter din lagerbeholdning til og fra en CSV-fil. Dette er nyttigt til at migrere dit lager til en ny forekomst af Homebox.",
@@ -601,11 +627,12 @@
},
"reports_sub": "Generer forskellige rapporter for dit lager.",
"toast": {
"asset_success": "",
"failed_ensure_ids": "",
"failed_ensure_import_refs": "",
"failed_set_primary_photos": "",
"failed_zero_datetimes": ""
"asset_success": "Aktiverne i { results } er blevet opdateret.",
"failed_create_missing_thumbnails": "Kunne ikke oprette manglende miniaturebilleder.",
"failed_ensure_ids": "Kunne ikke sikre aktiv-ID'er.",
"failed_ensure_import_refs": "Kunne ikke sikre importreferencer.",
"failed_set_primary_photos": "Kunne ikke indstille primære billeder.",
"failed_zero_datetimes": "Dato- og klokkeslætsværdier kunne ikke nulstilles."
}
}
}

View File

@@ -2,7 +2,7 @@
"components": {
"app": {
"create_modal": {
"createAndAddAnother": "Verwenden Sie {Umschalttaste} + {Eingabetaste}, um eine weitere zu erstellen und hinzuzufügen.",
"createAndAddAnother": "Verwenden Sie {shiftKey} + {enterKey}, um eine weitere zu erstellen und hinzuzufügen.",
"enter": "Eingabe",
"shift": "Shift"
},
@@ -24,6 +24,13 @@
"new_version_available_link": "Klicken Sie hier, um die Release Notes anzuzeigen"
}
},
"color_selector": {
"clear": "Farbe löschen",
"color": "Farbe",
"no_color": "Keine Farbe",
"no_color_selected": "Keine Farbe ausgewählt",
"randomize": "Zufällige Farbe"
},
"form": {
"password": {
"toggle_show": "Passwort anzeigen"
@@ -93,6 +100,8 @@
"item_photo": "Artikel Bild",
"item_quantity": "Anzahl der Artikel",
"parent_item": "Übergeordneter Gegenstand",
"product_tooltip_input_barcode": "Automatisch ausfüllen mit einem manuell bereitgestellten Barcode",
"product_tooltip_scan_barcode": "Automatisch ausfüllen mit einem Barcode von 📷",
"rotate_photo": "Photo drehen",
"set_as_primary_photo": "Festlegen als { isPrimary, select, true {non-} false {} other {}}primäres Foto",
"title": "Gegenstand erstellen",
@@ -105,18 +114,27 @@
"please_select_location": "Bitte einen Ort auswählen.",
"rotate_failed": "Drehen des Bildes fehlgeschlagen: {error}",
"rotate_process_failed": "Das gedrehte Bild konnte nicht verarbeitet werden",
"some_photos_failed": "{Anzahl, Plural, =0 {Keine Fotos zum Hochladen.} =1 {1 Foto konnte nicht hochgeladen werden.} andere {Einige Fotos konnten nicht hochgeladen 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_success": "{Anzahl, Plural, =0 {Keine Fotos hochgeladen.} =1 {Foto erfolgreich hochgeladen.} andere {Alle Fotos erfolgreich hochgeladen.}}",
"uploading_photos": "{Anzahl, Plural, =0 {Keine Fotos zum Hochladen} =1 {1 Foto wird hochgeladen...} andere {{Anzahl} Fotos werden hochgeladen...}}"
"upload_success": "{count, plural, =0 {Keine Fotos hochgeladen.} =1 {Foto erfolgreich hochgeladen.} other {Alle Fotos erfolgreich hochgeladen.}}",
"uploading_photos": "{count, plural, =0 {Keine Fotos zum Hochladen} =1 {1 Foto wird hochgeladen...} other {{count} Fotos werden hochgeladen...}}"
},
"upload_photos": "Upload Bilder",
"uploaded": "Bild hochgeladen"
},
"product_import": {
"barcode": "Produkt-Strichcode",
"db_source": "DB-Quelle",
"error_exception": "Beim Abrufen des Artikel-Barcodes ist ein Problem aufgetreten: ",
"error_invalid_barcode": "Ungültiger Barcode angegeben",
"error_not_found": "Kein Produkt mit dem angegebenen Barcode gefunden.",
"search_item": "Produkt suchen",
"title": "Produkt importieren"
},
"selector": {
"no_results": "Keine Ergebnisse gefunden",
"placeholder": "Auswählen...",
"search_placeholder": "Für Suche tippen..."
"placeholder": "Auswählen",
"search_placeholder": "Für Suche tippen"
},
"view": {
"selectable": {
@@ -136,6 +154,7 @@
},
"label": {
"create_modal": {
"label_color": "Label-Farbe",
"label_description": "Label-Beschreibung",
"label_name": "Label-Name",
"title": "Label erstellen",
@@ -168,7 +187,7 @@
"select_location": "Standort wählen"
},
"tree": {
"no_locations": "Keine Standorte verfügbar. Fügen Sie neue Standorte über die Schaltfläche\n `<`span class=\"link-primary\"`>`Erstellen`<`/span`>` in der Navigationsleiste hinzu."
"no_locations": "Keine Standorte verfügbar. Fügen Sie neue Standorte über die Schaltfläche\n `<span class=\"link-primary\">`Erstellen`</span>` in der Navigationsleiste hinzu."
}
},
"quick_menu": {
@@ -176,6 +195,9 @@
"shortcut_hint": "Verwenden Sie die Zifferntasten, um schnell eine Aktion auszuwählen."
}
},
"errors": {
"api_failure": "Backend-API-Aufruf fehlgeschlagen: "
},
"global": {
"add": "Hinzufügen",
"archived": "Archiviert",
@@ -196,14 +218,14 @@
"follow_dev": "Dem Entwickler folgen",
"footer": {
"api_link": "'<a href=\"https://homebox.software/en/api/\" target=\"_blank\">'API'</a>'",
"version_link": "'<a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/'{ version }\" target=\"_blank\"> Version: { version } Erstellt: { build } '</a>'"
"version_link": "'<'a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/{ version }\" target=\"_blank\"'>' Version: { version } Erstellt: { build } '</a>'"
},
"github": "GitHub-Projekt",
"insured": "Versichert",
"items": "Gegenstände",
"join_discord": "Discord beitreten",
"labels": "Labels",
"loading": "Wird geladen …",
"loading": "Wird geladen…",
"locations": "Lagerorte",
"maintenance": "Wartung",
"name": "Name",
@@ -375,6 +397,7 @@
"update_label": "Label aktualisieren"
},
"languages": {
"bs-BA": "Bosnisch (Bosnien und Herzegowina)",
"ca": "Katalanisch",
"cs-CZ": "Tschechisch",
"de": "Deutsch",
@@ -401,9 +424,10 @@
"th-TH": "Thailändisch",
"tr": "Türkisch",
"uk-UA": "Ukrainisch",
"zh-CN": "Chinesisch (einfach)",
"vi-VN": "Vietnamesisch",
"zh-CN": "Chinesisch (vereinfacht)",
"zh-HK": "Chinesisch (Hong Kong)",
"zh-MO": "Chinesisch (Macao)",
"zh-MO": "Chinesisch (Macau)",
"zh-TW": "Chinesisch (traditionell)"
},
"languages.da-DK": "Dänisch",
@@ -415,7 +439,7 @@
"collapse_tree": "Baum einklappen",
"expand_tree": "Baum ausklappen",
"location_items_delete_confirm": "Möchten Sie diesen Standort und alle darin enthaltenen Elemente wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
"no_results": "Keine Orte gefunden",
"no_results": "Keine Standorte gefunden",
"toast": {
"failed_delete_location": "Standort konnte nicht gelöscht werden",
"failed_load_location": "Standort konnte nicht geladen werden",
@@ -541,7 +565,7 @@
"page_width": "Seitenbreite",
"qr_code_example": "QR-Code Beispiel",
"tip_1": "Die Standardeinstellungen hier sind für die\n'<a href=\"https://www.avery.com/templates/5260\">'Avery 5260 Etikettenbögen'</a>'. Wenn Sie einen anderen Bogen verwenden,\nmüssen Sie die Einstellungen an Ihr Blatt anpassen.",
"tip_2": "Wenn Sie Ihr Blatt anpassen, werden die Abmessungen in Zoll angegeben. Beim Erstellen des 5260-Blattes habe ich festgestellt, dass die\nin deren Vorlage verwendeten Abmessungen nicht mit den für den Druck in den Feldern erforderlichen Abmessungen übereinstimmen.\n<b>Seien Sie auf einige Versuche und Irrtümer gefasst.</b>",
"tip_2": "Wenn Sie Ihr Blatt anpassen, werden die Abmessungen in Zoll angegeben. Beim Erstellen des 5260-Blattes habe ich festgestellt, dass die\nin deren Vorlage verwendeten Abmessungen nicht mit den für den Druck in den Feldern erforderlichen Abmessungen übereinstimmen.\n'<b>'Seien Sie auf einige Versuche und Irrtümer gefasst.'</b>'",
"tip_3": "Achten Sie beim Drucken auf Folgendes:\n'<ol><li>'Setzen Sie die Ränder auf 0 oder Keine'</li><li>'Setzen Sie die Skalierung auf 100 %'</li><li>'Deaktivieren Sie den beidseitigen Druck'</li><li>'Drucken Sie eine Testseite, bevor Sie mehrere Seiten drucken'</li></ol>'",
"tips": "Tipps",
"title": "Etikettengenerator",
@@ -551,6 +575,8 @@
}
},
"scanner": {
"barcode_detected_message": "Produkt-Barcode erkannt",
"barcode_fetch_data": "Produktdaten abrufen",
"error": "Beim Scannen ist ein Fehler aufgetreten",
"invalid_url": "Ungültige Barcode-URL",
"no_sources": "Keine Videoquellen verfügbar",

View File

@@ -100,6 +100,8 @@
"item_photo": "Item Photo 📷",
"item_quantity": "Item Quantity",
"parent_item": "Parent Item",
"product_tooltip_input_barcode": "Autofill with a manually provided barcode",
"product_tooltip_scan_barcode": "Autofill with a barcode from 📷",
"rotate_photo": "Rotate photo",
"set_as_primary_photo": "Set as { isPrimary, select, true {non-} false {} other {}}primary photo",
"title": "Create Item",
@@ -115,15 +117,24 @@
"some_photos_failed": "{count, plural, =0 {No photos to upload.} =1 {1 photo failed to upload.} other {Some photos failed to upload.}}",
"upload_failed": "Failed to upload photo: { photoName }",
"upload_success": "{count, plural, =0 {No photos uploaded.} =1 {Photo uploaded successfully.} other {All photos uploaded successfully.}}",
"uploading_photos": "{count, plural, =0 {No photos to upload} =1 {Uploading 1 photo...} other {Uploading {count} photos...}}"
"uploading_photos": "{count, plural, =0 {No photos to upload} =1 {Uploading 1 photo} other {Uploading {count} photos}}"
},
"upload_photos": "Upload Photos",
"uploaded": "Uploaded Photo"
},
"product_import": {
"barcode": "Product's barcode",
"db_source": "DB source",
"error_exception": "Exception occured while retrieving item barcode: ",
"error_invalid_barcode": "Invalid barcode provided",
"error_not_found": "No product found with given barcode.",
"search_item": "Search product",
"title": "Import product"
},
"selector": {
"no_results": "No Results Found",
"placeholder": "Select...",
"search_placeholder": "Type to search..."
"placeholder": "Select",
"search_placeholder": "Type to search"
},
"view": {
"selectable": {
@@ -176,7 +187,7 @@
"select_location": "Select a Location"
},
"tree": {
"no_locations": "No locations available. Add new locations through the\n `<`span class=\"link-primary\"`>`Create`<`/span`>` button on the navigation bar."
"no_locations": "No locations available. Add new locations through the\n '<span class=\"link-primary\">'Create'</span>' button on the navigation bar."
}
},
"quick_menu": {
@@ -184,6 +195,9 @@
"shortcut_hint": "Use the number keys to quickly select an action."
}
},
"errors": {
"api_failure": "Backend API call failed: "
},
"global": {
"add": "Add",
"archived": "Archived",
@@ -204,14 +218,14 @@
"follow_dev": "Follow the Developer",
"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/v'{ version }\" target=\"_blank\"> Version: { version } Build: { build } '</a>'"
"version_link": "'<'a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/{ version }\" target=\"_blank\"'>' Version: { version } Build: { build } '</a>'"
},
"github": "GitHub Project",
"insured": "Insured",
"items": "Items",
"join_discord": "Join the Discord",
"labels": "Labels",
"loading": "Loading...",
"loading": "Loading",
"locations": "Locations",
"maintenance": "Maintenance",
"name": "Name",
@@ -275,6 +289,18 @@
"delete_attachment_confirm": "Are you sure you want to delete this attachment?",
"delete_item_confirm": "Are you sure you want to delete this item?",
"description": "Description",
"duplicate": {
"prefix": "Copy of ",
"copy_maintenance": "Copy Maintenance",
"copy_attachments": "Copy Attachments",
"copy_custom_fields": "Copy Custom Fields",
"custom_prefix": "Copy Prefix",
"enable_custom_prefix": "Enable Custom Prefix",
"prefix_instructions": "This prefix will be added to the beginning of the duplicated item's name. Include a space at the end of the prefix to add a space between the prefix and the item name.",
"temporary_title": "Temporary Settings",
"title": "Duplicate Settings",
"override_instructions": "Hold shift when clicking the duplicate button to override these settings."
},
"details": "Details",
"drag_and_drop": "Drag and drop files here or click to select files",
"edit": {
@@ -285,7 +311,8 @@
"primary_photo_sub": "This option is only available for photos. Only one photo can be primary. If you select this option, the current primary photo, if any will be unselected.",
"select_type": "Select a type",
"title": "Attachment Edit"
}
},
"view_image": "View Image"
},
"edit_details": "Edit Details",
"field_selector": "Field Selector",
@@ -383,6 +410,7 @@
"update_label": "Update Label"
},
"languages": {
"bs-BA": "Bosnian (Bosnia and Herzegovina)",
"ca": "Catalan",
"cs-CZ": "Czech",
"da-DK": "Danish",
@@ -413,6 +441,7 @@
"th-TH": "Thai",
"tr": "Turkish",
"uk-UA": "Ukrainian",
"vi-VN": "Vietnamese",
"zh-CN": "Chinese (Simplified)",
"zh-HK": "Chinese (Hong Kong)",
"zh-MO": "Chinese (Macau)",
@@ -559,6 +588,8 @@
}
},
"scanner": {
"barcode_detected_message": "product barcode detected",
"barcode_fetch_data": "Fetch product data",
"error": "An error occurred while scanning",
"invalid_url": "Invalid barcode URL",
"no_sources": "No video sources available",

View File

@@ -24,6 +24,13 @@
"new_version_available_link": "Haz click aquí para ver las notas de la versión"
}
},
"color_selector": {
"clear": "Borrar color",
"color": "Color",
"no_color": "Sin color",
"no_color_selected": "Ningún color seleccionado",
"randomize": "Aleatorizar color"
},
"form": {
"password": {
"toggle_show": "Alternar Visibilidad de la Contraseña"
@@ -93,6 +100,8 @@
"item_photo": "Foto del artículo 📷",
"item_quantity": "Cantidad de Elementos",
"parent_item": "Elemento Padre",
"product_tooltip_input_barcode": "Autocompletar con un código de barras proporcionado manualmente",
"product_tooltip_scan_barcode": "Autocompletar con un código de barras desde 📷",
"rotate_photo": "Girar foto",
"set_as_primary_photo": "Establecer como { isPrimary, select, true {non-} false {} other {}} foto principal",
"title": "Crear Elemento",
@@ -108,15 +117,24 @@
"some_photos_failed": "{count, plural, =0 {No hay fotos para subir.} =1 {1 foto no se ha podido subir.} other {Algunas fotos no se han podido subir.}}",
"upload_failed": "Error al subir la foto: { photoName }",
"upload_success": "{count, plural, =0 {No hay fotos subidas.} =1 {Foto subida con éxito.} other {Todas las fotos subidas con éxito.}}",
"uploading_photos": "{count, plural, =0 {No hay fotos para subir} =1 {Subiendo 1 foto...} other {Subiendo {count} fotos...}}"
"uploading_photos": "{count, plural, =0 {No hay fotos para subir} =1 {Subiendo 1 foto} other {Subiendo {count} fotos}}"
},
"upload_photos": "Fotos Subidas",
"uploaded": "Foto Subida"
},
"product_import": {
"barcode": "Código de barras del producto",
"db_source": "Fuente de la base de datos",
"error_exception": "Se ha producido una excepción al recuperar el código de barras del artículo: ",
"error_invalid_barcode": "Código de barras proporcionado no válido",
"error_not_found": "No se ha encontrado ningún producto con código de barras.",
"search_item": "Buscar producto",
"title": "Importar producto"
},
"selector": {
"no_results": "Resultados No Encontrados",
"placeholder": "Seleccionar...",
"search_placeholder": "Escribe para buscar..."
"placeholder": "Seleccionar",
"search_placeholder": "Escribe para buscar"
},
"view": {
"selectable": {
@@ -136,6 +154,7 @@
},
"label": {
"create_modal": {
"label_color": "Color de Etiqueta",
"label_description": "Descripción de la etiqueta",
"label_name": "Nombre de la Etiqueta",
"title": "Crear Etiqueta",
@@ -168,7 +187,7 @@
"select_location": "Elegir una Ubicación"
},
"tree": {
"no_locations": "No hay ubicaciones disponibles. Añade nuevas ubicaciones mediante el botón de\n`<`span class=\"link-primary\"`>`Crear`<`/span`>` en la barra de navegación."
"no_locations": "No hay ubicaciones disponibles. Añade nuevas ubicaciones mediante el botón de\n'<span class=\"link-primary\">'Crear'</span>' en la barra de navegación."
}
},
"quick_menu": {
@@ -176,6 +195,9 @@
"shortcut_hint": "Usa las teclas numéricas para seleccionar rápidamente una acción."
}
},
"errors": {
"api_failure": "Error en la llamada a la API del backend: "
},
"global": {
"add": "Añadir",
"archived": "Archivado",
@@ -196,14 +218,14 @@
"follow_dev": "Seguir al Desarrollador",
"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ón: { version } Compilación: { build } '</a>'"
"version_link": "'<'a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/{ version }\" target=\"_blank\"'>' Versión: { version } Compilación: { build } '</a>'"
},
"github": "Proyecto GitHub",
"insured": "Asegurado",
"items": "Elementos",
"join_discord": "Únete al Discord",
"labels": "Etiquetas",
"loading": "Cargando...",
"loading": "Cargando",
"locations": "Ubicaciones",
"maintenance": "Mantenimiento",
"name": "Nombre",
@@ -529,7 +551,7 @@
"input_placeholder": "Escribe aquí",
"instruction_1": "El Generador de Etiquetas Homebox es una herramienta que para ayudarte a imprimir etiquetas para tu inventario Homebox. Están pensadas para\n ser etiquetas de impresión anticipada para que puedas imprimir muchas etiquetas y tenerlas listas para usarlas",
"instruction_2": "Como tal, estas etiquetas funcionan imprimiendo un código QR de URL e información ID de Activo en una etiqueta. Si has desactivadod\n ID de Activo en la configuración de tu Homebox, puedes seguir utilizando esta herramienta, pero los IDs de Activo no harán referencia a ningún elemento",
"instruction_3": "Esta función se encuentra en las primeras etapas de desarrollo y puede cambiar en futuras versiones. Si tienes algún comentario, indícalo\n en la<a href=\"https://github.com/sysadminsmedia/homebox/discussions/53\"> «Discusión de GitHub»</a>",
"instruction_3": "Esta función se encuentra en las primeras etapas de desarrollo y puede cambiar en futuras versiones. Si tienes algún comentario, indícalo\n en la '<a href=\"https://github.com/sysadminsmedia/homebox/discussions/53\">'Discusión de GitHub'</a>'",
"label_height": "Altura de la Etiqueta",
"label_width": "Ancho de la Etiqueta",
"measure_type": "Tipo de Medida",
@@ -551,6 +573,8 @@
}
},
"scanner": {
"barcode_detected_message": "código de barras del producto detectado",
"barcode_fetch_data": "Obtener datos del producto",
"error": "Se ha producido un error mientras se escaneaba",
"invalid_url": "URL de código de barras inválido",
"no_sources": "No hay fuentes de vídeo disponibles",
@@ -562,6 +586,10 @@
"tools": {
"actions": "Acciones de Inventario",
"actions_set": {
"create_missing_thumbnails": "Crear Miniaturas que Faltan",
"create_missing_thumbnails_button": "Crear Miniaturas",
"create_missing_thumbnails_confirm": "¿Estás seguro de que deseas crear las miniaturas que faltan? Esto puede tardar un poco y no se puede pausar.",
"create_missing_thumbnails_sub": "Crea miniaturas para todos los archivos adjuntos compatibles con la configuración actual. Esto es útil para los adjuntos que se subieron antes de la versión v0.20.0 de Homebox. Esto no sobreescribirá las miniaturas existentes, sólo creará nuevas para los adjuntos que no tienen una miniatura. Ten en cuenta que las miniaturas se crean en segundo plano y pueden tardar un poco en completarse.",
"ensure_ids": "Asegurar IDs de Activos",
"ensure_ids_button": "Asignar ID a los artículos",
"ensure_ids_confirm": "¿Estás seguro de que quieres asegurarte de que todos los activos tengan un ID? Esto puede tardar un tiempo y no puede deshacerse.",
@@ -573,12 +601,12 @@
"set_primary_photo_button": "Establecer Foto Principal",
"set_primary_photo_confirm": "¿Estás seguro de que quieres configurar las fotos principales? Esto puede tardar un tiempo y no se puede deshacer.",
"set_primary_photo_sub": "En la versión v0.10.0 de Homebox, se añadió el indicador de imagen principal a los ficheros adjuntos de tipo foto. Esta acción establecerá la primera imagen de cada artículo como su imagen principal, si no hay una imagen principal ya definida. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/pull/576\">'Ver PR #576 en GitHub'</a>'",
"zero_datetimes": "Cero Horas Elementos",
"zero_datetimes_button": "Cero Horas Elementos",
"zero_datetimes": "Poner a cero las horas de los artículos",
"zero_datetimes_button": "Poner a cero las horas de los artículos",
"zero_datetimes_confirm": "¿Estás seguro de que deseas restablecer todos los valores de fecha y hora? Esto puede tardar un tiempo y no se puede deshacer.",
"zero_datetimes_sub": "Restablece el valor de la hora para todos los campos de fecha/hora en tu inventario al principio de esa fecha. Esto se hace para corregir un error que se introdujo al principio del desarrollo de la aplicación, que causó que el valor de la hora se almacenase con la fecha, lo cual produjo problemas al mostrar valores precisos del campo. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/issues/236\" target=\"_blank\">'Ver el issue #236 de GitHub para más detalles.'</a>'"
},
"actions_sub": "Aplica Acciones a tu inventario de forma masiva. Estas son acciones irreversibles. '<b>'Ten Cuidado.'</b>'",
"actions_sub": "Aplica Acciones a tu inventario de forma masiva. Estas acciones son irreversibles. '<b>'Ten Cuidado.'</b>'",
"import_export": "Importar/Exportar",
"import_export_set": {
"export": "Exportar Inventario",
@@ -586,7 +614,7 @@
"export_sub": "Exporta el formato CSV estándar para Homebox. Esto exportará todos los elementos de tu inventario.",
"import": "Importar Inventario",
"import_button": "Importar Inventario",
"import_ref_confirm": "¿Estás seguro de que deseas asegurarse de que todos los activos tengan un import_ref? Esto puede tardar un tiempo y no se puede deshacer.",
"import_ref_confirm": "¿Estás seguro de que deseas asegurarte de que todos los activos tengan un import_ref? Esto puede tardar un tiempo y no se puede deshacer.",
"import_sub": "Importa el formato CSV estándar para Homebox. Sin una columna '<code>'HB.import_ref'</code>', esto '<b>'no'</b>' sobrescribirá cualquier elemento existente en tu inventario, sólo añadirá nuevos artículos. Las filas con una columna '<code>'HB.import_ref'</code>' se fusionan con los artículos existentes con la misma import_ref, si existe."
},
"import_export_sub": "Importa y exporta tu inventario a y desde un archivo CSV. Esto es útil para migrar tu inventario a una nueva instancia de Homebox.",
@@ -596,12 +624,13 @@
"asset_labels_button": "Generador de Etiquetas",
"asset_labels_sub": "Genera un PDF para impresión de etiquetas para un rango de IDs de Activos. Estas etiquetas no son específicas para tu inventario, por lo que puedes imprimirlas con antelación y aplicarlas a tu inventario cuando las recibas.",
"bill_of_materials": "Lista de Materiales",
"bill_of_materials_button": "Generar lista de materiales",
"bill_of_materials_button": "Generar Lista de Materiales",
"bill_of_materials_sub": "Genera un archivo CSV (Valores Separados por Comas) que puede importarse a un programa de hojas de cálculo. Es un resumen de tu inventario con información básica sobre artículos y precios."
},
"reports_sub": "Genera diferentes informes para tu inventario.",
"toast": {
"asset_success": "Se han actualizado { results } activos.",
"failed_create_missing_thumbnails": "No se han podido crear las miniaturas que faltaban.",
"failed_ensure_ids": "Error al asegurar los ID de los activos.",
"failed_ensure_import_refs": "Error al asegurar las ref. de importación.",
"failed_set_primary_photos": "No se han podido establecer las fotos principales.",

View File

@@ -1,20 +1,10 @@
{
"components": {
"app": {
"create_modal": {
"createAndAddAnother": "",
"enter": "",
"shift": ""
},
"import_dialog": {
"change_warning": "Olemassa olevien import_refs-tiedostojen tuonnin käyttäytyminen on muuttunut. Jos CSV-tiedostossa on import_ref, \nkohde päivitetään CSV-tiedoston arvoilla.",
"description": "Tuo CSV-tiedosto, joka sisältää kohteesi, tarrasi ja sijaintisi. Katso lisätietoja dokumentaatiosta \nvaadittu muoto.",
"title": "Tuo CSV-tiedosto",
"toast": {
"import_failed": "",
"import_success": "",
"please_select_file": ""
}
"title": "Tuo CSV-tiedosto"
},
"outdated": {
"current_version": "Nykyinen versio",
@@ -24,11 +14,6 @@
"new_version_available_link": "Klikkaa tästä nähdäksesi julkaisutiedot"
}
},
"form": {
"password": {
"toggle_show": ""
}
},
"global": {
"copy_text": {
"documentation": "dokumentointi",
@@ -66,12 +51,7 @@
"download": "Lataa Tarra",
"print": "Tulosta tarra",
"server_print": "Tulosta palvelimella",
"titles": "Tarra",
"toast": {
"load_status_failed": "",
"print_failed": "",
"print_success": ""
}
"titles": "Tarra"
},
"page_qr_code": {
"page_url": "Sivun URL",
@@ -82,41 +62,13 @@
}
},
"item": {
"attachments_list": {
"download": "",
"open_new_tab": ""
},
"create_modal": {
"delete_photo": "",
"item_description": "Kohteen Kuvaus",
"item_name": "Tuotteen Nimi",
"item_photo": "Kohteen Kuva 📷",
"item_quantity": "Tuotteen Määrä",
"parent_item": "",
"rotate_photo": "",
"set_as_primary_photo": "",
"title": "Luo Kohde",
"toast": {
"already_creating": "",
"create_failed": "",
"create_success": "",
"failed_load_parent": "",
"no_canvas_support": "",
"please_select_location": "",
"rotate_failed": "",
"rotate_process_failed": "",
"some_photos_failed": "",
"upload_failed": "",
"upload_success": "",
"uploading_photos": ""
},
"upload_photos": "Lataa Valokuvia",
"uploaded": ""
},
"selector": {
"no_results": "",
"placeholder": "",
"search_placeholder": ""
"upload_photos": "Lataa Valokuvia"
},
"view": {
"selectable": {
@@ -129,8 +81,7 @@
"headers": "Otsikko",
"page": "Sivu",
"rows_per_page": "Rivejä sivua kohti",
"table_settings": "Taulukon Asetukset",
"view_item": ""
"table_settings": "Taulukon Asetukset"
}
}
},
@@ -138,13 +89,7 @@
"create_modal": {
"label_description": "Etiketin Kuvaus",
"label_name": "Etiketin Nimi",
"title": "Luo Tarra",
"toast": {
"already_creating": "",
"create_failed": "",
"create_success": "",
"label_name_too_long": ""
}
"title": "Luo Tarra"
},
"selector": {
"select_labels": "Valitse Tarrat"
@@ -154,12 +99,7 @@
"create_modal": {
"location_description": "Sijainnin Kuvaus",
"location_name": "Sijainnin Nimi",
"title": "Luo Sijainti",
"toast": {
"already_creating": "",
"create_failed": "",
"create_success": ""
}
"title": "Luo Sijainti"
},
"selector": {
"no_location_found": "Sijaintia ei löytynyt",
@@ -184,11 +124,8 @@
"confirm": "Vahvistaa",
"create": "Luoda",
"create_and_add": "Luo ja lisää toinen",
"create_subitem": "",
"created": "Luotu",
"delete": "Poistaa",
"delete_confirm": "",
"demo_instance": "",
"details": "Tiedot",
"duplicate": "Kaksoiskappale",
"edit": "Muokkaa",
@@ -196,14 +133,13 @@
"follow_dev": "Seuraa kehittäjää",
"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\"> Versio: { version } Rakenna: { build } '</a>'"
"version_link": "'<'a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/{ version }\" target=\"_blank\"'>' Versio: { version } Rakenna: { build } '</a>'"
},
"github": "GitHub Projekti",
"insured": "Vakuuttaa",
"items": "Erä",
"join_discord": "Liity Discord",
"labels": "Tarra",
"loading": "",
"locations": "Sijainti",
"maintenance": "Huolto",
"name": "Nimi",
@@ -211,14 +147,11 @@
"password": "Salasana",
"quantity": "Määrä",
"read_docs": "Lue dokumentit",
"return_home": "",
"save": "Tallentaa",
"search": "Etsiä",
"sign_out": "Kirjaudu Ulos",
"submit": "Lähettää",
"unknown": "",
"update": "Päivitys",
"updating": "",
"value": "Arvo",
"version": "Versio: { version }",
"welcome": "Tervetuloa, { username }"
@@ -243,49 +176,27 @@
"set_email": "Mikä on sähköpostisi?",
"set_name": "Mikä sinun nimesi on?",
"set_password": "Aseta salasana",
"tagline": "Seuraa, Järjestä ja hallitse asioitasi.",
"title": "",
"toast": {
"invalid_email": "",
"invalid_email_password": "",
"login_success": "",
"problem_registering": "",
"user_registered": ""
}
"tagline": "Seuraa, Järjestä ja hallitse asioitasi."
},
"items": {
"add": "Lisätä",
"advanced": "Edistyksellinen",
"archived": "Arkistoitu",
"asset_id": "Omaisuuserän ID",
"associated_with_multiple": "",
"attachment": "Liite",
"attachments": "Liitteet",
"changes_persisted_immediately": "Liitteiden muutokset tallennetaan välittömästi",
"created_at": "Luotu Osoitteessa",
"custom_fields": "Mukautetut Kentät",
"delete_attachment_confirm": "",
"delete_item_confirm": "",
"description": "Kuvaus",
"details": "Tiedot",
"drag_and_drop": "Vedä ja pudota tiedostoja tähän tai valitse tiedostot napsauttamalla",
"edit": {
"edit_attachment_dialog": {
"attachment_title": "",
"attachment_type": "",
"primary_photo": "",
"primary_photo_sub": "",
"select_type": "",
"title": ""
}
},
"edit_details": "Muokkaa Tietoja",
"field_selector": "Kentän Valitsin",
"field_value": "Kentän Arvo",
"first": "Ensimmäinen",
"include_archive": "Sisällytä Arkistoidut Kohteet",
"insured": "Vakuuttaa",
"invalid_asset_id": "",
"last": "Viimeinen",
"lifetime_warranty": "Elinikäinen Takuu",
"location": "Paikka",
@@ -296,7 +207,6 @@
"name": "Nimi",
"negate_labels": "Poista Valitut Nimilaput",
"next_page": "Seuraava Sivu",
"no_attachments": "",
"no_results": "Kohteita Ei Löytynyt",
"notes": "Huomautus",
"only_with_photo": "Vain tuotteet, joissa on valokuva",
@@ -318,60 +228,24 @@
"receipts": "Kuitin",
"reset_search": "Haku nollataan",
"results": "{ total } Tulokset",
"select_field": "",
"serial_number": "Sarjanumero",
"show_advanced_view_options": "Näytä näkymän lisäasetukset",
"sold_at": "Myyty klo",
"sold_details": "Myydyt Tiedot",
"sold_price": "Myyty Hinta",
"sold_to": "Myyty",
"sync_child_locations": "",
"tip_1": "Sijainti-ja etikettisuodattimet käyttävät \" tai \" - toimintoa. Jos valitaan useampi kuin yksi, vain yksi on\n tarvitaan ottelu.",
"tip_2": "\"#\"- Etuliitteellä tehdyt haut kyselevät omaisuuserän tunnusta (esimerkki \"#000-001\")",
"tip_3": "Kenttäsuodattimet käyttävät \" tai \" - toimintoa. Jos valitaan useampi kuin yksi, tarvitaan vain yksi\n ottelu.",
"tips": "Vihje",
"tips_sub": "Etsi Vinkkejä",
"toast": {
"asset_not_found": "",
"attachment_deleted": "",
"attachment_updated": "",
"attachment_uploaded": "",
"child_items_location_no_longer_synced": "",
"child_items_location_synced": "",
"child_location_desync": "",
"error_loading_parent_data": "",
"failed_adjust_quantity": "",
"failed_delete_attachment": "",
"failed_delete_item": "",
"failed_duplicate_item": "",
"failed_load_asset": "",
"failed_load_item": "",
"failed_load_items": "",
"failed_save": "",
"failed_save_no_location": "",
"failed_search_items": "",
"failed_update_attachment": "",
"failed_upload_attachment": "",
"item_deleted": "",
"item_saved": "",
"quantity_cannot_negative": "",
"sync_child_location": ""
},
"updated_at": "Päivitetty Osoitteessa",
"warranty": "Takuu",
"warranty_details": "Takuun Tiedot",
"warranty_expires": "Takuu Päättyy"
},
"labels": {
"label_delete_confirm": "",
"no_results": "Tarroja Ei Löytynyt",
"toast": {
"failed_delete_label": "",
"failed_load_label": "",
"failed_update_label": "",
"label_deleted": "",
"label_updated": ""
},
"update_label": "Päivitä Nimiö"
},
"languages": {
@@ -388,8 +262,6 @@
"it": "Italia",
"ja-JP": "Japani",
"ko-KR": "Korea",
"lb-LU": "",
"lt-LT": "",
"nb-NO": "Norjalainen Bokmål",
"nl": "Hollanti",
"pl": "Puola",
@@ -414,15 +286,7 @@
"child_locations": "Lasten Sijainnit",
"collapse_tree": "Romahdus Puu",
"expand_tree": "Laajenna Puu",
"location_items_delete_confirm": "",
"no_results": "Sijainteja Ei Löytynyt",
"toast": {
"failed_delete_location": "",
"failed_load_location": "",
"failed_update_location": "",
"location_deleted": "",
"location_updated": ""
},
"update_location": "Päivitä Sijainti"
},
"maintenance": {
@@ -478,10 +342,7 @@
"currency_format": "Valuutan Muoto",
"current_password": "Nykyinen Salasana",
"delete_account": "Poista tili",
"delete_account_confirm": "",
"delete_account_sub": "Poista tilisi ja kaikki siihen liittyvät tiedot. Tätä ei voi perua.",
"delete_notifier_confirm": "",
"display_legacy_header": "",
"enabled": "Käyttöön",
"example": "Esimerkiksi",
"gen_invite": "Luo Kutsulinkki",
@@ -491,70 +352,22 @@
"language": "Kieli",
"new_password": "Uusi Salasana",
"no_notifiers": "Ilmoittajia ei ole määritetty",
"no_override": "",
"notifier_modal": "{ type, select, true {Edit} false {Create} other {Other}} Ilmoittaja",
"notifiers": "Ilmoittaja",
"notifiers_sub": "Saat ilmoituksia tulevista huoltomuistutuksista",
"override_locale": "",
"test": "Testi",
"theme_settings": "Teeman Asetukset",
"theme_settings_sub": "Teeman asetukset tallennetaan selaimesi paikalliseen tallennustilaan. Voit vaihtaa teemaa milloin tahansa. Jos olet\n jos sinulla on ongelmia teeman asettamisessa, yritä päivittää selaimesi.",
"toast": {
"account_deleted": "",
"failed_change_password": "",
"failed_create_notifier": "",
"failed_delete_account": "",
"failed_delete_notifier": "",
"failed_get_currencies": "",
"failed_test_notifier": "",
"failed_update_group": "",
"failed_update_notifier": "",
"group_updated": "",
"notifier_test_success": "",
"password_changed": ""
},
"update_group": "Päivitä Ryhmä",
"update_language": "Päivitä Kieli",
"url": "URL",
"user_profile": "käyttäjäprofiili",
"user_profile_sub": "Kutsu käyttäjiä ja Hallitse tiliäsi."
},
"reports": {
"label_generator": {
"asset_end": "",
"asset_start": "",
"base_url": "",
"bordered_labels": "",
"generate_page": "",
"input_placeholder": "",
"instruction_1": "",
"instruction_2": "",
"instruction_3": "",
"label_height": "",
"label_width": "",
"measure_type": "",
"page_bottom_padding": "",
"page_height": "",
"page_left_padding": "",
"page_right_padding": "",
"page_top_padding": "",
"page_width": "",
"qr_code_example": "",
"tip_1": "",
"tip_2": "",
"tip_3": "",
"tips": "",
"title": "",
"toast": {
"page_too_small_card": ""
}
}
},
"scanner": {
"error": "Skannattaessa tapahtui virhe",
"invalid_url": "Virheellinen viivakoodin URL",
"no_sources": "Videolähteitä ei ole saatavilla",
"permission_denied": "",
"select_video_source": "Valitse videolähde",
"title": "Skanneri",
"unsupported": "Media Stream API ei ole tuettu ilman HTTPS"
@@ -564,18 +377,15 @@
"actions_set": {
"ensure_ids": "Varmistetaan, että omaisuuserät ID:t",
"ensure_ids_button": "Varmistetaan omaisuuserät ID:t",
"ensure_ids_confirm": "",
"ensure_ids_sub": "Varmistaa, että kaikilla varastossasi olevilla kohteilla on voimassa oleva asset_id-kenttä. Tämä tehdään etsimällä tietokannan korkein nykyinen asset_id-kenttä ja soveltamalla seuraavaa arvoa jokaiselle kohteelle, jolla on unset asset_id-kenttä. Tämä tehdään created_at-kentän järjestyksessä.",
"ensure_import_refs": "Varmista Tuonnin Viitteet",
"ensure_import_refs_button": "Varmista Tuonnin Viitteet",
"ensure_import_refs_sub": "Varmistaa, että kaikilla varastossasi olevilla kohteilla on voimassa oleva import_ref-kenttä. Tämä tehdään luomalla satunnaisesti 8 merkin merkkijono jokaiselle kohteelle, jolla on unset import_ref-kenttä.",
"set_primary_photo": "Aseta oletuskuva",
"set_primary_photo_button": "Aseta Ensisijainen Kuva",
"set_primary_photo_confirm": "",
"set_primary_photo_sub": "Homebox versiossa v0.10.0 ensisijainen kuvakenttä lisättiin tyypin kuva liitteisiin. Tämä toiminto asettaa ensisijaiseksi kuvakentäksi tietokannan liitteet-taulukon ensimmäisen kuvan, jos sitä ei ole jo asetettu. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/pull/576\">'Katso GitHub PR #576\" </a>\"",
"zero_datetimes": "Nolla Kohteen Päivämääräajat",
"zero_datetimes_button": "Nolla Kohteen Päivämääräajat",
"zero_datetimes_confirm": "",
"zero_datetimes_sub": "Palauttaa varastosi kaikkien päivämääräkenttien aika-arvon päivämäärän alkuun. Tämän tarkoituksena on korjata virhe, joka otettiin käyttöön varhaisessa vaiheessa sivuston kehitystä, joka aiheutti aika-arvon tallentamisen ajan kanssa, joka aiheutti ongelmia päivämääräkentissä, jotka näyttävät tarkkoja arvoja. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/issues/236\" target=\"_blank\">'Katso Github Ongelma #236 lisää yksityiskohtia.'</a>'"
},
"actions_sub": "Käytä toimintoja varastoon irtotavarana. Nämä ovat peruuttamattomia toimia. '<b>'Ole varovainen.</b>'",
@@ -586,7 +396,6 @@
"export_sub": "Vie Homebox CSV-standardin. Tämä vie kaikki varastossasi olevat tuotteet.",
"import": "Tuo Varasto",
"import_button": "Tuo varasto",
"import_ref_confirm": "",
"import_sub": "Tuo CSV-standardimuodon Homebox. Ilman <code>'HB.import_ref</code> kolumni, tämä tulee <b>not</b>' korvaa inventaariossasi olevia esineitä, vaan lisää vain uusia esineitä. Rivit, joissa on <code>'HB.import_ref</code>' sarakkeen tiedot yhdistetään olemassa oleviin kohteisiin, joilla on sama import_ref, jos sellainen on olemassa."
},
"import_export_sub": "Tuo ja vie varastosi CSV-tiedostoon ja siitä. Tämä on hyödyllistä siirrettäessä varastosi uuteen Homebox ilmentymään.",
@@ -599,13 +408,6 @@
"bill_of_materials_button": "Luo BOM",
"bill_of_materials_sub": "Luo CSV-tiedoston (pilkulla erotetut arvot), joka voidaan tuoda taulukkolaskentaohjelmaan. Tämä on yhteenveto varastostasi, jossa on perustuotteet ja hintatiedot."
},
"reports_sub": "Luo erilaisia raportteja varastollesi.",
"toast": {
"asset_success": "",
"failed_ensure_ids": "",
"failed_ensure_import_refs": "",
"failed_set_primary_photos": "",
"failed_zero_datetimes": ""
}
"reports_sub": "Luo erilaisia raportteja varastollesi."
}
}

View File

@@ -24,6 +24,11 @@
"new_version_available_link": "Cliquez ici pour consulter les notes de version"
}
},
"color_selector": {
"color": "Couleur",
"no_color_selected": "Aucune couleur sélectionnée",
"randomize": "Couleur aléatoire"
},
"form": {
"password": {
"toggle_show": "Activer/désactiver l'affichage du mot de passe"
@@ -115,8 +120,8 @@
},
"selector": {
"no_results": "Aucun résultat trouvé",
"placeholder": "Sélectionner...",
"search_placeholder": "Tapez pour rechercher..."
"placeholder": "Sélectionner",
"search_placeholder": "Tapez pour rechercher"
},
"view": {
"selectable": {
@@ -130,7 +135,7 @@
"page": "Page",
"rows_per_page": "Lignes par page",
"table_settings": "Paramètres du Tableau",
"view_item": ""
"view_item": "Afficher l'élément"
}
}
},
@@ -168,7 +173,7 @@
"select_location": "Choisir un emplacement"
},
"tree": {
"no_locations": "Aucun emplacement disponible. Créez votre premier emplacement avec\nle bouton `<`span class=\"link-primary\"`>`Créer`<`/span`>` dans la barre de navigation."
"no_locations": "Aucun emplacement disponible. Créez votre premier emplacement avec\nle bouton '<span class=\"link-primary\">'Créer'</span>' dans la barre de navigation."
}
},
"quick_menu": {
@@ -196,14 +201,14 @@
"follow_dev": "Suivre le développeur",
"footer": {
"api_link": "'<a href=\"https://homebox.software/en/api/\" target=\"_blank\">'API'</a>'",
"version_link": "'<a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/'{ version }\" target=\"_blank\"> Version : { version } Commit : { build } '</a>'"
"version_link": "'<'a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/{ version }\" target=\"_blank\"'>' Version : { version } Commit : { build } '</a>'"
},
"github": "Projet GitHub",
"insured": "Assuré",
"items": "Articles",
"join_discord": "Rejoindre le Discord",
"labels": "Étiquettes",
"loading": "Chargement...",
"loading": "Chargement",
"locations": "Emplacements",
"maintenance": "Maintenance",
"name": "Nom",
@@ -401,6 +406,7 @@
"th-TH": "Thaï",
"tr": "Turc",
"uk-UA": "Ukrainien",
"vi-VN": "Vietnamien",
"zh-CN": "Chinois (simplifié)",
"zh-HK": "Chinois (Hong Kong)",
"zh-MO": "Chinois (Macao)",
@@ -562,6 +568,10 @@
"tools": {
"actions": "Actions dinventaire",
"actions_set": {
"create_missing_thumbnails": "Crée les miniatures manquantes",
"create_missing_thumbnails_button": "Crée les miniatures",
"create_missing_thumbnails_confirm": "Êtes-vous sûr de vouloir créer les vignettes manquantes ? Cette opération peut prendre un certain temps et ne peut pas être interrompue.",
"create_missing_thumbnails_sub": "Crée des miniatures pour toutes les pièces jointes prises en charge par la configuration actuelle. Ceci est utile pour les pièces jointes importées avant la version 0.20.0 de Homebox. Cette opération n'écrase pas les miniatures existantes, mais crée de nouvelles miniatures pour les pièces jointes sans miniature. Veuillez noter que la création des miniatures s'effectue en arrière-plan et peut prendre un certain temps.",
"ensure_ids": "Vérifier les ID de ressources",
"ensure_ids_button": "Vérifier les ID de ressources",
"ensure_ids_confirm": "Êtes-vous certain de vous assurer que toutes les ressources ont une ID ? Cela peut prendre du temps et est irréversible.",

View File

@@ -2,18 +2,18 @@
"components": {
"app": {
"create_modal": {
"createAndAddAnother": "",
"enter": "",
"shift": ""
"createAndAddAnother": "Nyomj {shiftKey} + {enterKey}t, hogy létrehozd ezt és hozzáadj egy újat.",
"enter": "Enter",
"shift": "Shift"
},
"import_dialog": {
"change_warning": "A meglévő import_ref-fel rendelkező tételek importálásának menete megváltozott. Ha a CSV fájlban van import_ref, \nakkor a tételt felülírják a CSV fájlban található értékek.",
"description": "Importálj egy CSV fájlt, amely tartalmazza a tételeidet, címkéidet és helyeidet. A szükséges formátumról bővebben \na dokumentációban olvashatsz.",
"title": "Importálás CSV-fájlból",
"toast": {
"import_failed": "",
"import_success": "",
"please_select_file": ""
"import_failed": "Sikertelen importálás. Kérlek próbáld újra később.",
"import_success": "Sikeres importálás!",
"please_select_file": "Kérlek válassz egy fájlt az importáláshoz."
}
},
"outdated": {
@@ -24,9 +24,16 @@
"new_version_available_link": "Kattints ide az újdonságok megtekintéséhez"
}
},
"color_selector": {
"clear": "Szín törlése",
"color": "Szín",
"no_color": "Nincs szín",
"no_color_selected": "Nincs kiválasztott szín",
"randomize": "Véletlenszerű színezés"
},
"form": {
"password": {
"toggle_show": ""
"toggle_show": "Jelszó megjelenítése"
}
},
"global": {
@@ -68,9 +75,9 @@
"server_print": "Nyomtatás a szerveren",
"titles": "Címkék",
"toast": {
"load_status_failed": "",
"print_failed": "",
"print_success": ""
"load_status_failed": "Sikertelen állapot betöltés",
"print_failed": "Sikertelen címke nyomtatás",
"print_success": "Címke kinyomtatva"
}
},
"page_qr_code": {
@@ -83,40 +90,51 @@
},
"item": {
"attachments_list": {
"download": "",
"open_new_tab": ""
"download": "Letöltés",
"open_new_tab": "Megnyitás új lapon"
},
"create_modal": {
"delete_photo": "",
"delete_photo": "Kép törlése",
"item_description": "Tétel leírása",
"item_name": "Tétel neve",
"item_photo": "Tétel fényképe 📷",
"item_quantity": "Tételek mennyisége",
"parent_item": "Szülő tétel",
"rotate_photo": "",
"set_as_primary_photo": "",
"product_tooltip_input_barcode": "Automatikus feltöltés kézzel megadott vonalkóddal",
"product_tooltip_scan_barcode": "Automatikus kitöltés vonalkóddal innen: 📷",
"rotate_photo": "Kép forgatása",
"set_as_primary_photo": "Beállítás {isPrimary , select, true {nem } false {} other {}}elsődleges fényképként",
"title": "Új elem létrehozása",
"toast": {
"already_creating": "",
"create_failed": "",
"create_success": "",
"failed_load_parent": "",
"no_canvas_support": "",
"please_select_location": "",
"rotate_failed": "",
"rotate_process_failed": "",
"some_photos_failed": "",
"upload_failed": "",
"upload_success": "",
"uploading_photos": ""
"already_creating": "Elem létrehozása már folyamatban",
"create_failed": "Sikertelen elem létrehozás",
"create_success": "Elem létrehozva",
"failed_load_parent": "Szülő elem betöltése sikertelen - válaszd ki manuálisan",
"no_canvas_support": "A böngésződ nem támogatja a canvas műveleteket",
"please_select_location": "Válassz egy helyet.",
"rotate_failed": "Sikertelen képforgatás: { error }",
"rotate_process_failed": "Elforgatott kép feldolgozása sikertelen",
"some_photos_failed": "{count, plural, =0 {Nincs feltölthető fénykép.} =1 {1 fénykép feltöltése nem sikerült.} other {Néhány fénykép feltöltése nem sikerült.}}",
"upload_failed": "Kép feltöltése sikertelen: { photoName }",
"upload_success": "{count, plural, =0 {Nincsenek feltöltött fényképek.} =1 {A fénykép feltöltése sikeres.} other {Minden fénykép feltöltése sikeres.}}",
"uploading_photos": "{count, plural, =0 {Nincs feltöltendő fénykép} =1 {1 fénykép feltöltése…} other {{count} fénykép feltöltése…}}"
},
"upload_photos": "Fotók feltöltése",
"uploaded": ""
"uploaded": "Feltöltött fénykép"
},
"product_import": {
"barcode": "Termék vonalkódja",
"db_source": "Adatbázis forrás",
"error_exception": "Kivétel történt a tétel vonalkódjának lekérése során: ",
"error_invalid_barcode": "Érvénytelen vonalkód",
"error_not_found": "Nem található termék a megadott vonalkóddal.",
"search_item": "Termék keresése",
"title": "Termék importálása"
},
"selector": {
"no_results": "",
"placeholder": "",
"search_placeholder": ""
"no_results": "Nincs Találat",
"placeholder": "Válassz…",
"search_placeholder": "Kezdj gépelni a kereséshez…"
},
"view": {
"selectable": {
@@ -130,20 +148,21 @@
"page": "Oldal",
"rows_per_page": "Sorok oldalanként",
"table_settings": "Táblázatbeállítások",
"view_item": ""
"view_item": "Elem megtekintése"
}
}
},
"label": {
"create_modal": {
"label_color": "Címke színe",
"label_description": "Címke leírása",
"label_name": "Címke neve",
"title": "Címke létrehozása",
"toast": {
"already_creating": "",
"create_failed": "",
"create_success": "",
"label_name_too_long": ""
"already_creating": "Címke létrehozása már folyamatban",
"create_failed": "Sikertelen címke létrehozás",
"create_success": "Címke létrehozva",
"label_name_too_long": "A címke neve nem lehet hosszabb 50 karakternél"
}
},
"selector": {
@@ -156,9 +175,9 @@
"location_name": "Hely neve",
"title": "Új hely létrehozása",
"toast": {
"already_creating": "",
"create_failed": "",
"create_success": ""
"already_creating": "Hely létrehozása már folyamatban",
"create_failed": "Sikertelen hely létrehozás",
"create_success": "Hely létrehozva"
}
},
"selector": {
@@ -168,7 +187,7 @@
"select_location": "Válassz egy helyet"
},
"tree": {
"no_locations": "Nincs elérhető hely. Adj hozzá új helyet a\n `<`span class=\"link-primary\"`>`Létrehozás`<`/span`>` gombbal a navigációs sávon."
"no_locations": "Nincs elérhető hely. Adj hozzá új helyet a\n '<span class=\"link-primary\">'Létrehozás'</span>' gombbal a navigációs sávon."
}
},
"quick_menu": {
@@ -176,6 +195,9 @@
"shortcut_hint": "Használd a számgombokat egy művelet gyors kiválasztásához."
}
},
"errors": {
"api_failure": "Backend API hívás sikertelen: "
},
"global": {
"add": "Hozzáadás",
"archived": "Archivált",
@@ -187,8 +209,8 @@
"create_subitem": "Alelem létrehozása",
"created": "Létrehozva",
"delete": "Törlés",
"delete_confirm": "",
"demo_instance": "",
"delete_confirm": "Biztosan törlöd ezt az elemet? ",
"demo_instance": "Ez egy demó példány",
"details": "Részletek",
"duplicate": "Másolás",
"edit": "Szerkesztés",
@@ -196,14 +218,14 @@
"follow_dev": "Kövesd a fejlesztőt",
"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\"> Verzió: { version } Build: { build } '</a>'"
"version_link": "'<'a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/{ version }\" target=\"_blank\"'>' Verzió: { version } Build: { build } '</a>'"
},
"github": "Github projekt",
"insured": "Biztosítva",
"items": "Tételek",
"join_discord": "Csatlakozz a Discordhoz",
"labels": "Címkék",
"loading": "",
"loading": "Betöltés…",
"locations": "Helyek",
"maintenance": "Karbantartás",
"name": "Név",
@@ -211,14 +233,14 @@
"password": "Jelszó",
"quantity": "Mennyiség",
"read_docs": "Olvasd el a dokumentációt",
"return_home": "",
"return_home": "Vissza a kezdőlapra",
"save": "Mentés",
"search": "Keresés",
"sign_out": "Kijelentkezés",
"submit": "Elküldés",
"unknown": "",
"unknown": "Ismeretlen",
"update": "Módosítás",
"updating": "",
"updating": "Frissítés",
"value": "Érték",
"version": "Verzió: { version }",
"welcome": "Üdv, { username }"
@@ -244,13 +266,13 @@
"set_name": "Mi a neved?",
"set_password": "Állíts be egy jelszót!",
"tagline": "Kövesd nyomon, rendszerezd és kezeld a dolgaidat.",
"title": "",
"title": "Rendezd és címkézd a dolgaidat",
"toast": {
"invalid_email": "",
"invalid_email_password": "",
"login_success": "",
"problem_registering": "",
"user_registered": ""
"invalid_email": "E-mail cím érvénytelen",
"invalid_email_password": "Az E-mail vagy jelszó érvénytelen",
"login_success": "Sikeres bejelentkezés",
"problem_registering": "Hiba a felhasználó regisztrálásakor",
"user_registered": "Felhasználó regisztrálva"
}
},
"items": {
@@ -258,25 +280,25 @@
"advanced": "Haladó",
"archived": "Archivált",
"asset_id": "Eszközazonosító",
"associated_with_multiple": "",
"associated_with_multiple": "Ez az eszköz Id több elemhez van hozzárendelve",
"attachment": "Melléklet",
"attachments": "Mellékletek",
"changes_persisted_immediately": "A mellékletek módosításai azonnal mentésre kerülnek",
"created_at": "Létrehozás dátuma",
"custom_fields": "Egyedi mezők",
"delete_attachment_confirm": "",
"delete_item_confirm": "",
"delete_attachment_confirm": "Biztos törlöd ezt a mellékletet?",
"delete_item_confirm": "Biztosan törlöd ezt az elemet?",
"description": "Leírás",
"details": "Részletek",
"drag_and_drop": "Húzd ide a fájlokat, vagy kattints a fájlok kiválasztásához",
"edit": {
"edit_attachment_dialog": {
"attachment_title": "",
"attachment_type": "",
"primary_photo": "",
"primary_photo_sub": "",
"select_type": "",
"title": ""
"attachment_title": "Melléklet Cím",
"attachment_type": "Melléklet típusa",
"primary_photo": "Elsődleges fénykép",
"primary_photo_sub": "Ez a lehetőség csak fényképeknél érhető el. Csak egy fotó lehet elsődleges. Ha kiválasztod ezt a műveletet, a jelenlegi elsődleges fotó elveszti ezt a jellegét.",
"select_type": "Válassz típust",
"title": "Melléklet szerkesztése"
}
},
"edit_details": "Részletek szerkesztése",
@@ -285,7 +307,7 @@
"first": "Első",
"include_archive": "Archivált elemek belefoglalása",
"insured": "Biztosítva",
"invalid_asset_id": "",
"invalid_asset_id": "Érvénytelen eszközazonosító",
"last": "Utolsó",
"lifetime_warranty": "Élettartam garancia",
"location": "Hely",
@@ -296,7 +318,7 @@
"name": "Név",
"negate_labels": "Címkeválasztás negálása",
"next_page": "Következő oldal",
"no_attachments": "",
"no_attachments": "Nem található melléklet",
"no_results": "Egy elem sem található",
"notes": "Megjegyzések",
"only_with_photo": "Csak fényképes tételek",
@@ -318,44 +340,44 @@
"receipts": "Számlák",
"reset_search": "Alaphelyzet",
"results": "{total} találat",
"select_field": "",
"select_field": "Válaszd ki a mezőt",
"serial_number": "Sorozatszám",
"show_advanced_view_options": "További beállítások megjelenítése",
"sold_at": "Eladás dátuma",
"sold_details": "Eladás részletei",
"sold_price": "Eladási ár",
"sold_to": "Vevő",
"sync_child_locations": "",
"sync_child_locations": "Gyermekelemek helyeinek szinkronizálása",
"tip_1": "A hely- és címkeszűrők a „vagy” műveletet használják. Ha egynél többet választasz ki,\n bármelyik egyezése esetén megjelenik a tétel.",
"tip_2": "A '#' előtaggal ellátott keresések egy eszközazonosítót fognak lekérdezni (például '#000-001')",
"tip_3": "A mezőszűrők a „vagy” műveletet használják. Ha egynél többet választasz ki,\n bármelyik egyezése esetén megjelenik a tétel.",
"tips": "Tippek",
"tips_sub": "Tippek a kereséshez",
"toast": {
"asset_not_found": "",
"attachment_deleted": "",
"attachment_updated": "",
"attachment_uploaded": "",
"child_items_location_no_longer_synced": "",
"child_items_location_synced": "",
"child_location_desync": "",
"error_loading_parent_data": "",
"failed_adjust_quantity": "",
"failed_delete_attachment": "",
"failed_delete_item": "",
"failed_duplicate_item": "",
"failed_load_asset": "",
"failed_load_item": "",
"failed_load_items": "",
"failed_save": "",
"failed_save_no_location": "",
"failed_search_items": "",
"failed_update_attachment": "",
"failed_upload_attachment": "",
"item_deleted": "",
"item_saved": "",
"quantity_cannot_negative": "",
"sync_child_location": ""
"asset_not_found": "Az eszköz nem található",
"attachment_deleted": "Melléklet törölve",
"attachment_updated": "Melléklet frissítve",
"attachment_uploaded": "Melléklet feltöltve",
"child_items_location_no_longer_synced": "A gyermekelemek helye a továbbiakban nem lesz szinkronizálva ezzel a tétellel.",
"child_items_location_synced": "A gyermekelemek helye szinkronizálva lett ezzel a tétellel",
"child_location_desync": "A hely módosítása de-szinkronizálja a szülő helyéről",
"error_loading_parent_data": "Hiba történt a szülőadatok betöltése során",
"failed_adjust_quantity": "Mennyiség beállítása sikertelen",
"failed_delete_attachment": "Melléklet törlése sikertelen",
"failed_delete_item": "Tétel törlése sikertelen",
"failed_duplicate_item": "Tétel másolása sikertelen",
"failed_load_asset": "Eszköz betöltése sikertelen",
"failed_load_item": "Tétel betöltése sikertelen",
"failed_load_items": "Tételek betöltése sikertelen",
"failed_save": "Tétel mentése sikertelen",
"failed_save_no_location": "Tétel mentése sikertelen: nincs kiválasztott hely",
"failed_search_items": "Tételek keresése sikertelen",
"failed_update_attachment": "Melléklet frissítése sikertelen",
"failed_upload_attachment": "Melléklet feltöltése sikertelen",
"item_deleted": "Tétel törölve",
"item_saved": "Tétel mentve",
"quantity_cannot_negative": "A mennyiség nem lehet negatív",
"sync_child_location": "A kiválasztott szülő szinkronizálja gyermekei tartózkodási helyét a sajátjával. Hely frissítve."
},
"updated_at": "Változtatás dátuma",
"warranty": "Garancia",
@@ -363,14 +385,14 @@
"warranty_expires": "Garancia vége"
},
"labels": {
"label_delete_confirm": "",
"label_delete_confirm": "Biztos vagy benne, hogy törölni szeretnéd ezt a címkét? A művelet nem visszafordítható.",
"no_results": "Nem található címke",
"toast": {
"failed_delete_label": "",
"failed_load_label": "",
"failed_update_label": "",
"label_deleted": "",
"label_updated": ""
"failed_delete_label": "Címke törlése sikertelen",
"failed_load_label": "Címke betöltése sikertelen",
"failed_update_label": "Címke frissítése sikertelen",
"label_deleted": "Címke törölve",
"label_updated": "Címke frissítve"
},
"update_label": "Címke módosítása"
},
@@ -414,14 +436,14 @@
"child_locations": "Tartalmazott helyek",
"collapse_tree": "Fanézet becsukása",
"expand_tree": "Fa kibontása",
"location_items_delete_confirm": "",
"location_items_delete_confirm": "Biztosan törlöd ezt a helyet és az összes elemét? Ez a művelet nem visszavonható.",
"no_results": "Nem található hely",
"toast": {
"failed_delete_location": "",
"failed_load_location": "",
"failed_update_location": "",
"location_deleted": "",
"location_updated": ""
"failed_delete_location": "Hely törlése sikertelen",
"failed_load_location": "Hely betöltése sikertelen",
"failed_update_location": "Hely frissítése sikertelen",
"location_deleted": "Hely törölve",
"location_updated": "Hely frissítve"
},
"update_location": "Hely módosítása"
},
@@ -478,10 +500,10 @@
"currency_format": "Pénz formátum",
"current_password": "Jelenlegi jelszó",
"delete_account": "Fiók törlése",
"delete_account_confirm": "",
"delete_account_confirm": "Biztosan törlöd a fiókodat? Ha te vagy az utolsó tag a csoportodban, minden adatod törlődik. Ez a művelet nem visszafordítható.",
"delete_account_sub": "Törlöd a fiókodat és az összes kapcsolódó adatot. Ezt a műveletet nem lehet visszavonni.",
"delete_notifier_confirm": "",
"display_legacy_header": "",
"delete_notifier_confirm": "Biztos, hogy törölni akarod ezt az értesítőt?",
"display_legacy_header": "{ currentValue, select, true {Legacy fejléc letiltása} false {Legacy fejléc engedélyezése} other {Nincs találat}}",
"enabled": "Engedélyezve",
"example": "Példa",
"gen_invite": "Meghívó link létrehozása",
@@ -500,18 +522,18 @@
"theme_settings": "Téma Beállítások",
"theme_settings_sub": "A témabeállítások a böngésző helyi tárhelyén tárolódnak. Bármikor megváltoztathatod a témát. Ha problémába\n ütközöl a téma beállításakor, próbáld meg frissíteni az oldalt a böngésződben.",
"toast": {
"account_deleted": "",
"failed_change_password": "",
"failed_create_notifier": "",
"failed_delete_account": "",
"failed_delete_notifier": "",
"failed_get_currencies": "",
"failed_test_notifier": "",
"failed_update_group": "",
"failed_update_notifier": "",
"group_updated": "",
"notifier_test_success": "",
"password_changed": ""
"account_deleted": "Fiókodat sikeresen töröltük.",
"failed_change_password": "Jelszó megváltoztatása sikertelen.",
"failed_create_notifier": "Értesítő létrehozása sikertelen.",
"failed_delete_account": "Nem sikerült törölni a fiókodat.",
"failed_delete_notifier": "Értesítő törlése sikertelen.",
"failed_get_currencies": "Pénznemek lekérése sikertelen",
"failed_test_notifier": "Nem sikerült tesztelni az értesítőt.",
"failed_update_group": "Csoport frissítése sikertelen",
"failed_update_notifier": "Nem sikerült frissíteni az értesítőt.",
"group_updated": "Csoport frissítve",
"notifier_test_success": "Az értesítő tesztje sikeres volt.",
"password_changed": "A jelszóváltoztatás sikeres volt."
},
"update_group": "Csoport módosítása",
"update_language": "Nyelv átállítása",
@@ -521,40 +543,42 @@
},
"reports": {
"label_generator": {
"asset_end": "",
"asset_start": "",
"base_url": "",
"bordered_labels": "",
"generate_page": "",
"input_placeholder": "",
"instruction_1": "",
"instruction_2": "",
"instruction_3": "",
"label_height": "",
"label_width": "",
"measure_type": "",
"page_bottom_padding": "",
"page_height": "",
"page_left_padding": "",
"page_right_padding": "",
"page_top_padding": "",
"page_width": "",
"qr_code_example": "",
"tip_1": "",
"tip_2": "",
"tip_3": "",
"tips": "",
"title": "",
"asset_end": "Utolsó eszköz",
"asset_start": "Első eszköz",
"base_url": "Alap URL",
"bordered_labels": "Keretes címkék",
"generate_page": "Oldal létrehozása",
"input_placeholder": "Írj ide",
"instruction_1": "A Homebox Label Generator egy olyan eszköz, amely segít a Homebox-leltár címkéinek nyomtatásában. Ezeket előre\n kinyomtathatod, hogy bármikor felragaszthass egy új, még használatlan címkét",
"instruction_2": "Ezek a címkék ezért úgy működnek, hogy egy URL QR-kódot és eszközazonosítót nyomtatnak egy címkére. Ha kikapcsoltad\n az eszközazonosítókat a Homebox beállításokban, akkor is használhatod ezt az eszközt, de nem mutat majd az eszközazonosító semmilyen tételre",
"instruction_3": "Ez a funkció korai fejlesztési szakaszban van, és a jövőbeli kiadásokban változhat, ha visszajelzésed van, kérlek\nírd meg nekünk a '<a href=\"https://github.com/sysadminsmedia/homebox/discussions/53\">'GitHub vitafórumon'</a>'",
"label_height": "Címke magassága",
"label_width": "Címke szélessége",
"measure_type": "Mértékegység",
"page_bottom_padding": "Oldal alsó margója",
"page_height": "Oldal magassága",
"page_left_padding": "Oldal bal margója",
"page_right_padding": "Oldal jobb margója",
"page_top_padding": "Oldal felső margója",
"page_width": "Oldalszélesség",
"qr_code_example": "QR-kód példa",
"tip_1": "Az alapértelmezett beállítások\n'<a href=\"https://www.avery.com/templates/5260\">'Avery 5260 címkeívek'</a>'nek felelnek meg. Ha más íveket használsz,\n módosítanod kell a beállításokat, hogy a laphoz igazodjanak.",
"tip_2": "Ha a lapot testre szabod, a méretek hüvelykben vannak megadva. A 5260 ívek létrehozásakor feltűnt,\n hogy a hivatalosan megadott méretek nem azonosak a megfelelő szövegdoboz méretekkel.\n'<b>'Szükséged lehet néhány próbálkozásra a megfelelő méretek megtalálásához.'</b>'",
"tip_3": "Nyomtatáskor ügyelj a következőkre:\n '<ol><li>'Állítsd a margókat 0-ra vagy Nincsre'</li><li>'Állítsd a skálázást 100% -ra'</li><li>'Tiltsd le a kétoldalas nyomtatást'</li><li>'Több oldal nyomtatása előtt nyomtass tesztoldalt'</li></ol>'",
"tips": "Tippek",
"title": "Címkegenerátor",
"toast": {
"page_too_small_card": ""
"page_too_small_card": "Az oldal mérete túl kicsi a kártya méretéhez képest"
}
}
},
"scanner": {
"barcode_detected_message": "termék vonalkód észlelve",
"barcode_fetch_data": "Termékadatok lekérése",
"error": "Hiba történt a szkennelés közben",
"invalid_url": "Érvénytelen vonalkód URL",
"no_sources": "Nincs elérhető videóforrás",
"permission_denied": "",
"permission_denied": "A kamera engedélye megtagadva, engedélyezd a kamerához való hozzáférést a böngésző beállításaiban",
"select_video_source": "Videóforrás kiválasztása",
"title": "Szkenner",
"unsupported": "A Media Stream API nem támogatott HTTPS nélkül"
@@ -562,20 +586,24 @@
"tools": {
"actions": "Készletműveletek",
"actions_set": {
"create_missing_thumbnails": "Hiányzó bélyegképek létrehozása",
"create_missing_thumbnails_button": "Bélyegképek létrehozása",
"create_missing_thumbnails_confirm": "Biztosan létrehozod a hiányzó bélyegképeket? Ez eltarthat egy ideig, és nem lehet szüneteltetni.",
"create_missing_thumbnails_sub": "Bélyegképeket hoz létre minden olyan melléklethez, melyet a jelenlegi konfiguráció támogat. Ez abban az esetben lehet hasznos, ha a mellékletek a Homebox v0.20.0 verziója előtt lettek feltöltve. A folyamat nem ír felül már létező bélyegképeket, csak újakat készít, ha a melléklet még nem rendelkezik ilyennel. Figyelem, a bélyegképeket egy háttérfolyamat generálja, és egy ideig eltarthat, amíg mind elkészülnek.",
"ensure_ids": "Eszközazonosítók meglétének biztosítása",
"ensure_ids_button": "Eszközazonosítók generálása",
"ensure_ids_confirm": "",
"ensure_ids_confirm": "Biztos elindítod az eszközazonosítók generálását? Ez eltarthat egy ideig, és nem lehet visszavonni.",
"ensure_ids_sub": "Biztosítja, hogy a készletben lévő összes tétel rendelkezzen érvényes asset_id (eszközazonosító) mezővel. Ehhez megkeresi a legmagasabb asset_id mezőértéket az adatbázisban és minden olyan tételhez, amelynek nem beállított az asset_id mezője, rendre eggyel növelt értéket állít be. Ezt a created_at mezők értékének (a tétel létrehozásának dátuma) sorrendjében teszi.",
"ensure_import_refs": "Importálási hivatkozások meglétének biztosítása",
"ensure_import_refs_button": "Hivatkozások generálása",
"ensure_import_refs_sub": "Biztosítja, hogy a készletben lévő összes tétel rendelkezzen érvényes import_ref mezővel. Véletlenszerűen generál egy 8 hosszúságú karakterláncot minden olyan tételhez, amelynél az import_ref mező üres.",
"set_primary_photo": "Elsődleges fénykép hozzárendelése",
"set_primary_photo_button": "Hozzárendelés",
"set_primary_photo_confirm": "",
"set_primary_photo_confirm": "Biztosan elindítod az elsődleges fényképek hozzárendelését? Ez eltarthat egy ideig, és nem lehet visszavonni.",
"set_primary_photo_sub": "A Homebox v0.10.0 verziójában hozzáadtuk a fénykép típusú mellékletekhez az elsődleges fényképként történő megjelölés lehetőségét. Ezzel a művelettel a mellékletekben található első fényképet állítod be elsődleges fényképnek, ha ilyen a tételhez még nincs kiválasztva. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/pull/576\">'Lásd az #576 GitHub PR-t'</a>'",
"zero_datetimes": "Idő törlése a tételek dátummezőiből",
"zero_datetimes_button": "Dátummezők javítása",
"zero_datetimes_confirm": "",
"zero_datetimes_confirm": "Biztosan elindítod az összes dátummező időértékének törlését? Ez eltarthat egy ideig, és nem lehet visszavonni.",
"zero_datetimes_sub": "Visszaállítja a dátumot és időt tartalmazó mezők értékét a dátum kezdetére a teljes készletben. Ezzel javíthatsz egy olyan bugot, mely során az oldal fejlesztésének korai szakaszában az időértékek mentése a dátumok pontos megjelenítésében hibát okozott. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/issues/236\" target=\"_blank\">'Lásd a #236 Github Issue-t további részletekért.'</a>'"
},
"actions_sub": "Műveletek tömeges alkalmazása a készletre. Ezeket a vissza nem vonható műveleteket csak '<b>'kellő körültekintés mellett használd'</b>'.",
@@ -586,7 +614,7 @@
"export_sub": "Exportálja a Homebox szabványos CSV formátumát. Ez minden készletedben található tételt exportál.",
"import": "Készlet importálása",
"import_button": "Készlet importálása",
"import_ref_confirm": "",
"import_ref_confirm": "Biztos elindítod az importálási hivatkozások meglétének biztosítását? Ez eltarthat egy ideig, és nem lehet visszavonni.",
"import_sub": "Importálja a Homebox szabványos CSV formátumát. Amennyiben nem található '<code>'HB.import_ref'</code>' oszlop a fájlban, ez '<b>'nem'</b>' ír felül létező tételeket a készletedben, csak újakat ad hozzá. Azon sorok, melyeknél a '<code>'HB.import_ref'</code>' oszlop értéke megegyezik egy létező tétel import_ref mezőjének értékével, a sor tartalma beolvad a létező tételbe."
},
"import_export_sub": "Készlet importálása és exportálása CSV-fájlba és CSV-fájlból. Ez hasznos lehet a készleted átmozgatásához a Homebox egy új példányába.",
@@ -601,11 +629,12 @@
},
"reports_sub": "Hozz létre különböző jelentéseket a készletedhez.",
"toast": {
"asset_success": "",
"failed_ensure_ids": "",
"failed_ensure_import_refs": "",
"failed_set_primary_photos": "",
"failed_zero_datetimes": ""
"asset_success": "{ results } eszköz frissítve.",
"failed_create_missing_thumbnails": "Hiányzó bélyegképek létrehozása sikertelen.",
"failed_ensure_ids": "Nem sikerült biztosítani az eszközazonosítók létezését.",
"failed_ensure_import_refs": "Importálási hivatkozások meglétének biztosítása sikertelen.",
"failed_set_primary_photos": "Nem sikerült beállítani az elsődleges fényképeket.",
"failed_zero_datetimes": "Nem sikerült visszaállítani a dátum- és időértékeket."
}
}
}

View File

@@ -1,20 +1,10 @@
{
"components": {
"app": {
"create_modal": {
"createAndAddAnother": "",
"enter": "",
"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",
"toast": {
"import_failed": "",
"import_success": "",
"please_select_file": ""
}
"title": "Impor CSV"
},
"outdated": {
"current_version": "Versi Terkini",
@@ -24,11 +14,6 @@
"new_version_available_link": "Klik di sini untuk melihat informasi rilis"
}
},
"form": {
"password": {
"toggle_show": ""
}
},
"global": {
"copy_text": {
"documentation": "dokumentasi",
@@ -61,62 +46,20 @@
"yesterday": "kemaren"
},
"label_maker": {
"browser_print": "",
"confirm_description": "",
"download": "Unduh Label",
"print": "",
"server_print": "",
"titles": "",
"toast": {
"load_status_failed": "",
"print_failed": "",
"print_success": ""
}
"download": "Unduh Label"
},
"page_qr_code": {
"page_url": "Halaman URL",
"qr_tooltip": ""
"page_url": "Halaman URL"
},
"password_score": {
"password_strength": "Kompleksitas kata sandi"
}
},
"item": {
"attachments_list": {
"download": "",
"open_new_tab": ""
},
"create_modal": {
"delete_photo": "",
"item_description": "Deskripsi item",
"item_name": "Nama item",
"item_photo": "",
"item_quantity": "",
"parent_item": "",
"rotate_photo": "",
"set_as_primary_photo": "",
"title": "Buat item",
"toast": {
"already_creating": "",
"create_failed": "",
"create_success": "",
"failed_load_parent": "",
"no_canvas_support": "",
"please_select_location": "",
"rotate_failed": "",
"rotate_process_failed": "",
"some_photos_failed": "",
"upload_failed": "",
"upload_success": "",
"uploading_photos": ""
},
"upload_photos": "",
"uploaded": ""
},
"selector": {
"no_results": "",
"placeholder": "",
"search_placeholder": ""
"title": "Buat item"
},
"view": {
"selectable": {
@@ -126,11 +69,8 @@
"table": "Tabel"
},
"table": {
"headers": "",
"page": "Halaman",
"rows_per_page": "Baris per halaman",
"table_settings": "",
"view_item": ""
"rows_per_page": "Baris per halaman"
}
}
},
@@ -138,87 +78,54 @@
"create_modal": {
"label_description": "Keterangan/Deskripsi",
"label_name": "Nama",
"title": "Buat label",
"toast": {
"already_creating": "",
"create_failed": "",
"create_success": "",
"label_name_too_long": ""
}
},
"selector": {
"select_labels": ""
"title": "Buat label"
}
},
"location": {
"create_modal": {
"location_description": "Deskripsi lokasi",
"location_name": "Nama lokasi",
"title": "Tambah Lokasi",
"toast": {
"already_creating": "",
"create_failed": "",
"create_success": ""
}
"title": "Tambah Lokasi"
},
"selector": {
"no_location_found": "",
"parent_location": "Lokasi Induk",
"search_location": "",
"select_location": ""
"parent_location": "Lokasi Induk"
},
"tree": {
"no_locations": "Tidak ada lokasi yang tersedia. Tambahkan lokasi melalui tombol\n`<`span class=\"link-primary\"`>`Buat`<`/span`>` menu navigasi."
}
},
"quick_menu": {
"no_results": "",
"shortcut_hint": "Gunakan tombol angka untuk memilih."
}
},
"global": {
"add": "Tambah",
"archived": "",
"build": "Kompilasi: { build }",
"cancel": "",
"confirm": "Konfirmasi",
"create": "Buat",
"create_and_add": "Buat dan Tambah Baru",
"create_subitem": "",
"created": "Behasil dibuat",
"delete": "Hapus",
"delete_confirm": "",
"demo_instance": "",
"details": "Detail",
"duplicate": "Duplikat",
"edit": "Sunting",
"email": "Email",
"follow_dev": "Ikuti Pengembang",
"footer": {
"api_link": "",
"version_link": ""
},
"github": "Github project",
"insured": "",
"items": "Barang",
"join_discord": "Bergabunglah dengan Discord",
"labels": "Label",
"loading": "",
"locations": "Lokasi",
"maintenance": "Perbaikan",
"name": "Nama",
"navigate": "Navigasi",
"password": "Kata Sandi",
"quantity": "",
"read_docs": "Baca Dokumen",
"return_home": "",
"save": "Simpan",
"search": "Cari",
"sign_out": "Keluar",
"submit": "Kirim",
"unknown": "",
"update": "Perbaharui",
"updating": "",
"value": "Nilai",
"version": "Versi:{ version }",
"welcome": "Selamay datang, { username }"
@@ -243,49 +150,27 @@
"set_email": "Apa email Anda?",
"set_name": "Apa nama anda?",
"set_password": "Password Anda",
"tagline": "Lacak, Atur, dan Kelola Barang-barangmu.",
"title": "",
"toast": {
"invalid_email": "",
"invalid_email_password": "",
"login_success": "",
"problem_registering": "",
"user_registered": ""
}
"tagline": "Lacak, Atur, dan Kelola Barang-barangmu."
},
"items": {
"add": "Tambah",
"advanced": "Tingkat Lanjut",
"archived": "Diarsipkan",
"asset_id": "ID Aset",
"associated_with_multiple": "",
"attachment": "Lampiran",
"attachments": "Lampiran",
"changes_persisted_immediately": "Perubahan lampiran akan segera disimpan",
"created_at": "Dibuat Pada",
"custom_fields": "Informasi Tambahan",
"delete_attachment_confirm": "",
"delete_item_confirm": "",
"description": "Deskripsi",
"details": "Detail",
"drag_and_drop": "Seret dan lepas file di sini atau klik untuk memilih file",
"edit": {
"edit_attachment_dialog": {
"attachment_title": "",
"attachment_type": "",
"primary_photo": "",
"primary_photo_sub": "",
"select_type": "",
"title": ""
}
},
"edit_details": "Edit Detail",
"field_selector": "Selektor",
"field_value": "Nilai",
"first": "Pertama",
"include_archive": "Sertakan Item yang Diarsipkan",
"insured": "Diasuransikan",
"invalid_asset_id": "",
"last": "Terakhir",
"lifetime_warranty": "Garansi seumur hidup",
"location": "Lokasi",
@@ -296,7 +181,6 @@
"name": "Nama",
"negate_labels": "Negasikan Label yang Dipilih",
"next_page": "Halaman Berikutnya",
"no_attachments": "",
"no_results": "Item tidak ditemukan",
"notes": "Catatan",
"only_with_photo": "Hanya item dengan foto",
@@ -318,88 +202,42 @@
"receipts": "Resi",
"reset_search": "Reset Pencarian",
"results": "{ total } Hasil",
"select_field": "",
"serial_number": "Nomor Seri",
"show_advanced_view_options": "Tampilkan opsi lanjutan",
"sold_at": "Dijual Pada",
"sold_details": "Detail Penjualan",
"sold_price": "Harga Jual",
"sold_to": "Dijual Kepada",
"sync_child_locations": "",
"tip_1": "Filter untuk lokasi dan label bekerja dengan operasi 'OR'. Artinya, jika Anda memilih lebih dari satu filter, hanya satu\nfilter yang harus cocok agar data ditampilkan.",
"tip_2": "Pencarian yang diawali dengan '#'' akan meminta ID aset (contoh '#000 -001 ')",
"tip_3": "Filter untuk lokasi dan label bekerja dengan operasi 'OR'. Artinya, jika Anda memilih lebih dari satu filter, hanya satu\nfilter yang harus cocok agar data ditampilkan.",
"tips": "Anjuran",
"tips_sub": "Tips Pencarian",
"toast": {
"asset_not_found": "",
"attachment_deleted": "",
"attachment_updated": "",
"attachment_uploaded": "",
"child_items_location_no_longer_synced": "",
"child_items_location_synced": "",
"child_location_desync": "",
"error_loading_parent_data": "",
"failed_adjust_quantity": "",
"failed_delete_attachment": "",
"failed_delete_item": "",
"failed_duplicate_item": "",
"failed_load_asset": "",
"failed_load_item": "",
"failed_load_items": "",
"failed_save": "",
"failed_save_no_location": "",
"failed_search_items": "",
"failed_update_attachment": "",
"failed_upload_attachment": "",
"item_deleted": "",
"item_saved": "",
"quantity_cannot_negative": "",
"sync_child_location": ""
},
"updated_at": "Diperbarui pada",
"warranty": "Garansi",
"warranty_details": "Rincian Garansi",
"warranty_expires": "Garansi Kedaluwarsa"
},
"labels": {
"label_delete_confirm": "",
"no_results": "Tidak Ditemukan Label",
"toast": {
"failed_delete_label": "",
"failed_load_label": "",
"failed_update_label": "",
"label_deleted": "",
"label_updated": ""
},
"update_label": "Perbarui Label"
},
"languages": {
"ca": "Catalan",
"cs-CZ": "",
"de": "Jerman",
"en": "Bahasa Inggris",
"es": "Spanyol",
"fi-FI": "",
"fr": "Prancis",
"hu": "Hungaria",
"id-ID": "",
"it": "Italia",
"ja-JP": "Jepang",
"ko-KR": "",
"lb-LU": "",
"lt-LT": "",
"nb-NO": "",
"nl": "Belanda",
"pl": "Polandia",
"pt-BR": "Portugis (Brasil)",
"pt-PT": "Bahasa Portugis (Portugal)",
"ru": "Rusia",
"sl": "Bahasa Slovenia",
"sq-AL": "",
"sv": "Swedia",
"ta-IN": "",
"th-TH": "",
"tr": "Turki",
"uk-UA": "Ukraina",
"zh-CN": "Mandarin (Disederhanakan)",
@@ -413,16 +251,7 @@
"locations": {
"child_locations": "Lokasi Turunan",
"collapse_tree": "Ciutkan",
"expand_tree": "",
"location_items_delete_confirm": "",
"no_results": "Lokasi tidak ditemukan",
"toast": {
"failed_delete_location": "",
"failed_load_location": "",
"failed_update_location": "",
"location_deleted": "",
"location_updated": ""
},
"update_location": "Perbarui Lokasi"
},
"maintenance": {
@@ -468,7 +297,6 @@
"locations": "Lokasi",
"maintenance": "Pemeliharaan",
"profile": "Profil",
"scanner": "",
"search": "Cari",
"tools": "Tools"
},
@@ -478,10 +306,7 @@
"currency_format": "Format Mata uang",
"current_password": "Kata sandi saat ini",
"delete_account": "Hapus Akun",
"delete_account_confirm": "",
"delete_account_sub": "Hapus akun Anda dan semua data terkait.",
"delete_notifier_confirm": "",
"display_legacy_header": "",
"enabled": "Tersedia",
"example": "Contoh",
"gen_invite": "Buat Tautan Undangan",
@@ -491,91 +316,32 @@
"language": "Bahasa",
"new_password": "Kata Sandi Baru",
"no_notifiers": "Tidak ada notifier yang dikonfigurasi",
"no_override": "",
"notifier_modal": "{ type, select, true {Ubah} false {Buat} other {Lainnya}} Notifier",
"notifiers": "Notifier",
"notifiers_sub": "Dapatkan pemberitahuan untuk pengingat pemeliharaan mendatang",
"override_locale": "",
"test": "Pengujian",
"theme_settings": "Pengaturan Tampilan",
"theme_settings_sub": "Pengaturan tema disimpan di penyimpanan lokal browser Anda. Anda dapat mengubah tema kapan saja. Jika Anda\nmengalami masalah dalam mengatur tema, coba refresh browser Anda.",
"toast": {
"account_deleted": "",
"failed_change_password": "",
"failed_create_notifier": "",
"failed_delete_account": "",
"failed_delete_notifier": "",
"failed_get_currencies": "",
"failed_test_notifier": "",
"failed_update_group": "",
"failed_update_notifier": "",
"group_updated": "",
"notifier_test_success": "",
"password_changed": ""
},
"update_group": "Perbarui Grup",
"update_language": "Perbarui Bahasa",
"url": "URL",
"user_profile": "Profil Pengguna",
"user_profile_sub": "Undang pengguna, dan kelola akun Anda."
},
"reports": {
"label_generator": {
"asset_end": "",
"asset_start": "",
"base_url": "",
"bordered_labels": "",
"generate_page": "",
"input_placeholder": "",
"instruction_1": "",
"instruction_2": "",
"instruction_3": "",
"label_height": "",
"label_width": "",
"measure_type": "",
"page_bottom_padding": "",
"page_height": "",
"page_left_padding": "",
"page_right_padding": "",
"page_top_padding": "",
"page_width": "",
"qr_code_example": "",
"tip_1": "",
"tip_2": "",
"tip_3": "",
"tips": "",
"title": "",
"toast": {
"page_too_small_card": ""
}
}
},
"scanner": {
"error": "",
"invalid_url": "",
"no_sources": "",
"permission_denied": "",
"select_video_source": "",
"title": "",
"unsupported": ""
},
"tools": {
"actions": "Kelola Inventaris",
"actions_set": {
"ensure_ids": "Pastikan ID Aset",
"ensure_ids_button": "Pastikan ID Aset",
"ensure_ids_confirm": "",
"ensure_ids_sub": "Memastikan semua item dalam inventaris Anda memiliki kolom asset_id yang valid. Hal ini dilakukan dengan mencari nilai asset_id tertinggi saat ini dalam database dan menerapkan nilai berikutnya ke setiap item yang kolom asset_id-nya belum diatur. Proses ini diurutkan berdasarkan kolom created_at.",
"ensure_import_refs": "Pastikan Ref Impor",
"ensure_import_refs_button": "Pastikan Ref Impor",
"ensure_import_refs_sub": "Memastikan semua item dalam inventaris Anda memiliki kolom import_ref yang valid. Hal ini dilakukan dengan membuat string 8 karakter acak untuk setiap item yang kolom import_ref-nya belum diatur.",
"set_primary_photo": "Atur Foto Utama",
"set_primary_photo_button": "Atur Foto Utama",
"set_primary_photo_confirm": "",
"set_primary_photo_sub": "Pada Homebox versi 0.10.0, kolom gambar utama ditambahkan ke lampiran bertipe foto. Ini akan mengatur kolom gambar utama ke gambar pertama dalam lampiran di database, jika belum diatur. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/pull/576\">'See GitHub PR #576'</a>'",
"zero_datetimes": "Format Waktu Kosong",
"zero_datetimes_button": "Format Waktu Kosong",
"zero_datetimes_confirm": "",
"zero_datetimes_sub": "Mengatur ulang nilai waktu untuk semua bidang tanggal dan waktu di inventaris Anda ke awal tanggal. Ini untuk memperbaiki bug yang muncul di awal pengembangan situs yang menyebabkan nilai waktu disimpan bersama waktu, yang mengakibatkan masalah pada tampilan nilai tanggal yang akurat. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/issues/236\" target=\"_blank\">'See Github Issue #236 for more details.'</a>'"
},
"actions_sub": "Terapkan tindakan ke inventaris Anda secara massal. Tindakan ini tidak dapat diurungkan. <b>Hati-hati.</b>",
@@ -586,7 +352,6 @@
"export_sub": "Mengekspor semua item ke file CSV dengan format standar Homebox.",
"import": "Impor Inventaris",
"import_button": "Impor Inventaris",
"import_ref_confirm": "",
"import_sub": "Mengimpor CSV dengan format standar Homebox. Tanpa kolom '<code>HB.import_ref</code>', impor ini <b>tidak</b> akan menimpa item yang sudah ada di inventaris Anda, hanya menambahkan item baru. Baris dengan kolom '<code>HB.import_ref</code>' akan digabungkan ke item yang sudah ada dengan import_ref yang sama, jika ada."
},
"import_export_sub": "Impor dan ekspor inventaris Anda ke dan dari file CSV. Ini berguna untuk memigrasikan inventaris Anda ke instance Homebox yang baru.",
@@ -599,13 +364,6 @@
"bill_of_materials_button": "Download Daftar Inventaris",
"bill_of_materials_sub": "Menghasilkan file CSV (Comma Separated Values) yang dapat diimpor ke program spreadsheet. Ini adalah ringkasan inventaris Anda dengan info dasar beserta harganya."
},
"reports_sub": "Buat laporan yang berbeda untuk inventaris Anda.",
"toast": {
"asset_success": "",
"failed_ensure_ids": "",
"failed_ensure_import_refs": "",
"failed_set_primary_photos": "",
"failed_zero_datetimes": ""
}
"reports_sub": "Buat laporan yang berbeda untuk inventaris Anda."
}
}

View File

@@ -24,6 +24,13 @@
"new_version_available_link": "Clicca qui per visualizzare le note di rilascio"
}
},
"color_selector": {
"clear": "Pulisci il colore",
"color": "Colore",
"no_color": "Nessun colore",
"no_color_selected": "Nessun colore selezionato",
"randomize": "Colore casuale"
},
"form": {
"password": {
"toggle_show": "Attiva/disattiva visualizzazione password"
@@ -93,6 +100,7 @@
"item_photo": "Foto dell'articolo 📷",
"item_quantity": "Quantità Articoli",
"parent_item": "Articolo principale",
"product_tooltip_scan_barcode": "Riempimento automatico con un codice a barre da 📷",
"rotate_photo": "Ruota foto",
"set_as_primary_photo": "Imposta come { isPrimary, select, true {non} false {} other {}} foto principale",
"title": "Crea Articolo",
@@ -113,10 +121,17 @@
"upload_photos": "Carica Foto",
"uploaded": "Foto caricata"
},
"product_import": {
"barcode": "Codice a barre del prodotto",
"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",
"search_item": "Cerca prodotto",
"title": "Importa prodotto"
},
"selector": {
"no_results": "Nessun risultato trovato",
"placeholder": "Seleziona...",
"search_placeholder": "Digita per cercare"
"placeholder": "Seleziona",
"search_placeholder": "Scrivi per cercare"
},
"view": {
"selectable": {
@@ -168,7 +183,7 @@
"select_location": "Seleziona una posizione"
},
"tree": {
"no_locations": "Nessuna posizione disponibile. Aggiungi nuove posizioni mediante il pulsante\n`<`span class=\"link-primary\"`>`Crea`<`/span`>` nella barra di navigazione."
"no_locations": "Nessuna posizione disponibile. Aggiungi nuove posizioni mediante il pulsante\n'<span class=\"link-primary\">'Crea'</span>' nella barra di navigazione."
}
},
"quick_menu": {
@@ -196,14 +211,14 @@
"follow_dev": "Segui lo Sviluppatore",
"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\"> Versione: { version } Build: { build } '</a>'"
"version_link": "'<'a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/{ version }\" target=\"_blank\"'>' Versione: { version } Build: { build } '</a>'"
},
"github": "Progetto GitHub",
"insured": "Assicurato",
"items": "Articoli",
"join_discord": "Unisciti a Discord",
"labels": "Etichette",
"loading": "Caricamento...",
"loading": "Caricamento",
"locations": "Posizioni",
"maintenance": "Manutenzione",
"name": "Nome",
@@ -280,12 +295,12 @@
}
},
"edit_details": "Modifica dettagli",
"field_selector": "Campo Selezione",
"field_selector": "Selezione in base ai campi",
"field_value": "Campo valore",
"first": "Primo",
"include_archive": "Includi Articoli Archiviati",
"insured": "Assicurato",
"invalid_asset_id": "ID dell'asset non valido.",
"invalid_asset_id": "ID dell'asset non valido",
"last": "Ultimo",
"lifetime_warranty": "Garanzia a vita",
"location": "Luogo",
@@ -311,7 +326,7 @@
"purchase_date": "Data di acquisto",
"purchase_details": "Dettagli dell'acquisto",
"purchase_price": "Prezzo di acquisto",
"purchased_from": "Acqistato da",
"purchased_from": "Acquistato da",
"quantity": "Quantità",
"query_id": "ID dell'Asset in Ricerca: { id }",
"receipt": "Ricevuta",
@@ -321,7 +336,7 @@
"select_field": "Seleziona un campo",
"serial_number": "Numero seriale",
"show_advanced_view_options": "Mostra opzioni di visualizzazione avanzate",
"sold_at": "Venduto su",
"sold_at": "Venduto il",
"sold_details": "Dettagli di vendita",
"sold_price": "Prezzo di vendita",
"sold_to": "Venduto a",
@@ -332,49 +347,19 @@
"tips": "Suggerimenti",
"tips_sub": "Suggerimenti per la Ricerca",
"toast": {
"asset_not_found": "",
"attachment_deleted": "",
"attachment_updated": "",
"attachment_uploaded": "",
"child_items_location_no_longer_synced": "",
"child_items_location_synced": "",
"child_location_desync": "",
"error_loading_parent_data": "",
"failed_adjust_quantity": "",
"failed_delete_attachment": "",
"failed_delete_item": "",
"failed_duplicate_item": "",
"failed_load_asset": "",
"failed_load_item": "",
"failed_load_items": "",
"failed_save": "",
"failed_save_no_location": "",
"failed_search_items": "",
"failed_update_attachment": "",
"failed_upload_attachment": "",
"item_deleted": "",
"item_saved": "",
"quantity_cannot_negative": "",
"sync_child_location": ""
"quantity_cannot_negative": "La quantità non può essere negativa"
},
"updated_at": "Aggiornato Il",
"warranty": "Garanzia",
"warranty_details": "Dettagli garanzia",
"warranty_expires": "Garanzia scaduta"
"warranty_expires": "La garanzia scade il"
},
"labels": {
"label_delete_confirm": "",
"no_results": "Nessuna etichetta trovata",
"toast": {
"failed_delete_label": "",
"failed_load_label": "",
"failed_update_label": "",
"label_deleted": "",
"label_updated": ""
},
"update_label": "Aggiorna etichetta"
},
"languages": {
"bs-BA": "Bosniaco (Bosnia ed Erzegovina)",
"ca": "Catalano",
"cs-CZ": "Ceco",
"de": "Tedesco",
@@ -401,6 +386,7 @@
"th-TH": "Tailandese",
"tr": "Turco",
"uk-UA": "Ucraino",
"vi-VN": "Vietnamita",
"zh-CN": "Cinese (semplificato)",
"zh-HK": "Cinese Mandarino",
"zh-MO": "Cinese (Macao)",
@@ -414,15 +400,7 @@
"child_locations": "Ubicazione figlia",
"collapse_tree": "Contrai albero",
"expand_tree": "Espandi albero",
"location_items_delete_confirm": "",
"no_results": "Nessuna posizione trovata",
"toast": {
"failed_delete_location": "",
"failed_load_location": "",
"failed_update_location": "",
"location_deleted": "",
"location_updated": ""
},
"update_location": "Aggiorna ubicazione"
},
"maintenance": {
@@ -478,9 +456,7 @@
"currency_format": "Formato Valuta",
"current_password": "Password Corrente",
"delete_account": "Elimina Account",
"delete_account_confirm": "",
"delete_account_sub": "Elimina il tuo account e tutti i dati associati. Questa operazione non può essere annullata.",
"delete_notifier_confirm": "",
"display_legacy_header": "{ currentValue, select, true {Disable Legacy Header} false {Enable Legacy Header} other {Not Hit}}",
"enabled": "Abilitato",
"example": "Esempio",
@@ -499,20 +475,6 @@
"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": "",
"failed_change_password": "",
"failed_create_notifier": "",
"failed_delete_account": "",
"failed_delete_notifier": "",
"failed_get_currencies": "",
"failed_test_notifier": "",
"failed_update_group": "",
"failed_update_notifier": "",
"group_updated": "",
"notifier_test_success": "",
"password_changed": ""
},
"update_group": "Aggiorna Gruppo",
"update_language": "Aggiorna Lingua",
"url": "URL",
@@ -521,33 +483,13 @@
},
"reports": {
"label_generator": {
"asset_end": "",
"asset_start": "",
"base_url": "",
"bordered_labels": "",
"generate_page": "",
"input_placeholder": "",
"instruction_1": "",
"instruction_2": "",
"instruction_3": "",
"label_height": "",
"label_width": "",
"measure_type": "",
"page_bottom_padding": "",
"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\n '<a href=\"https://www.avery.com/templates/5260\">'fogli di etichette ''Avery 5260 '</a>'. Se stai utilizzando un foglio differente,\n\n devi modificare le impostazioni affinchè corrispondano al tuo foglio.",
"tip_2": "",
"tip_3": "",
"tips": "",
"title": "",
"toast": {
"page_too_small_card": ""
}
"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."
}
},
"scanner": {
@@ -564,18 +506,15 @@
"actions_set": {
"ensure_ids": "Verifica ID delle risorse",
"ensure_ids_button": "Verifica ID delle risorse",
"ensure_ids_confirm": "",
"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": "",
"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": "",
"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>'",
@@ -586,7 +525,6 @@
"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": "",
"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.",
@@ -599,13 +537,6 @@
"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.",
"toast": {
"asset_success": "",
"failed_ensure_ids": "",
"failed_ensure_import_refs": "",
"failed_set_primary_photos": "",
"failed_zero_datetimes": ""
}
"reports_sub": "Genera diversi report per il tuo inventario."
}
}

View File

@@ -2,31 +2,38 @@
"components": {
"app": {
"create_modal": {
"createAndAddAnother": "",
"enter": "",
"shift": ""
"createAndAddAnother": "{shiftKey} + {enterKey}を使用すると、別のアイテムをそのまま追加できます。",
"enter": "入る",
"shift": "シフト"
},
"import_dialog": {
"change_warning": "【注意】インポート時の動作が変更されました'<br>'\n選択されたCSVファイルにimport_refsの値が存在する場合、該当するアイテムはCSVファイルの値で上書きされます。",
"description": "アイテム、ラベル、ロケーション情報を含む CSV ファイルをインポートします。\nデータの形式など詳細については、ドキュメントを参照してください。",
"title": ".csvファイルのインポート",
"toast": {
"import_failed": "",
"import_success": "",
"please_select_file": ""
"import_failed": "読み込みに失敗しました。もう一度お試しください。",
"import_success": "インポートに成功しました",
"please_select_file": "ファイルを選択してください"
}
},
"outdated": {
"current_version": "現在のバージョン",
"dismiss": "",
"dismiss": "却下",
"latest_version": "最新バージョン",
"new_version_available": "利用可能な更新があります",
"new_version_available_link": "クリックしてリリースノートを表示"
}
},
"color_selector": {
"clear": "色をリセット",
"color": "色",
"no_color": "色設定なし",
"no_color_selected": "色を選択していません",
"randomize": "色をランダムに変更する"
},
"form": {
"password": {
"toggle_show": ""
"toggle_show": "パスワードの表示を切り替え"
}
},
"global": {
@@ -41,7 +48,7 @@
"days": "日",
"hour": "時間",
"hours": "時間",
"in": "",
"in": "{0}内",
"just-now": "たった今",
"last-month": "先月",
"last-week": "先週",
@@ -65,17 +72,17 @@
"confirm_description": "この管理ラベルを印刷しますか?",
"download": "画像として保存",
"print": "プリンターで印刷",
"server_print": "",
"server_print": "サーバーで印刷",
"titles": "管理ラベルの出力",
"toast": {
"load_status_failed": "",
"print_failed": "",
"print_success": ""
"load_status_failed": "ステータスの読み込みに失敗しました",
"print_failed": "ラベルの印刷に失敗しました",
"print_success": "ラベルを印刷しました"
}
},
"page_qr_code": {
"page_url": "ページ URL",
"qr_tooltip": ""
"qr_tooltip": "QRコードを表示"
},
"password_score": {
"password_strength": "パスワード強度"
@@ -83,40 +90,51 @@
},
"item": {
"attachments_list": {
"download": "",
"open_new_tab": ""
"download": "ダウンロード",
"open_new_tab": "新しいタブで開く"
},
"create_modal": {
"delete_photo": "",
"delete_photo": "写真を削除",
"item_description": "説明",
"item_name": "名称",
"item_photo": "",
"item_quantity": "",
"parent_item": "",
"rotate_photo": "",
"set_as_primary_photo": "",
"item_photo": "商品写真 📷",
"item_quantity": "数量",
"parent_item": "関連アイテム",
"product_tooltip_input_barcode": "バーコードを手動入力して自動取得を試みる",
"product_tooltip_scan_barcode": "バーコードを撮影して自動取得を試みる",
"rotate_photo": "写真を回転",
"set_as_primary_photo": "{isPrimary, select, true {non} false {} other {}} サムネイルに設定",
"title": "アイテム情報の追加",
"toast": {
"already_creating": "",
"create_failed": "",
"create_success": "",
"failed_load_parent": "",
"no_canvas_support": "",
"please_select_location": "",
"rotate_failed": "",
"rotate_process_failed": "",
"some_photos_failed": "",
"upload_failed": "",
"upload_success": "",
"uploading_photos": ""
"already_creating": "既に同じアイテムがあります",
"create_failed": "アイテムを作成できませんでした",
"create_success": "アイテムを作成しました",
"failed_load_parent": "親アイテムの読み込みに失敗しました。手動で選択してください",
"no_canvas_support": "このブラウザはHTML5 canvasをサポートしていません",
"please_select_location": "ロケーションを選択",
"rotate_failed": "画像の回転ができませんでした。エラー内容: { error }",
"rotate_process_failed": "画像回転の処理でエラーが発生しました",
"some_photos_failed": "{count, plural, = 0 {アップロード可能な画像がありません} = 1 {画像のアップロードに失敗しました} other {一部の画像のアップロードに失敗しました}}",
"upload_failed": "画像のアップロードに失敗しました ({ photoName })",
"upload_success": "{count, plural, = 0 {画像はアップロードされていません} = 1 {画像をアップロードしました} other {すべての画像をアップロードしました}}",
"uploading_photos": "{count, plural, = 0 {アップロード可能な画像がありません} = 1 {画像をアップロードしています} other {{count}枚の画像をアップロードしています…}}"
},
"upload_photos": "",
"uploaded": ""
"upload_photos": "画像をアップロード",
"uploaded": "アップロードした画像"
},
"product_import": {
"barcode": "商品のバーコード",
"db_source": "情報提供元",
"error_exception": "バーコード情報を取得できませんでした ",
"error_invalid_barcode": "このバーコードは使えません",
"error_not_found": "入力されたバーコードの製品が見つかりません。手動登録が必要です。",
"search_item": "商品を検索",
"title": "商品をインポート"
},
"selector": {
"no_results": "",
"placeholder": "",
"search_placeholder": ""
"no_results": "一致するものがありません",
"placeholder": "選択してください…",
"search_placeholder": "入力してください"
},
"view": {
"selectable": {
@@ -126,28 +144,29 @@
"table": "テーブル"
},
"table": {
"headers": "",
"headers": "ヘッダー",
"page": "ページ",
"rows_per_page": "表示件数",
"table_settings": "",
"view_item": ""
"table_settings": "テーブル表示の設定",
"view_item": "アイテムを見る"
}
}
},
"label": {
"create_modal": {
"label_color": "ラベルの色",
"label_description": "ラベルの説明",
"label_name": "ラベル名",
"title": "ラベルの追加",
"toast": {
"already_creating": "",
"create_failed": "",
"create_success": "",
"label_name_too_long": ""
"already_creating": "既に同じラベルがあります",
"create_failed": "ラベルを作成できませんでした",
"create_success": "ラベルを作成しました",
"label_name_too_long": "ラベル名は50文字以内で入力してください"
}
},
"selector": {
"select_labels": ""
"select_labels": "ラベルを選択"
}
},
"location": {
@@ -156,69 +175,72 @@
"location_name": "名称",
"title": "ロケーション(場所)の追加",
"toast": {
"already_creating": "",
"create_failed": "",
"create_success": ""
"already_creating": "既に同じロケーションがあります",
"create_failed": "ロケーションを作成できませんでした",
"create_success": "ロケーションを作成しました"
}
},
"selector": {
"no_location_found": "",
"no_location_found": "一致するロケーションがありません",
"parent_location": "親項目 (選択された項目の下位にネスト)",
"search_location": "",
"select_location": ""
"search_location": "ロケーションを検索",
"select_location": "ロケーションを選択"
},
"tree": {
"no_locations": "場所の項目は存在しません。\nナビゲーションバー上部の\"Create\"から追加してください。"
"no_locations": "ロケーションが設定されていません。\n左上の '<span class=\"link-primary\">'項目の追加'</span>' ボタンでロケーションを追加してください。"
}
},
"quick_menu": {
"no_results": "",
"no_results": "一致する項目はありません",
"shortcut_hint": "数字キーを入力することで対応する操作を実行できます"
}
},
"errors": {
"api_failure": "サーバーとの通信に失敗しました "
},
"global": {
"add": "追加",
"archived": "",
"archived": "アーカイブ済み",
"build": "ビルド番号: { build }",
"cancel": "",
"cancel": "キャンセル",
"confirm": "確認が必要です",
"create": "追加する",
"create": "項目の追加",
"create_and_add": "続けて追加できます",
"create_subitem": "",
"created": "",
"create_subitem": "サブアイテムを作成",
"created": "作成済み",
"delete": "削除",
"delete_confirm": "",
"demo_instance": "",
"delete_confirm": "このアイテムを削除しますか? ",
"demo_instance": "これはデモ環境です。データは新しいバージョンになると初期化されます。",
"details": "製品の情報",
"duplicate": "複製",
"edit": "編集",
"email": "メール",
"follow_dev": "開発者をフォローする",
"footer": {
"api_link": "",
"version_link": ""
"api_link": "'<a href=\"https://homebox.software/en/api/\" target=\"_blank\">'APIリファレンス(英語)'</a>'",
"version_link": "'<'a href=\"https://github.com/sysadminsmedia/homebox/releases/tag/{ version }\" target=\"_blank\"'>' Version: { version } Build: { build } '</a>'"
},
"github": "GitHub プロジェクト",
"insured": "",
"insured": "保険適用",
"items": "アイテム",
"join_discord": "Discordサーバーへの参加",
"labels": "ラベル",
"loading": "",
"loading": "読込中…",
"locations": "ロケーション",
"maintenance": "メンテナンス情報",
"name": "名前",
"navigate": "",
"navigate": "移動",
"password": "パスワード",
"quantity": "数量",
"read_docs": "ドキュメントを読む",
"return_home": "",
"return_home": "トップページに戻る",
"save": "保存",
"search": "検索",
"sign_out": "ログアウト",
"submit": "送信",
"unknown": "",
"unknown": "不明",
"update": "アップデート",
"updating": "",
"updating": "更新中",
"value": "値",
"version": "現在のバージョン: { version }",
"welcome": "{ username } でログイン中"
@@ -239,18 +261,18 @@
"joining_group": "既存のグループの招待を受け取りました!",
"login": "ログイン",
"register": "新規登録",
"remember_me": "ログインを維持する",
"remember_me": "ログインしたままにする",
"set_email": "メールアドレスを入力してください",
"set_name": "お名前は何ですか?",
"set_password": "パスワードを入力してください",
"tagline": "Track, Organize, and Manage your Things.",
"title": "",
"title": "Organize and Tag Your Stuff",
"toast": {
"invalid_email": "",
"invalid_email_password": "",
"login_success": "",
"problem_registering": "",
"user_registered": ""
"invalid_email": "無効なメールアドレス",
"invalid_email_password": "無効なメールアドレスまたはパスワードです",
"login_success": "ログインしました",
"problem_registering": "ユーザー情報を登録できませんでした",
"user_registered": "ユーザーを登録しました"
}
},
"items": {
@@ -258,25 +280,25 @@
"advanced": "詳細項目の表示",
"archived": "アーカイブ済み",
"asset_id": "Asset ID",
"associated_with_multiple": "",
"associated_with_multiple": "このAsset IDは重複しています",
"attachment": "添付ファイル",
"attachments": "添付ファイル",
"changes_persisted_immediately": "添付ファイルの保存は自動で行われます",
"created_at": "作成日",
"custom_fields": "カスタム項目",
"delete_attachment_confirm": "",
"delete_item_confirm": "",
"delete_attachment_confirm": "この添付ファイルを削除しますか?",
"delete_item_confirm": "このアイテムを削除しますか?",
"description": "説明",
"details": "この製品の情報",
"drag_and_drop": "添付ファイルはドラッグ&ドロップ、もしくはクリックで追加できます",
"edit": {
"edit_attachment_dialog": {
"attachment_title": "",
"attachment_type": "",
"primary_photo": "",
"primary_photo_sub": "",
"select_type": "",
"title": ""
"attachment_title": "添付ファイルのタイトル",
"attachment_type": "添付ファイルの種類",
"primary_photo": "サムネイルに設定",
"primary_photo_sub": "この画像をアイテムサムネイルに設定します。既に画像が設定されている場合は、このチェックボックスを有効にすると設定が上書きされます。",
"select_type": "種類を選択する",
"title": "添付ファイルの編集"
}
},
"edit_details": "基本的な情報の編集",
@@ -285,7 +307,7 @@
"first": "最初の",
"include_archive": "「現在は使用していない」にチェックを入れた項目を検索に含める",
"insured": "保険適用",
"invalid_asset_id": "",
"invalid_asset_id": "このAsset IDは無効です",
"last": "最後",
"lifetime_warranty": "無期限保証",
"location": "場所",
@@ -296,7 +318,7 @@
"name": "名称",
"negate_labels": "選択されたラベルを除外",
"next_page": "次の ページ",
"no_attachments": "",
"no_attachments": "添付ファイルはありません",
"no_results": "一致する項目はありません",
"notes": "備考",
"only_with_photo": "写真付きのアイテムのみ",
@@ -318,44 +340,44 @@
"receipts": "レシート(領収書)",
"reset_search": "検索条件をクリア",
"results": "検索結果: { total } 件",
"select_field": "",
"select_field": "フィールドを選択",
"serial_number": "シリアル番号(S/N)",
"show_advanced_view_options": "高度なオプションを表示",
"sold_at": "売却日",
"sold_details": "売却時の情報",
"sold_price": "売却価格",
"sold_to": "売却先",
"sync_child_locations": "",
"sync_child_locations": "ペア設定されたアイテムの場所も同時に変更する",
"tip_1": "フィルター条件を複数選択した場合、\nいずれかに該当している項目すべてをフィルター条件に一致しているとみなします。",
"tip_2": "Asset IDで検索する場合は「#」を先頭に入力してください (例: '#000-001')",
"tip_3": "カスタム値絞り込み(Field Selector)は、\n複数選択された場合にどれかが該当するアイテムすべてを一致しているとみなしします。",
"tips": "ヒント",
"tips_sub": "検索に関するヒント",
"toast": {
"asset_not_found": "",
"attachment_deleted": "",
"attachment_updated": "",
"attachment_uploaded": "",
"child_items_location_no_longer_synced": "",
"child_items_location_synced": "",
"child_location_desync": "",
"error_loading_parent_data": "",
"failed_adjust_quantity": "",
"failed_delete_attachment": "",
"failed_delete_item": "",
"failed_duplicate_item": "",
"failed_load_asset": "",
"failed_load_item": "",
"failed_load_items": "",
"failed_save": "",
"failed_save_no_location": "",
"failed_search_items": "",
"failed_update_attachment": "",
"failed_upload_attachment": "",
"item_deleted": "",
"item_saved": "",
"quantity_cannot_negative": "",
"sync_child_location": ""
"asset_not_found": "アイテムがありません",
"attachment_deleted": "添付ファイルを削除しました",
"attachment_updated": "添付ファイルを更新",
"attachment_uploaded": "添付ファイルをアップロードしました",
"child_items_location_no_longer_synced": "子アイテムのロケーションはこのアイテムと同期されなくなります。",
"child_items_location_synced": "子アイテムのロケーションはこのアイテムと同期されています",
"child_location_desync": "ロケーションを変更すると、親アイテムのロケーション同期が解除されます",
"error_loading_parent_data": "親アイテムの読み込み中にエラーが発生しました",
"failed_adjust_quantity": "数量の変更に失敗しました",
"failed_delete_attachment": "添付ファイルの削除に失敗しました",
"failed_delete_item": "アイテムの削除に失敗しました",
"failed_duplicate_item": "アイテムの複製に失敗しました",
"failed_load_asset": "アセットの読み込みに失敗しました",
"failed_load_item": "アイテムの読み込みに失敗しました",
"failed_load_items": "アイテムの読み込みに失敗しました",
"failed_save": "アイテム設定の保存に失敗しました",
"failed_save_no_location": "アイテム設定の保存に失敗しました: ロケーションを選択してください",
"failed_search_items": "アイテム検索に失敗しました",
"failed_update_attachment": "添付ファイルの更新に失敗しました",
"failed_upload_attachment": "添付ファイルのアップロードに失敗しました",
"item_deleted": "アイテムを削除しました",
"item_saved": "アイテム設定を保存しました",
"quantity_cannot_negative": "数量はマイナスにできません",
"sync_child_location": "ペア設定されたアイテムのロケーションも更新されました。"
},
"updated_at": "更新日",
"warranty": "保証書",
@@ -363,18 +385,19 @@
"warranty_expires": "保証期間"
},
"labels": {
"label_delete_confirm": "",
"label_delete_confirm": "このラベルを削除しますか?元に戻すことはできません。",
"no_results": "一致するラベルはありません",
"toast": {
"failed_delete_label": "",
"failed_load_label": "",
"failed_update_label": "",
"label_deleted": "",
"label_updated": ""
"failed_delete_label": "ラベルの削除に失敗しました",
"failed_load_label": "ラベルの読み込みに失敗しました",
"failed_update_label": "ラベルの更新に失敗しました",
"label_deleted": "ラベルが削除されました",
"label_updated": "ラベル設定が更新されました"
},
"update_label": "ラベル設定の変更"
},
"languages": {
"bs-BA": "ボスニア語 (Bosnia and Herzegovina)",
"ca": "カタルーニャ語 (カタロニア語)",
"cs-CZ": "チェコ語",
"de": "ドイツ語",
@@ -387,9 +410,9 @@
"it": "イタリア語",
"ja-JP": "日本語",
"ko-KR": "韓国語",
"lb-LU": "",
"lt-LT": "",
"nb-NO": "",
"lb-LU": "ルクセンブルク語 (Luxembourg)",
"lt-LT": "リトアニア語 (Lithuania)",
"nb-NO": "ノルウェー語 (ブークモール)",
"nl": "オランダ語",
"pl": "ポーランド語",
"pt-BR": "ポルトガル語 (ブラジル)",
@@ -401,6 +424,7 @@
"th-TH": "タイ語",
"tr": "トルコ語",
"uk-UA": "ウクライナ語",
"vi-VN": "ベトナム語",
"zh-CN": "中国語 (簡体字)",
"zh-HK": "中国語 (香港)",
"zh-MO": "中国語 (マカオ)",
@@ -413,15 +437,15 @@
"locations": {
"child_locations": "属しているその他のロケーション",
"collapse_tree": "ツリーを折りたたむ",
"expand_tree": "",
"location_items_delete_confirm": "",
"no_results": "指定された場所は見つかりません",
"expand_tree": "ツリーを展開",
"location_items_delete_confirm": "このロケーションと、紐づいているアイテムをすべて削除します。'<br>'このロケーションに紐づけられたアイテムはすべて削除されます。'<br>'元に戻すことはできません。",
"no_results": "ロケーションはありません",
"toast": {
"failed_delete_location": "",
"failed_load_location": "",
"failed_update_location": "",
"location_deleted": "",
"location_updated": ""
"failed_delete_location": "ロケーションの削除に失敗しました",
"failed_load_location": "ロケーションの読み込みに失敗しました",
"failed_update_location": "ロケーション設定の更新に失敗しました",
"location_deleted": "ロケーションを削除しました",
"location_updated": "ロケーション設定を更新しました"
},
"update_location": "場所情報の変更"
},
@@ -478,10 +502,10 @@
"currency_format": "通貨の種類",
"current_password": "現在のパスワード",
"delete_account": "アカウントの削除 (永久的です!)",
"delete_account_confirm": "",
"delete_account_confirm": "アカウントを削除しますか?あなたがこのグループのメンバーの最後の一人の場合、アイテム・ロケーション・ラベルなどすべてのデータが削除されます。'<br>'【最終確認】'<u>'この削除機能に再確認ポップアップはありません。続行した場合、すぐにデータが削除され復元もできません。'<u>'",
"delete_account_sub": "アカウントと関連するデータをすべて削除します。元に戻すことはできません。",
"delete_notifier_confirm": "",
"display_legacy_header": "",
"delete_notifier_confirm": "この通知設定を削除してもよろしいですか?",
"display_legacy_header": "{currentValue, select, true {レガシーヘッダーを無効にする} false {レガシーヘッダーを有効にする} other {ヒットしない}}",
"enabled": "有効",
"example": "例",
"gen_invite": "招待リンクの作成",
@@ -491,27 +515,27 @@
"language": "言語",
"new_password": "新しいパスワード",
"no_notifiers": "通知機能は設定されていません",
"no_override": "",
"notifier_modal": "",
"no_override": "既定の言語",
"notifier_modal": "{type, select, true {編集} false {作成} other {その他}}通知者",
"notifiers": "通知",
"notifiers_sub": "メンテナンスなどのリマインダー通知を受け取れます",
"override_locale": "",
"override_locale": "日時・通貨の言語を設定 (上記の言語設定とは別の言語を設定可能)",
"test": "テスト",
"theme_settings": "テーマ設定",
"theme_settings_sub": "テーマ設定はブラウザに保存されます。いつでも変更できます。\nテーマ設定によって問題が発生した場合は、再読み込みを行ってください。",
"toast": {
"account_deleted": "",
"failed_change_password": "",
"failed_create_notifier": "",
"failed_delete_account": "",
"failed_delete_notifier": "",
"failed_get_currencies": "",
"failed_test_notifier": "",
"failed_update_group": "",
"failed_update_notifier": "",
"group_updated": "",
"notifier_test_success": "",
"password_changed": ""
"account_deleted": "アカウントを削除しました",
"failed_change_password": "パスワードを変更できませんでした",
"failed_create_notifier": "通知設定の作成に失敗しました。",
"failed_delete_account": "アカウントの削除に失敗しました",
"failed_delete_notifier": "通知設定の削除に失敗しました",
"failed_get_currencies": "通貨情報の取得に失敗しました",
"failed_test_notifier": "通知の送信テストに失敗しました",
"failed_update_group": "グループ設定の更新に失敗しました",
"failed_update_notifier": "通知設定の更新に失敗しました",
"group_updated": "グループ設定を更新しました",
"notifier_test_success": "通知の送信テストに成功しました",
"password_changed": "パスワードを変更しました"
},
"update_group": "グループ設定を更新",
"update_language": "言語更新",
@@ -521,61 +545,67 @@
},
"reports": {
"label_generator": {
"asset_end": "",
"asset_start": "",
"base_url": "",
"bordered_labels": "",
"generate_page": "",
"input_placeholder": "",
"instruction_1": "",
"instruction_2": "",
"instruction_3": "",
"label_height": "",
"label_width": "",
"measure_type": "",
"page_bottom_padding": "",
"page_height": "",
"page_left_padding": "",
"page_right_padding": "",
"page_top_padding": "",
"page_width": "",
"qr_code_example": "",
"tip_1": "",
"tip_2": "",
"tip_3": "",
"tips": "",
"title": "",
"asset_end": "Asset ID 印刷終了位置 (このIDは含みません)",
"asset_start": "Asset ID 印刷開始位置 (このIDを含みます)",
"base_url": "QRコードのURL (通常変更する必要はありません。URL見本を参照し設定してください。)",
"bordered_labels": "ラベルの境界線を印刷",
"generate_page": "印刷プレビューを取得",
"input_placeholder": "ここに入力",
"instruction_1": "Homebox ラベルジェネレーターは、Homeboxに登録されたアイテムの管理シールを印刷するツールです。\nシールはアイテムを登録する前に印刷できるため、事前に大量作成することが可能です",
"instruction_2": "シールには、HomeboxのQRコードやAsset IDを印刷できます。\nAsset IDを無効にしている場合でもこのツールは使用できますが、Asset IDを利用してアイテムページを開く機能などは利用できません。",
"instruction_3": "この機能はまだ開発中であり、今後の更新で改修される可能性があります。\nバグ情報や機能提案がある方は、ぜひ'<a href=\"https://github.com/sysadminsmedia/homebox/discussions/53\">'GitHubディスカッションに投稿をお願いします。'</a>'",
"label_height": "シール 高さ",
"label_width": "シール 横幅",
"measure_type": "メジャータイプ",
"page_bottom_padding": "用紙 下余白",
"page_height": "用紙サイズ 高さ",
"page_left_padding": "用紙 左余白",
"page_right_padding": "用紙 右余白",
"page_top_padding": "用紙 上余白",
"page_width": "用紙サイズ 横幅",
"qr_code_example": "QRコード URL見本",
"tip_1": "この機能の既定値は、'<a href=\"https://www.avery.com/templates/5260\">'Avery 5260 label sheets'</a>'と適合する値になっています。\nそれ以外のシートを使用する場合は、必ず設定を調整する必要があります。",
"tip_2": "シールをカスタマイズする場合、寸法はインチ単位で設定してください。\n5260シートも、既定値が寸法と一致しないことがあります。\n'<br>' '<b>'試行錯誤が必要になることを覚悟してください。'<u>'必ず寸法や印刷位置の検証を行ってから、シール用紙に印刷してください。'</u>' '</b>'",
"tip_3": "印刷時は、以下のことに気を付けてください\n'<ol><li>'余白を0 または なし に設定する\n'</li><li>'拡大率を100%に設定する\n'</li><li>'両面印刷を無効にする\n'</li><li>'ページを印刷する前に必ずテスト印刷をする\n'</li><li>'たくさんの紙くずが発生することを覚悟する",
"tips": "仕様",
"title": "管理シール印刷機能",
"toast": {
"page_too_small_card": ""
"page_too_small_card": "印刷可能範囲がシールサイズより小さいため印刷プレビューを生成できません。用紙サイズ・シールサイズ・余白を変更してください。"
}
}
},
"scanner": {
"barcode_detected_message": "バーコードが見つかりました",
"barcode_fetch_data": "製品のデータを取得",
"error": "不明なエラーが発生しました。スキャンは利用できません。",
"invalid_url": "バーコードが無効です",
"no_sources": "",
"permission_denied": "",
"select_video_source": "",
"title": "",
"no_sources": "映像デバイスが見つかりません",
"permission_denied": "カメラアクセスが拒否されています。ブラウザ設定・スマホ設定で許可してください。",
"select_video_source": "映像デバイスを選択",
"title": "スキャン",
"unsupported": "メディアストリームAPIはHTTPSのみをサポートしています"
},
"tools": {
"actions": "全てのアイテムに対する変更",
"actions_set": {
"create_missing_thumbnails": "サムネイルを自動設定",
"create_missing_thumbnails_button": "サムネイルを作成",
"create_missing_thumbnails_confirm": "サムネイルを作成してもよろしいですか?件数によっては時間がかかる場合があり、一時停止することはできません。既存のサムネイルは上書きされません。元に戻すことはできません。",
"create_missing_thumbnails_sub": "アップロードされた添付ファイルのサムネイルを自動で作成します。既にサムネイルがある場合はスキップされます。サムネイルが存在しない場合に、自動で作成します。性能やファイル数により、処理が完了するまでに時間がかかる場合があります。",
"ensure_ids": "アイテムのAsset IDの確認",
"ensure_ids_button": "確認の実行",
"ensure_ids_confirm": "",
"ensure_ids_confirm": "すべてのアイテムにAsset IDがあることを確認します。この処理には時間がかかる場合があり、この処理によってAsset IDが変更されても元に戻すことはできません。",
"ensure_ids_sub": "全てのアイテムに、有効なAsset IDが存在することを確認します。'<br>'もし存在しないアイテムがあった場合は、登録された日付が古い順にAsset IDの登録を行います。",
"ensure_import_refs": "Import Refsの確認",
"ensure_import_refs_button": "確認の実行",
"ensure_import_refs_sub": "全てのアイテムに、有効な import_refに対応する値 が設定されていることを確認します。'<br>' 設定されていない場合は、ランダムな8文字の文字列を設定します。",
"set_primary_photo": "アイテムの見出し画像の自動設定",
"ensure_import_refs": "import_refsの確認",
"ensure_import_refs_button": "確認する",
"ensure_import_refs_sub": "全てのアイテムに、有効な import_ref値 が設定されていることを確認します。'<br>' 設定されていない場合は、ランダムな8文字の文字列を設定します。'<br>'import_refは内部管理用のIDで、ユーザーは見ることができません。",
"set_primary_photo": "アイテムサムネイルの自動設定",
"set_primary_photo_button": "操作を実行",
"set_primary_photo_confirm": "",
"set_primary_photo_sub": "Homebox v0.10.0にて、画像の添付ファイルのうち1枚を見出し画像(Primary Photo)として設定できるようになりました。'<br>'未設定のアイテムは、この機能を利用して自動で設定できます。'<br>'この機能は、見出し画像が未設定のアイテムと関連付けられている中で最も古い画像ファイルを自動的にアイテムの見出し画像として設定します。'<br>'詳しくは、'<a class=\"link\" href=\"https://github.com/hay-kot/homebox/pull/576\">'このページをご覧ください (GitHub PR #576)'</a>'",
"set_primary_photo_confirm": "サムネイルを自動設定します。'<br>'件数によっては時間がかかる場合があり、元に戻すことはできません。",
"set_primary_photo_sub": "Homebox v0.10.0 (2023年10月10日リリース)にて、アイテムのサムネイル画像を設定できるようになりました。'<br>'サムネイルが設定されていないアイテムは、この機能を利用して自動で設定できます。'<br>'この機能は、サムネイルが未設定のアイテムの添付画像の中で最も古い画像ファイルを自動的にサムネイルとして設定します。'<br>'詳しくは、'<a class=\"link\" target=\"_brank\" href=\"https://github.com/hay-kot/homebox/pull/576\">'このページをご覧ください (GitHub PR #576)'</a>'",
"zero_datetimes": "アイテムに設定された日時を消去",
"zero_datetimes_button": "操作を実行",
"zero_datetimes_confirm": "",
"zero_datetimes_confirm": "すべてアイテムの日付と時刻の値をリセットします。'<br>'件数によっては時間がかかる場合があり、元に戻すことはできません。",
"zero_datetimes_sub": "全てのアイテムの'<b>'日付をリセット'</b>'します。'<br>'これは、初期(v0.8.0 / 2023-02-18以前)に発生したバグの修正に必要です。日付が適切に表示されなくなるといった問題が発生している場合に限り、実行してください。'<br>'バグが発生していない場合は実行は不要です。'<a class=\"link\" href=\"https://github.com/hay-kot/homebox/issues/236\" target=\"_blank\">'詳しくはこちらをご覧ください (GitHub Issue #236)'</a>'"
},
"actions_sub": "以下の機能は、すべてのアイテムのデータに影響を与えます。'<b>'これらの操作を元に戻すことはできません。'</b>'",
@@ -586,8 +616,8 @@
"export_sub": "Homeboxで利用可能なCSV形式のファイルをエクスポートします。'<br>'すべてのアイテムが対象です。一部のアイテムを選択してエクスポートすることはできません。",
"import": "インポート (CSVの取り込み)",
"import_button": "CSVファイルを選択",
"import_ref_confirm": "",
"import_sub": "Homeboxで利用可能なCSVファイルをインポートします。'<br>''<code>'HB.import_ref'</code>'が存在しないアイテムに重複がある場合は上書きされません。'<br>''<code>'HB.import_ref'</code>'が存在するアイテムに重複がある場合は、同じ'<code>'HB.import_ref'</code>'を持つアイテムのデータが上書きされます。'<br>'どちらも、重複していない場合は関係なく追加されます。"
"import_ref_confirm": "すべてのアイテムに import_ref の値があることを確認します。'<br>'件数によっては時間がかかる場合があり、元に戻すことはできません。",
"import_sub": "Homeboxで利用可能なCSVファイルをインポートします。\n'<code>'HB.import_ref'</code>'が存在しないアイテムに重複がある場合は上書きされません。\n'<code>'HB.import_ref'</code>'が存在するアイテムに重複がある場合は、同じ'<code>'HB.import_ref'</code>'を持つアイテムのデータが上書きされます。\nどちらも、重複していない場合は関係なく追加されます。"
},
"import_export_sub": "登録されたアイテムをCSVファイルにインポートおよびエクスポートします。Homeboxのソフトウェアを切り替える場合などに便利です。",
"reports": "レポート",
@@ -601,11 +631,12 @@
},
"reports_sub": "アイテムに関するデータの含まれたファイルを作成できます。",
"toast": {
"asset_success": "",
"failed_ensure_ids": "",
"failed_ensure_import_refs": "",
"failed_set_primary_photos": "",
"failed_zero_datetimes": ""
"asset_success": "{results}件のアイテムが更新されました。",
"failed_create_missing_thumbnails": "サムネイルの自動作成ができませんでした。",
"failed_ensure_ids": "Asset IDの確認に失敗しました。",
"failed_ensure_import_refs": "import_ref の確認に失敗しました。",
"failed_set_primary_photos": "サムネイルの設定に失敗しました。",
"failed_zero_datetimes": "日時のリセットに失敗しました。"
}
}
}

Some files were not shown because too many files have changed in this diff Show More