Compare commits

..

284 Commits

Author SHA1 Message Date
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
127 changed files with 7185 additions and 3680 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

@@ -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:
@@ -83,7 +83,7 @@ jobs:
- name: Login to Docker Hub
uses: docker/login-action@v3
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -159,7 +159,7 @@ jobs:
- name: Login to Docker Hub
uses: docker/login-action@v3
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -204,7 +204,7 @@ jobs:
- name: Create manifest list and push Dockerhub
id: push-dockerhub
working-directory: /tmp/digests
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.DOCKERHUB_REPO }}@sha256:%s ' *)

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
@@ -78,7 +78,7 @@ jobs:
- name: Login to Docker Hub
uses: docker/login-action@v3
if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -152,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 }}
@@ -194,7 +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",
}
]
}
}

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

@@ -14,6 +14,7 @@ builds:
- linux
- windows
- darwin
- freebsd
goarch:
- amd64
- "386"
@@ -25,11 +26,16 @@ builds:
goarch: arm
- goos: windows
goarch: "386"
- goos: freebsd
goarch: arm
- goos: freebsd
goarch: "386"
tags:
- >-
{{- if eq .Arch "riscv64" }}nodynamic
{{- else if eq .Arch "arm" }}nodynamic
{{- else if eq .Arch "386" }}nodynamic
{{- else if eq .Os "freebsd" }}nodynamic
{{ end }}
signs:

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

@@ -205,7 +205,7 @@ func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r
}(bucket)
// Set the Content-Disposition header for RFC6266 compliance
disposition := "attachment; filename*=UTF-8''" + url.QueryEscape(doc.Title)
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

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

@@ -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,81 +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)
// Set windows paths to use forward slashes required by go-cloud
storageDir = strings.ReplaceAll(storageDir, "\\", "/")
if !strings.HasPrefix(storageDir, absBase+"/") && storageDir != absBase {
log.Fatal().
Str("path", storageDir).
Msg("invalid storage path: you tried to use a prefix that is not a subdirectory of the base path")
}
// Create with more restrictive permissions
if err := os.MkdirAll(storageDir, 0o750); err != nil {
log.Fatal().
Err(err).
Msg("failed to create data directory")
}
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)
}
@@ -186,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...)
@@ -258,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,11 +1,11 @@
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
@@ -14,90 +14,91 @@ require (
github.com/gen2brain/jpegxl v0.4.5
github.com/gen2brain/webp v0.5.5
github.com/go-chi/chi/v5 v5.2.2
github.com/go-playground/validator/v10 v10.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
@@ -105,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
@@ -140,68 +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.2.4 // 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.1.2 // 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/tinylib/msgp v1.1.8 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/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,22 +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/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.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
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=
@@ -181,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=
@@ -193,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=
@@ -227,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=
@@ -272,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=
@@ -295,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=
@@ -321,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.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/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=
@@ -342,45 +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-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/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.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
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,14 +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/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=
@@ -428,20 +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/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/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=
@@ -459,121 +426,102 @@ 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.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.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.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.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=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.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.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
@@ -581,66 +529,39 @@ golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.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.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
golang.org/x/tools v0.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=
@@ -648,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=
@@ -671,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

@@ -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,6 +5,15 @@ import (
"context"
"crypto/md5"
"fmt"
"image"
"io"
"io/fs"
"net/http"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/evanoberholster/imagemeta"
"github.com/gen2brain/avif"
"github.com/gen2brain/heic"
@@ -16,13 +25,6 @@ import (
"github.com/sysadminsmedia/homebox/backend/pkgs/utils"
"github.com/zeebo/blake3"
"golang.org/x/image/draw"
"image"
"io"
"io/fs"
"net/http"
"path/filepath"
"strings"
"time"
"github.com/google/uuid"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent"
@@ -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
}
@@ -319,16 +338,19 @@ func (r *AttachmentRepo) Update(ctx context.Context, gid uuid.UUID, id uuid.UUID
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, gid, updatedAttachment.ID)

View File

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

@@ -3,7 +3,7 @@ services:
image: homebox
build:
context: .
dockerfile: ./Dockerfile.rootless
dockerfile: ./Dockerfile.hardened
args:
- COMMIT=head
- BUILD_TIME=0001-01-01T00:00:00Z

View File

@@ -1,105 +0,0 @@
<template>
<div class="tab-content">
<div class="card">
<div class="card-header">
<h2 class="card-title">Basic Configuration</h2>
<p class="card-description">Configure the basic settings for your Homebox instance.</p>
</div>
<div class="card-content">
<div class="form-row">
<label for="rootless">Use Rootless Image</label>
<div class="toggle-switch">
<input
type="checkbox"
id="rootless"
v-model="config.rootless"
/>
<label for="rootless"></label>
</div>
</div>
<div class="form-group">
<label for="port">External Port</label>
<input
type="text"
id="port"
v-model="config.port"
/>
<p class="help-text">Only used if HTTPS is not enabled</p>
</div>
<div class="form-group">
<label for="maxFileUpload">Max File Upload (MB)</label>
<input
type="text"
id="maxFileUpload"
v-model="config.maxFileUpload"
/>
</div>
<div class="form-row">
<label for="allowAnalytics">Allow Analytics</label>
<div class="toggle-switch">
<input
type="checkbox"
id="allowAnalytics"
v-model="config.allowAnalytics"
/>
<label for="allowAnalytics"></label>
</div>
</div>
<div class="separator"></div>
<div class="form-row">
<label for="allowRegistration">Allow Registration</label>
<div class="toggle-switch">
<input
type="checkbox"
id="allowRegistration"
v-model="config.allowRegistration"
/>
<label for="allowRegistration"></label>
</div>
</div>
<div class="form-row">
<label for="autoIncrementAssetId">Auto Increment Asset ID</label>
<div class="toggle-switch">
<input
type="checkbox"
id="autoIncrementAssetId"
v-model="config.autoIncrementAssetId"
/>
<label for="autoIncrementAssetId"></label>
</div>
</div>
<div class="form-row">
<label for="checkGithubRelease">Check GitHub Release</label>
<div class="toggle-switch">
<input
type="checkbox"
id="checkGithubRelease"
v-model="config.checkGithubRelease"
/>
<label for="checkGithubRelease"></label>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
defineProps({
config: {
type: Object,
required: true
}
})
</script>
<style scoped>
@import 'common.css';
</style>

View File

@@ -1,288 +0,0 @@
<template>
<div class="config-generator">
<div class="config-layout">
<div class="config-form">
<div class="tabs">
<div class="tab-list">
<button
v-for="tab in tabs"
:key="tab.value"
class="tab-button"
:class="{ active: activeTab === tab.value }"
@click="activeTab = tab.value"
>
{{ tab.label }}
</button>
</div>
<BasicConfig
v-show="activeTab === 'basic'"
:config="config"
/>
<DatabaseConfig
v-show="activeTab === 'database'"
:config="config"
:show-password="showPassword"
@toggle-password="showPassword = !showPassword"
@regenerate-password="regeneratePassword"
/>
<HttpsConfig
v-show="activeTab === 'https'"
:config="config"
/>
<StorageConfig
v-show="activeTab === 'storage'"
:config="config"
/>
</div>
</div>
<ConfigPreview
:config="generateDockerCompose(config)"
@copy="copyToClipboard"
@download="downloadConfig"
/>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import BasicConfig from './BasicConfig.vue'
import DatabaseConfig from './DatabaseConfig.vue'
import HttpsConfig from './HttpsConfig.vue'
import StorageConfig from './StorageConfig.vue'
import ConfigPreview from './ConfigPreview.vue'
import { generateDockerCompose } from './dockerComposeGenerator'
const showPassword = ref(false)
const activeTab = ref('basic')
const tabs = [
{ label: 'Basic', value: 'basic' },
{ label: 'Database', value: 'database' },
{ label: 'HTTPS', value: 'https' },
{ label: 'Storage', value: 'storage' }
]
function generateRandomPassword(length = 16) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+"
let password = ""
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * charset.length)
password += charset[randomIndex]
}
return password
}
const config = reactive({
image: "ghcr.io/sysadminsmedia/homebox:latest",
rootless: false,
port: "3100",
logLevel: "info",
logFormat: "text",
maxFileUpload: "10",
allowAnalytics: false,
// HTTPS options
httpsOption: "none", // none, traefik, nginx, caddy, cloudflared
// Traefik config
traefikConfig: {
domain: "homebox.example.com",
email: "",
},
// Nginx config
nginxConfig: {
domain: "homebox.example.com",
port: "443",
sslCertPath: "/etc/nginx/ssl/cert.pem",
sslKeyPath: "/etc/nginx/ssl/key.pem",
},
// Caddy config
caddyConfig: {
domain: "homebox.example.com",
email: "",
},
// Cloudflared config
cloudflaredConfig: {
tunnel: "homebox-tunnel",
domain: "homebox.example.com",
token: "",
},
databaseType: "sqlite",
postgresConfig: {
host: "postgres",
port: "5432",
username: "homebox",
password: generateRandomPassword(),
database: "homebox",
},
allowRegistration: true,
autoIncrementAssetId: true,
checkGithubRelease: true,
// Storage Configuration
storageType: "local", // local, s3, gcs, azure
storageConfig: {
// Local storage settings
local: {
type: "volume", // "volume" or "directory"
directory: "./homebox-data",
volumeName: "homebox-data",
path: "/data", // Custom path for local storage
},
// S3 storage settings
s3: {
bucket: "",
region: "",
endpoint: "", // For S3-compatible storage
awsAccessKeyId: "",
awsSecretAccessKey: "",
awsSessionToken: "", // Optional for temporary credentials
prefixPath: "", // Storage prefix path
awsSdk: "v2", // AWS SDK version
disableSSL: false,
s3ForcePathStyle: false,
sseType: "", // Server-side encryption type
kmsKeyId: "", // KMS key ID for encryption
fips: false,
dualstack: false,
accelerate: false,
isCompatible: false, // Whether using S3-compatible storage
compatibleService: "", // minio, cloudflare-r2, backblaze-b2, custom
},
// Google Cloud Storage settings
gcs: {
bucket: "",
projectId: "",
credentialsPath: "/app/gcs-credentials.json", // Path to service account key
prefixPath: "", // Storage prefix path
},
// Azure Blob Storage settings
azure: {
container: "",
storageAccount: "",
storageKey: "",
sasToken: "", // Optional SAS token
useEmulator: false,
emulatorEndpoint: "localhost:10001", // For local emulator
prefixPath: "", // Storage prefix path
},
// Container storage volumes (for non-local storage types)
containerStorage: {
postgresStorage: {
type: "volume",
directory: "./postgres-data",
volumeName: "postgres-data",
},
traefikStorage: {
type: "volume",
directory: "./traefik-data",
volumeName: "traefik-data",
},
nginxStorage: {
type: "volume",
directory: "./nginx-data",
volumeName: "nginx-data",
},
caddyStorage: {
type: "volume",
directory: "./caddy-data",
volumeName: "caddy-data",
},
cloudflaredStorage: {
type: "volume",
directory: "./cloudflared-data",
volumeName: "cloudflared-data",
},
},
},
})
function regeneratePassword() {
config.postgresConfig.password = generateRandomPassword()
alert('A new random password has been generated for the database.')
}
function copyToClipboard() {
navigator.clipboard.writeText(generateDockerCompose(config))
alert('Docker Compose configuration has been copied to your clipboard.')
}
function downloadConfig() {
const element = document.createElement("a")
const file = new Blob([generateDockerCompose(config)], { type: "text/plain" })
element.href = URL.createObjectURL(file)
element.download = "docker-compose.yml"
document.body.appendChild(element)
element.click()
document.body.removeChild(element)
}
</script>
<style>
.config-generator {
font-family: var(--vp-font-family-base);
color: var(--vp-c-text-1);
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
}
.title {
font-size: 2rem;
font-weight: 600;
margin-bottom: 2rem;
color: var(--vp-c-brand);
}
.config-layout {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
}
.tabs {
width: 100%;
}
.tab-list {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
margin-bottom: 1rem;
}
.tab-button {
padding: 0.5rem;
background-color: var(--vp-c-bg-mute);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.tab-button.active {
background-color: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.tab-button:hover:not(.active) {
background-color: var(--vp-c-bg-alt);
}
</style>

View File

@@ -1,81 +0,0 @@
<template>
<div class="config-preview">
<div class="card">
<div class="card-header">
<div class="card-title-with-actions">
<h2 class="card-title">Docker Compose Configuration</h2>
<div class="card-actions">
<button class="icon-button" @click="$emit('copy')" title="Copy to clipboard">
Copy
</button>
<button class="icon-button" @click="$emit('download')" title="Download as file">
Download
</button>
</div>
</div>
<p class="card-description">This configuration will be saved as docker-compose.yml</p>
</div>
<div class="card-content">
<textarea
class="code-preview"
readonly
:value="config"
></textarea>
</div>
</div>
</div>
</template>
<script setup>
defineProps({
config: {
type: String,
required: true
},
})
defineEmits(['copy', 'download'])
</script>
<style scoped>
@import './common.css';
.config-preview {
height: 100%;
}
.card {
height: 100%;
display: flex;
flex-direction: column;
}
.card-content {
flex: 1;
}
.card-title-with-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-actions {
display: flex;
gap: 0.5rem;
}
.code-preview {
width: 100%;
height: 600px;
font-family: monospace;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background-color: var(--vp-c-bg);
color: var(--vp-c-text-1);
resize: none;
white-space: pre;
overflow: auto;
}
</style>

View File

@@ -1,112 +0,0 @@
<template>
<div class="tab-content">
<div class="card">
<div class="card-header">
<h2 class="card-title">Database Configuration</h2>
<p class="card-description">Configure the database for your Homebox instance.</p>
</div>
<div class="card-content">
<div class="form-group">
<label for="databaseType">Database Type</label>
<select id="databaseType" v-model="config.databaseType">
<option value="sqlite">SQLite (Default)</option>
<option value="postgres">PostgreSQL</option>
</select>
</div>
<div v-if="config.databaseType === 'postgres'" class="nested-form">
<div class="form-group">
<label for="postgresHost">PostgreSQL Host</label>
<input
type="text"
id="postgresHost"
v-model="config.postgresConfig.host"
/>
</div>
<div class="form-group">
<label for="postgresPort">PostgreSQL Port</label>
<input
type="text"
id="postgresPort"
v-model="config.postgresConfig.port"
/>
</div>
<div class="form-group">
<label for="postgresUsername">PostgreSQL Username</label>
<input
type="text"
id="postgresUsername"
v-model="config.postgresConfig.username"
/>
</div>
<div class="form-group">
<label for="postgresPassword">PostgreSQL Password</label>
<div class="password-input">
<input
:type="showPassword ? 'text' : 'password'"
id="postgresPassword"
v-model="config.postgresConfig.password"
/>
<button
class="icon-button"
@click="$emit('togglePassword')"
type="button"
>
<span v-if="showPassword">Hide</span>
<span v-else>Show</span>
</button>
<button
class="icon-button"
@click="$emit('regeneratePassword')"
type="button"
title="Generate new random password"
>
Regenerate
</button>
</div>
</div>
<div class="form-group">
<label for="postgresDatabase">PostgreSQL Database</label>
<input
type="text"
id="postgresDatabase"
v-model="config.postgresConfig.database"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
defineProps({
config: {
type: Object,
required: true
},
showPassword: {
type: Boolean,
default: false
}
})
defineEmits(['togglePassword', 'regeneratePassword'])
</script>
<style scoped>
@import './common.css';
.password-input {
display: flex;
gap: 0.5rem;
}
.password-input input {
flex: 1;
}
</style>

View File

@@ -1,179 +0,0 @@
<template>
<div class="tab-content">
<div class="card">
<div class="card-header">
<h2 class="card-title">HTTPS Configuration</h2>
<p class="card-description">Configure HTTPS for your Homebox instance.</p>
</div>
<div class="card-content">
<div class="form-group">
<label for="httpsOption">HTTPS Option</label>
<select id="httpsOption" v-model="config.httpsOption">
<option value="none">None (HTTP only)</option>
<option value="traefik">Traefik (Automatic HTTPS with Let's Encrypt)</option>
<option value="nginx">Nginx (Custom SSL certificates)</option>
<option value="caddy">Caddy (Automatic HTTPS with Let's Encrypt)</option>
<option value="cloudflared">Cloudflare Tunnel</option>
</select>
</div>
<!-- Traefik Configuration -->
<div v-if="config.httpsOption === 'traefik'" class="nested-form">
<h3>Traefik Configuration</h3>
<p class="help-text">Traefik automatically handles HTTPS certificates via Let's Encrypt</p>
<div class="form-group">
<label for="traefikDomain">Domain Name</label>
<input
type="text"
id="traefikDomain"
v-model="config.traefikConfig.domain"
placeholder="homebox.example.com"
/>
<p class="help-text">The domain name must be pointed to your server's IP address</p>
</div>
<div class="form-group">
<label for="traefikEmail">Email Address (for Let's Encrypt)</label>
<input
type="email"
id="traefikEmail"
v-model="config.traefikConfig.email"
placeholder="your-email@example.com"
/>
<p class="help-text">Required for Let's Encrypt certificate notifications</p>
</div>
</div>
<!-- Nginx Configuration -->
<div v-if="config.httpsOption === 'nginx'" class="nested-form">
<h3>Nginx Configuration</h3>
<p class="help-text">Nginx requires you to provide SSL certificates</p>
<div class="form-group">
<label for="nginxDomain">Domain Name</label>
<input
type="text"
id="nginxDomain"
v-model="config.nginxConfig.domain"
placeholder="homebox.example.com"
/>
</div>
<div class="form-group">
<label for="nginxPort">HTTPS Port</label>
<input
type="text"
id="nginxPort"
v-model="config.nginxConfig.port"
/>
</div>
<div class="form-group">
<label for="nginxSslCert">SSL Certificate Path</label>
<input
type="text"
id="nginxSslCert"
v-model="config.nginxConfig.sslCertPath"
/>
<p class="help-text">Path to SSL certificate file inside the Nginx container</p>
</div>
<div class="form-group">
<label for="nginxSslKey">SSL Key Path</label>
<input
type="text"
id="nginxSslKey"
v-model="config.nginxConfig.sslKeyPath"
/>
<p class="help-text">Path to SSL key file inside the Nginx container</p>
</div>
</div>
<!-- Caddy Configuration -->
<div v-if="config.httpsOption === 'caddy'" class="nested-form">
<h3>Caddy Configuration</h3>
<p class="help-text">Caddy automatically handles HTTPS certificates via Let's Encrypt</p>
<div class="form-group">
<label for="caddyDomain">Domain Name</label>
<input
type="text"
id="caddyDomain"
v-model="config.caddyConfig.domain"
placeholder="homebox.example.com"
/>
<p class="help-text">The domain name must be pointed to your server's IP address</p>
</div>
<div class="form-group">
<label for="caddyEmail">Email Address (for Let's Encrypt)</label>
<input
type="email"
id="caddyEmail"
v-model="config.caddyConfig.email"
placeholder="your-email@example.com"
/>
<p class="help-text">Optional: Used for Let's Encrypt certificate notifications</p>
</div>
</div>
<!-- Cloudflared Configuration -->
<div v-if="config.httpsOption === 'cloudflared'" class="nested-form">
<h3>Cloudflare Tunnel Configuration</h3>
<p class="help-text">Cloudflare Tunnel provides secure access without exposing ports</p>
<div class="form-group">
<label for="cloudflaredTunnel">Tunnel Name</label>
<input
type="text"
id="cloudflaredTunnel"
v-model="config.cloudflaredConfig.tunnel"
/>
</div>
<div class="form-group">
<label for="cloudflaredDomain">Domain Name</label>
<input
type="text"
id="cloudflaredDomain"
v-model="config.cloudflaredConfig.domain"
placeholder="homebox.example.com"
/>
<p class="help-text">The domain must be managed by Cloudflare</p>
</div>
<div class="form-group">
<label for="cloudflaredToken">Tunnel Token</label>
<input
type="password"
id="cloudflaredToken"
v-model="config.cloudflaredConfig.token"
placeholder="Your Cloudflare Tunnel token"
/>
<p class="help-text">Create a tunnel in the Cloudflare Zero Trust dashboard to get a token</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
defineProps({
config: {
type: Object,
required: true
}
})
</script>
<style scoped>
@import './common.css';
h3 {
font-size: 1rem;
font-weight: 600;
margin: 0 0 0.5rem;
}
</style>

View File

@@ -1,552 +0,0 @@
<template>
<div class="storage-config">
<h3>Storage Configuration</h3>
<!-- Storage Type Selector -->
<div class="form-group">
<label for="storageType">Storage Type</label>
<select id="storageType" v-model="config.storageType" class="form-input">
<option value="local">Local Storage</option>
<option value="s3">Amazon S3 / S3-Compatible</option>
<option value="gcs">Google Cloud Storage</option>
<option value="azure">Azure Blob Storage</option>
</select>
<p class="form-help">Choose where Homebox will store your data</p>
</div>
<!-- Local Storage Configuration -->
<div v-if="config.storageType === 'local'" class="storage-section">
<h4>Local Storage Settings</h4>
<div class="form-group">
<label for="localType">Storage Type</label>
<select id="localType" v-model="config.storageConfig.local.type" class="form-input">
<option value="volume">Docker Volume</option>
<option value="directory">Host Directory</option>
</select>
</div>
<div v-if="config.storageConfig.local.type === 'directory'" class="form-group">
<label for="localDirectory">Host Directory Path</label>
<input
id="localDirectory"
v-model="config.storageConfig.local.directory"
type="text"
class="form-input"
placeholder="./homebox-data"
/>
<p class="form-help">Path on the host system where data will be stored</p>
</div>
<div v-if="config.storageConfig.local.type === 'volume'" class="form-group">
<label for="localVolume">Volume Name</label>
<input
id="localVolume"
v-model="config.storageConfig.local.volumeName"
type="text"
class="form-input"
placeholder="homebox-data"
/>
</div>
<div class="form-group">
<label for="localPath">Custom Storage Path (Optional)</label>
<input
id="localPath"
v-model="config.storageConfig.local.path"
type="text"
class="form-input"
placeholder="/data"
/>
<p class="form-help">Custom path inside the container. Leave as /data for default.</p>
</div>
</div>
<!-- S3 Storage Configuration -->
<div v-if="config.storageType === 's3'" class="storage-section">
<h4>S3 Storage Settings</h4>
<div class="form-group">
<label>
<input
type="checkbox"
v-model="config.storageConfig.s3.isCompatible"
class="form-checkbox"
/>
Use S3-Compatible Storage (MinIO, Cloudflare R2, Backblaze B2, etc.)
</label>
</div>
<div v-if="config.storageConfig.s3.isCompatible" class="form-group">
<label for="s3Service">S3-Compatible Service</label>
<select id="s3Service" v-model="config.storageConfig.s3.compatibleService" class="form-input">
<option value="">Custom/Other</option>
<option value="minio">MinIO</option>
<option value="cloudflare-r2">Cloudflare R2</option>
<option value="backblaze-b2">Backblaze B2</option>
</select>
</div>
<div class="form-group">
<label for="s3Bucket">Bucket Name</label>
<input
id="s3Bucket"
v-model="config.storageConfig.s3.bucket"
type="text"
class="form-input"
placeholder="my-homebox-bucket"
required
/>
</div>
<div v-if="!config.storageConfig.s3.isCompatible" class="form-group">
<label for="s3Region">AWS Region</label>
<input
id="s3Region"
v-model="config.storageConfig.s3.region"
type="text"
class="form-input"
placeholder="us-east-1"
required
/>
</div>
<div v-if="config.storageConfig.s3.isCompatible" class="form-group">
<label for="s3Endpoint">Endpoint URL</label>
<input
id="s3Endpoint"
v-model="config.storageConfig.s3.endpoint"
type="text"
class="form-input"
:placeholder="getS3EndpointPlaceholder()"
/>
<p class="form-help">The endpoint URL for your S3-compatible service</p>
</div>
<div class="form-group">
<label for="s3AccessKey">AWS Access Key ID</label>
<input
id="s3AccessKey"
v-model="config.storageConfig.s3.awsAccessKeyId"
type="text"
class="form-input"
placeholder="AKIAIOSFODNN7EXAMPLE"
required
/>
</div>
<div class="form-group">
<label for="s3SecretKey">AWS Secret Access Key</label>
<input
id="s3SecretKey"
v-model="config.storageConfig.s3.awsSecretAccessKey"
type="password"
class="form-input"
placeholder="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
required
/>
</div>
<div class="form-group">
<label for="s3SessionToken">AWS Session Token (Optional)</label>
<input
id="s3SessionToken"
v-model="config.storageConfig.s3.awsSessionToken"
type="password"
class="form-input"
placeholder="For temporary credentials"
/>
<p class="form-help">Only needed for temporary AWS credentials</p>
</div>
<div class="form-group">
<label for="s3PrefixPath">Storage Prefix Path (Optional)</label>
<input
id="s3PrefixPath"
v-model="config.storageConfig.s3.prefixPath"
type="text"
class="form-input"
placeholder="homebox/"
/>
<p class="form-help">Prefix for all stored objects in the bucket</p>
</div>
<!-- Advanced S3 Settings -->
<details class="advanced-settings">
<summary>Advanced S3 Settings</summary>
<div class="form-group">
<label for="s3AwsSdk">AWS SDK Version</label>
<select id="s3AwsSdk" v-model="config.storageConfig.s3.awsSdk" class="form-input">
<option value="v2">v2 (Recommended)</option>
<option value="v1">v1</option>
</select>
</div>
<div class="form-group">
<label>
<input
type="checkbox"
v-model="config.storageConfig.s3.disableSSL"
class="form-checkbox"
/>
Disable SSL
</label>
</div>
<div class="form-group">
<label>
<input
type="checkbox"
v-model="config.storageConfig.s3.s3ForcePathStyle"
class="form-checkbox"
/>
Force Path Style Access
</label>
</div>
<div class="form-group">
<label for="s3SseType">Server-Side Encryption</label>
<select id="s3SseType" v-model="config.storageConfig.s3.sseType" class="form-input">
<option value="">None</option>
<option value="AES256">AES256</option>
<option value="aws:kms">AWS KMS</option>
<option value="aws:kms:dsse">AWS KMS DSSE</option>
</select>
</div>
<div v-if="config.storageConfig.s3.sseType.includes('kms')" class="form-group">
<label for="s3KmsKey">KMS Key ID</label>
<input
id="s3KmsKey"
v-model="config.storageConfig.s3.kmsKeyId"
type="text"
class="form-input"
placeholder="arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"
/>
</div>
<div class="form-group">
<label>
<input
type="checkbox"
v-model="config.storageConfig.s3.fips"
class="form-checkbox"
/>
Use FIPS Endpoints
</label>
</div>
<div class="form-group">
<label>
<input
type="checkbox"
v-model="config.storageConfig.s3.dualstack"
class="form-checkbox"
/>
Use Dual-Stack Endpoints
</label>
</div>
<div class="form-group">
<label>
<input
type="checkbox"
v-model="config.storageConfig.s3.accelerate"
class="form-checkbox"
/>
Use S3 Transfer Acceleration
</label>
</div>
</details>
</div>
<!-- Google Cloud Storage Configuration -->
<div v-if="config.storageType === 'gcs'" class="storage-section">
<h4>Google Cloud Storage Settings</h4>
<div class="form-group">
<label for="gcsBucket">Bucket Name</label>
<input
id="gcsBucket"
v-model="config.storageConfig.gcs.bucket"
type="text"
class="form-input"
placeholder="my-homebox-bucket"
required
/>
</div>
<div class="form-group">
<label for="gcsProject">Project ID</label>
<input
id="gcsProject"
v-model="config.storageConfig.gcs.projectId"
type="text"
class="form-input"
placeholder="my-gcp-project"
/>
</div>
<div class="form-group">
<label for="gcsCredentialsPath">Service Account Key Path</label>
<input
id="gcsCredentialsPath"
v-model="config.storageConfig.gcs.credentialsPath"
type="text"
class="form-input"
placeholder="/app/gcs-credentials.json"
/>
<p class="form-help">Path to the service account JSON key file inside the container</p>
</div>
<div class="form-group">
<label for="gcsPrefixPath">Storage Prefix Path (Optional)</label>
<input
id="gcsPrefixPath"
v-model="config.storageConfig.gcs.prefixPath"
type="text"
class="form-input"
placeholder="homebox/"
/>
<p class="form-help">Prefix for all stored objects in the bucket</p>
</div>
<div class="info-box">
<h5>📋 Setup Instructions:</h5>
<ol>
<li>Create a service account in your GCP project</li>
<li>Grant Storage Admin permissions to the service account</li>
<li>Download the JSON key file</li>
<li>Mount the key file as a read-only volume in your container</li>
<li>Set GOOGLE_APPLICATION_CREDENTIALS environment variable</li>
</ol>
</div>
</div>
<!-- Azure Blob Storage Configuration -->
<div v-if="config.storageType === 'azure'" class="storage-section">
<h4>Azure Blob Storage Settings</h4>
<div class="form-group">
<label>
<input
type="checkbox"
v-model="config.storageConfig.azure.useEmulator"
class="form-checkbox"
/>
Use Azure Storage Emulator (for development)
</label>
</div>
<div class="form-group">
<label for="azureContainer">Container Name</label>
<input
id="azureContainer"
v-model="config.storageConfig.azure.container"
type="text"
class="form-input"
placeholder="homebox-container"
required
/>
</div>
<div v-if="!config.storageConfig.azure.useEmulator" class="form-group">
<label for="azureAccount">Storage Account Name</label>
<input
id="azureAccount"
v-model="config.storageConfig.azure.storageAccount"
type="text"
class="form-input"
placeholder="mystorageaccount"
required
/>
</div>
<div v-if="!config.storageConfig.azure.useEmulator" class="form-group">
<label for="azureKey">Storage Account Key</label>
<input
id="azureKey"
v-model="config.storageConfig.azure.storageKey"
type="password"
class="form-input"
placeholder="Your Azure storage account key"
required
/>
</div>
<div v-if="!config.storageConfig.azure.useEmulator" class="form-group">
<label for="azureSas">SAS Token (Optional)</label>
<input
id="azureSas"
v-model="config.storageConfig.azure.sasToken"
type="password"
class="form-input"
placeholder="?sv=2021-06-08&ss=b&srt=sco&sp=rwdlacupx&se=..."
/>
<p class="form-help">Use SAS token instead of storage account key</p>
</div>
<div v-if="config.storageConfig.azure.useEmulator" class="form-group">
<label for="azureEmulatorEndpoint">Emulator Endpoint</label>
<input
id="azureEmulatorEndpoint"
v-model="config.storageConfig.azure.emulatorEndpoint"
type="text"
class="form-input"
placeholder="localhost:10001"
/>
</div>
<div class="form-group">
<label for="azurePrefixPath">Storage Prefix Path (Optional)</label>
<input
id="azurePrefixPath"
v-model="config.storageConfig.azure.prefixPath"
type="text"
class="form-input"
placeholder="homebox/"
/>
<p class="form-help">Prefix for all stored objects in the container</p>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps } from 'vue'
const props = defineProps({
config: {
type: Object,
required: true
}
})
function getS3EndpointPlaceholder() {
const service = props.config.storageConfig.s3.compatibleService
switch (service) {
case 'minio':
return 'http://minio:9000'
case 'cloudflare-r2':
return 'https://<account-id>.r2.cloudflarestorage.com'
case 'backblaze-b2':
return 'https://s3.us-west-004.backblazeb2.com'
default:
return 'https://your-s3-compatible-endpoint.com'
}
}
</script>
<style scoped>
.storage-config {
padding: 1.5rem;
background-color: var(--vp-c-bg-soft);
border-radius: 8px;
}
.storage-section {
margin-top: 1.5rem;
padding: 1rem;
background-color: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.25rem;
font-weight: 500;
color: var(--vp-c-text-1);
}
.form-input {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background-color: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-size: 0.875rem;
}
.form-input:focus {
outline: none;
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 2px var(--vp-c-brand-light);
}
.form-checkbox {
width: auto;
margin-right: 0.5rem;
}
.form-help {
margin-top: 0.25rem;
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.advanced-settings {
margin-top: 1rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
}
.advanced-settings summary {
padding: 0.75rem;
background-color: var(--vp-c-bg-mute);
cursor: pointer;
font-weight: 500;
}
.advanced-settings[open] summary {
border-bottom: 1px solid var(--vp-c-divider);
}
.advanced-settings .form-group {
margin: 1rem;
}
.info-box {
margin-top: 1rem;
padding: 1rem;
background-color: var(--vp-c-bg-alt);
border-left: 4px solid var(--vp-c-brand);
border-radius: 4px;
}
.info-box h5 {
margin: 0 0 0.5rem 0;
color: var(--vp-c-text-1);
}
.info-box ol {
margin: 0;
padding-left: 1.25rem;
}
.info-box li {
margin-bottom: 0.25rem;
font-size: 0.875rem;
color: var(--vp-c-text-2);
}
h3 {
margin: 0 0 1.5rem 0;
color: var(--vp-c-text-1);
font-size: 1.25rem;
font-weight: 600;
}
h4 {
margin: 0 0 1rem 0;
color: var(--vp-c-text-1);
font-size: 1.1rem;
font-weight: 600;
}
</style>

View File

@@ -1,95 +0,0 @@
<template>
<div class="storage-selector">
<h3>{{ label }}</h3>
<p class="help-text">{{ description }}</p>
<div class="radio-group">
<div class="radio-option">
<input
type="radio"
:id="`${storageKey}-volume`"
value="volume"
v-model="config.storageConfig[storageKey].type"
/>
<label :for="`${storageKey}-volume`">Docker Volume</label>
</div>
<div class="radio-option">
<input
type="radio"
:id="`${storageKey}-directory`"
value="directory"
v-model="config.storageConfig[storageKey].type"
/>
<label :for="`${storageKey}-directory`">Host Directory</label>
</div>
</div>
<div v-if="config.storageConfig[storageKey].type === 'volume'" class="form-group">
<label :for="`${storageKey}-volume-name`">Volume Name</label>
<input
type="text"
:id="`${storageKey}-volume-name`"
v-model="config.storageConfig[storageKey].volumeName"
/>
</div>
<div v-else class="form-group">
<label :for="`${storageKey}-directory-path`">Directory Path</label>
<input
type="text"
:id="`${storageKey}-directory-path`"
v-model="config.storageConfig[storageKey].directory"
/>
<p class="help-text">Absolute path recommended (e.g., /home/user/data)</p>
</div>
</div>
</template>
<script setup>
defineProps({
storageKey: {
type: String,
required: true
},
label: {
type: String,
required: true
},
description: {
type: String,
required: true
},
config: {
type: Object,
required: true
}
})
</script>
<style scoped>
@import './common.css';
.storage-selector {
margin-bottom: 2rem;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
}
.storage-selector h3 {
font-size: 1rem;
font-weight: 600;
margin: 0 0 0.5rem;
}
.radio-group {
display: flex;
gap: 1.5rem;
margin: 1rem 0;
}
.radio-option {
display: flex;
align-items: center;
gap: 0.5rem;
}
</style>

View File

@@ -1,150 +0,0 @@
.card {
background-color: var(--vp-c-bg-soft);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
margin-bottom: 1.5rem;
}
.card-header {
padding: 1.5rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
margin: 0 0 0.5rem;
border-top: 0px;
padding-top: 0px;
}
.card-description {
color: var(--vp-c-text-2);
font-size: 0.875rem;
margin: 0;
}
.card-content {
padding: 1.5rem;
}
.tab-content {
width: 100%;
}
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
font-weight: 500;
margin-bottom: 0.5rem;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 0.5rem;
border: 1px soli var(--vp-c-divider);
border-radius: 4px;
background-color: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-size: 0.875rem;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 2px rgba(var(--vp-c-brand-rgb), 0.1);
}
.form-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.25rem;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 40px;
height: 20px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-switch label {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--vp-c-divider);
transition: .4s;
border-radius: 20px;
}
.toggle-switch label:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 2px;
bottom: 2px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
.toggle-switch input:checked + label {
background-color: var(--vp-c-brand);
}
.toggle-switch input:checked + label:before {
transform: translateX(20px);
}
.help-text {
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-top: 0.25rem;
}
.separator {
height: 1px;
background-color: var(--vp-c-divider);
margin: 1.5rem 0;
}
.nested-form {
margin-top: 1rem;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
}
.icon-button {
padding: 0.5rem;
background-color: var(--vp-c-bg-mute);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s ease;
}
.icon-button:hover {
background-color: var(--vp-c-bg-alt);
}

View File

@@ -1,440 +0,0 @@
export function generateDockerCompose(config: any): string {
const services: any = {}
const volumes: any = {}
const networks: any = {
homebox: {
driver: 'bridge'
}
}
// Generate Homebox service
services.homebox = generateHomeboxService(config)
// Add database service if PostgreSQL is selected
if (config.databaseType === 'postgres') {
services.postgres = generatePostgresService(config)
if (config.storageConfig.containerStorage.postgresStorage.type === 'volume') {
volumes[config.storageConfig.containerStorage.postgresStorage.volumeName] = null
}
}
// Ensure homebox-data volume exists if SQLite is selected
if (config.databaseType === 'sqlite') {
volumes['homebox-data'] = null
}
// Add reverse proxy services based on HTTPS option
switch (config.httpsOption) {
case 'traefik':
services.traefik = generateTraefikService(config)
if (config.storageConfig.containerStorage.traefikStorage.type === 'volume') {
volumes[config.storageConfig.containerStorage.traefikStorage.volumeName] = null
}
break
case 'nginx':
services.nginx = generateNginxService(config)
if (config.storageConfig.containerStorage.nginxStorage.type === 'volume') {
volumes[config.storageConfig.containerStorage.nginxStorage.volumeName] = null
}
break
case 'caddy':
services.caddy = generateCaddyService(config)
if (config.storageConfig.containerStorage.caddyStorage.type === 'volume') {
volumes[config.storageConfig.containerStorage.caddyStorage.volumeName] = null
}
break
case 'cloudflared':
services.cloudflared = generateCloudflaredService(config)
if (config.storageConfig.containerStorage.cloudflaredStorage.type === 'volume') {
volumes[config.storageConfig.containerStorage.cloudflaredStorage.volumeName] = null
}
break
}
// Add Homebox storage volume only for local storage
if (config.storageType === 'local' && config.storageConfig.local.type === 'volume') {
volumes[config.storageConfig.local.volumeName] = null
}
const compose = {
version: '3.8',
services,
...(Object.keys(volumes).length > 0 && {volumes}),
networks
}
return `# Generated Homebox Docker Compose Config Generator 1.0 Beta
# Storage Type: ${config.storageType.toUpperCase()}
# Generated on: ${new Date().toISOString()}
${yaml.stringify(compose)}`
}
function generateHomeboxService(config: any): any {
const service: any = {
image: config.rootless ? config.image.replace(':latest', ':latest-rootless') : config.image,
container_name: 'homebox',
restart: 'unless-stopped',
environment: generateEnvironmentVariables(config),
networks: ['homebox']
}
// Add ports for direct access (when no reverse proxy is used)
if (config.httpsOption === 'none') {
service.ports = [`${config.port}:7745`]
}
// Configure storage based on storage type
if (config.storageType === 'local') {
service.volumes = generateLocalStorageVolumes(config)
} else {
// For cloud storage, we might still need some local volumes for certain files
service.volumes = generateCloudStorageVolumes(config)
}
// Always mount homebox-data at /data if SQLite is used
if (config.databaseType === 'sqlite') {
if (!service.volumes) service.volumes = []
// Only add if not already present
if (!service.volumes.some(v => v.startsWith('homebox-data:'))) {
service.volumes.push('homebox-data:/data')
}
}
return service
}
function generateEnvironmentVariables(config: any): string[] {
const env: string[] = [
`HBOX_LOG_LEVEL=${config.logLevel}`,
`HBOX_LOG_FORMAT=${config.logFormat}`,
`HBOX_MAX_UPLOAD_SIZE=${config.maxFileUpload}`,
`HBOX_AUTO_INCREMENT_ASSET_ID=${config.autoIncrementAssetId}`,
`HBOX_WEB_PORT=7745`
]
// Database configuration
if (config.databaseType === 'postgres') {
env.push(
`HBOX_DATABASE_DRIVER=postgres`,
`HBOX_DATABASE_HOST=${config.postgresConfig.host}`,
`HBOX_DATABASE_PORT=${config.postgresConfig.port}`,
`HBOX_DATABASE_NAME=${config.postgresConfig.database}`,
`HBOX_DATABASE_USER=${config.postgresConfig.username}`,
`HBOX_DATABASE_PASS=${config.postgresConfig.password}`
)
}
// Registration settings
if (!config.allowRegistration) {
env.push('HBOX_OPTIONS_ALLOW_REGISTRATION=false')
}
// Analytics settings
if (!config.allowAnalytics) {
env.push('HBOX_OPTIONS_ALLOW_ANALYTICS=false')
}
// GitHub release check
if (!config.checkGithubRelease) {
env.push('HBOX_OPTIONS_CHECK_GITHUB_RELEASE=false')
}
// Storage configuration
env.push(...generateStorageEnvironmentVariables(config))
return env
}
function generateStorageEnvironmentVariables(config: any): string[] {
const env: string[] = []
switch (config.storageType) {
case 'local':
const storagePath = config.storageConfig.local.path || '/data'
env.push(`HBOX_STORAGE_CONN_STRING=file://${storagePath}`)
if (config.storageConfig.local.prefixPath) {
env.push(`HBOX_STORAGE_PREFIX_PATH=${config.storageConfig.local.prefixPath}`)
}
break
case 's3':
const s3Config = config.storageConfig.s3
let connectionString = `s3://${s3Config.bucket}?awssdk=${s3Config.awsSdk}`
if (s3Config.region && !s3Config.isCompatible) {
connectionString += `&region=${s3Config.region}`
}
if (s3Config.endpoint) {
connectionString += `&endpoint=${s3Config.endpoint}`
}
if (s3Config.disableSSL) {
connectionString += '&disableSSL=true'
}
if (s3Config.s3ForcePathStyle) {
connectionString += '&s3ForcePathStyle=true'
}
if (s3Config.sseType) {
connectionString += `&sseType=${s3Config.sseType}`
}
if (s3Config.kmsKeyId) {
connectionString += `&kmskeyid=${s3Config.kmsKeyId}`
}
if (s3Config.fips) {
connectionString += '&fips=true'
}
if (s3Config.dualstack) {
connectionString += '&dualstack=true'
}
if (s3Config.accelerate) {
connectionString += '&accelerate=true'
}
env.push(`HBOX_STORAGE_CONN_STRING=${connectionString}`)
if (s3Config.prefixPath) {
env.push(`HBOX_STORAGE_PREFIX_PATH=${s3Config.prefixPath}`)
}
// AWS credentials
env.push(`AWS_ACCESS_KEY_ID=${s3Config.awsAccessKeyId}`)
env.push(`AWS_SECRET_ACCESS_KEY=${s3Config.awsSecretAccessKey}`)
if (s3Config.awsSessionToken) {
env.push(`AWS_SESSION_TOKEN=${s3Config.awsSessionToken}`)
}
break
case 'gcs':
const gcsConfig = config.storageConfig.gcs
env.push(`HBOX_STORAGE_CONN_STRING=gcs://${gcsConfig.bucket}`)
if (gcsConfig.prefixPath) {
env.push(`HBOX_STORAGE_PREFIX_PATH=${gcsConfig.prefixPath}`)
}
env.push(`GOOGLE_APPLICATION_CREDENTIALS=${gcsConfig.credentialsPath}`)
break
case 'azure':
const azureConfig = config.storageConfig.azure
let azureConnectionString = `azblob://${azureConfig.container}`
if (azureConfig.useEmulator) {
azureConnectionString += `?protocol=http&domain=${azureConfig.emulatorEndpoint}`
}
env.push(`HBOX_STORAGE_CONN_STRING=${azureConnectionString}`)
if (azureConfig.prefixPath) {
env.push(`HBOX_STORAGE_PREFIX_PATH=${azureConfig.prefixPath}`)
}
if (!azureConfig.useEmulator) {
env.push(`AZURE_STORAGE_ACCOUNT=${azureConfig.storageAccount}`)
if (azureConfig.sasToken) {
env.push(`AZURE_STORAGE_SAS_TOKEN=${azureConfig.sasToken}`)
} else {
env.push(`AZURE_STORAGE_KEY=${azureConfig.storageKey}`)
}
}
break
}
return env
}
function generateLocalStorageVolumes(config: any): string[] {
const volumes: string[] = []
if (config.storageConfig.local.type === 'volume') {
const mountPath = config.storageConfig.local.path || '/data'
volumes.push(`${config.storageConfig.local.volumeName}:${mountPath}`)
} else {
const mountPath = config.storageConfig.local.path || '/data'
volumes.push(`${config.storageConfig.local.directory}:${mountPath}`)
}
return volumes
}
function generateCloudStorageVolumes(config: any): string[] {
const volumes: string[] = []
// For cloud storage, we might still need local volumes for certain files like GCS credentials
if (config.storageType === 'gcs') {
volumes.push('/path/to/gcs-credentials.json:/app/gcs-credentials.json:ro')
}
return volumes
}
function generatePostgresService(config: any): any {
const service: any = {
image: 'postgres:17-alpine',
container_name: 'homebox_postgres',
restart: 'unless-stopped',
environment: [
`POSTGRES_USER=${config.postgresConfig.username}`,
`POSTGRES_PASSWORD=${config.postgresConfig.password}`,
`POSTGRES_DB=${config.postgresConfig.database}`
],
networks: ['homebox']
}
if (config.storageConfig.containerStorage.postgresStorage.type === 'volume') {
service.volumes = [`${config.storageConfig.containerStorage.postgresStorage.volumeName}:/var/lib/postgresql/data`]
} else {
service.volumes = [`${config.storageConfig.containerStorage.postgresStorage.directory}:/var/lib/postgresql/data`]
}
return service
}
function generateTraefikService(config: any): any {
const service: any = {
image: 'traefik:v3.0',
container_name: 'traefik',
restart: 'unless-stopped',
command: [
'--api.dashboard=true',
'--providers.docker=true',
'--providers.docker.exposedbydefault=false',
'--entrypoints.web.address=:80',
'--entrypoints.websecure.address=:443',
'--certificatesresolvers.letsencrypt.acme.tlschallenge=true',
`--certificatesresolvers.letsencrypt.acme.email=${config.traefikConfig.email}`,
'--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json'
],
ports: ['80:80', '443:443'],
networks: ['homebox'],
labels: [
'traefik.enable=true',
'traefik.http.routers.traefik.rule=Host(`traefik.${config.traefikConfig.domain}`)',
'traefik.http.routers.traefik.entrypoints=websecure',
'traefik.http.routers.traefik.tls.certresolver=letsencrypt',
'traefik.http.routers.traefik.service=api@internal'
]
}
if (config.storageConfig.containerStorage.traefikStorage.type === 'volume') {
service.volumes = [
'/var/run/docker.sock:/var/run/docker.sock:ro',
`${config.storageConfig.containerStorage.traefikStorage.volumeName}:/letsencrypt`
]
} else {
service.volumes = [
'/var/run/docker.sock:/var/run/docker.sock:ro',
`${config.storageConfig.containerStorage.traefikStorage.directory}:/letsencrypt`
]
}
return service
}
function generateNginxService(config: any): any {
// This would generate an Nginx service with SSL configuration
// Implementation would depend on specific Nginx configuration needs
return {
image: 'nginx:alpine',
container_name: 'nginx',
restart: 'unless-stopped',
ports: [`${config.nginxConfig.port}:443`, '80:80'],
networks: ['homebox']
}
}
function generateCaddyService(config: any): any {
return {
image: 'caddy:alpine',
container_name: 'caddy',
restart: 'unless-stopped',
ports: ['80:80', '443:443'],
networks: ['homebox']
}
}
function generateCloudflaredService(config: any): any {
return {
image: 'cloudflare/cloudflared:latest',
container_name: 'cloudflared',
restart: 'unless-stopped',
command: `tunnel --no-autoupdate run --token ${config.cloudflaredConfig.token}`,
networks: ['homebox']
}
}
// Simple YAML stringifier (basic implementation
const yaml = {
stringify(obj: any, indent = 0, parentKey = "", isTopLevel = true): string {
const spaces = ' '.repeat(indent)
const nextSpaces = ' '.repeat(indent + 1)
if (obj === null || obj === undefined) {
return 'null'
}
if (typeof obj === 'string') {
if (parentKey === 'environment') {
// Should not be used, handled by stringifyEnv
return obj
}
if (obj.includes(':') || obj.includes('#') || obj.includes('\n') || /^[0-9]/.test(obj) || obj.includes('${')) {
return `"${obj.replace(/"/g, '\\"')}"`
}
return obj
}
if (typeof obj === 'number' || typeof obj === 'boolean') {
return String(obj)
}
if (Array.isArray(obj)) {
if (obj.length === 0) return '[]'
if (parentKey === 'environment') {
return yaml.stringifyEnv(obj, indent)
}
// For arrays under object keys, indent dashes at the same level as the parent key's value (spaces)
return '\n' + obj.map(item => `${spaces}- ${this.stringify(item, indent + 1, '', false).replace(/^\s+/, '')}`).join('\n')
}
if (typeof obj === 'object') {
const keys = Object.keys(obj)
if (keys.length === 0) return '{}'
return (isTopLevel ? '' : '\n') + keys.map(key => {
const value = this.stringify(obj[key], indent + 1, key, false)
// If value is an array, ensure correct indentation
if (Array.isArray(obj[key])) {
// Place key at current indent, then array items at next indent
return `${isTopLevel ? '' : spaces}${key}:${value}`
}
if (value.startsWith('\n')) {
return `${isTopLevel ? '' : spaces}${key}:${value}`
}
return `${isTopLevel ? '' : spaces}${key}: ${value}`
}).join('\n')
}
return String(obj)
},
stringifyEnv(envArr: string[], indent = 0): string {
const spaces = ' '.repeat(indent)
return '\n' + envArr.map(env => {
const eqIdx = env.indexOf('=')
if (eqIdx !== -1) {
const key = env.slice(0, eqIdx + 1)
let value = env.slice(eqIdx + 1)
// Only quote the value if it contains special YAML characters
if (value.match(/[:#\n]|^\d|\${/)) {
value = `"${value.replace(/"/g, '\\"')}"`
}
return `${spaces}- ${key}${value}`
}
return `${spaces}- ${env}`
}).join('\n')
}
}

View File

@@ -1,90 +0,0 @@
// types.ts
export type StorageType = "volume" | "directory"
export type HttpsOption = "none" | "traefik" | "nginx" | "caddy" | "cloudflared"
export type DatabaseType = "sqlite" | "postgres"
export interface StorageDetail {
type: StorageType
directory: string
volumeName: string
}
export interface StorageConfig {
homeboxStorage: StorageDetail
postgresStorage: StorageDetail
traefikStorage: StorageDetail
nginxStorage: StorageDetail
caddyStorage: StorageDetail
cloudflaredStorage: StorageDetail
}
export interface PostgresConfig {
host: string
port: string
username: string
password: string
database: string
}
export interface TraefikConfig {
domain: string
email: string
}
export interface NginxConfig {
domain: string
port: string
sslCertPath: string
sslKeyPath: string
}
export interface CaddyConfig {
domain: string
email: string
}
export interface CloudflaredConfig {
tunnel: string // Note: This wasn't used in the generator function, but kept for completeness
domain: string
token: string
}
export interface AppConfig {
image: string // Not directly used in generator, but part of the config
rootless: boolean
port: string
logLevel: string
logFormat: string
maxFileUpload: string
allowAnalytics: boolean
httpsOption: HttpsOption
traefikConfig: TraefikConfig
nginxConfig: NginxConfig
caddyConfig: CaddyConfig
cloudflaredConfig: CloudflaredConfig
databaseType: DatabaseType
postgresConfig: PostgresConfig
allowRegistration: boolean
autoIncrementAssetId: boolean
checkGithubRelease: boolean
storageConfig: StorageConfig
}
// Types for the generated Docker Compose structure
export interface DockerService {
image: string
container_name: string
restart: string
environment?: string[]
volumes: string[]
ports?: string[]
expose?: string[]
labels?: string[]
command?: string[]
depends_on?: string[]
}
export interface DockerServices {
[key: string]: DockerService
}

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:

View File

@@ -42,7 +42,28 @@ $ docker run -d \
1. Create a `docker-compose.yml` file.
<ConfigEditor />
```yaml
services:
homebox:
image: ghcr.io/sysadminsmedia/homebox:latest
# image: ghcr.io/sysadminsmedia/homebox:latest-rootless
container_name: homebox
restart: always
environment:
- HBOX_LOG_LEVEL=info
- HBOX_LOG_FORMAT=text
- HBOX_WEB_MAX_FILE_UPLOAD=10
# Please consider allowing analytics to help us improve Homebox (basic computer information, no personal data)
- HBOX_OPTIONS_ALLOW_ANALYTICS=false
volumes:
- homebox-data:/data/
ports:
- 3100:7745
volumes:
homebox-data:
driver: local
```
::: info
If you use the `rootless` image, and instead of using named volumes you would prefer using a hostMount directly (e.g., `volumes: [ /path/to/data/folder:/data ]`) you need to `chown` the chosen directory in advance to the `65532` user (as shown in the Docker example above).
@@ -82,7 +103,3 @@ You can learn more about Docker by [reading the official Docker documentation.](
2. Extract the archive.
3. Run the `homebox` executable.
4. The web interface will be accessible on port 7745 by default. Access the page by navigating to `http://local.ip.address:7745/` (replace with the right ip address)
<script setup>
import ConfigEditor from '../.vitepress/components/ConfigEditor.vue'
</script>

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
@@ -140,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";
@@ -148,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";
@@ -167,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();
@@ -267,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) {
@@ -386,7 +432,7 @@
loading.value = false;
if (close) {
closeDialog("create-item");
closeDialog(DialogID.CreateItem);
navigateTo(`/item/${data.id}`);
}
}
@@ -465,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

@@ -98,7 +98,7 @@
"parent_location": "Ubicació pare"
},
"tree": {
"no_locations": "No hi ha ubicacions disponibles. Afegiu ubicacions amb el botó\n `<`span class=\"link-primary\"`>`Crea`<`/span`>` a la barra de navegació."
"no_locations": "No hi ha ubicacions disponibles. Afegiu ubicacions amb el botó\n '<span class=\"link-primary\">'Crea'</span>' a la barra de navegació."
}
}
},
@@ -355,6 +355,6 @@
"bill_of_materials": "Llista de materials",
"bill_of_materials_button": "Genera llista de materials"
},
"reports_sub": "Genera informes per a l'inventari"
"reports_sub": "Genera informes per a l'inventari."
}
}

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",
@@ -120,6 +122,15 @@
"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…",
@@ -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é",
@@ -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

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

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"
},
@@ -28,7 +28,8 @@
"clear": "Farbe löschen",
"color": "Farbe",
"no_color": "Keine Farbe",
"no_color_selected": "Keine Farbe ausgewählt"
"no_color_selected": "Keine Farbe ausgewählt",
"randomize": "Zufällige Farbe"
},
"form": {
"password": {
@@ -99,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",
@@ -111,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.} other {Alle Fotos erfolgreich hochgeladen.}}",
"uploading_photos": "{Anzahl, plural, =0 {Keine Fotos zum Hochladen} =1 {1 Foto wird hochgeladen...} other {{Anzahl} Fotos werden hochgeladen...}}"
"upload_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": {
@@ -142,6 +154,7 @@
},
"label": {
"create_modal": {
"label_color": "Label-Farbe",
"label_description": "Label-Beschreibung",
"label_name": "Label-Name",
"title": "Label erstellen",
@@ -174,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": {
@@ -182,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",
@@ -209,7 +225,7 @@
"items": "Gegenstände",
"join_discord": "Discord beitreten",
"labels": "Labels",
"loading": "Wird geladen …",
"loading": "Wird geladen…",
"locations": "Lagerorte",
"maintenance": "Wartung",
"name": "Name",
@@ -381,6 +397,7 @@
"update_label": "Label aktualisieren"
},
"languages": {
"bs-BA": "Bosnisch (Bosnien und Herzegowina)",
"ca": "Katalanisch",
"cs-CZ": "Tschechisch",
"de": "Deutsch",
@@ -407,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",
@@ -421,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",
@@ -547,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",
@@ -557,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",
@@ -120,6 +122,15 @@
"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…",
@@ -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",
@@ -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

@@ -100,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",
@@ -120,6 +122,15 @@
"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…",
@@ -184,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",
@@ -537,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",
@@ -559,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",
@@ -585,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",
@@ -598,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.",
@@ -608,7 +624,7 @@
"asset_labels_button": "Generador de Etiquetas",
"asset_labels_sub": "Genera un PDF para impresión de etiquetas para un rango de IDs de Activos. Estas etiquetas no son específicas para tu inventario, por lo que puedes imprimirlas con antelación y aplicarlas a tu inventario cuando las recibas.",
"bill_of_materials": "Lista de Materiales",
"bill_of_materials_button": "Generar lista de materiales",
"bill_of_materials_button": "Generar Lista de Materiales",
"bill_of_materials_sub": "Genera un archivo CSV (Valores Separados por Comas) que puede importarse a un programa de hojas de cálculo. Es un resumen de tu inventario con información básica sobre artículos y precios."
},
"reports_sub": "Genera diferentes informes para tu inventario.",

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": {
@@ -129,7 +134,8 @@
"headers": "En-têtes",
"page": "Page",
"rows_per_page": "Lignes par page",
"table_settings": "Paramètres du Tableau"
"table_settings": "Paramètres du Tableau",
"view_item": "Afficher l'élément"
}
}
},
@@ -167,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": {
@@ -202,7 +208,7 @@
"items": "Articles",
"join_discord": "Rejoindre le Discord",
"labels": "Étiquettes",
"loading": "Chargement...",
"loading": "Chargement",
"locations": "Emplacements",
"maintenance": "Maintenance",
"name": "Nom",
@@ -400,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)",
@@ -561,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

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

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": {
@@ -203,7 +218,7 @@
"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",
@@ -331,16 +346,20 @@
"tip_3": "I filtri di campo utilizzano l'operazione 'OR'. Se ne viene selezionato più di uno, ne sarà\n richiesto solo uno per una corrispondenza.",
"tips": "Suggerimenti",
"tips_sub": "Suggerimenti per la Ricerca",
"toast": {
"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": {
"no_results": "Nessuna etichetta trovata",
"update_label": "Aggiorna etichetta"
},
"languages": {
"bs-BA": "Bosniaco (Bosnia ed Erzegovina)",
"ca": "Catalano",
"cs-CZ": "Ceco",
"de": "Tedesco",
@@ -367,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)",
@@ -469,7 +489,7 @@
"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_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": {

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