Compare commits

...

12 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
c92488e90a Final verification: All tests pass and code builds successfully
Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>
2025-10-11 00:16:23 +00:00
copilot-swe-agent[bot]
47f87cbe30 Fix attachment upload error handling to return errors to UI
Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>
2025-10-11 00:14:26 +00:00
copilot-swe-agent[bot]
cc2acbdaac Initial plan 2025-10-11 00:04:17 +00:00
Tonya
28c3e102a2 feat: add a markdown preview for description and notes (#1043)
* feat: add a markdown preview for description and notes

* feat: add char count for md
2025-10-10 12:37:57 +00:00
zebrapurring
116e39531b Fix failing tests (#1009)
* chore: ignore all .data directories

* fix: date locale for unit tests

* test: disable parallelism to prevent database locks

* chore: fix lint errors

* chore: remove unused function

---------

Co-authored-by: zebrapurring <>
Co-authored-by: Tonya <tonya@tokia.dev>
2025-10-09 11:51:51 +00:00
rienkim
8a90b9c133 Set single connection pool for sqlite3 (#1039) 2025-10-08 14:58:29 -04:00
rienkim
ef52009f57 Feat/Added label maker custom font (#1038)
* Add label maker font config

* Add document for label maker font config

* Add test for custom font

* Fix custom font setup documentation

- Fallback font is gofont which don't support CJK characters

* Fix golangci-lint error

* Update custom-font-setup.md

* Fix typo
2025-10-08 14:49:22 -04:00
dependabot[bot]
76154263e0 Bump the npm_and_yarn group across 2 directories with 1 update (#1032)
Bumps the npm_and_yarn group with 1 update in the / directory: [nuxt](https://github.com/nuxt/nuxt/tree/HEAD/packages/nuxt).
Bumps the npm_and_yarn group with 1 update in the /frontend directory: [nuxt](https://github.com/nuxt/nuxt/tree/HEAD/packages/nuxt).


Updates `nuxt` from 4.0.3 to 4.1.0
- [Release notes](https://github.com/nuxt/nuxt/releases)
- [Commits](https://github.com/nuxt/nuxt/commits/v4.1.0/packages/nuxt)

Updates `nuxt` from 4.0.3 to 4.1.0
- [Release notes](https://github.com/nuxt/nuxt/releases)
- [Commits](https://github.com/nuxt/nuxt/commits/v4.1.0/packages/nuxt)

---
updated-dependencies:
- dependency-name: nuxt
  dependency-version: 4.1.0
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: nuxt
  dependency-version: 4.1.0
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-30 20:20:18 -04:00
Matthew Kilgore
108194e7fd Merge remote-tracking branch 'origin/main' 2025-09-29 19:21:19 -04:00
Matthew Kilgore
bf845ae0f7 Update bounty page 2025-09-29 19:21:05 -04:00
dependabot[bot]
9be6a8c888 Bump the npm_and_yarn group across 2 directories with 1 update (#1001)
Bumps the npm_and_yarn group with 1 update in the / directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).
Bumps the npm_and_yarn group with 1 update in the /frontend directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 5.4.18 to 5.4.20
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.20/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.20/packages/vite)

Updates `vite` from 7.1.3 to 7.1.5
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.20/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.20/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 5.4.20
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-version: 7.1.5
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-29 12:07:33 -04:00
Alan Mooiman
38a987676e Fix frontend CI (#1028) 2025-09-29 08:18:43 -04:00
27 changed files with 3613 additions and 4457 deletions

2
.gitignore vendored
View File

@@ -60,7 +60,7 @@ backend/app/api/static/public/*
backend/api
docs/.vitepress/cache/
/.data/
.data/
# Playwright
frontend/test-results/

View File

@@ -22,6 +22,7 @@
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"eslint.format.enable": true,
"eslint.validate": ["javascript", "typescript", "vue"],
"eslint.useFlatConfig": true,
"css.validate": false,
"tailwindCSS.includeLanguages": {

View File

@@ -138,6 +138,13 @@ func run(cfg *config.Config) error {
)
}
if strings.ToLower(cfg.Database.Driver) == "sqlite3" {
db := c.Sql()
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
log.Info().Msg("SQLite connection pool configured: max_open=1, max_idle=1")
}
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)

View File

@@ -50,6 +50,7 @@ func (svc *ItemService) AttachmentAdd(ctx Context, itemID uuid.UUID, filename st
_, err = svc.repo.Attachments.Create(ctx, itemID, repo.ItemCreateAttachment{Title: filename, Content: file}, attachmentType, primary)
if err != nil {
log.Err(err).Msg("failed to create attachment")
return repo.ItemOut{}, err
}
return svc.repo.Items.GetOneByGroup(ctx, ctx.GID, itemID)

View File

@@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
)
func TestItemService_AddAttachment(t *testing.T) {
@@ -60,3 +61,53 @@ func TestItemService_AddAttachment(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, contents, string(bts))
}
func TestItemService_AddAttachment_InvalidStorage(t *testing.T) {
// Create a service with an invalid storage path to simulate the issue
svc := &ItemService{
repo: tRepos,
filepath: "/nonexistent/path/that/should/not/exist",
}
// Create a temporary repo with invalid storage config
invalidRepos := repo.New(tClient, tbus, config.Storage{
PrefixPath: "/",
ConnString: "file:///nonexistent/directory/that/does/not/exist",
}, "mem://{{ .Topic }}", config.Thumbnail{
Enabled: false,
Width: 0,
Height: 0,
})
svc.repo = invalidRepos
loc, err := invalidRepos.Locations.Create(context.Background(), tGroup.ID, repo.LocationCreate{
Description: "test",
Name: "test-invalid",
})
require.NoError(t, err)
assert.NotNil(t, loc)
itmC := repo.ItemCreate{
Name: fk.Str(10),
Description: fk.Str(10),
LocationID: loc.ID,
}
itm, err := invalidRepos.Items.Create(context.Background(), tGroup.ID, itmC)
require.NoError(t, err)
assert.NotNil(t, itm)
t.Cleanup(func() {
err := invalidRepos.Items.Delete(context.Background(), itm.ID)
require.NoError(t, err)
})
contents := fk.Str(1000)
reader := strings.NewReader(contents)
// Attempt to add attachment with invalid storage - should return an error
_, err = svc.AttachmentAdd(tCtx, itm.ID, "testfile.txt", "attachment", false, reader)
// This should return an error now (after the fix)
assert.Error(t, err, "AttachmentAdd should return an error when storage is invalid")
}

View File

@@ -218,9 +218,8 @@ func (r *AttachmentRepo) Create(ctx context.Context, itemID uuid.UUID, doc ItemC
// Upload the file to the storage bucket
path, err := r.UploadFile(ctx, itemGroup, doc)
if err != nil {
err := tx.Rollback()
if err != nil {
return nil, err
if rollbackErr := tx.Rollback(); rollbackErr != nil {
return nil, rollbackErr
}
return nil, err
}

View File

@@ -3,18 +3,18 @@ package repo
import (
"testing"
"github.com/sysadminsmedia/homebox/backend/pkgs/textutils"
"github.com/stretchr/testify/assert"
"github.com/sysadminsmedia/homebox/backend/pkgs/textutils"
)
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 string
itemName string
searchQuery string
shouldMatch bool
description string
}{
{
name: "Spanish accented item, search without accents",
@@ -155,25 +155,25 @@ func TestItemsRepository_AccentInsensitiveSearch(t *testing.T) {
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')",
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')",
t.Logf("✗ %s: Item '%s' should NOT be found with search '%s' (normalized: '%s')",
tc.description, tc.itemName, tc.searchQuery, normalizedSearch)
}
})

View File

@@ -71,6 +71,8 @@ type LabelMakerConf struct {
DynamicLength bool `yaml:"bool" conf:"default:true"`
LabelServiceUrl *string `yaml:"label_service_url"`
LabelServiceTimeout *time.Duration `yaml:"label_service_timeout"`
RegularFontPath *string `yaml:"regular_font_path"`
BoldFontPath *string `yaml:"bold_font_path"`
}
type BarcodeAPIConf struct {

View File

@@ -27,6 +27,13 @@ import (
"golang.org/x/image/font/gofont/gomedium"
)
type FontType int
const (
FontTypeRegular FontType = iota
FontTypeBold
)
type GenerateParameters struct {
Width int
Height int
@@ -140,6 +147,48 @@ func wrapText(text string, face font.Face, maxWidth int, maxHeight int, lineHeig
return wrappedLines, ""
}
func loadFont(cfg *config.Config, fontType FontType) (*truetype.Font, error) {
var fontPath *string
var fallbackData []byte
switch fontType {
case FontTypeRegular:
if cfg != nil && cfg.LabelMaker.RegularFontPath != nil {
fontPath = cfg.LabelMaker.RegularFontPath
}
fallbackData = gomedium.TTF
case FontTypeBold:
if cfg != nil && cfg.LabelMaker.BoldFontPath != nil {
fontPath = cfg.LabelMaker.BoldFontPath
}
fallbackData = gobold.TTF
default:
return nil, fmt.Errorf("unknown font type: %d", fontType)
}
if fontPath != nil && *fontPath != "" {
data, err := os.ReadFile(*fontPath)
if err != nil {
log.Printf("Failed to load font from %s: %v, using fallback font", *fontPath, err)
} else {
font, err := truetype.Parse(data)
if err != nil {
log.Printf("Failed to parse font from %s: %v, using fallback font", *fontPath, err)
} else {
log.Printf("Successfully loaded font from %s", *fontPath)
return font, nil
}
}
}
font, err := truetype.Parse(fallbackData)
if err != nil {
return nil, err
}
return font, nil
}
func GenerateLabel(w io.Writer, params *GenerateParameters, cfg *config.Config) error {
if err := params.Validate(); err != nil {
return err
@@ -165,12 +214,12 @@ func GenerateLabel(w io.Writer, params *GenerateParameters, cfg *config.Config)
qr.DisableBorder = true
qrImage := qr.Image(params.QrSize)
regularFont, err := truetype.Parse(gomedium.TTF)
regularFont, err := loadFont(cfg, FontTypeRegular)
if err != nil {
return err
}
boldFont, err := truetype.Parse(gobold.TTF)
boldFont, err := loadFont(cfg, FontTypeBold)
if err != nil {
return err
}

View File

@@ -0,0 +1,187 @@
package labelmaker
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
"golang.org/x/image/font/gofont/gobold"
"golang.org/x/image/font/gofont/gomedium"
)
func TestLoadFont_WithNilConfig(t *testing.T) {
font, err := loadFont(nil, FontTypeRegular)
require.NoError(t, err)
assert.NotNil(t, font)
font, err = loadFont(nil, FontTypeBold)
require.NoError(t, err)
assert.NotNil(t, font)
}
func TestLoadFont_WithEmptyConfig(t *testing.T) {
cfg := &config.Config{}
font, err := loadFont(cfg, FontTypeRegular)
require.NoError(t, err)
assert.NotNil(t, font)
font, err = loadFont(cfg, FontTypeBold)
require.NoError(t, err)
assert.NotNil(t, font)
}
func TestLoadFont_WithCustomFontPath(t *testing.T) {
tempDir := t.TempDir()
fontPath := filepath.Join(tempDir, "test-font.ttf")
err := os.WriteFile(fontPath, gomedium.TTF, 0644)
require.NoError(t, err)
cfg := &config.Config{
LabelMaker: config.LabelMakerConf{
RegularFontPath: &fontPath,
},
}
font, err := loadFont(cfg, FontTypeRegular)
require.NoError(t, err)
assert.NotNil(t, font)
}
func TestLoadFont_WithNonExistentFontPath(t *testing.T) {
nonExistentPath := "/non/existent/path/font.ttf"
cfg := &config.Config{
LabelMaker: config.LabelMakerConf{
RegularFontPath: &nonExistentPath,
},
}
font, err := loadFont(cfg, FontTypeRegular)
require.NoError(t, err)
assert.NotNil(t, font)
}
func TestLoadFont_UnknownFontType(t *testing.T) {
cfg := &config.Config{}
_, err := loadFont(cfg, FontType(999))
require.Error(t, err)
assert.Contains(t, err.Error(), "unknown font type")
}
func TestLoadFont_BoldFontWithCustomPath(t *testing.T) {
tempDir := t.TempDir()
fontPath := filepath.Join(tempDir, "test-bold-font.ttf")
err := os.WriteFile(fontPath, gobold.TTF, 0644)
require.NoError(t, err)
cfg := &config.Config{
LabelMaker: config.LabelMakerConf{
BoldFontPath: &fontPath,
},
}
font, err := loadFont(cfg, FontTypeBold)
require.NoError(t, err)
assert.NotNil(t, font)
}
func TestLoadFont_EmptyStringPath(t *testing.T) {
emptyPath := ""
cfg := &config.Config{
LabelMaker: config.LabelMakerConf{
RegularFontPath: &emptyPath,
},
}
font, err := loadFont(cfg, FontTypeRegular)
require.NoError(t, err)
assert.NotNil(t, font)
}
func TestLoadFont_CJKRendering(t *testing.T) {
cjkFontPath := filepath.Join("testdata", "NotoSansKR-VF.ttf")
tests := []struct {
name string
text string
fontPath string
shouldHaveGlyph bool
}{
{
name: "Korean with default font",
text: "한글",
fontPath: "",
shouldHaveGlyph: false,
},
{
name: "Chinese with default font",
text: "中文",
fontPath: "",
shouldHaveGlyph: false,
},
{
name: "Japanese with default font",
text: "ひらがなカタカナ",
fontPath: "",
shouldHaveGlyph: false,
},
{
name: "Korean with Noto Sans CJK",
text: "한글",
fontPath: cjkFontPath,
shouldHaveGlyph: true,
},
{
name: "Chinese with Noto Sans CJK",
text: "中文",
fontPath: cjkFontPath,
shouldHaveGlyph: true,
},
{
name: "Japanese with Noto Sans CJK",
text: "ひらがなカタカナ",
fontPath: cjkFontPath,
shouldHaveGlyph: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var cfg *config.Config
if tt.fontPath != "" {
if _, err := os.Stat(tt.fontPath); os.IsNotExist(err) {
t.Skipf("Font file not found: %s", tt.fontPath)
}
cfg = &config.Config{
LabelMaker: config.LabelMakerConf{
RegularFontPath: &tt.fontPath,
},
}
}
font, err := loadFont(cfg, FontTypeRegular)
require.NoError(t, err)
require.NotNil(t, font)
hasAllGlyphs := true
for _, r := range tt.text {
if font.Index(r) == 0 {
hasAllGlyphs = false
break
}
}
if tt.shouldHaveGlyph {
assert.True(t, hasAllGlyphs, "Font should render %s characters", tt.name)
} else {
assert.False(t, hasAllGlyphs, "Default font should not render %s characters", tt.name)
}
})
}
}

View File

@@ -0,0 +1,92 @@
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

View File

@@ -0,0 +1,15 @@
# Test Data
This directory contains font files used only for testing purposes.
## Fonts
- **NotoSansKR-VF.ttf**: Noto Sans CJK Korean Variable Font
- Used for testing CJK (Chinese, Japanese, Korean) character rendering
- License: See `NotoSans-LICENSE` file
## Notes
- These fonts are **only used during testing** and are **not included in production builds**
- Go's build system automatically excludes `testdata` directories from production builds
- The fonts support rendering of Korean, Chinese, and Japanese characters

View File

@@ -48,6 +48,8 @@ aside: false
| HBOX_LABEL_MAKER_PRINT_COMMAND | | the command to use for printing labels. if empty, label printing is disabled. <span v-pre>`{{.FileName}}`</span> in the command will be replaced with the png filename of the label |
| HBOX_LABEL_MAKER_DYNAMIC_LENGTH | true | allow label generation with open length. `HBOX_LABEL_MAKER_HEIGHT` is still used for layout and minimal height. If not used, long text may be cut off, but all labels have the same size. |
| HBOX_LABEL_MAKER_ADDITIONAL_INFORMATION | | Additional information added to the label like name or phone number |
| HBOX_LABEL_MAKER_REGULAR_FONT_PATH | | path to regular font file for label generation (e.g., `/fonts/NotoSansKR-Regular.ttf`). If not set, uses embedded font. Supports TTF format. |
| HBOX_LABEL_MAKER_BOLD_FONT_PATH | | path to bold font file for label generation (e.g., `/fonts/NotoSansKR-Bold.ttf`). If not set, uses embedded font. Supports TTF format. |
| HBOX_THUMBNAIL_ENABLED | true | enable thumbnail generation for images, supports PNG, JPEG, AVIF, WEBP, GIF file types |
| HBOX_THUMBNAIL_WIDTH | 500 | width for generated thumbnails in pixels |
| HBOX_THUMBNAIL_HEIGHT | 500 | height for generated thumbnails in pixels |
@@ -192,6 +194,8 @@ OPTIONS
--label-maker-print-command/$HBOX_LABEL_MAKER_PRINT_COMMAND <string>
--label-maker-dynamic-length/$HBOX_LABEL_MAKER_DYNAMIC_LENGTH <bool> (default: true)
--label-maker-additional-information/$HBOX_LABEL_MAKER_ADDITIONAL_INFORMATION <string>
--label-maker-regular-font-path/$HBOX_LABEL_MAKER_REGULAR_FONT_PATH <string>
--label-maker-bold-font-path/$HBOX_LABEL_MAKER_BOLD_FONT_PATH <string>
--thumbnail-enabled/$HBOX_THUMBNAIL_ENABLED <bool> (default: true)
--thumbnail-width/$HBOX_THUMBNAIL_WIDTH <int> (default: 500)
--thumbnail-height/$HBOX_THUMBNAIL_HEIGHT <int> (default: 500)

View File

@@ -1,19 +1,7 @@
# Bounty Program
## About
As part of our commitment to open source, and building an active community around Homebox (and hopefully active pool of developers), we are enabling bounties on issues.
::: danger Bounties Paused
Do to issues with the bounty provider we were using, the bounty program is currently paused. We are working on a solution and will update this page when bounties are available again.
After digging through several platforms, we ended up settling on [boss.dev](https://www.boss.dev/) as it has some of the lowest fees we could possibly find for any of these platforms other than spinning one up ourselves (which we currently aren't in a position to do).
While it's not the perfect solution, we think it's about the best one we could find at the moment to lower the rates as much as possible to make sure everyone get's the highest payouts possible. (Some we found were as high as a combined 16%!!!)
We hope that by enabling bounties on issues, people who have the means and want certain features implemented quicker can now sponsor issues, and in turn everyone contributing code can potentially earn some money for their hard work.
## Contributor
As a contributor wanting to accept money from bounties all you need to do is simply register for an account via GitHub, and attach a bank account (or debit card in the USA).
## Sponsor
Sign in with a GitHub account, and then attach a credit card to your account.
## Commands to use boss.dev
There is documentation on their website regarding commands that you can put in comments to use the bounty system. [boss.dev Documentation](https://www.boss.dev/doc)
For more information, please check our [Blog post](https://sysadminsjournal.com/navigating-the-future-of-homeboxs-bounties/).
:::

View File

@@ -0,0 +1,80 @@
# External Font Support for Label Maker
Label maker supports external font files.
## Quick Start
### Docker/Podman Setup
1. **Download external fonts** (e.g., Noto Sans KR):
- Download from [Google Fonts](https://fonts.google.com/noto/specimen/Noto+Sans+KR)
- Or use the Variable Font from [GitHub](https://github.com/notofonts/noto-cjk)
2. **Create a fonts directory**:
```bash
mkdir -p ./fonts
# Place your font files in this directory
# e.g., NotoSansKR-VF.ttf
```
3. **Mount the fonts directory and set environment variables**:
```yaml
# docker-compose.yml
services:
homebox:
image: homebox:latest
volumes:
- ./data:/data
- ./fonts:/fonts:ro # Mount fonts directory as read-only
environment:
- HBOX_LABEL_MAKER_REGULAR_FONT_PATH=/fonts/NotoSansKR-VF.ttf
- HBOX_LABEL_MAKER_BOLD_FONT_PATH=/fonts/NotoSansKR-VF.ttf
ports:
- 3100:7745
```
Or with podman:
```bash
podman run -d \
--name homebox \
-p 3100:7745 \
-v ./data:/data \
-v ./fonts:/fonts:ro \
-e HBOX_LABEL_MAKER_REGULAR_FONT_PATH=/fonts/NotoSansKR-VF.ttf \
-e HBOX_LABEL_MAKER_BOLD_FONT_PATH=/fonts/NotoSansKR-VF.ttf \
homebox:latest
```
4. **Restart the container** and test label generation with Chinese, Japanese, Korean text!
## Supported Fonts
- **Format**: TTF (TrueType Font)
- **Recommended Fonts**:
- Noto Sans KR (Korean)
- Noto Sans CJK (Chinese, Japanese, Korean)
- Noto Sans SC (Simplified Chinese)
- Noto Sans JP (Japanese)
## Fallback Behavior
1. **External font specified** → Tries to load from `HBOX_LABEL_MAKER_*_FONT_PATH`
2. **External font fails or not specified** → Falls back to bundled Go fonts (Latin-only, **does not support CJK characters**)
## Troubleshooting
### Labels still show squares (□□□)
- Check if the font file exists at the specified path
- Verify the font file format (must be TTF, not OTF)
- Check container logs: `podman logs homebox | grep -i font`
### Font file not found
- Ensure the volume is correctly mounted
- Check file permissions (font files should be readable)
- Use absolute paths in environment variables
## Why External Fonts?
- **Smaller base image**: No need to embed large font files (~10MB per font)
- **Flexibility**: Easily switch fonts without rebuilding the image
- **Multi-language support**: Add support for any language by mounting appropriate fonts

View File

@@ -1005,12 +1005,12 @@
.markdown :where(ul) {
list-style: disc;
margin-left: 2rem;
margin-left: 1rem;
}
.markdown :where(ol) {
list-style: decimal;
margin-left: 2rem;
margin-left: 1rem;
}
/* Heading Styles */
.markdown :where(h1) {

View File

@@ -0,0 +1,83 @@
<script setup lang="ts">
import { ref, watch } from "vue";
import Markdown from "@/components/global/Markdown.vue";
import { Checkbox } from "@/components/ui/checkbox";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
const props = withDefaults(
defineProps<{
modelValue?: string | null;
label?: string | null;
maxLength?: number;
minLength?: number;
}>(),
{
modelValue: null,
label: null,
maxLength: -1,
minLength: -1,
}
);
const emit = defineEmits(["update:modelValue"]);
const local = ref(props.modelValue ?? "");
watch(
() => props.modelValue,
v => {
if (v !== local.value) local.value = v ?? "";
}
);
watch(local, v => emit("update:modelValue", v === "" ? null : v));
const showPreview = ref(false);
const id = useId();
const isLengthInvalid = computed(() => {
if (typeof local.value !== "string") return false;
const len = local.value.length;
const max = props.maxLength ?? -1;
const min = props.minLength ?? -1;
return (max !== -1 && len > max) || (min !== -1 && len < min);
});
const lengthIndicator = computed(() => {
if (typeof local.value !== "string") return "";
const max = props.maxLength ?? -1;
if (max !== -1) {
return `${local.value.length}/${max}`;
}
return "";
});
</script>
<template>
<div class="w-full">
<div class="mb-2 grid grid-cols-1 items-center gap-2 md:grid-cols-4">
<div class="min-w-0">
<Label :for="id" class="flex min-w-0 items-center gap-2 px-1">
<span class="truncate" :title="props.label ?? ''">{{ props.label }}</span>
<span class="grow" />
<span class="ml-2 text-sm" :class="{ 'text-destructive': isLengthInvalid }">{{ lengthIndicator }}</span>
</Label>
</div>
<div class="col-span-1 flex items-center justify-start gap-2 md:col-span-3 md:justify-end">
<label class="text-xs text-slate-500">{{ $t("global.preview") }}</label>
<Checkbox v-model="showPreview" />
</div>
</div>
<div class="flex w-full flex-col gap-4">
<Textarea :id="id" v-model="local" autosize class="resize-none" />
<div v-if="showPreview">
<Markdown :source="local" />
</div>
</div>
</div>
</template>

View File

@@ -25,14 +25,13 @@
<template>
<!-- eslint-disable-next-line vue/no-v-html -->
<div class="markdown text-wrap break-words" v-html="raw" />
<div class="markdown prose text-wrap break-words" v-html="raw" />
</template>
<style scoped>
* {
word-wrap: break-word; /*Fix for long words going out of emelent bounds and issue #407 */
overflow-wrap: break-word; /*Fix for long words going out of emelent bounds and issue #407 */
white-space: pre-wrap; /*Fix for long words going out of emelent bounds and issue #407 */
}
.markdown {
max-width: 100%;

View File

@@ -1,3 +1,5 @@
import type { CurrenciesCurrency } from "~/lib/api/types/data-contracts";
export function validDate(dt: Date | string | null | undefined): boolean {
if (!dt) {
return false;
@@ -42,7 +44,7 @@ function clampDecimals(currency: string, decimals: number): number {
}
// Type guard to validate currency response shape with strict validation
function isValidCurrencyItem(item: any): item is { code: string; decimals: number } {
function isValidCurrencyItem(item: CurrenciesCurrency): boolean {
if (
typeof item !== "object" ||
item === null ||

View File

@@ -1,20 +1,9 @@
import { describe, expect, test } from "vitest";
import { factorRange, format, parse, zeroTime } from "./datelib";
describe("format", () => {
test("should format a date as a string", () => {
const date = new Date(2020, 1, 1);
expect(format(date)).toBe("2020-02-01");
});
test("should return the string if a string is passed in", () => {
expect(format("2020-02-01")).toBe("2020-02-01");
});
});
import { factorRange, parse, zeroTime } from "./datelib";
describe("zeroTime", () => {
test("should zero out the time", () => {
const date = new Date(2020, 1, 1, 12, 30, 30);
const date = new Date(Date.UTC(2020, 1, 1, 12, 30, 30));
const zeroed = zeroTime(date);
expect(zeroed.getHours()).toBe(0);
expect(zeroed.getMinutes()).toBe(0);

View File

@@ -1,19 +1,9 @@
import { addDays } from "date-fns";
/*
* Formats a date as a string
* */
export function format(date: Date | string): string {
if (typeof date === "string") {
return date;
}
return date.toISOString().split("T")[0]!;
}
export function zeroTime(date: Date): Date {
return new Date(
new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() - date.getTimezoneOffset() * 60000
);
const result = new Date(date.getTime());
result.setHours(0, 0, 0, 0);
return result;
}
export function factorRange(offset: number = 7): [Date, Date] {

View File

@@ -279,7 +279,8 @@
"updating": "Updating",
"value": "Value",
"version": "Version: { version }",
"welcome": "Welcome, { username }"
"welcome": "Welcome, { username }",
"preview": "Preview"
},
"home": {
"labels": "Labels",

View File

@@ -9,7 +9,7 @@
"lint:fix": "eslint --ext \".ts,.js,.vue\" --ignore-pattern \"components/ui\" . --fix",
"lint:ci": "eslint --ext \".ts,.js,.vue\" --ignore-pattern \"components/ui\" . --max-warnings 1",
"typecheck": "pnpm nuxi typecheck --noEmit",
"test:ci": "TEST_SHUTDOWN_API_SERVER=true vitest --run --config ./test/vitest.config.ts",
"test:ci": "TEST_SHUTDOWN_API_SERVER=true vitest --run --config ./test/vitest.config.ts --no-file-parallelism",
"test:local": "TEST_SHUTDOWN_API_SERVER=false && vitest --run --config ./test/vitest.config.ts",
"test:watch": " TEST_SHUTDOWN_API_SERVER=false vitest --config ./test/vitest.config.ts"
},
@@ -32,7 +32,7 @@
"h3": "^1.7.1",
"intl-messageformat": "^10.7.16",
"isomorphic-fetch": "^3.0.0",
"nuxt": "4.0.3",
"nuxt": "4.1.0",
"prettier": "^3.6.2",
"shadcn-nuxt": "2.2.0",
"typescript": "5.9.2",
@@ -74,7 +74,7 @@
"tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7",
"vaul-vue": "^0.4.1",
"vite": "^7.1.3",
"vite": "^7.1.5",
"vue": "3.5.20",
"vue-router": "^4.5.1",
"vue-sonner": "^2.0.8"

View File

@@ -22,6 +22,7 @@
import { DialogID } from "~/components/ui/dialog-provider/utils";
import FormTextField from "~/components/Form/TextField.vue";
import FormTextArea from "~/components/Form/TextArea.vue";
import MarkdownEditor from "~/components/Form/MarkdownEditor.vue";
import FormDatePicker from "~/components/Form/DatePicker.vue";
import FormCheckbox from "~/components/Form/Checkbox.vue";
import LocationSelector from "~/components/Location/Selector.vue";
@@ -147,7 +148,7 @@
type DateKeys<T> = Extract<keyof T, keyof { [K in keyof T as T[K] extends Date | string ? K : never]: any }>;
type TextFormField = {
type: "text" | "textarea";
type: "text" | "textarea" | "markdown";
label: string;
ref: NonNullableStringKeys<ItemOut>;
maxLength?: number;
@@ -188,7 +189,7 @@
ref: "quantity",
},
{
type: "textarea",
type: "markdown",
label: "items.description",
ref: "description",
maxLength: 1000,
@@ -212,7 +213,7 @@
maxLength: 255,
},
{
type: "textarea",
type: "markdown",
label: "items.notes",
ref: "notes",
maxLength: 1000,
@@ -613,6 +614,13 @@
:max-length="field.maxLength"
:min-length="field.minLength"
/>
<MarkdownEditor
v-else-if="field.type === 'markdown'"
v-model="item[field.ref]"
:label="$t(field.label)"
:max-length="field.maxLength"
:min-length="field.minLength"
/>
<FormTextField
v-else-if="field.type === 'text'"
v-model="item[field.ref]"

3917
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

3463
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff