feat: Add quick wins batch (#42, #43, #25, #35, #20, #36, #39)
Some checks failed
CI / style (push) Has been cancelled
CI / build-client (push) Has been cancelled
CI / build-amd64 (push) Has been cancelled
CI / build-tester (push) Has been cancelled
CI / test (cockroachdb) (push) Has been cancelled
CI / test (mariadb) (push) Has been cancelled
CI / test (postgres) (push) Has been cancelled
CI / test (sqlite) (push) Has been cancelled
CI / build-arm64 (push) Has been cancelled
CI / build-armv7 (push) Has been cancelled
CI / publish-images (push) Has been cancelled
CI / publish-release (push) Has been cancelled

- #42: Allow spools heavier than theoretical max (remove weight clamps)
- #43: Show filament custom fields on spool detail page
- #25: Sort spools by custom fields and remaining weight
- #35: Global search box for spool list
- #20: Gallery view for spools (color grid with progress bars)
- #36: Spool-level color override with ColorPicker
- #39: Cost analytics endpoint, total spent & avg cost/kg stats
- Fix: Filament select refresh after inline creation
- Fix: Parse temperatures from filament comments

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-22 23:09:42 -06:00
parent 5f66302e73
commit 801d3da02a
18 changed files with 524 additions and 45 deletions

View File

@@ -42,6 +42,7 @@ async def create(
initial_weight: Optional[float] = None,
spool_weight: Optional[float] = None,
extra_weight: Optional[float] = None,
color_hex: Optional[str] = None,
used_weight: Optional[float] = None,
first_used: Optional[datetime] = None,
last_used: Optional[datetime] = None,
@@ -70,7 +71,10 @@ async def create(
"remaining_weight can only be used if the initial_weight is "
"defined or the filament has a weight set.",
)
used_weight = max(initial_weight - remaining_weight, 0)
if remaining_weight > initial_weight:
# Spool is heavier than expected (manufacturing tolerance), adjust initial_weight
initial_weight = remaining_weight
used_weight = initial_weight - remaining_weight
else:
used_weight = 0
@@ -86,6 +90,7 @@ async def create(
initial_weight=initial_weight,
spool_weight=spool_weight,
extra_weight=extra_weight,
color_hex=color_hex,
used_weight=used_weight,
price=price,
first_used=first_used,
@@ -117,6 +122,7 @@ async def get_by_id(db: AsyncSession, spool_id: int) -> models.Spool:
async def find( # noqa: C901, PLR0912
*,
db: AsyncSession,
q: Optional[str] = None,
filament_name: Optional[str] = None,
filament_id: Optional[Union[int, Sequence[int]]] = None,
filament_material: Optional[str] = None,
@@ -155,6 +161,20 @@ async def find( # noqa: C901, PLR0912
stmt = add_where_clause_str_opt(stmt, models.Spool.location, location)
stmt = add_where_clause_str_opt(stmt, models.Spool.lot_nr, lot_nr)
# Global search: OR across multiple text fields
if q is not None and q.strip():
search_term = f"%{q.strip()}%"
stmt = stmt.where(
sqlalchemy.or_(
models.Filament.name.ilike(search_term),
models.Filament.material.ilike(search_term),
models.Vendor.name.ilike(search_term),
models.Spool.location.ilike(search_term),
models.Spool.lot_nr.ilike(search_term),
models.Spool.comment.ilike(search_term),
)
)
# Filter by color_hex (exact match, supports multiple comma-separated values)
if filament_color_hex is not None:
if isinstance(filament_color_hex, str):
@@ -223,6 +243,18 @@ async def find( # noqa: C901, PLR0912
sorts.append(models.Filament.name)
elif fieldstr == "price":
sorts.append(coalesce(models.Spool.price, models.Filament.price))
elif fieldstr.startswith("extra."):
# Sort by custom field value
extra_key = fieldstr[len("extra."):]
extra_alias = sqlalchemy.orm.aliased(models.SpoolField)
stmt = stmt.outerjoin(
extra_alias,
sqlalchemy.and_(
extra_alias.spool_id == models.Spool.id,
extra_alias.key == extra_key,
),
)
sorts.append(extra_alias.value)
else:
sorts.append(parse_nested_field(models.Spool, fieldstr))
@@ -260,7 +292,10 @@ async def update(
elif k == "remaining_weight":
if spool.initial_weight is None:
raise ItemCreateError("remaining_weight can only be used if initial_weight is set.")
spool.used_weight = max(spool.initial_weight - v, 0)
if v > spool.initial_weight:
# Spool is heavier than expected (manufacturing tolerance), adjust initial_weight
spool.initial_weight = v
spool.used_weight = spool.initial_weight - v
elif isinstance(v, datetime):
setattr(spool, k, utc_timezone_naive(v))
elif k == "extra":