Compare commits

...

320 Commits

Author SHA1 Message Date
Matthew Kilgore
364e6cd91f Fix arm builds not getting runner 2025-11-16 16:03:04 -05:00
Matthew Kilgore
82f018b996 Use native Github runners 2025-11-16 15:49:12 -05:00
Matt
c70005a4bc Merge branch 'main' into mk/kill-32-bit 2025-09-24 21:29:42 -04:00
Matt
825e72bceb Add funding information for contributors 2025-09-24 20:03:58 -04:00
Matt
8547fb9bb3 Add database type selection to bug report template
Added a dropdown for selecting the database type in the bug report template.
2025-09-24 11:28:11 -04:00
Tonya
f66624774e Change Item Card to use object contain by default for images (#1020)
* feat: add legacy image fit preference and adjustable image display in card component

* feat: add blurred bg image when object contain

* fix: add alt text for image and improve objectContain
2025-09-24 16:09:15 +01:00
Guy Taggar
33ec0c4aff Fix typo (#1019)
* Fix typo

* Change to plural
2025-09-24 09:48:44 -04:00
Tonya
6cd9e2779f Use Tanstack table for Selectable Table, quick actions (#998)
* feat: implement example of data table

* feat: load item data into table

* chore: begin switching dialogs

* feat: implement old dialog for controlling headers and page size

* feat: get table into relatively usable state

* feat: enhance dropdown actions for multi-selection and CSV download

* feat: enhance table cell and dropdown button styles for better usability

* feat: json download for table

* feat: add expanded row component for item details in data table

* chore: add translation support

* feat: restore table on home page

* fix: oops need ids

* feat: move card view to use tanstack to allow for pagination

* feat: switch the items search to use ItemViewSelectable

* fix: update pagination handling and improve button click logic

* feat: improve selectable table

* feat: add indeterminate to checkbox

* feat: overhaul maintenance dialog to use new system and add maintenance options to table

* feat: add label ids and location id to item patch api

* feat: change location and labels in table view

* feat: add quick actions preference and enable toggle in table settings

* fix: lint

* fix: remove sized 1 pages

* fix: attempt to fix type error

* fix: various issues

* fix: remove

* fix: refactor item fetching logic to use useAsyncData for improved reactivity and improve use confirm

* fix: sort backend issues

* fix: enhance CSV export functionality by escaping fields to prevent formula injection

* fix: put aria sort on th not button

* chore: update api types
2025-09-24 02:37:38 +01:00
Matthew Kilgore
a5d63ac4e1 In theory SLSA provenience for binary builds 2025-09-23 21:05:22 -04:00
Matt
ba45203ea3 beautify the readme a bit (#1014)
* beautify the readme a bit

* Revert CSS updates, Github filters them out

* Enhance README with Lemmy badge and description update
2025-09-23 13:20:50 -04:00
Matt
609b7a606b Generate OpenAPI 3 schemas from the swagger 2.0 generation (#1017)
* Generate OpenAPI 3 schemas from the swagger 2.0 generation

* Update API description URL in index.md
2025-09-23 12:14:37 -04:00
Katos
b56505452f Merge pull request #1006 from sysadminsmedia/update-currencies
Update currencies.json
2025-09-14 15:54:48 +01:00
katosdev
118bce4441 chore: update currencies.json 2025-09-14 14:52:41 +00:00
Katos
b535cdeb96 Refactor update-currencies workflow with enhancements
Update currencies workflow to fix errors and introduce improvements
2025-09-14 15:52:17 +01:00
Katos
3b0e986f01 Refactor update-currencies workflow file 2025-09-13 18:57:10 +01:00
Choong Jun Jin
8f8dbf4a3a Fea: add decimal support to currency system with ISO 4217 data integration (#976)
* feat: add decimal support to currency system with ISO 4217 data integration

* Harden currency formatting: add decimal bounds, input validation, and robust error handling

* Fixed issues raised by coderrabitai

* Fixed linting issue
2025-09-13 11:51:54 -04:00
Matt
85e280105d Remove 32bit builds 2025-09-09 15:50:28 -04:00
Weblate
3183b38114 Translated using Weblate (French)
Currently translated at 100.0% (518 of 518 strings)

Translated using Weblate (French)

Currently translated at 100.0% (518 of 518 strings)

Translated using Weblate (French)

Currently translated at 100.0% (518 of 518 strings)

Translated using Weblate (French)

Currently translated at 100.0% (518 of 518 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (518 of 518 strings)

Translated using Weblate (Romanian)

Currently translated at 60.1% (311 of 517 strings)

Translated using Weblate (Romanian)

Currently translated at 60.1% (311 of 517 strings)

Translated using Weblate (Romanian)

Currently translated at 60.1% (311 of 517 strings)

Translated using Weblate (Romanian)

Currently translated at 60.1% (311 of 517 strings)

Translated using Weblate (Polish)

Currently translated at 97.8% (506 of 517 strings)

Translated using Weblate (Italian)

Currently translated at 82.5% (427 of 517 strings)

Translated using Weblate (Italian)

Currently translated at 82.5% (427 of 517 strings)

Translated using Weblate (German)

Currently translated at 99.8% (516 of 517 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (517 of 517 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (517 of 517 strings)

Translated using Weblate (German)

Currently translated at 98.4% (509 of 517 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (517 of 517 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (517 of 517 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (517 of 517 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (506 of 506 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (506 of 506 strings)

Co-authored-by: Adam Havránek <adamhavra@seznam.cz>
Co-authored-by: Erwin van Londen <translate.sysadminsm.treachery437@passmail.net>
Co-authored-by: Hannes Salen <hannes.salen@gmail.com>
Co-authored-by: Jose Riha <jose1711@gmail.com>
Co-authored-by: MyMemory <noreply-mt-mymemory@weblate.org>
Co-authored-by: Philipp Walter <philipp.walter@scodex.de>
Co-authored-by: Saverio Salatino <saverio.salatino@gmail.com>
Co-authored-by: Supertriton <tristan.marie@laposte.net>
Co-authored-by: The Frog <frog@blackbox.net>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Weblate Translation Memory <noreply-mt-weblate-translation-memory@weblate.org>
Co-authored-by: vizu <bogdan.vizureanu@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/cs/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/de/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/fr/
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/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/ro/
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/sk/
Translation: Homebox/Frontend
2025-09-08 09:42:46 +00:00
confiks
0408b1c03b Use CatmullRom instead of ApproxBiLinear for thumbnail generation (#964) 2025-09-05 11:19:46 -04:00
Copilot
a2e108eac4 Make attachment storage paths relative in database with cross-platform support (#967) 2025-09-05 11:12:51 -04:00
James Droste
227b81c6af Set default postgres sql_mode to require (#986)
fixes #985. libpq does not support the current default (prefer). This
sets the default sql_mode to match libpq's default which is require
2025-09-05 08:39:11 -04:00
Choong Jun Jin
3ef25d6463 Fix: add focus-triggered preloading to ItemSelector (#980)
* fix: add focus-triggered preloading to ItemSelector with proper error handling and complete localization

* Removed machine translated files

---------

Co-authored-by: Tonya <tonya@tokia.dev>
2025-09-04 16:29:34 +01:00
Tonya
d4e28e6f3b Upgrade frontend deps, including nuxt (#982)
* feat: begin upgrading deps, still very buggy

* feat: progress

* feat: sort all type issues

* fix: sort type issues

* fix: import sonner styles

* fix: nuxt is the enemy

* fix: try sorting issue with workflows

* fix: update vitest config for dynamic import of path and defineConfig

* fix: add missing import

* fix: add time out to try and fix issues

* fix: add ui:ci:preview task for frontend build in CI mode

* fix: i was silly

* feat: add go:ci:with-frontend task for CI mode and remove ui:ci:preview from e2e workflow

* fix: update baseURL in Playwright config for local testing to use port 7745

* fix: update E2E_BASE_URL and remove wait for timeout in login test for smoother execution
2025-09-04 09:00:25 +01:00
Matthieu Evrin
790352da34 fix(item): remove line break in Items label in location view (#975)
fix: prevent items word wrapped in firefox

Signed-off-by: lekaf974 <matthieu.evrin@gmail.com>
2025-09-01 22:52:14 +01:00
tonyaellie
52a6a31098 fix: import close dialog 2025-08-27 19:28:28 +00:00
Katos
1d02285b0d Merge pull request #962 from sysadminsmedia/katos/screenshots
Migrate Screenshots from Imgur to Github
2025-08-24 17:19:22 +01:00
Katos
19563d8b38 Update readme to point to new screenshots folder 2025-08-24 17:16:37 +01:00
Katos
282977e82c Upload example screenshots
Upload screenshots to Github repository
2025-08-24 17:15:46 +01:00
Katos
769d5c5b95 Create screenshots folder and readme 2025-08-24 17:15:07 +01:00
rapidcow
b8f7ce7eb2 doc fix: match configure option names with help message (#959)
* doc fix: match configure option names with help message (1/2)

This is a first commit in an attempt to reconcile the differences
between the /en/configure/index doc page and the automatically
generated help message.  This addresses typos including, though not
limited to, Discussion #954, titled "[doc] apparent typo in the
documentation of GitHub release check option".

This commit fixes the CLI help command, preserving the original
order, while manually matching the option names with the help
message generated by the backend api executable.

Options are only checked for spelling correctness and existence.
In particular, the following are removed because i could not
find them in the help message.

   * --swagger-host/$HBOX_SWAGGER_HOST <string> (default: localhost:7745)
   * --swagger-scheme/$HBOX_SWAGGER_SCHEME <string> (default: http)

The following default values have also been updated:

   * --storage-conn-string/$HBOX_STORAGE_CONN_STRING
      (a slash is added to the URI path)
   * --database-sqlite-path/$HBOX_DATABASE_SQLITE_PATH
      (a query param '&_time_format=sqlite' is added)
   * --database-ssl-mode/$HBOX_DATABASE_SSL_MODE
      (default 'prefer' added)

* doc fix: match configure option names with help message (2/2)

This is a second commit in an attempt to reconcile the differences
between the /en/configure/index doc page and the automatically
generated help message.  See the previous commit for details.

This commit fixes the Markdown table.

Options are only checked for spelling correctness and existence.
The following rows are deleted in particular:

   * HBOX_SWAGGER_HOST
   * HBOX_SWAGGER_SCHEME

The following default values are updated:

   * HBOX_STORAGE_CONN_STRING
      (a slash is added to the URI path)
   * HBOX_DATABASE_SQLITE_PATH
      (a query param '&_time_format=sqlite' is added)
   * HBOX_DATABASE_SSL_MODE
      (default 'prefer' added)
2025-08-23 21:17:26 -04:00
Matthew Kilgore
62ed3fabc2 Fix broken test version of binary build 2025-08-23 17:29:21 -04:00
Matthew Kilgore
304fc7f11f Fix YAML maybe 2025-08-23 17:24:10 -04:00
Matthew Kilgore
1b7a7a1999 Fix YAML maybe 2025-08-23 17:22:29 -04:00
Matthew Kilgore
a63f08ad87 Fix YAML maybe 2025-08-23 17:21:21 -04:00
Matthew Kilgore
9cb1a3f83c Fix YAML maybe 2025-08-23 17:21:01 -04:00
Matthew Kilgore
f86d38412b Fix YAML maybe 2025-08-23 17:20:16 -04:00
Matthew Kilgore
cbbe056d01 Let us test binary builds without publishing new tags 2025-08-23 17:17:10 -04:00
Katos
5f6b1a0805 Update binaries-publish.yaml
Add COSIGN_PWD and COSIGN_YES to workflow to rectify issues with binaries building on Action
2025-08-23 20:07:12 +01:00
Tonya
27e9eb2277 improve dialogs, option to open image dialog in edit then delete (#951)
* fix: change Content-Disposition to inline for proper document display in attachments

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

* chore: remove unneeded console log

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

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

* Fix CodeRabbit issues

---------

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

* Remove healthcheck that can't work

* Pin action dependencies

* Further cleanup and hardening

* Fix broken hardened build

* Enhance Dockerfile with healthcheck and optimizations

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

---------

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

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

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

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

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

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

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

Translated using Weblate (Slovak)

Currently translated at 97.2% (492 of 506 strings)

Translated using Weblate (Ukrainian)

Currently translated at 64.0% (324 of 506 strings)

Translated using Weblate (Hungarian)

Currently translated at 99.4% (503 of 506 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (505 of 506 strings)

Translated using Weblate (Catalan)

Currently translated at 54.5% (276 of 506 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.4% (503 of 506 strings)

Translated using Weblate (Spanish)

Currently translated at 99.4% (503 of 506 strings)

Translated using Weblate (Turkish)

Currently translated at 86.1% (436 of 506 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (506 of 506 strings)

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

Translated using Weblate (English)

Currently translated at 100.0% (506 of 506 strings)

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

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

* run task ui:fix

* fix label selector when creating an item

* feat: sort styles for line wrapping

---------

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

Translated using Weblate (English)

Currently translated at 100.0% (506 of 506 strings)

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

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

Translated using Weblate (Japanese)

Currently translated at 97.6% (494 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 95.4% (483 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 95.4% (483 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 95.4% (483 of 506 strings)

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

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

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

Translated using Weblate (Japanese)

Currently translated at 95.4% (483 of 506 strings)

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

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

Translated using Weblate (Japanese)

Currently translated at 95.0% (481 of 506 strings)

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

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

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

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

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

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

Translated using Weblate (Japanese)

Currently translated at 91.6% (464 of 506 strings)

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

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

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

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

Translated using Weblate (Japanese)

Currently translated at 90.7% (459 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 90.5% (458 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 90.1% (456 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 89.9% (455 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 89.7% (454 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 89.5% (453 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 89.3% (452 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 89.1% (451 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 88.9% (450 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 88.7% (449 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 88.5% (448 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 88.3% (447 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 88.1% (446 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 87.9% (445 of 506 strings)

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

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

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

Translated using Weblate (Japanese)

Currently translated at 87.5% (443 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 87.1% (441 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 86.9% (440 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 86.7% (439 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 86.5% (438 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 86.3% (437 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 86.1% (436 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 85.9% (435 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 85.7% (434 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 85.1% (431 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 84.7% (429 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 84.5% (428 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 83.3% (422 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 83.2% (421 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 83.0% (420 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 82.8% (419 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 82.2% (416 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 82.0% (415 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 81.8% (414 of 506 strings)

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

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

Translated using Weblate (Japanese)

Currently translated at 81.2% (411 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 81.0% (410 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 80.8% (409 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 80.4% (407 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 80.2% (406 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 80.0% (405 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 79.8% (404 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 79.6% (403 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 79.4% (402 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 79.2% (401 of 506 strings)

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

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

Translated using Weblate (Japanese)

Currently translated at 78.6% (398 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 78.4% (397 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 78.2% (396 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 78.0% (395 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 77.8% (394 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 77.6% (393 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 77.4% (392 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 77.2% (391 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 76.8% (389 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 76.8% (389 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 76.6% (388 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 76.2% (386 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 75.8% (384 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 75.6% (383 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 75.2% (381 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 75.0% (380 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 74.9% (379 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 74.7% (378 of 506 strings)

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

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

Translated using Weblate (Japanese)

Currently translated at 74.1% (375 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 73.3% (371 of 506 strings)

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

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

Translated using Weblate (Japanese)

Currently translated at 72.3% (366 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 72.3% (366 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 70.5% (357 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 70.1% (355 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 69.5% (352 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 69.3% (351 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 69.1% (350 of 506 strings)

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

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

Translated using Weblate (Japanese)

Currently translated at 68.3% (346 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 68.1% (345 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 67.9% (344 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 67.7% (343 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 67.5% (342 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 67.1% (340 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 66.9% (339 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 66.7% (338 of 506 strings)

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

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

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

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

Translated using Weblate (Japanese)

Currently translated at 66.2% (335 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 65.8% (333 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 65.4% (331 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 64.4% (326 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 64.0% (324 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 63.6% (322 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 63.2% (320 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 62.6% (317 of 506 strings)

Translated using Weblate (Japanese)

Currently translated at 62.6% (317 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 62.0% (314 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 61.4% (311 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 61.2% (310 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 60.6% (307 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 60.2% (305 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 59.4% (301 of 506 strings)

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

Translated using Weblate (Japanese)

Currently translated at 58.8% (298 of 506 strings)

Translated using Weblate (English)

Currently translated at 100.0% (506 of 506 strings)

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

Translated using Weblate (Italian)

Currently translated at 82.4% (417 of 506 strings)

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

Translated using Weblate (German)

Currently translated at 99.8% (505 of 506 strings)

Translated using Weblate (German)

Currently translated at 99.8% (505 of 506 strings)

Translated using Weblate (Italian)

Currently translated at 82.4% (417 of 506 strings)

Translated using Weblate (Italian)

Currently translated at 82.4% (417 of 506 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (506 of 506 strings)

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

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

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

Translated using Weblate (Danish)

Currently translated at 99.4% (501 of 504 strings)

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

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

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

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

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

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

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

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

* Fix linting errors

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

Translated using Weblate (Italian)

Currently translated at 81.5% (411 of 504 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (504 of 504 strings)

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

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

Translated using Weblate (Dutch)

Currently translated at 100.0% (504 of 504 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (504 of 504 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (504 of 504 strings)

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

Translated using Weblate (German)

Currently translated at 98.8% (498 of 504 strings)

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

Translated using Weblate (German)

Currently translated at 98.4% (496 of 504 strings)

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

Translated using Weblate (German)

Currently translated at 97.6% (492 of 504 strings)

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

Translated using Weblate (German)

Currently translated at 97.4% (491 of 504 strings)

Translated using Weblate (German)

Currently translated at 97.4% (491 of 504 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 100.0% (504 of 504 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 99.4% (501 of 504 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 99.2% (500 of 504 strings)

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

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (504 of 504 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (504 of 504 strings)

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

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.4% (501 of 504 strings)

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

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.2% (500 of 504 strings)

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

Translated using Weblate (Czech)

Currently translated at 100.0% (504 of 504 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (504 of 504 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (504 of 504 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 98.0% (494 of 504 strings)

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

This reverts commit 2f51ba419b.

* Fix windows build

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

Translated using Weblate (Polish)

Currently translated at 100.0% (492 of 492 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (492 of 492 strings)

Translated using Weblate (Albanian)

Currently translated at 19.1% (94 of 492 strings)

Translated using Weblate (French)

Currently translated at 99.3% (489 of 492 strings)

Translated using Weblate (Swedish)

Currently translated at 68.2% (336 of 492 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (492 of 492 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 97.3% (479 of 492 strings)

Translated using Weblate (Catalan)

Currently translated at 56.0% (276 of 492 strings)

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

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

Currently translated at 37.1% (183 of 492 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 72.5% (357 of 492 strings)

Translated using Weblate (German)

Currently translated at 99.3% (489 of 492 strings)

Translated using Weblate (Italian)

Currently translated at 81.9% (403 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 100.0% (492 of 492 strings)

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

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

Before (no resizing possible):

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

After (size specified):

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

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

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

* Clean up error handling, add uptime to analytics

* Better analytics scheduling

* Even better logic for scheduling the analytics (hopefully)

* Some cleanup

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

Translated using Weblate (Hungarian)

Currently translated at 99.5% (490 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 99.3% (489 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 99.1% (488 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 98.5% (485 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 98.3% (484 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 97.7% (481 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 97.1% (478 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 97.1% (478 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 96.3% (474 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 96.1% (473 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 95.9% (472 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 95.7% (471 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 93.6% (461 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 93.4% (460 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 93.2% (459 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 92.6% (456 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 92.2% (454 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 92.0% (453 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 89.6% (441 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 89.4% (440 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 89.2% (439 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 89.0% (438 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 88.6% (436 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 88.2% (434 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 87.3% (430 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 85.7% (422 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 85.5% (421 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 84.7% (417 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 84.5% (416 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 84.1% (414 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 83.5% (411 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 81.9% (403 of 492 strings)

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

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

Translated using Weblate (Hungarian)

Currently translated at 80.8% (398 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 79.4% (391 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 78.4% (386 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 73.5% (362 of 492 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 70.7% (348 of 492 strings)

Translated using Weblate (Hungarian)

Currently translated at 70.7% (348 of 492 strings)

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

* Efficiendy improvements and small fixes

* Fix tests to improve coverage

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

* Fix potential issues.

* Remove legacy linting ignore comment

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

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

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>
2025-07-12 13:36:21 -04:00
Copilot
c2cfa10336 Fix nil pointer dereference panic in thumbnail subscription during shutdown (#892)
* Initial plan

* Fix nil pointer dereference in thumbnail subscription handling

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

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>
2025-07-12 11:40:50 -04:00
Balki
850ed476d4 Support listening on unix sockets and systemd sockets (#878) 2025-07-12 09:58:16 -04:00
Ahmosys
adea83d421 fix(frontend/location): preserve parent location when using "Create and Add another" (#879)
* fix(frontend/location): preserve parent in "Create and Add another" modal flow

* fix: normalize line endings

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

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

* Fix GitHub Actions Docker workflow syntax errors

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

* Fix GitHub Actions expression syntax for if conditions

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>
2025-07-10 14:29:30 -04:00
Weblate
746bd50f24 Translated using Weblate (Slovenian)
Currently translated at 100.0% (492 of 492 strings)

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

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

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

Translated using Weblate (Norwegian Bokmål)

Currently translated at 99.3% (489 of 492 strings)

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

* Fix frontend duplicate tag creation issue

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>
2025-07-09 03:48:46 +00:00
Weblate
ee5c43dc29 Translated using Weblate (French)
Currently translated at 99.3% (489 of 492 strings)

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

* Fix warranty section visibility when lifetime warranty is enabled

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>
2025-07-07 11:24:26 -04:00
Weblate
f9b1327507 Translated using Weblate (Slovak)
Currently translated at 100.0% (492 of 492 strings)

Co-authored-by: Jose Riha <jose1711@gmail.com>
Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/sk/
Translation: Homebox/Frontend
2025-07-07 14:00:40 +00:00
230 changed files with 38078 additions and 8013 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

1
.github/FUNDING.yml vendored
View File

@@ -1 +1,2 @@
open_collective: homebox
github: [tankerkiller125,katosdev,tonyaellie]

View File

@@ -58,6 +58,17 @@ body:
- Other
validations:
required: true
- type: dropdown
id: database
attributes:
label: Database Type
description: What database backend are you using?
multiple: false
options:
- SQLite
- PostgreSQL
validations:
required: true
- type: dropdown
id: arch
attributes:

BIN
.github/screenshots/1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
.github/screenshots/10.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
.github/screenshots/2.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

BIN
.github/screenshots/3.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
.github/screenshots/4.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
.github/screenshots/5.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

BIN
.github/screenshots/6.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
.github/screenshots/7.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

BIN
.github/screenshots/8.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
.github/screenshots/9.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

8
.github/screenshots/readme.md vendored Normal file
View File

@@ -0,0 +1,8 @@
# Screenshots
These screenshots are taken from our public [Demo](https://demo.homebox.software) instance.
Note that whilst we will make every effort to ensure that these are maintained and updated, they may be outdated or missing functionality and we would always advise reviewing our demo instances:
- [Demo](https://demo.homebox.software)
- [Nightly](https://nightly.homebox.software)
- [VNext](https://vnext.homebox.software/)

View File

@@ -1,16 +1,33 @@
#!/usr/bin/env python3
import csv
import io
import json
import logging
import os
import sys
from pathlib import Path
import requests
from requests.adapters import HTTPAdapter, Retry
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
API_URL = 'https://restcountries.com/v3.1/all?fields=name,common,currencies'
# Default to a pinned commit for supply-chain security
DEFAULT_ISO_4217_URL = 'https://raw.githubusercontent.com/datasets/currency-codes/052b3088938ba32028a14e75040c286c5e142145/data/codes-all.csv'
ISO_4217_URL = os.environ.get('ISO_4217_URL', DEFAULT_ISO_4217_URL)
SAVE_PATH = Path('backend/internal/core/currencies/currencies.json')
TIMEOUT = 10 # seconds
# Known currency decimal overrides
CURRENCY_DECIMAL_OVERRIDES = {
"BTC": 8, # Bitcoin uses 8 decimal places
"JPY": 0, # Japanese Yen has no decimal places
"BHD": 3, # Bahraini Dinar uses 3 decimal places
}
DEFAULT_DECIMALS = 2
MIN_DECIMALS = 0
MAX_DECIMALS = 6
def setup_logging():
logging.basicConfig(
@@ -19,7 +36,93 @@ def setup_logging():
)
def get_currency_decimals(code, iso_data):
"""
Get the decimal places for a currency code.
Checks overrides first, then ISO data, then uses default.
Clamps result to safe range [MIN_DECIMALS, MAX_DECIMALS].
"""
# Normalize the input code
normalized_code = (code or "").strip().upper()
# First check overrides
if normalized_code in CURRENCY_DECIMAL_OVERRIDES:
decimals = CURRENCY_DECIMAL_OVERRIDES[normalized_code]
# Then check ISO data
elif normalized_code in iso_data:
decimals = iso_data[normalized_code]
# Finally use default
else:
decimals = DEFAULT_DECIMALS
# Ensure it's an integer and clamp to safe range
try:
decimals = int(decimals)
except (ValueError, TypeError):
decimals = DEFAULT_DECIMALS
return max(MIN_DECIMALS, min(MAX_DECIMALS, decimals))
def fetch_iso_4217_data():
"""
Fetch ISO 4217 currency data to get minor units (decimal places).
Returns a dict mapping currency code to minor units.
"""
# Log the resolved URL for transparency
logging.info("Fetching ISO 4217 data from: %s", ISO_4217_URL)
if not ISO_4217_URL.lower().startswith("https://"):
logging.error("Refusing non-HTTPS ISO_4217_URL: %s", ISO_4217_URL)
return {}
session = requests.Session()
retries = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=frozenset(['GET'])
)
session.mount('https://', HTTPAdapter(max_retries=retries))
try:
# Add Accept header for CSV content
headers = {'Accept': 'text/csv'}
resp = session.get(ISO_4217_URL, timeout=TIMEOUT, headers=headers)
resp.raise_for_status()
except requests.exceptions.RequestException as e:
logging.error("Failed to fetch ISO 4217 data: %s", e)
return {}
# Parse CSV data
iso_data = {}
try:
# Decode with utf-8-sig to strip BOM if present
csv_content = resp.content.decode('utf-8-sig')
csv_reader = csv.DictReader(io.StringIO(csv_content))
for row in csv_reader:
code = row.get('AlphabeticCode', '').strip()
minor_unit = row.get('MinorUnit', '').strip()
if code and minor_unit != 'N.A.':
try:
# Convert minor unit to int (decimal places)
iso_data[code] = int(minor_unit) if minor_unit.isdigit() else 2
except (ValueError, TypeError):
iso_data[code] = 2 # Default to 2 if parsing fails
logging.info("Successfully loaded decimal data for %d currencies from ISO 4217", len(iso_data))
return iso_data
except Exception as e:
logging.error("Failed to parse ISO 4217 CSV data: %s", e)
return {}
def fetch_currencies():
# First, fetch ISO 4217 data for decimal places
iso_data = fetch_iso_4217_data()
session = requests.Session()
retries = Retry(
total=3,
@@ -46,11 +149,15 @@ def fetch_currencies():
for country in countries:
country_name = country.get('name', {}).get('common') or "Unknown"
for code, info in country.get('currencies', {}).items():
# Get decimal places using the helper function
decimals = get_currency_decimals(code, iso_data)
results.append({
'code': code,
'local': country_name,
'symbol': info.get('symbol', ''),
'name': info.get('name', '')
'code': code,
'local': country_name,
'symbol': info.get('symbol', ''),
'name': info.get('name', ''),
'decimals': decimals
})
# sort by country name for consistency

View File

@@ -1,6 +1,7 @@
name: Publish Release Binaries
on:
workflow_dispatch:
push:
tags: [ 'v*.*.*' ]
@@ -8,6 +9,12 @@ jobs:
goreleaser:
name: goreleaser
runs-on: ubuntu-latest
outputs:
hashes: ${{ steps.binary.outputs.hashes }}
permissions:
contents: write
packages: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -37,11 +44,85 @@ jobs:
go install github.com/sigstore/cosign/cmd/cosign@latest
- name: Run GoReleaser
id: releaser
if: startsWith(github.ref, 'refs/tags/')
uses: goreleaser/goreleaser-action@v5
with:
workdir: "backend"
distribution: goreleaser
version: "~> v2"
args: release --clean
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COSIGN_PWD: ${{ secrets.COSIGN_PWD }}
COSIGN_YES: "true"
- name: Generate binary hashes
if: startsWith(github.ref, 'refs/tags/')
id: binary
env:
ARTIFACTS: "${{ steps.releaser.outputs.artifacts }}"
run: |
set -euo pipefail
checksum_file=$(echo "$ARTIFACTS" | jq -r '.[] | select (.type=="Checksum") | .path')
echo "hashes=$(cat $checksum_file | base64 -w0)" >> "$GITHUB_OUTPUT"
- name: Run GoReleaser No Release
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
uses: goreleaser/goreleaser-action@v5
with:
workdir: "backend"
distribution: goreleaser
version: "~> v2"
args: release --clean --snapshot --skip=publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COSIGN_PWD: ${{ secrets.COSIGN_PWD }}
COSIGN_YES: "true"
binary-provenance:
if: startsWith(github.ref, 'refs/tags/')
needs: [ goreleaser ]
permissions:
actions: read # To read the workflow path.
id-token: write # To sign the provenance.
contents: write # To add assets to a release.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.9.0
with:
base64-subjects: "${{ needs.goreleaser.outputs.hashes }}"
upload-assets: true # upload to a new release
verification-with-slsa-verifier:
if: startsWith(github.ref, 'refs/tags/')
needs: [goreleaser, binary-provenance]
runs-on: ubuntu-latest
permissions: read-all
steps:
- name: Install the verifier
uses: slsa-framework/slsa-verifier/actions/installer@v2.4.0
- name: Download assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PROVENANCE: "${{ needs.binary-provenance.outputs.provenance-name }}"
run: |
set -euo pipefail
gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "*.tar.gz"
gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "*.zip"
gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "$PROVENANCE"
- name: Verify assets
env:
CHECKSUMS: ${{ needs.goreleaser.outputs.hashes }}
PROVENANCE: "${{ needs.binary-provenance.outputs.provenance-name }}"
run: |
set -euo pipefail
checksums=$(echo "$CHECKSUMS" | base64 -d)
while read -r line; do
fn=$(echo $line | cut -d ' ' -f2)
echo "Verifying $fn"
slsa-verifier verify-artifact --provenance-path "$PROVENANCE" \
--source-uri "github.com/$GITHUB_REPOSITORY" \
--source-tag "$GITHUB_REF_NAME" \
"$fn"
done <<<"$checksums"

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,209 @@
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: ${{ matrix.runner }}
permissions:
contents: read
packages: write
id-token: write
attestations: write
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
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:
@@ -37,7 +37,7 @@ env:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
@@ -47,10 +47,11 @@ jobs:
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
- linux/arm/v7
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
steps:
- name: Enable Debug Logs
@@ -83,7 +84,7 @@ jobs:
- name: Login to Docker Hub
uses: docker/login-action@v3
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')) && secrets.DOCKER_USERNAME != ''
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -159,7 +160,7 @@ jobs:
- name: Login to Docker Hub
uses: docker/login-action@v3
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')) && secrets.DOCKER_USERNAME != ''
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -204,7 +205,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/')) && secrets.DOCKER_USERNAME != ''
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

@@ -37,7 +37,7 @@ permissions:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ${{ matrix.runner }}
permissions:
contents: read # Allows access to repository contents (read-only)
packages: write # Allows pushing to GHCR
@@ -47,10 +47,11 @@ jobs:
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
- linux/arm/v7
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
steps:
- name: Checkout repository
@@ -78,7 +79,7 @@ jobs:
- name: Login to Docker Hub
uses: docker/login-action@v3
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')) && secrets.DOCKER_USERNAME != ''
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -152,7 +153,7 @@ jobs:
- name: Login to Docker Hub
uses: docker/login-action@v3
if: secrets.DOCKER_USERNAME != ''
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -195,7 +196,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/')) && secrets.DOCKER_USERNAME != ''
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:

View File

@@ -5,18 +5,22 @@ on:
branches: [ main ]
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
update-currencies:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: '3.8'
cache: 'pip'
@@ -25,15 +29,14 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests
pip install -r .github/workflows/update-currencies/requirements.txt
- name: Run currency update script
run: python .github/scripts/update_currencies.py
- name: Check for file changes
id: changes
- name: Check for currencies.json changes
run: |
if git diff --quiet; then
if git diff --quiet -- backend/internal/core/currencies/currencies.json; then
echo "changed=false" >> $GITHUB_ENV
else
echo "changed=true" >> $GITHUB_ENV
@@ -41,14 +44,16 @@ jobs:
- name: Create Pull Request
if: env.changed == 'true'
uses: peter-evans/create-pull-request@v7.0.8
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: update-currencies
base: main
title: "Update currencies.json"
commit-message: "chore: update currencies.json"
path: backend/internal/core/currencies/currencies.json
path: .
add-paths: |
backend/internal/core/currencies/currencies.json
- name: No updates needed
if: env.changed == 'false'

View File

@@ -4,7 +4,7 @@
},
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"package.json": "package-lock.json, yarn.lock, .eslintrc.js, tsconfig.json, .prettierrc, .editorconfig, pnpm-lock.yaml, postcss.config.js, tailwind.config.js",
"package.json": "package-lock.json, yarn.lock, eslint.config.mjs, tsconfig.json, .prettierrc, .editorconfig, pnpm-lock.yaml, postcss.config.js, tailwind.config.js",
"docker-compose.yml": "Dockerfile, .dockerignore, docker-compose.dev.yml, docker-compose.yml",
"README.md": "LICENSE, SECURITY.md"
},
@@ -22,6 +22,7 @@
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"eslint.format.enable": true,
"eslint.useFlatConfig": true,
"css.validate": false,
"tailwindCSS.includeLanguages": {
"vue": "html",

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

@@ -2,36 +2,59 @@
<img src="/docs/public/lilbox.svg" height="200"/>
</div>
<h1 align="center" style="margin-top: -10px"> HomeBox </h1>
<p align="center" style="width: 100;">
<h1 align="center" style="margin-top: -10px;"> HomeBox </h1>
<p align="center" style="width: 100%;">
<a href="https://homebox.software/en/">Docs</a>
|
<a href="https://demo.homebox.software">Demo</a>
|
<a href="https://discord.gg/aY4DCkpNA9">Discord</a>
</p>
<p align="center" style="width: 100%;">
<img src="https://img.shields.io/github/check-runs/sysadminsmedia/homebox/main" alt="Github Checks"/>
<img src="https://img.shields.io/github/license/sysadminsmedia/homebox"/>
<img src="https://img.shields.io/github/v/release/sysadminsmedia/homebox?sort=semver&display_name=release"/>
<img src="https://img.shields.io/weblate/progress/homebox?server=https%3A%2F%2Ftranslate.sysadminsmedia.com"/>
</p>
<p align="center" style="width: 100%;">
<img src="https://img.shields.io/reddit/subreddit-subscribers/homebox"/>
<img src="https://img.shields.io/mastodon/follow/110749314839831923?domain=infosec.exchange"/>
<img src="https://img.shields.io/lemmy/homebox%40lemmy.world?label=lemmy"/>
</p>
## What is HomeBox
HomeBox is the inventory and organization system built for the Home User! With a focus on simplicity and ease of use, Homebox is the perfect solution for your home inventory, organization, and management needs. While developing this project, I've tried to keep the following principles in mind:
HomeBox is the inventory and organization system built for the Home User! With a focus on simplicity and ease of use, Homebox is the perfect solution for your home inventory, organization, and management needs. While developing this project, We've tried to keep the following principles in mind:
- _Simple_ - Homebox is designed to be simple and easy to use. No complicated setup or configuration required. Use either a single docker container, or deploy yourself by compiling the binary for your platform of choice.
- _Blazingly Fast_ - Homebox is written in Go, which makes it extremely fast and requires minimal resources to deploy. In general, idle memory usage is less than 50MB for the whole container.
- _Portable_ - Homebox is designed to be portable and run on anywhere. We use SQLite and an embedded Web UI to make it easy to deploy, use, and backup.
- 🧘 _Simple but Expandable_ - Homebox is designed to be simple and easy to use. No complicated setup or configuration required. But expandable to whatever level of infrastructure you want to put into it.
- 🚀 _Blazingly Fast_ - Homebox is written in Go, which makes it extremely fast and requires minimal resources to deploy. In general, idle memory usage is less than 50MB for the whole container.
- 📦 _Portable_ - Homebox is designed to be portable and run on anywhere. We use SQLite and an embedded Web UI to make it easy to deploy, use, and backup.
### Key Features
- 📇 Rich Organization - Organize your items into categories, locations, and tags. You can also create custom fields to store additional information about your items.
- 🔍 Powerful Search - Quickly find items in your inventory using the powerful search feature.
- 📸 Image Upload - Upload images of your items to make it easy to identify them.
- 📄 Document and Warranty Tracking - Keep track of important documents and warranties for your items.
- 💰 Purchase & Maintenance Tracking - Track purchase dates, prices, and maintenance schedules for your items.
- 📱 Responsive Design - Homebox is designed to work on any device, including desktops, tablets, and smartphones.
## Screenshots
![Login Screen](.github/screenshots/1.png)
![Dashboard](.github/screenshots/2.png)
![Item View](.github/screenshots/3.png)
![Create Item](.github/screenshots/9.png)
![Search](.github/screenshots/8.png)
# Screenshots
Check out screenshots of the project [here](https://imgur.com/a/5gLWt2j).
You can also try the demo instances of Homebox:
- [Demo](https://demo.homebox.software)
- [Nightly](https://nightly.homebox.software)
- [VNext](https://vnext.homebox.software/)
## Quick Start
[Configuration & Docker Compose](https://homebox.software/en/quick-start.html)
```bash
# If using the rootless image, ensure data
# If using the rootless or hardened image, ensure data
# folder has correct permissions
mkdir -p /path/to/data/folder
chown 65532:65532 -R /path/to/data/folder
@@ -43,6 +66,7 @@ docker run -d \
--volume /path/to/data/folder/:/data \
ghcr.io/sysadminsmedia/homebox:latest
# ghcr.io/sysadminsmedia/homebox:latest-rootless
# ghcr.io/sysadminsmedia/homebox:latest-hardened
```
<!-- CONTRIBUTING -->
@@ -51,14 +75,20 @@ docker run -d \
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
If you are not a coder, you can still contribute financially. Financial contributions help me prioritize working on this project over others and helps me know that there is a real demand for project development.
To get started with code based contributions, please see our [contributing guide](https://homebox.software/en/contribute/get-started.html).
If you are not a coder and can't help translate, you can still contribute financially. Financial contributions help us maintain the project and keep demos running.
## Help us Translate
We want to make sure that Homebox is available in as many languages as possible. If you are interested in helping us translate Homebox, please help us via our [Weblate instance](https://translate.sysadminsmedia.com/projects/homebox/).
[![Translation status](http://translate.sysadminsmedia.com/widget/homebox/multi-auto.svg)](http://translate.sysadminsmedia.com/engage/homebox/)
[![Translation status](https://translate.sysadminsmedia.com/widget/homebox/multi-auto.svg)](https://translate.sysadminsmedia.com/engage/homebox/)
## Credits
- Original project by [@hay-kot](https://github.com/hay-kot)
- Logo by [@lakotelman](https://github.com/lakotelman)
### Contributors
<a href="https://github.com/sysadminsmedia/homebox/graphs/contributors">
<img src="https://contrib.rocks/image?repo=sysadminsmedia/homebox" />
</a>

View File

@@ -23,10 +23,13 @@ tasks:
INTERNAL: "../../../internal"
PKGS: "../../../pkgs"
cmds:
- swag fmt --dir={{ .API }}
- swag init --dir={{ .API }},{{ .INTERNAL }}/core/services,{{ .INTERNAL }}/data/repo --parseDependency
- cp -r ./docs/swagger.json ../../../../docs/en/api/openapi-2.0.json
- cp -r ./docs/swagger.yaml ../../../../docs/en/api/openapi-2.0.yaml
- npx -y -p swagger2openapi swagger2openapi --outfile ./docs/openapi-3.json ./docs/swagger.json
- npx -y -p swagger2openapi swagger2openapi --yaml --outfile ./docs/openapi-3.yaml ./docs/swagger.json
- cp -r ./docs/swagger.json ../../../../docs/en/api/swagger-2.0.json
- cp -r ./docs/swagger.yaml ../../../../docs/en/api/swagger-2.0.yaml
- cp -r ./docs/openapi-3.json ../../../../docs/en/api/openapi-3.0.json
- cp -r ./docs/openapi-3.yaml ../../../../docs/en/api/openapi-3.0.yaml
sources:
- "./backend/app/api/**/*"
- "./backend/internal/data/**"
@@ -93,6 +96,16 @@ tasks:
- go run ./app/api/ {{ .CLI_ARGS }} &
silent: true
go:ci:with-frontend:
desc: Run backend with frontend in CI mode
dir: frontend
cmds:
- pnpm install
- pnpm run build
- cp -r ./.output/public ../backend/app/api/static/
- task: go:ci
silent: true
go:test:
desc: Runs all go tests using gotestsum - supports passing gotestsum args
dir: backend
@@ -201,12 +214,11 @@ tasks:
desc: Runs end-to-end test on a live server
dir: frontend
cmds:
- task: go:ci
- task: ui:ci
- task: go:ci:with-frontend
- pnpm exec playwright install-deps
- pnpm exec playwright install
- sleep 30
- TEST_SHUTDOWN_API_SERVER=true pnpm exec playwright test -c ./test/playwright.config.ts {{ .CLI_ARGS }}
- TEST_SHUTDOWN_API_SERVER=true E2E_BASE_URL=http://localhost:7745 pnpm exec playwright test -c ./test/playwright.config.ts {{ .CLI_ARGS }}
pr:
desc: Runs all tasks required for a PR

View File

@@ -14,22 +14,22 @@ builds:
- linux
- windows
- darwin
- freebsd
goarch:
- amd64
- "386"
- arm
- arm64
- riscv64
ignore:
- goos: windows
goarch: arm
- goos: windows
goarch: "386"
flags:
- -trimpath
ldflags:
- -s -w
- -X main.version={{.Version}}
- -X main.commit={{.Commit}}
- -X main.date={{.Date}}
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:
@@ -50,7 +50,6 @@ archives:
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
# use zip for windows archives

View File

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

@@ -186,7 +186,7 @@ func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r
log.Err(err).Msg("failed to open bucket")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
file, err := bucket.NewReader(ctx, doc.Path, nil)
file, err := bucket.NewReader(ctx, ctrl.repo.Attachments.GetFullPath(doc.Path), nil)
if err != nil {
log.Err(err).Msg("failed to open file")
return validate.NewRequestError(err, http.StatusInternalServerError)
@@ -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": [
@@ -2163,6 +2240,9 @@ const docTemplate = `{
"code": {
"type": "string"
},
"decimals": {
"type": "integer"
},
"local": {
"type": "string"
},
@@ -3063,6 +3143,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": {
@@ -3355,6 +3483,19 @@ const docTemplate = `{
"id": {
"type": "string"
},
"labelIds": {
"type": "array",
"items": {
"type": "string"
},
"x-nullable": true,
"x-omitempty": true
},
"locationId": {
"type": "string",
"x-nullable": true,
"x-omitempty": true
},
"quantity": {
"type": "integer",
"x-nullable": true,
@@ -3573,7 +3714,7 @@ const docTemplate = `{
},
"description": {
"type": "string",
"maxLength": 255
"maxLength": 1000
},
"name": {
"type": "string",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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": [
@@ -2161,6 +2238,9 @@
"code": {
"type": "string"
},
"decimals": {
"type": "integer"
},
"local": {
"type": "string"
},
@@ -3061,6 +3141,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": {
@@ -3353,6 +3481,19 @@
"id": {
"type": "string"
},
"labelIds": {
"type": "array",
"items": {
"type": "string"
},
"x-nullable": true,
"x-omitempty": true
},
"locationId": {
"type": "string",
"x-nullable": true,
"x-omitempty": true
},
"quantity": {
"type": "integer",
"x-nullable": true,
@@ -3571,7 +3712,7 @@
},
"description": {
"type": "string",
"maxLength": 255
"maxLength": 1000
},
"name": {
"type": "string",

View File

@@ -34,6 +34,8 @@ definitions:
properties:
code:
type: string
decimals:
type: integer
local:
type: string
name:
@@ -646,6 +648,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:
@@ -841,6 +875,16 @@ definitions:
properties:
id:
type: string
labelIds:
items:
type: string
type: array
x-nullable: true
x-omitempty: true
locationId:
type: string
x-nullable: true
x-omitempty: true
quantity:
type: integer
x-nullable: true
@@ -991,7 +1035,7 @@ definitions:
color:
type: string
description:
maxLength: 255
maxLength: 1000
type: string
name:
maxLength: 255
@@ -1947,6 +1991,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 +2613,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,43 @@ 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 +353,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 +373,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 +394,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 +425,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 +528,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 +568,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 +590,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

@@ -7,6 +7,7 @@ import (
_ "embed"
"encoding/json"
"io"
"log"
"slices"
"strings"
"sync"
@@ -15,6 +16,24 @@ import (
//go:embed currencies.json
var defaults []byte
const (
MinDecimals = 0
MaxDecimals = 18
)
// clampDecimals ensures the decimals value is within a safe range [0, 18]
func clampDecimals(decimals int, code string) int {
original := decimals
if decimals < MinDecimals {
decimals = MinDecimals
log.Printf("WARNING: Currency %s had negative decimals (%d), normalized to %d", code, original, decimals)
} else if decimals > MaxDecimals {
decimals = MaxDecimals
log.Printf("WARNING: Currency %s had excessive decimals (%d), normalized to %d", code, original, decimals)
}
return decimals
}
type CollectorFunc func() ([]Currency, error)
func CollectJSON(reader io.Reader) CollectorFunc {
@@ -25,6 +44,11 @@ func CollectJSON(reader io.Reader) CollectorFunc {
return nil, err
}
// Clamp decimals during collection to ensure early normalization
for i := range currencies {
currencies[i].Decimals = clampDecimals(currencies[i].Decimals, currencies[i].Code)
}
return currencies, nil
}
}
@@ -48,10 +72,11 @@ func CollectionCurrencies(collectors ...CollectorFunc) ([]Currency, error) {
}
type Currency struct {
Name string `json:"name"`
Code string `json:"code"`
Local string `json:"local"`
Symbol string `json:"symbol"`
Name string `json:"name"`
Code string `json:"code"`
Local string `json:"local"`
Symbol string `json:"symbol"`
Decimals int `json:"decimals"`
}
type CurrencyRegistry struct {
@@ -62,7 +87,10 @@ type CurrencyRegistry struct {
func NewCurrencyService(currencies []Currency) *CurrencyRegistry {
registry := make(map[string]Currency, len(currencies))
for i := range currencies {
registry[currencies[i].Code] = currencies[i]
// Clamp decimals to safe range before adding to registry
currency := currencies[i]
currency.Decimals = clampDecimals(currency.Decimals, currency.Code)
registry[currency.Code] = currency
}
return &CurrencyRegistry{

File diff suppressed because it is too large Load Diff

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

@@ -52,8 +52,8 @@ func TestItemService_AddAttachment(t *testing.T) {
// Check that the file exists
storedPath := afterAttachment.Attachments[0].Path
// {root}/{group}/{item}/{attachment}
assert.Equal(t, path.Join("/", tGroup.ID.String(), "documents"), path.Dir(storedPath))
// path should now be relative: {group}/{documents}
assert.Equal(t, path.Join(tGroup.ID.String(), "documents"), path.Dir(storedPath))
// Check that the file contents are correct
bts, err := os.ReadFile(path.Join(os.TempDir(), storedPath))

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,25 @@
-- +goose Up
-- Make attachment paths relative by removing the prefix path
-- This migration converts absolute paths to relative paths by finding the UUID/documents pattern
-- Update Unix-style paths that contain "/documents/" by extracting the part starting from the UUID
-- The approach: find the "/documents/" substring, go back 37 characters (UUID + slash),
-- and extract from there to get "uuid/documents/hash"
UPDATE attachments
SET path = SUBSTRING(path FROM POSITION('/documents/' IN path) - 36)
WHERE path LIKE '%/documents/%'
AND POSITION('/documents/' IN path) > 36;
-- Update Windows-style paths that contain "\documents\" by extracting the part starting from the UUID
-- Convert backslashes to forward slashes in the process for consistency
UPDATE attachments
SET path = REPLACE(SUBSTRING(path FROM POSITION('\documents\' IN path) - 36), '\', '/')
WHERE path LIKE '%\documents\%'
AND POSITION('\documents\' IN path) > 36;
-- For paths that already look like relative paths (start with UUID), leave them unchanged
-- This handles cases where the migration might be run multiple times
-- +goose Down
-- Note: This down migration cannot be safely implemented because we don't know
-- what the original prefix paths were. This is a one-way migration.

View File

@@ -0,0 +1,25 @@
-- +goose Up
-- Make attachment paths relative by removing the prefix path
-- This migration converts absolute paths to relative paths by finding the UUID/documents pattern
-- Update Unix-style paths that contain "/documents/" by extracting the part starting from the UUID
-- The approach: find the "/documents/" substring, go back 37 characters (UUID + slash),
-- and extract from there to get "uuid/documents/hash"
UPDATE attachments
SET path = SUBSTR(path, INSTR(path, '/documents/') - 36)
WHERE path LIKE '%/documents/%'
AND INSTR(path, '/documents/') > 36;
-- Update Windows-style paths that contain "\documents\" by extracting the part starting from the UUID
-- Convert backslashes to forward slashes in the process for consistency
UPDATE attachments
SET path = REPLACE(SUBSTR(path, INSTR(path, '\documents\') - 36), '\', '/')
WHERE path LIKE '%\documents\%'
AND INSTR(path, '\documents\') > 36;
-- For paths that already look like relative paths (start with UUID), leave them unchanged
-- This handles cases where the migration might be run multiple times
-- +goose Down
-- Note: This down migration cannot be safely implemented because we don't know
-- what the original prefix paths were. This is a one-way migration.

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"
@@ -96,17 +98,42 @@ func ToItemAttachment(attachment *ent.Attachment) ItemAttachment {
}
func (r *AttachmentRepo) path(gid uuid.UUID, hash string) string {
return filepath.Join(r.storage.PrefixPath, gid.String(), "documents", hash)
return filepath.Join(gid.String(), "documents", hash)
}
func (r *AttachmentRepo) fullPath(relativePath string) string {
return filepath.Join(r.storage.PrefixPath, relativePath)
}
func (r *AttachmentRepo) GetFullPath(relativePath string) string {
return r.fullPath(relativePath)
}
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 +346,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)
@@ -363,7 +393,7 @@ func (r *AttachmentRepo) Delete(ctx context.Context, gid uuid.UUID, itemId uuid.
log.Err(err).Msg("failed to open bucket for thumbnail deletion")
return err
}
err = thumbBucket.Delete(ctx, thumb.Path)
err = thumbBucket.Delete(ctx, r.fullPath(thumb.Path))
if err != nil {
return err
}
@@ -385,7 +415,7 @@ func (r *AttachmentRepo) Delete(ctx context.Context, gid uuid.UUID, itemId uuid.
log.Err(err).Msg("failed to close bucket")
}
}(bucket)
err = bucket.Delete(ctx, doc.Path)
err = bucket.Delete(ctx, r.fullPath(doc.Path))
if err != nil {
return err
}
@@ -453,7 +483,7 @@ func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmen
}
}(bucket)
origFile, err := bucket.Open(path)
origFile, err := bucket.Open(r.fullPath(path))
if err != nil {
err := tx.Rollback()
if err != nil {
@@ -763,14 +793,15 @@ func (r *AttachmentRepo) UploadFile(ctx context.Context, itemGroup *ent.Group, d
ContentType: contentType,
ContentMD5: md5hash.Sum(nil),
}
path := r.path(itemGroup.ID, fmt.Sprintf("%x", hashOut))
err = bucket.WriteAll(ctx, path, contentBytes, options)
relativePath := r.path(itemGroup.ID, fmt.Sprintf("%x", hashOut))
fullPath := r.fullPath(relativePath)
err = bucket.WriteAll(ctx, fullPath, contentBytes, options)
if err != nil {
log.Err(err).Msg("failed to write file to bucket")
return "", err
}
return path, nil
return relativePath, nil
}
func isImageFile(mimetype string) bool {
@@ -820,7 +851,7 @@ func (r *AttachmentRepo) processThumbnailFromImage(ctx context.Context, groupId
}
newWidth, newHeight := calculateThumbnailDimensions(bounds.Dx(), bounds.Dy(), r.thumbnail.Width, r.thumbnail.Height)
dst := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))
draw.ApproxBiLinear.Scale(dst, dst.Rect, img, img.Bounds(), draw.Over, nil)
draw.CatmullRom.Scale(dst, dst.Rect, img, img.Bounds(), draw.Over, nil)
buf := new(bytes.Buffer)
err := webp.Encode(buf, dst, webp.Options{Quality: 80, Lossless: false})

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"`
@@ -111,9 +120,11 @@ type (
}
ItemPatch struct {
ID uuid.UUID `json:"id"`
Quantity *int `json:"quantity,omitempty" extensions:"x-nullable,x-omitempty"`
ImportRef *string `json:"-,omitempty" extensions:"x-nullable,x-omitempty"`
ID uuid.UUID `json:"id"`
Quantity *int `json:"quantity,omitempty" extensions:"x-nullable,x-omitempty"`
ImportRef *string `json:"-,omitempty" extensions:"x-nullable,x-omitempty"`
LocationID uuid.UUID `json:"locationId" extensions:"x-nullable,x-omitempty"`
LabelIDs []uuid.UUID `json:"labelIds" extensions:"x-nullable,x-omitempty"`
}
ItemSummary struct {
@@ -360,14 +371,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),
),
)
}
@@ -794,7 +816,20 @@ func (e *ItemsRepository) GetAllZeroImportRef(ctx context.Context, gid uuid.UUID
}
func (e *ItemsRepository) Patch(ctx context.Context, gid, id uuid.UUID, data ItemPatch) error {
q := e.db.Item.Update().
tx, err := e.db.Tx(ctx)
if err != nil {
return err
}
committed := false
defer func() {
if !committed {
if err := tx.Rollback(); err != nil {
log.Warn().Err(err).Msg("failed to rollback transaction during item patch")
}
}
}()
q := tx.Item.Update().
Where(
item.ID(id),
item.HasGroupWith(group.ID(gid)),
@@ -808,8 +843,81 @@ func (e *ItemsRepository) Patch(ctx context.Context, gid, id uuid.UUID, data Ite
q.SetQuantity(*data.Quantity)
}
if data.LocationID != uuid.Nil {
q.SetLocationID(data.LocationID)
}
err = q.Exec(ctx)
if err != nil {
return err
}
if data.LabelIDs != nil {
currentLabels, err := tx.Item.Query().Where(item.ID(id), item.HasGroupWith(group.ID(gid))).QueryLabel().All(ctx)
if err != nil {
return err
}
set := newIDSet(currentLabels)
addLabels := []uuid.UUID{}
for _, l := range data.LabelIDs {
if set.Contains(l) {
set.Remove(l)
} else {
addLabels = append(addLabels, l)
}
}
if len(addLabels) > 0 {
if err := tx.Item.Update().
Where(item.ID(id), item.HasGroupWith(group.ID(gid))).
AddLabelIDs(addLabels...).
Exec(ctx); err != nil {
return err
}
}
if set.Len() > 0 {
if err := tx.Item.Update().
Where(item.ID(id), item.HasGroupWith(group.ID(gid))).
RemoveLabelIDs(set.Slice()...).
Exec(ctx); err != nil {
return err
}
}
}
if data.LocationID != uuid.Nil {
itemEnt, err := tx.Item.Query().Where(item.ID(id), item.HasGroupWith(group.ID(gid))).Only(ctx)
if err != nil {
return err
}
if itemEnt.SyncChildItemsLocations {
children, err := tx.Item.Query().Where(item.ID(id), item.HasGroupWith(group.ID(gid))).QueryChildren().All(ctx)
if err != nil {
return err
}
for _, child := range children {
childLocation, err := child.QueryLocation().First(ctx)
if err != nil {
return err
}
if data.LocationID != childLocation.ID {
err = child.Update().SetLocationID(data.LocationID).Exec(ctx)
if err != nil {
return err
}
}
}
}
}
if err := tx.Commit(); err != nil {
return err
}
committed = true
e.publishMutationEvent(gid)
return q.Exec(ctx)
return nil
}
func (e *ItemsRepository) GetAllCustomFieldValues(ctx context.Context, gid uuid.UUID, name string) ([]string, error) {
@@ -993,3 +1101,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:require"`
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

@@ -50,7 +50,7 @@ document.head.appendChild(elementStyle);
<client-only>
<elements-api
:key="componentKey"
apiDescriptionUrl="https://raw.githubusercontent.com/sysadminsmedia/homebox/refs/heads/main/docs/en/api/openapi-2.0.json"
apiDescriptionUrl="https://raw.githubusercontent.com/sysadminsmedia/homebox/refs/heads/main/docs/en/api/openapi-3.0.json"
router="hash"
layout="responsive"
hideSchemas="true"

4563
docs/en/api/openapi-3.0.json Normal file

File diff suppressed because it is too large Load Diff

2898
docs/en/api/openapi-3.0.yaml Normal file

File diff suppressed because it is too large Load Diff

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": [
@@ -2161,6 +2238,9 @@
"code": {
"type": "string"
},
"decimals": {
"type": "integer"
},
"local": {
"type": "string"
},
@@ -3061,6 +3141,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": {
@@ -3353,6 +3481,19 @@
"id": {
"type": "string"
},
"labelIds": {
"type": "array",
"items": {
"type": "string"
},
"x-nullable": true,
"x-omitempty": true
},
"locationId": {
"type": "string",
"x-nullable": true,
"x-omitempty": true
},
"quantity": {
"type": "integer",
"x-nullable": true,
@@ -3571,7 +3712,7 @@
},
"description": {
"type": "string",
"maxLength": 255
"maxLength": 1000
},
"name": {
"type": "string",

View File

@@ -34,6 +34,8 @@ definitions:
properties:
code:
type: string
decimals:
type: integer
local:
type: string
name:
@@ -646,6 +648,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:
@@ -841,6 +875,16 @@ definitions:
properties:
id:
type: string
labelIds:
items:
type: string
type: array
x-nullable: true
x-omitempty: true
locationId:
type: string
x-nullable: true
x-omitempty: true
quantity:
type: integer
x-nullable: true
@@ -991,7 +1035,7 @@ definitions:
color:
type: string
description:
maxLength: 255
maxLength: 1000
type: string
name:
maxLength: 255
@@ -1947,6 +1991,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 +2613,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,16 +11,16 @@ 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 |
| HBOX_OPTIONS_CURRENCY_CONFIG | | json configuration file containing additional currencies |
| HBOX_OPTIONS_ALLOW_ANALYTICS | false | Allows the homebox team to view extremely basic information about the system that your running on. This helps make decisions regarding builds and other general decisions. |
| HBOX_WEB_MAX_UPLOAD | 10 | maximum file upload size supported in MB |
| HBOX_WEB_MAX_UPLOAD_SIZE | 10 | maximum file upload size supported in MB |
| HBOX_WEB_READ_TIMEOUT | 10s | Read timeout of HTTP sever |
| HBOX_WEB_WRITE_TIMEOUT | 10s | Write timeout of HTTP server |
| HBOX_WEB_IDLE_TIMEOUT | 30s | Idle timeout of HTTP server |
| HBOX_STORAGE_CONN_STRING | file://./ | path to the data directory, do not change this if you're using docker |
| HBOX_STORAGE_CONN_STRING | file:///./ | path to the data directory, do not change this if you're using docker |
| HBOX_STORAGE_PREFIX_PATH | .data | prefix path for the storage, if not set the storage will be used as is |
| HBOX_LOG_LEVEL | `info` | log level to use, can be one of `trace`, `debug`, `info`, `warn`, `error`, `critical` |
| HBOX_LOG_FORMAT | `text` | log format to use, can be one of: `text`, `json` |
@@ -29,17 +29,18 @@ aside: false
| HBOX_MAILER_USERNAME | | email user to use |
| HBOX_MAILER_PASSWORD | | email password to use |
| HBOX_MAILER_FROM | | email from address to use |
| HBOX_SWAGGER_HOST | 7745 | swagger host to use, if not set swagger will be disabled |
| HBOX_SWAGGER_SCHEMA | `http` | swagger schema to use, can be one of: `http`, `https` |
| HBOX_DATABASE_DRIVER | sqlite3 | sets the correct database type (`sqlite3` or `postgres`) |
| 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_SQLITE_PATH | ./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite | 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_OPTIONS_CHECK_GITHUB_RELEASE | true | check for new github releases |
| HBOX_DATABASE_SSL_MODE | prefer | 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_ROOT_CERT | | sets the sslrootcert for a postgres connection (should be a path) |
| HBOX_OPTIONS_GITHUB_RELEASE_CHECK | 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 |
| HBOX_LABEL_MAKER_PADDING | 32 | space between elements on label |
@@ -51,6 +52,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:
@@ -79,8 +158,8 @@ OPTIONS
--mode/$HBOX_MODE <string> (default: development)
--web-port/$HBOX_WEB_PORT <string> (default: 7745)
--web-host/$HBOX_WEB_HOST <string>
--web-max-file-upload/$HBOX_WEB_MAX_FILE_UPLOAD <int> (default: 10)
--storage-conn-string/$HBOX_STORAGE_CONN_STRING <string> (default: file://./)
--web-max-upload-size/$HBOX_WEB_MAX_UPLOAD_SIZE <int> (default: 10)
--storage-conn-string/$HBOX_STORAGE_CONN_STRING <string> (default: file:///./)
--storage-prefix-path/$HBOX_STORAGE_PREFIX_PATH <string> (default: .data)
--log-level/$HBOX_LOG_LEVEL <string> (default: info)
--log-format/$HBOX_LOG_FORMAT <string> (default: text)
@@ -89,31 +168,29 @@ OPTIONS
--mailer-username/$HBOX_MAILER_USERNAME <string>
--mailer-password/$HBOX_MAILER_PASSWORD <string>
--mailer-from/$HBOX_MAILER_FROM <string>
--swagger-host/$HBOX_SWAGGER_HOST <string> (default: localhost:7745)
--swagger-scheme/$HBOX_SWAGGER_SCHEME <string> (default: http)
--demo/$HBOX_DEMO <bool>
--debug-enabled/$HBOX_DEBUG_ENABLED <bool> (default: false)
--debug-port/$HBOX_DEBUG_PORT <string> (default: 4000)
--database-driver/$HBOX_DATABASE_DRIVER <string> (default: sqlite3)
--database-sqlite-path/$HBOX_DATABASE_SQLITE_PATH <string> (default: ./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1)
--database-sqlite-path/$HBOX_DATABASE_SQLITE_PATH <string> (default: ./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite)
--database-host/$HBOX_DATABASE_HOST <string>
--database-port/$HBOX_DATABASE_PORT <string>
--database-username/$HBOX_DATABASE_USERNAME <string>
--database-password/$HBOX_DATABASE_PASSWORD <string>
--database-database/$HBOX_DATABASE_DATABASE <string>
--database-ssl-mode/$HBOX_DATABASE_SSL_MODE <string>
--database-ssl-mode/$HBOX_DATABASE_SSL_MODE <string> (default: prefer)
--options-allow-registration/$HBOX_OPTIONS_ALLOW_REGISTRATION <bool> (default: true)
--options-auto-increment-asset-id/$HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID <bool> (default: true)
--options-currency-config/$HBOX_OPTIONS_CURRENCY_CONFIG <string>
--options-check-github-release/$HBOX_OPTIONS_CHECK_GITHUB_RELEASE <bool> (default: true)
--options-github-release-check/$HBOX_OPTIONS_GITHUB_RELEASE_CHECK <bool> (default: true)
--options-allow-analytics/$HBOX_OPTIONS_ALLOW_ANALYTICS <bool> (default: false)
--label-maker-width/$HBOX_LABEL_MAKER_WIDTH <int> (default: 526)
--label-maker-height/$HBOX_LABEL_MAKER_HEIGHT <int> (default: 200)
--label-maker-padding/$HBOX_LABEL_MAKER_PADDING <int> (default: 32)
--label-maker-margin/$HBOX_LABEL_MAKER_MARGIN <int> (default: 32)
--label-maker-margin/$HBOX_LABEL_MAKER_MARGIN <int> (default: 32)
--label-maker-font-size/$HBOX_LABEL_MAKER_FONT_SIZE <float> (default: 32.0)
--label-maker-print-command/$HBOX_LABEL_MAKER_PRINT_COMMAND <string>
--label-maker-additional-information/$HBOX_LABEL_MAKER_DYNAMIC_LENGTH <string> (default: true)
--label-maker-dynamic-length/$HBOX_LABEL_MAKER_DYNAMIC_LENGTH <bool> (default: true)
--label-maker-additional-information/$HBOX_LABEL_MAKER_ADDITIONAL_INFORMATION <string>
--thumbnail-enabled/$HBOX_THUMBNAIL_ENABLED <bool> (default: true)
--thumbnail-width/$HBOX_THUMBNAIL_WIDTH <int> (default: 500)

View File

@@ -102,3 +102,11 @@ The copy to clipboard functionality requires a secure context (HTTPS or localhos
To enable this feature:
- Use HTTPS by setting up a reverse proxy (like Nginx or Caddy)
- OR access Homebox through localhost
## Open Multiple Items in New Tabs
By default browsers prevent opening multiple tabs with one click, to allow for the `View Items` button to work you therefore need to enable a setting usually called `Allow pop-ups and redirects` or similar for the domain you're using Homebox on.
- Chrome: [Block or allow pop-ups in Chrome](https://support.google.com/chrome/answer/95472?hl=en-GB&co=GENIE.Platform%3DDesktop#zippy=%2Callow-pop-ups-and-redirects-from-a-site)
- Firefox: [Pop-up blocker settings, exceptions and troubleshooting](https://support.mozilla.org/en-US/kb/pop-blocker-settings-exceptions-troubleshooting)
- Safari: [Block pop-up ads and windows in Safari](https://support.apple.com/en-gb/102524)

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

@@ -1,55 +0,0 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
"eslint:recommended",
"plugin:vue/essential",
"plugin:@typescript-eslint/recommended",
"@nuxtjs/eslint-config-typescript",
"plugin:vue/vue3-recommended",
"plugin:prettier/recommended",
"plugin:tailwindcss/recommended",
],
parserOptions: {
ecmaVersion: "latest",
parser: "@typescript-eslint/parser",
sourceType: "module",
},
plugins: ["vue", "@typescript-eslint"],
rules: {
"no-console": 0,
"no-unused-vars": "off",
"vue/multi-word-component-names": "off",
"vue/no-setup-props-destructure": 0,
"vue/no-multiple-template-root": 0,
"vue/no-v-model-argument": 0,
"vue/no-v-html": 0,
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/ban-ts-comment": 0,
"tailwindcss/no-custom-classname": "warn",
"@typescript-eslint/no-unused-vars": [
"error",
{
ignoreRestSiblings: true,
destructuredArrayIgnorePattern: "_",
caughtErrors: "none",
},
],
"prettier/prettier": [
"warn",
{
arrowParens: "avoid",
semi: true,
tabWidth: 2,
useTabs: false,
vueIndentScriptAndStyle: true,
singleQuote: false,
trailingComma: "es5",
printWidth: 120,
},
],
},
};

View File

@@ -6,7 +6,7 @@
<NuxtLayout>
<Html :lang="locale" :data-theme="theme || 'homebox'" />
<Link rel="icon" type="image/svg" href="/favicon.svg"></Link>
<Link rel="icon" type="image/svg" href="/favicon.svg" />
<Link rel="apple-touch-icon" href="/apple-touch-icon.png" size="180x180" />
<Link rel="mask-icon" href="/mask-icon.svg" color="#5b7f67" />
<Meta name="theme-color" content="#5b7f67" />

View File

@@ -1046,4 +1046,32 @@
:root {
--header-height: 4rem;
--header-height-mobile: 7rem;
}
/* Non-scoped styles for regular text */
.break-all {
word-break: break-all;
max-width: 100%;
}
/* Handle very long words */
pre,
code,
a,
p,
span,
div,
td,
th,
li,
blockquote,
h1,
h2,
h3,
h4,
h5,
h6 {
overflow-wrap: anywhere;
word-break: normal;
hyphens: auto;
}

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,15 @@
<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";
import { Dialog, DialogFooter, DialogHeader, DialogScrollContent, DialogTitle } from "@/components/ui/dialog";
import { Shortcut } from "@/components/ui/shortcut";
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,
@@ -50,7 +51,7 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
type Props = {
modelValue: boolean;
modelValue?: boolean;
};
const { t } = useI18n();
@@ -65,13 +66,13 @@
const api = useUserApi();
const importCsv = ref<File | null>(null);
const importCsv = ref<File | undefined>(undefined);
const importLoading = ref(false);
const importRef = ref<HTMLInputElement>();
whenever(
() => !dialog.value,
() => {
importCsv.value = null;
importCsv.value = undefined;
}
);
@@ -101,7 +102,7 @@
// Reset
dialog.value = false;
importLoading.value = false;
importCsv.value = null;
importCsv.value = undefined;
if (importRef.value) {
importRef.value.value = "";

View File

@@ -29,12 +29,12 @@
import { lt } from "semver";
import {
AlertDialog,
AlertDialogAction,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogAction,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { useDialog } from "@/components/ui/dialog-provider";

View File

@@ -1,12 +1,13 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { DialogID, type NoParamDialogIDs, type OptionalDialogIDs } from "@/components/ui/dialog-provider/utils";
import {
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "~/components/ui/command";
import { Shortcut } from "~/components/ui/shortcut";
@@ -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,8 +13,27 @@
<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>
<video ref="video" class="aspect-video w-full rounded-lg bg-muted shadow" poster="data:image/gif,AAAA" />
<div class="mt-4">
<Select v-model="selectedSource">
<SelectTrigger class="w-full">
@@ -33,17 +52,20 @@
</template>
<script setup lang="ts">
import { ref, watch, computed } from "vue";
import { BrowserMultiFormatReader, NotFoundException } from "@zxing/library";
import { computed, ref, watch } from "vue";
import { BarcodeFormat, BrowserMultiFormatReader, NotFoundException } from "@zxing/library";
import { useI18n } from "vue-i18n";
import { Dialog, DialogHeader, DialogTitle, DialogScrollContent } from "@/components/ui/dialog";
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";
import { DialogID } from "@/components/ui/dialog-provider/utils";
import { Dialog, DialogHeader, DialogScrollContent, DialogTitle } from "@/components/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Button, ButtonGroup } 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)) {
@@ -85,12 +113,12 @@
if (devices.length > 0) {
for (let i = 0; i < devices.length; i++) {
if (devices[i].label.toLowerCase().includes("back")) {
selectedSource.value = devices[i].deviceId;
if (devices[i]!.label.toLowerCase().includes("back")) {
selectedSource.value = devices[i]!.deviceId;
}
}
if (!selectedSource.value) {
selectedSource.value = devices[0].deviceId;
selectedSource.value = devices[0]!.deviceId;
}
} else {
errorMessage.value = t("scanner.no_sources");
@@ -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 class="col-start-1 row-start-2 bg-sidebar" />
<div class="col-start-1 row-start-3 bg-background-accent" />
<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

@@ -3,7 +3,7 @@
<CardHeader v-if="$slots.title" class="px-4 py-5 sm:px-6">
<component :is="collapsable ? 'button' : 'div'" v-on="collapsable ? { click: toggle } : {}">
<h3 class="flex items-center text-lg font-medium leading-6">
<slot name="title"></slot>
<slot name="title" />
<template v-if="collapsable">
<span class="ml-2 transition-transform" :class="{ 'rotate-180': collapsed }">
<MdiChevronDown class="size-6" />
@@ -13,10 +13,10 @@
</component>
<div>
<p v-if="$slots.subtitle" class="mt-1 max-w-2xl text-sm text-gray-500">
<slot name="subtitle"></slot>
<slot name="subtitle" />
</p>
<template v-if="$slots['title-actions']">
<slot name="title-actions"></slot>
<slot name="title-actions" />
</template>
</div>
</CardHeader>

View File

@@ -3,6 +3,7 @@
<CardTitle class="flex items-center">
<slot />
</CardTitle>
<slot name="subtitle" />
<CardDescription v-if="$slots.description">
<slot name="description" />
</CardDescription>

View File

@@ -2,24 +2,24 @@
<div class="flex flex-col gap-10 py-6 md:flex-row">
<div class="flex-1">
<h4 class="mb-1 text-lg font-semibold">
<slot name="title"></slot>
<slot name="title" />
</h4>
<p class="text-sm">
<slot></slot>
<slot />
</p>
</div>
<div class="flex items-center">
<template v-if="to">
<NuxtLink :to="to" :class="buttonVariants({ size: 'lg' })" class="min-w-52 grow">
<slot name="button">
<slot name="title"></slot>
<slot name="title" />
</slot>
</NuxtLink>
</template>
<template v-else>
<Button class="min-w-52 grow" size="lg" @click="$emit('action')">
<slot name="button">
<slot name="title"></slot>
<slot name="title" />
</slot>
</Button>
</template>

View File

@@ -11,7 +11,7 @@
const props = defineProps({
modelValue: {
type: String,
required: true,
required: false,
default: "",
},
label: {
@@ -81,7 +81,7 @@
:aria-label="`${t('components.color_selector.color')}: ${modelValue || t('components.color_selector.no_color_selected')}`"
role="button"
tabindex="0"
@click="$refs.colorInput.click()"
@click="($refs.colorInput as HTMLInputElement).click()"
/>
<span v-if="showHex" class="font-mono text-xs text-muted-foreground">{{
modelValue || t("components.color_selector.no_color")
@@ -122,7 +122,7 @@
:aria-label="`${t('components.color_selector.color')}: ${modelValue || t('components.color_selector.no_color_selected')}`"
role="button"
tabindex="0"
@click="$refs.colorInput.click()"
@click="($refs.colorInput as HTMLInputElement).click()"
/>
<span v-if="showHex" class="font-mono text-xs text-muted-foreground">{{
modelValue || t("components.color_selector.no_color")

View File

@@ -10,11 +10,11 @@
</template>
<script setup lang="ts">
// @ts-ignore
import VueDatePicker from "@vuepic/vue-datepicker";
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 +34,7 @@
},
});
const isDark = useIsDark();
const isDark = useIsThemeInList(darkThemes);
const formatDate = (date: Date | string | number) => fmtDate(date, "human", "date");

View File

@@ -1,7 +1,6 @@
<template>
<div class="relative">
<FormTextField v-model="value" :placeholder="localizedPlaceholder" :label="localizedLabel" :type="inputType">
</FormTextField>
<FormTextField v-model="value" :placeholder="localizedPlaceholder" :label="localizedLabel" :type="inputType" />
<TooltipProvider :delay-duration="0">
<Tooltip>
<TooltipTrigger as-child>
@@ -22,7 +21,8 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import MdiEye from "~icons/mdi/eye";
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import FormTextField from "@/components/Form/TextField.vue";
const { t } = useI18n();
type Props = {

View File

@@ -2,7 +2,7 @@
<div v-if="!inline" class="flex w-full flex-col gap-1.5">
<Label :for="id" class="flex w-full px-1">
<span>{{ label }}</span>
<span class="grow"></span>
<span class="grow" />
<span :class="{ 'text-destructive': isLengthInvalid }">
{{ lengthIndicator }}
</span>
@@ -18,7 +18,7 @@
<div v-else class="sm:grid sm:grid-cols-4 sm:items-start sm:gap-4">
<Label :for="id" class="flex w-full px-1 py-2">
<span>{{ label }}</span>
<span class="grow"></span>
<span class="grow" />
<span :class="{ 'text-destructive': isLengthInvalid }">
{{ lengthIndicator }}
</span>

View File

@@ -2,7 +2,7 @@
<div v-if="!inline" class="flex w-full flex-col gap-1.5">
<Label :for="id" class="flex w-full px-1">
<span> {{ label }} </span>
<span class="grow"></span>
<span class="grow" />
<span
:class="{
'text-destructive':
@@ -26,7 +26,7 @@
<div v-else class="sm:grid sm:grid-cols-4 sm:items-start sm:gap-4">
<Label class="flex w-full px-1 py-2" :for="id">
<span> {{ label }} </span>
<span class="grow"></span>
<span class="grow" />
<span
:class="{
'text-destructive':

View File

@@ -43,7 +43,7 @@
import MdiDownload from "~icons/mdi/download";
import MdiOpenInNew from "~icons/mdi/open-in-new";
import { buttonVariants } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
const props = defineProps({
attachments: {

View File

@@ -0,0 +1,261 @@
<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 { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Separator } from "@/components/ui/separator";
import BaseCard from "@/components/Base/Card.vue";
import FormTextField from "@/components/Form/TextField.vue";
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;
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function extractValue(data: Record<string, any>, 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,8 +1,30 @@
<template>
<Card class="overflow-hidden">
<Card class="relative overflow-hidden">
<div v-if="tableRow" class="absolute left-1 top-1 z-10">
<Checkbox
class="size-5 bg-accent hover:bg-background-accent"
:model-value="tableRow.getIsSelected()"
:aria-label="$t('components.item.view.selectable.select_card')"
@update:model-value="tableRow.toggleSelected()"
/>
</div>
<NuxtLink :to="`/item/${item.id}`">
<div class="relative h-[200px]">
<img v-if="imageUrl" class="h-[200px] w-full object-cover shadow-md" loading="lazy" :src="imageUrl" alt="" />
<img
v-if="imageUrl && objectContain"
class="absolute h-[200px] w-full object-cover blur-md"
loading="lazy"
:src="imageUrl"
alt=""
/>
<img
v-if="imageUrl"
class="absolute h-[200px] w-full shadow-md"
:class="objectContain ? 'object-contain' : 'object-cover'"
loading="lazy"
:src="imageUrl"
:alt="item.name"
/>
<div class="absolute inset-x-1 bottom-1">
<Badge class="text-wrap bg-secondary text-secondary-foreground hover:bg-secondary/70 hover:underline">
<NuxtLink v-if="item.location" :to="`/location/${item.location.id}`">
@@ -32,7 +54,7 @@
{{ $t("global.archived") }}
</TooltipContent>
</Tooltip>
<div class="grow"></div>
<div class="grow" />
<Tooltip>
<TooltipTrigger>
<Badge>
@@ -62,8 +84,13 @@
import { Card } from "@/components/ui/card";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Separator } from "@/components/ui/separator";
import Markdown from "@/components/global/Markdown.vue";
import LabelChip from "@/components/Label/Chip.vue";
import type { Row } from "@tanstack/vue-table";
import { Checkbox } from "@/components/ui/checkbox";
const api = useUserApi();
const preferences = useViewPreferences();
const imageUrl = computed(() => {
if (!props.item.imageId) {
@@ -90,8 +117,15 @@
required: false,
default: () => [],
},
tableRow: {
type: Object as () => Row<ItemSummary>,
required: false,
default: () => null,
},
});
const objectContain = computed(() => imageUrl.value !== "/no-image.jpg" && !preferences.value.legacyImageFit);
const locationString = computed(
() => props.locationFlatTree.find(l => l.id === props.item.location?.id)?.treeString || props.item.location?.name
);

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
@@ -10,6 +39,8 @@
:items="results"
item-text="name"
no-results-text="Type to search..."
:is-loading="isLoading"
:trigger-search="triggerSearch"
/>
<FormTextField
ref="nameInput"
@@ -140,6 +171,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 +180,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";
@@ -158,6 +192,10 @@
import { useDialog, useDialogHotkey } from "~/components/ui/dialog-provider";
import LabelSelector from "~/components/Label/Selector.vue";
import ItemSelector from "~/components/Item/Selector.vue";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/components/ui/tooltip";
import LocationSelector from "~/components/Location/Selector.vue";
import FormTextField from "~/components/Form/TextField.vue";
import FormTextArea from "~/components/Form/TextArea.vue";
interface PhotoPreview {
photoName: string;
@@ -167,9 +205,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();
@@ -183,7 +221,7 @@
const router = useRouter();
const parent = ref();
const { query, results } = useItemSearch(api, { immediate: false });
const { query, results, isLoading, triggerSearch } = useItemSearch(api, { immediate: false });
const subItemCreateParam = useRouteQuery("subItemCreate", "n");
const subItemCreate = ref();
@@ -244,8 +282,8 @@
function setPrimary(index: number) {
const primary = form.photos.findIndex(p => p.primary);
if (primary !== -1) form.photos[primary].primary = false;
if (primary !== index) form.photos[index].primary = true;
if (primary !== -1) form.photos[primary]!.primary = false;
if (primary !== index) form.photos[index]!.primary = true;
}
function previewImage(event: Event) {
@@ -267,55 +305,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) {
@@ -330,7 +382,7 @@
loading.value = true;
if (shift.value) close = false;
if (shift?.value) close = false;
const out: ItemCreate = {
parentId: form.parentId,
@@ -386,7 +438,7 @@
loading.value = false;
if (close) {
closeDialog("create-item");
closeDialog(DialogID.CreateItem);
navigateTo(`/item/${data.id}`);
}
}
@@ -394,7 +446,7 @@
function dataURLtoFile(dataURL: string, fileName: string) {
try {
const arr = dataURL.split(",");
const mimeMatch = arr[0].match(/:(.*?);/);
const mimeMatch = arr[0]!.match(/:(.*?);/);
if (!mimeMatch || !mimeMatch[1]) {
throw new Error("Invalid data URL format");
}
@@ -405,7 +457,7 @@
throw new Error("Invalid mime type, expected image");
}
const bstr = atob(arr[arr.length - 1]);
const bstr = atob(arr[arr.length - 1]!);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
@@ -454,8 +506,8 @@
// Encode image to data-uri with base64
try {
form.photos[index].fileBase64 = offScreenCanvas.toDataURL(imageType, 100);
form.photos[index].file = dataURLtoFile(form.photos[index].fileBase64, form.photos[index].photoName);
form.photos[index]!.fileBase64 = offScreenCanvas.toDataURL(imageType, 100);
form.photos[index]!.file = dataURLtoFile(form.photos[index]!.fileBase64, form.photos[index]!.photoName);
} catch (error) {
toast.error(t("components.item.create_modal.toast.rotate_process_failed"));
console.error(error);
@@ -465,4 +517,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 { Button, buttonVariants } 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

@@ -18,7 +18,13 @@
<Command :ignore-filter="true">
<CommandInput v-model="search" :placeholder="localizedSearchPlaceholder" :display-value="_ => ''" />
<CommandEmpty>
{{ localizedNoResultsText }}
<div v-if="isLoading" class="flex items-center justify-center p-4">
<div class="size-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
<span class="ml-2">{{ t("components.item.selector.searching") }}</span>
</div>
<div v-else>
{{ localizedNoResultsText }}
</div>
</CommandEmpty>
<CommandList>
<CommandGroup>
@@ -37,7 +43,7 @@
</template>
<script setup lang="ts">
import { ref, computed, watch } from "vue";
import { computed, ref, watch } from "vue";
import { Check, ChevronsUpDown } from "lucide-vue-next";
import fuzzysort from "fuzzysort";
import { useVModel } from "@vueuse/core";
@@ -47,7 +53,6 @@
import { Label } from "~/components/ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
import { cn } from "~/lib/utils";
import { useId } from "#imports";
const { t } = useI18n();
@@ -56,9 +61,9 @@
};
interface Props {
label: string;
modelValue: string | ItemsObject | null | undefined;
items: ItemsObject[] | string[];
label?: string;
modelValue?: string | ItemsObject | null | undefined;
items?: ItemsObject[] | string[];
itemText?: string;
itemValue?: string;
search?: string;
@@ -66,6 +71,8 @@
noResultsText?: string;
placeholder?: string;
excludeItems?: ItemsObject[];
isLoading?: boolean;
triggerSearch?: () => Promise<boolean>;
}
const emit = defineEmits(["update:modelValue", "update:search"]);
@@ -80,12 +87,15 @@
noResultsText: undefined,
placeholder: undefined,
excludeItems: undefined,
isLoading: false,
triggerSearch: undefined,
});
const id = useId();
const open = ref(false);
const search = ref(props.search);
const value = useVModel(props, "modelValue", emit);
const hasInitialSearch = ref(false);
const localizedSearchPlaceholder = computed(
() => props.searchPlaceholder ?? t("components.item.selector.search_placeholder")
@@ -93,6 +103,32 @@
const localizedNoResultsText = computed(() => props.noResultsText ?? t("components.item.selector.no_results"));
const localizedPlaceholder = computed(() => props.placeholder ?? t("components.item.selector.placeholder"));
// Trigger search when popover opens for the first time if no results exist
async function handlePopoverOpen() {
if (hasInitialSearch.value || props.items.length !== 0 || !props.triggerSearch) return;
try {
const success = await props.triggerSearch();
if (success) {
// Only mark as attempted after successful completion
hasInitialSearch.value = true;
}
// If not successful, leave hasInitialSearch false to allow retries
} catch (err) {
console.error("triggerSearch failed:", err);
// Leave hasInitialSearch false to allow retries on subsequent opens
}
}
watch(
() => open.value,
isOpen => {
if (isOpen) {
handlePopoverOpen();
}
}
);
watch(
() => props.search,
val => {
@@ -108,7 +144,7 @@
);
function isStrings(arr: string[] | ItemsObject[]): arr is string[] {
return typeof arr[0] === "string";
return arr.length > 0 && typeof arr[0] === "string";
}
function displayValue(item: string | ItemsObject | null | undefined): string {

View File

@@ -0,0 +1,169 @@
<script setup lang="ts">
import { Dialog, DialogContent, DialogFooter, DialogTitle, DialogHeader } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { useDialog } from "@/components/ui/dialog-provider";
import { DialogID } from "~/components/ui/dialog-provider/utils";
import type { ItemPatch, ItemSummary, LabelOut, LocationSummary } from "~/lib/api/types/data-contracts";
import LocationSelector from "~/components/Location/Selector.vue";
import MdiLoading from "~icons/mdi/loading";
import { toast } from "~/components/ui/sonner";
import { useI18n } from "vue-i18n";
import LabelSelector from "~/components/Label/Selector.vue";
const { closeDialog, registerOpenDialogCallback } = useDialog();
const api = useUserApi();
const { t } = useI18n();
const labelStore = useLabelStore();
const allLabels = computed(() => labelStore.labels);
const items = ref<ItemSummary[]>([]);
const saving = ref(false);
const enabled = reactive({
changeLocation: false,
addLabels: false,
removeLabels: false,
});
const newLocation = ref<LocationSummary | null>(null);
const addLabels = ref<string[]>([]);
const removeLabels = ref<string[]>([]);
const availableToAddLabels = ref<LabelOut[]>([]);
const availableToRemoveLabels = ref<LabelOut[]>([]);
const intersectLabelIds = (items: ItemSummary[]): string[] => {
if (items.length === 0) return [];
const counts = new Map<string, number>();
for (const it of items) {
const seen = new Set<string>();
for (const l of it.labels || []) seen.add(l.id);
for (const id of seen) counts.set(id, (counts.get(id) || 0) + 1);
}
return [...counts.entries()].filter(([_, c]) => c === items.length).map(([id]) => id);
};
const unionLabelIds = (items: ItemSummary[]): string[] => {
const s = new Set<string>();
for (const it of items) for (const l of it.labels || []) s.add(l.id);
return Array.from(s);
};
onMounted(() => {
const cleanup = registerOpenDialogCallback(DialogID.ItemChangeDetails, params => {
items.value = params.items;
enabled.changeLocation = params.changeLocation ?? false;
enabled.addLabels = params.addLabels ?? false;
enabled.removeLabels = params.removeLabels ?? false;
if (params.changeLocation && params.items.length > 0) {
// if all locations are the same then set the current location to said location
if (
params.items[0]!.location &&
params.items.every(item => item.location?.id === params.items[0]!.location?.id)
) {
newLocation.value = params.items[0]!.location;
}
}
if (params.addLabels && params.items.length > 0) {
const intersection = intersectLabelIds(params.items);
availableToAddLabels.value = allLabels.value.filter(l => !intersection.includes(l.id));
}
if (params.removeLabels && params.items.length > 0) {
const union = unionLabelIds(params.items);
availableToRemoveLabels.value = allLabels.value.filter(l => union.includes(l.id));
}
});
onUnmounted(cleanup);
});
const save = async () => {
const location = newLocation.value;
const labelsToAdd = addLabels.value;
const labelsToRemove = removeLabels.value;
if (!items.value.length || (enabled.changeLocation && !location)) {
return;
}
saving.value = true;
await Promise.allSettled(
items.value.map(async item => {
const patch: ItemPatch = {
id: item.id,
};
if (enabled.changeLocation) {
patch.locationId = location!.id;
}
let currentLabels = item.labels.map(l => l.id);
if (enabled.addLabels) {
currentLabels = currentLabels.concat(labelsToAdd);
}
if (enabled.removeLabels) {
currentLabels = currentLabels.filter(l => !labelsToRemove.includes(l));
}
if (enabled.addLabels || enabled.removeLabels) {
patch.labelIds = Array.from(new Set(currentLabels));
}
const { error, data } = await api.items.patch(item.id, patch);
if (error) {
console.error("failed to update item", item.id, data);
toast.error(t("components.item.view.change_details.failed_to_update_item"));
return;
}
})
);
closeDialog(DialogID.ItemChangeDetails, true);
enabled.changeLocation = false;
enabled.addLabels = false;
enabled.removeLabels = false;
items.value = [];
addLabels.value = [];
removeLabels.value = [];
availableToAddLabels.value = [];
availableToRemoveLabels.value = [];
saving.value = false;
};
</script>
<template>
<Dialog :dialog-id="DialogID.ItemChangeDetails">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ $t("components.item.view.change_details.title") }}</DialogTitle>
</DialogHeader>
<LocationSelector v-if="enabled.changeLocation" v-model="newLocation" />
<LabelSelector
v-if="enabled.addLabels"
v-model="addLabels"
:labels="availableToAddLabels"
:name="$t('components.item.view.change_details.add_labels')"
/>
<LabelSelector
v-if="enabled.removeLabels"
v-model="removeLabels"
:labels="availableToRemoveLabels"
:name="$t('components.item.view.change_details.remove_labels')"
/>
<DialogFooter>
<Button type="submit" :disabled="saving || (enabled.changeLocation && !newLocation)" @click="save">
<span v-if="!saving">{{ $t("global.save") }}</span>
<MdiLoading v-else class="animate-spin" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>

View File

@@ -5,15 +5,33 @@
import MdiTable from "~icons/mdi/table";
import { Badge } from "@/components/ui/badge";
import { Button, ButtonGroup } from "@/components/ui/button";
import BaseSectionHeader from "@/components/Base/SectionHeader.vue";
import DataTable from "./table/data-table.vue";
import { makeColumns } from "./table/columns";
import { useI18n } from "vue-i18n";
import type { Pagination } from "./pagination";
import MaintenanceEditModal from "@/components/Maintenance/EditModal.vue";
import ItemChangeDetails from "./ItemChangeDetails.vue";
type Props = {
const props = defineProps<{
view?: ViewType;
items: ItemSummary[];
};
locationFlatTree?: FlatTreeItem[];
pagination?: Pagination;
}>();
const emit = defineEmits<{
(e: "refresh"): void;
}>();
const preferences = useViewPreferences();
const { t } = useI18n();
const columns = computed(() =>
makeColumns(t, () => {
emit("refresh");
})
);
const props = defineProps<Props>();
const viewSet = computed(() => {
return !!props.view;
});
@@ -25,17 +43,29 @@
function setViewPreference(view: ViewType) {
preferences.value.itemDisplayView = view;
}
const externalPagination = computed(() => !!props.pagination);
</script>
<template>
<section>
<BaseSectionHeader class="mb-2 mt-4 flex items-center justify-between">
<div class="flex gap-2">
<MaintenanceEditModal />
<ItemChangeDetails />
<BaseSectionHeader class="flex items-center justify-between" :class="{ 'mb-2 mt-4': !externalPagination }">
<div class="flex gap-2 text-nowrap">
{{ $t("components.item.view.selectable.items") }}
<Badge>
<Badge v-if="!externalPagination">
{{ items.length }}
</Badge>
</div>
<template #subtitle>
<div
id="selectable-subtitle"
class="flex grow items-center px-2"
:class="{ hidden: !preferences.quickActions.enabled }"
/>
</template>
<template #description>
<div v-if="!viewSet">
<ButtonGroup>
@@ -56,15 +86,26 @@
</template>
</BaseSectionHeader>
<template v-if="itemView === 'table'">
<ItemViewTable :items="items" />
</template>
<template v-else>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
<ItemCard v-for="item in items" :key="item.id" :item="item" />
<div class="hidden first:block">{{ $t("components.item.view.selectable.no_items") }}</div>
</div>
</template>
<p v-if="externalPagination && pagination!.totalSize > 0" class="mb-4 flex items-center text-base font-medium">
{{ $t("items.results", { total: pagination!.totalSize }) }}
<span class="ml-auto text-base">
{{
$t("items.pages", {
page: pagination!.page,
totalPages: Math.ceil(pagination!.totalSize / pagination!.pageSize),
})
}}
</span>
</p>
<DataTable
:view="itemView"
:columns="preferences.quickActions.enabled ? columns : columns.filter(c => c.enableHiding !== false)"
:data="items"
:location-flat-tree="locationFlatTree"
:external-pagination="pagination"
@refresh="$emit('refresh')"
/>
</section>
</template>

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