Skip to content

fix: various bugs#1596

Open
tankerkiller125 wants to merge 1 commit into
mainfrom
mk/fixes
Open

fix: various bugs#1596
tankerkiller125 wants to merge 1 commit into
mainfrom
mk/fixes

Conversation

@tankerkiller125

Copy link
Copy Markdown
Contributor

What type of PR is this?

  • bug

What this PR does / why we need it:

Fixes various bugs with 0.26.x

Which issue(s) this PR fixes:

Fixes: #1592
Fixes: #1591
Fixes: #1589
Fixes: #1578
Fixes: #1574
Fixes: #1538

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Summary by CodeRabbit

  • New Features

    • Added support for manufacturer and model number when creating items.
    • Item details now show a derived location based on its parent location.
    • Label generation now uses the item’s parent location and works more reliably for missing location data.
  • Bug Fixes

    • Uploads now show a clear “file too large” message when they exceed the server limit.
    • Quantity fields now reject negative values.
    • Moving items no longer incorrectly reassigns child items when locations change.

Walkthrough

This PR fixes several bugs: multipart form parsing now returns 413 for oversized uploads instead of generic errors; export/import Pub/Sub topics are cached and reused instead of shut down per publish (fixing repeat backup failures); entity parent changes no longer reparent children, and a derived Location field is added; manufacturer/model fields are preserved on barcode import; label generator no longer blanks labels missing a location edge.

Changes

Multipart Upload Size Handling

Layer / File(s) Summary
413 detection helper
backend/app/api/handlers/v1/controller.go
multipartFormError detects http.MaxBytesError and returns 413 with a byte-limit message, otherwise 400.
Handler wiring
backend/app/api/handlers/v1/v1_ctrl_entities.go, v1_ctrl_entities_attachments.go, v1_ctrl_exports.go
Three handlers now return multipartFormError(err) on parse failure instead of ad-hoc error construction.
Frontend toast handling
frontend/pages/item/[id]/index/edit.vue, frontend/pages/location/[id]/index/edit.vue, frontend/locales/en.json
Upload error toasts show an "attachment too large" message on HTTP 413, added new locale string.

Pub/Sub Topic Reuse

Layer / File(s) Summary
Topic caching
backend/internal/core/services/service_exports.go, service_exports_test.go
ExportService caches opened topics in a mutex-protected map via openTopic, reused by publishExportJob/publishImportJob instead of shutting down per call; regression test added.

Entity Location Derivation & Reparent Fix

Layer / File(s) Summary
Contract and Create persistence
backend/internal/data/repo/repo_entities.go
EntityCreate gains ModelNumber/Manufacturer, EntityOut gains Location, and Create persists the new fields.
Ancestor-based location derivation
backend/internal/data/repo/repo_entities.go
getOneTx walks the parent chain via new nearestLocationAncestor/maxAncestorDepth to populate Location.
Remove child reparenting, validate quantity
backend/internal/data/repo/repo_entities.go
UpdateByGroup/Patch no longer reparent children; validateQuantity rejects negative values; tests cover reparenting, derived location, manufacturer persistence, and negative quantity across write paths (repo_entities_test.go).
CreateModal manufacturer/model and templates
frontend/components/Entity/CreateModal.vue
Form gains manufacturer/modelNumber copied from imported product, template restore logic adjusted, quantity gets min=0.
Generated types
frontend/lib/api/types/data-contracts.ts
EntityCreate fields made optional/nullable; EntityOut.location added.
Item edit page location/parent split
frontend/pages/item/[id]/index/edit.vue
New location ref distinct from parent, save validation and payload derivation updated, Location selector rewired, min added to number fields.

Label Generator Fix

Layer / File(s) Summary
Derive location from parent, always render
frontend/pages/reports/label-generator.vue
getItem derives location from item.parent?.name; loop always calls getItem instead of skipping items without a location edge.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested labels: 🕷️ bug, go

Suggested reviewers: katosdev, tonyaellie

Security recommendation: Since this PR introduces a proper 413 response for oversized multipart uploads, double-check that maxParseMemory is still enforced consistently server-side (not just client-facing messaging) so a crafted request can't bypass the limit before ParseMultipartForm runs — a small DoS-hardening review here goes a long way. 🛡️

Poem

A rabbit hopped through topics stale,
Reopened each one, no more "shutdown" tale.
Boxes keep their children near,
Locations traced through ancestors clear.
Uploads too big? 413 now rings true,
Labels bloom again — hop, hop, hooray for you! 🐰📦

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is related to the PR, but it is too generic to describe the main changes. Use a more descriptive title that names the main fixes, such as backup retries, location handling, barcode imports, labels, and upload errors.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description includes the required type, summary, and fixed issues, and only omits optional sections like testing and reviewer notes.
Linked Issues check ✅ Passed All listed objectives appear covered: [#1592, #1591, #1589, #1578, #1574, #1538].
Out of Scope Changes check ✅ Passed No clear out-of-scope changes are evident; the diff aligns with the stated bug fixes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch mk/fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot requested review from katosdev and tonyaellie July 3, 2026 21:38
@coderabbitai coderabbitai Bot added go Pull requests that update Go code 🕷️ bug Something isn't working labels Jul 3, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/components/Entity/CreateModal.vue (1)

545-569: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Clear template state for barcode imports When params.product is present, any prior selectedTemplate/templateData still survives, so create() can take the template endpoint and drop manufacturer/modelNumber. Call clearTemplate() in that branch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/components/Entity/CreateModal.vue` around lines 545 - 569, When
CreateModal.vue handles a barcode product via the params.product branch, any
previously selected template state can still remain and cause create() to use
the template endpoint instead of the product payload. Update the logic around
the params.product handling in CreateModal.vue to clear the template state there
by calling clearTemplate(), so selectedTemplate/templateData do not override the
looked-up manufacturer/modelNumber before submission.
🧹 Nitpick comments (3)
backend/app/api/handlers/v1/controller.go (1)

29-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a unit test for multipartFormError.

This function is the crux of the #1538 fix. Since http.MaxBytesError is trivial to construct (&http.MaxBytesError{Limit: n}), a small table test asserting 413 for that case and 400 otherwise would lock in the behavior cheaply.

🧪 Example test sketch
func TestMultipartFormError(t *testing.T) {
	err := multipartFormError(&http.MaxBytesError{Limit: 1024})
	var re *validate.RequestError
	require.ErrorAs(t, err, &re)
	require.Equal(t, http.StatusRequestEntityTooLarge, re.Status)

	err = multipartFormError(errors.New("boundary not found"))
	require.ErrorAs(t, err, &re)
	require.Equal(t, http.StatusBadRequest, re.Status)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/api/handlers/v1/controller.go` around lines 29 - 37, Add a small
table-driven unit test for multipartFormError to lock in the `#1538` behavior:
verify that when the input is an http.MaxBytesError the function returns a
validate.RequestError with HTTP 413, and that a non-MaxBytes error path returns
a validate.RequestError with HTTP 400. Use multipartFormError and
validate.RequestError as the key symbols so the test stays focused on this
helper.
backend/internal/data/repo/repo_entities.go (1)

494-511: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Ancestor walk issues one query per level.

The derivation runs a separate DB round-trip for each ancestor above the loaded parent. For typical depths (2–3) this is negligible, but since GetOne is invoked after every create/update/patch and on item detail views, deep hierarchies pay a linear query cost. The maxAncestorDepth = 64 guard against cycles is a nice touch. If deep nesting becomes common, a recursive CTE or a single walk query would collapse this to one round-trip.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/data/repo/repo_entities.go` around lines 494 - 511, The
ancestor lookup in the entity derivation loop is doing one database round-trip
per parent hop, which makes deep hierarchies expensive. Update the logic around
the walk in repo_entities.go, especially the loop using cur, next, and
maxAncestorDepth, to fetch ancestors in a single query path if possible, such as
a recursive CTE or equivalent batched walk, while preserving the existing
not-found handling and cycle guard behavior.
frontend/components/Entity/CreateModal.vue (1)

548-551: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Consider validating/sanitizing third-party barcode API data before it flows into persisted records.

params.product.manufacturer/modelNumber originate from an external barcode lookup service and are copied straight into form state and eventually the create payload with no length/content validation on the client. Since the backend schema caps these at 255 chars (@maxLength 255 in data-contracts.ts), an oversized or malformed value from the third-party API would just fail server-side, but it's worth confirming the backend enforces this consistently and that these values are HTML-escaped everywhere they're later rendered (e.g. label generator, PDF/print views) to avoid any stored-content issues from an untrusted upstream source.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/components/Entity/CreateModal.vue` around lines 548 - 551, The
barcode lookup values copied in CreateModal.vue into form.manufacturer and
form.modelNumber come from an untrusted third-party source and can exceed or
contain malformed content before reaching persisted records. Add client-side
sanitization/length guards at the point where params.product is mapped into the
form, and verify the backend’s data-contracts.ts maxLength 255 enforcement is
consistently applied so oversized values cannot be stored. Also ensure any later
rendering paths that consume these fields, such as the label generator and
PDF/print views, HTML-escape the values before display.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/internal/core/services/service_exports.go`:
- Around line 550-573: The cached topics created by ExportService.openTopic are
never shut down, so buffered messages/resources may remain unflushed at service
exit. Add a single service-teardown shutdown path that iterates over
ExportService.topics and calls Shutdown() on each cached pubsub.Topic during the
app’s graceful shutdown, while keeping openTopic free of per-publish Shutdown
for mem://.

---

Outside diff comments:
In `@frontend/components/Entity/CreateModal.vue`:
- Around line 545-569: When CreateModal.vue handles a barcode product via the
params.product branch, any previously selected template state can still remain
and cause create() to use the template endpoint instead of the product payload.
Update the logic around the params.product handling in CreateModal.vue to clear
the template state there by calling clearTemplate(), so
selectedTemplate/templateData do not override the looked-up
manufacturer/modelNumber before submission.

---

Nitpick comments:
In `@backend/app/api/handlers/v1/controller.go`:
- Around line 29-37: Add a small table-driven unit test for multipartFormError
to lock in the `#1538` behavior: verify that when the input is an
http.MaxBytesError the function returns a validate.RequestError with HTTP 413,
and that a non-MaxBytes error path returns a validate.RequestError with HTTP
400. Use multipartFormError and validate.RequestError as the key symbols so the
test stays focused on this helper.

In `@backend/internal/data/repo/repo_entities.go`:
- Around line 494-511: The ancestor lookup in the entity derivation loop is
doing one database round-trip per parent hop, which makes deep hierarchies
expensive. Update the logic around the walk in repo_entities.go, especially the
loop using cur, next, and maxAncestorDepth, to fetch ancestors in a single query
path if possible, such as a recursive CTE or equivalent batched walk, while
preserving the existing not-found handling and cycle guard behavior.

In `@frontend/components/Entity/CreateModal.vue`:
- Around line 548-551: The barcode lookup values copied in CreateModal.vue into
form.manufacturer and form.modelNumber come from an untrusted third-party source
and can exceed or contain malformed content before reaching persisted records.
Add client-side sanitization/length guards at the point where params.product is
mapped into the form, and verify the backend’s data-contracts.ts maxLength 255
enforcement is consistently applied so oversized values cannot be stored. Also
ensure any later rendering paths that consume these fields, such as the label
generator and PDF/print views, HTML-escape the values before display.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3014419b-2d63-4732-8195-1fae7d59cbc6

📥 Commits

Reviewing files that changed from the base of the PR and between 91797ff and a6016dd.

⛔ Files ignored due to path filters (9)
  • backend/app/api/static/docs/docs.go is excluded by !backend/app/api/static/docs/**
  • backend/app/api/static/docs/openapi-3.json is excluded by !backend/app/api/static/docs/**
  • backend/app/api/static/docs/openapi-3.yaml is excluded by !backend/app/api/static/docs/**
  • backend/app/api/static/docs/swagger.json is excluded by !backend/app/api/static/docs/**
  • backend/app/api/static/docs/swagger.yaml is excluded by !backend/app/api/static/docs/**
  • docs/public/api/openapi-3.0.json is excluded by !docs/public/api/**
  • docs/public/api/openapi-3.0.yaml is excluded by !docs/public/api/**
  • docs/public/api/swagger-2.0.json is excluded by !docs/public/api/**
  • docs/public/api/swagger-2.0.yaml is excluded by !docs/public/api/**
📒 Files selected for processing (14)
  • backend/app/api/handlers/v1/controller.go
  • backend/app/api/handlers/v1/v1_ctrl_entities.go
  • backend/app/api/handlers/v1/v1_ctrl_entities_attachments.go
  • backend/app/api/handlers/v1/v1_ctrl_exports.go
  • backend/internal/core/services/service_exports.go
  • backend/internal/core/services/service_exports_test.go
  • backend/internal/data/repo/repo_entities.go
  • backend/internal/data/repo/repo_entities_test.go
  • frontend/components/Entity/CreateModal.vue
  • frontend/lib/api/types/data-contracts.ts
  • frontend/locales/en.json
  • frontend/pages/item/[id]/index/edit.vue
  • frontend/pages/location/[id]/index/edit.vue
  • frontend/pages/reports/label-generator.vue

Comment on lines +550 to +573
// openTopic returns the long-lived publisher topic for name, opening and
// caching it on first use. The topic is reused across publishes and stays
// open for the process lifetime — see the topics field doc for why a
// per-publish Shutdown must not be reintroduced here.
func (s *ExportService) openTopic(ctx context.Context, name string) (*pubsub.Topic, error) {
s.topicsMu.Lock()
defer s.topicsMu.Unlock()
if topic, ok := s.topics[name]; ok {
return topic, nil
}
conn, err := utils.GenerateSubPubConn(s.pubSubConn, name)
if err != nil {
return err
return nil, err
}
topic, err := pubsub.OpenTopic(ctx, conn)
if err != nil {
return nil, err
}
if s.topics == nil {
s.topics = make(map[string]*pubsub.Topic)
}
s.topics[name] = topic
return topic, nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how ExportService is stopped and whether topics are ever shut down/flushed.
rg -nP 'func\s+\(s\s+\*ExportService\)' backend/internal/core/services/service_exports.go
rg -nP '\.Shutdown\(' backend/internal/core/services
rg -nP 'GenerateSubPubConn' backend/internal -A3

Repository: sysadminsmedia/homebox

Length of output: 2988


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the ExportService shape and surrounding lifecycle hooks.
sed -n '1,220p' backend/internal/core/services/service_exports.go
printf '\n---\n'
sed -n '540,640p' backend/internal/core/services/service_exports.go
printf '\n---\n'
sed -n '1,220p' backend/internal/core/services/service.go 2>/dev/null || true

# Find any close/shutdown hooks for ExportService or its owning services.
rg -n '\b(Close|Shutdown|Stop|Dispose)\b' backend/internal/core/services backend/internal -g'*.go'

# Inspect the pubsub wrapper used by openTopic.
rg -n 'func\s+OpenTopic|type\s+Topic|func\s+\(.*Topic.*\)\s+Shutdown|type\s+Conn|GenerateSubPubConn' backend/internal -g'*.go' -A6 -B4

Repository: sysadminsmedia/homebox

Length of output: 27868


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate ExportService construction and whether any service lifecycle hook closes cached topics.
rg -n 'ExportService|AllServices|New.*Service|Close\(\) error|Shutdown\(' backend/internal -g'*.go' -A4 -B4

printf '\n--- utils.GenerateSubPubConn ---\n'
sed -n '1,240p' backend/internal/sys/config/* 2>/dev/null || true
rg -n 'func\s+GenerateSubPubConn|GenerateSubPubConn\(' backend/internal backend/pkgs -g'*.go' -A8 -B4

printf '\n--- pubsub topic tests/docs in repo ---\n'
rg -n 'OpenTopic|Shutdown.*pubsub|shared singleton topic|mem://|collection_export|collection_import' backend/internal -g'*.go' -A4 -B4

Repository: sysadminsmedia/homebox

Length of output: 50378


🌐 Web query:

gocloud.dev/pubsub OpenTopic context shutdown topic Send docs

💡 Result:

In the Go Cloud Development Kit (Go CDK) pubsub package, opening a topic and managing its lifecycle involves the following standard patterns: Opening a Topic To interact with a publish/subscribe system, you open a topic using the pubsub.OpenTopic function [1][2]. This function takes a context and a URL string that identifies the specific topic, with the URL scheme determining the driver (e.g., gcppubsub://) [3][1]. Example: ctx:= context.Background topic, err:= pubsub.OpenTopic(ctx, "gcppubsub://projects/myproject/topics/mytopic") if err!= nil { log.Fatal(err) } defer topic.Shutdown(ctx) Sending Messages Messages are sent using the Send method on the *pubsub.Topic object [1][4]. Unlike some other Pub/Sub client libraries, the Go CDK's Send method is synchronous: it only returns after the message has been sent to the service or has failed to be sent [4][5]. err:= topic.Send(ctx, &pubsub.Message{ Body: []byte("Hello, World!"), }) Shutdown The Shutdown method is used to gracefully disconnect the topic [1][6]. It flushes any pending message sends and performs necessary cleanup [1][6]. It is important to call Shutdown when you are finished with the topic to ensure all outstanding messages are processed [1][6]. The context passed to Shutdown is respected during the flushing process; if the context is cancelled, the shutdown procedure may be interrupted [6]. Note: The Go CDK pubsub API (gocloud.dev/pubsub) is distinct from the raw Google Cloud Pub/Sub Go client library (cloud.google.com/go/pubsub) [3][6]. The latter uses Stop instead of Shutdown to manage background resources [7][8]. Always ensure you are using the methods appropriate for the specific library you have imported [1][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the exact pubsub connection-string helper and any service close methods.
sed -n '1,80p' backend/pkgs/utils/generator.go
printf '\n---\n'
rg -n 'func\s+\(s\s+\*ExportService\)\s+(Close|Shutdown)|func\s+\(s\s+\*AllServices\)\s+(Close|Shutdown)|func\s+\(.*\)\s+(Close|Shutdown)\(' backend/internal/core/services backend/internal/core -g'*.go' -A4 -B4

Repository: sysadminsmedia/homebox

Length of output: 916


Add a service shutdown path for cached topics
openTopic should stay free of per-publish Shutdown for mem://, but the cached *pubsub.Topic values still need a single Shutdown() on service teardown so buffered sends/resources are flushed before exit. Wire that into the app’s graceful shutdown path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/core/services/service_exports.go` around lines 550 - 573,
The cached topics created by ExportService.openTopic are never shut down, so
buffered messages/resources may remain unflushed at service exit. Add a single
service-teardown shutdown path that iterates over ExportService.topics and calls
Shutdown() on each cached pubsub.Topic during the app’s graceful shutdown, while
keeping openTopic free of per-publish Shutdown for mem://.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🕷️ bug Something isn't working go Pull requests that update Go code

Projects

None yet

1 participant