From 3d5080b2b200ed0612f955c72fa4ca0c8a3d6503 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 8 Nov 2025 16:27:10 +1100 Subject: [PATCH 1/3] feat: display benefits as individual lines with icon, name, and type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhanced the inventory tab to show each benefit on its own line instead of an icon grid. Each benefit now displays with: - Icon (40x40px) on the left - Benefit name and type on the right in format: "Name (TYPE)" Changes: - Backend: Send full benefit data (name, type, image_url) instead of just URLs - Frontend: Render benefits as vertical list of horizontal lines - CSS: Add new styles for benefit items (.benefit-item, .benefit-icon, .benefit-info) - Removed: Icon grid layout (chunks of 3) and rewards text field 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/web/managers/inventory.py | 16 +++++++++- web/static/app.js | 38 +++++++++++++++++----- web/static/styles.css | 60 +++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 9 deletions(-) diff --git a/src/web/managers/inventory.py b/src/web/managers/inventory.py index c736bef..6856942 100644 --- a/src/web/managers/inventory.py +++ b/src/web/managers/inventory.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +import logging from typing import TYPE_CHECKING, Any @@ -12,6 +13,9 @@ if TYPE_CHECKING: from src.web.managers.cache import ImageCache +logger = logging.getLogger("TwitchDrops") + + class InventoryManager: """Manages drop campaign inventory display in the web interface. @@ -41,6 +45,16 @@ class InventoryManager: drops_data = [] for drop in campaign.drops: + # Collect full benefit data (filter out benefits without images) + benefits_data = [ + { + "name": benefit.name, + "type": benefit.type.name, + "image_url": str(benefit.image_url), + } + for benefit in drop.benefits + if benefit.image_url + ] drops_data.append( { "id": drop.id, @@ -50,7 +64,7 @@ class InventoryManager: "progress": drop.progress, "is_claimed": drop.is_claimed, "can_claim": drop.can_claim, - "rewards": drop.rewards_text(), + "benefits": benefits_data, "starts_at": drop.starts_at.isoformat(), "ends_at": drop.ends_at.isoformat(), } diff --git a/web/static/app.js b/web/static/app.js index 19eef05..fd7f0ba 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -494,14 +494,36 @@ function renderInventory() { } const claimedText = t.gui?.inventory?.status?.claimed || 'Claimed'; - const dropsHtml = campaign.drops.map(drop => ` -
-
${drop.name}
-
${drop.rewards}
-
${drop.current_minutes} / ${drop.required_minutes} minutes (${Math.round(drop.progress * 100)}%)
- ${drop.is_claimed ? `
✓ ${claimedText}
` : ''} -
- `).join(''); + const dropsHtml = campaign.drops.map(drop => { + // Generate HTML for each benefit as its own line + let benefitsHtml = ''; + if (drop.benefits && drop.benefits.length > 0) { + benefitsHtml = drop.benefits.map(benefit => + `
+ ${benefit.name} +
+ ${benefit.name} + (${benefit.type}) +
+
` + ).join(''); + } + + return ` +
+
+
+
${drop.name}
+
+
+
+ ${benefitsHtml} +
+
${drop.current_minutes} / ${drop.required_minutes} minutes (${Math.round(drop.progress * 100)}%)
+ ${drop.is_claimed ? `
✓ ${claimedText}
` : ''} +
+ `; + }).join(''); // Make campaign name clickable if link_url is available const campaignNameHtml = campaign.link_url diff --git a/web/static/styles.css b/web/static/styles.css index 9fec853..a9369b3 100644 --- a/web/static/styles.css +++ b/web/static/styles.css @@ -500,6 +500,66 @@ header h1 { border-left-color: var(--accent-color); } +.drop-item-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 8px; +} + +.drop-item-info { + flex: 1; + min-width: 0; +} + +.benefits-list { + margin-top: 8px; + margin-bottom: 8px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.benefit-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px; + background: var(--bg-panel); + border-radius: 4px; + border: 1px solid var(--border-color); +} + +.benefit-icon { + width: 40px; + height: 40px; + border-radius: 4px; + object-fit: cover; + border: 2px solid var(--border-color); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + flex-shrink: 0; +} + +.benefit-info { + flex: 1; + display: flex; + align-items: center; + gap: 6px; + min-width: 0; +} + +.benefit-name { + font-weight: 500; + font-size: 14px; + color: var(--text-primary); +} + +.benefit-type { + font-size: 12px; + color: var(--text-secondary); + font-style: italic; +} + /* Settings */ .settings-container { max-width: 800px; From 4710a9522d3a175299346a10b84a047dd47f2b16 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 8 Nov 2025 16:29:57 +1100 Subject: [PATCH 2/3] fix logs permission for persistence --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index e2bdb04..630757b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,6 +40,7 @@ COPY web/ ./web/ # Create data directory for persistent storage RUN mkdir -p /app/data && chmod 777 /app/data +RUN mkdir -p /app/logs && chmod 777 /app/logs # Expose web port EXPOSE 8080 From c39eec9ac77ae73061705be30e3def4955c99c12 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 8 Nov 2025 16:38:07 +1100 Subject: [PATCH 3/3] ruff fix --- src/__main__.py | 1 + src/config/constants.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/__main__.py b/src/__main__.py index e040005..fbc9bf4 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -1,4 +1,5 @@ from __future__ import annotations + import argparse import asyncio import logging diff --git a/src/config/constants.py b/src/config/constants.py index 2bfa09c..b54d761 100644 --- a/src/config/constants.py +++ b/src/config/constants.py @@ -9,6 +9,7 @@ from datetime import timedelta from enum import Enum, auto from typing import TYPE_CHECKING, Any, Literal, NewType + if TYPE_CHECKING: from collections import abc # noqa from typing import TypeAlias @@ -57,7 +58,6 @@ ONLINE_DELAY = timedelta(seconds=120) WATCH_INTERVAL = timedelta(seconds=59) - class State(Enum): """Application state machine states."""