Skip to content
Open

Dev #269

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5e7b6b8
docs: expand CLAUDE.md with comprehensive codebase reference
claude Feb 20, 2026
4f70cf9
Merge pull request #1 from 5ymb01/claude/add-claude-documentation-zAB2d
5ymb01 Feb 20, 2026
d86fb89
fix: three bugs in weather and stocks plugins
claude Feb 21, 2026
18c0591
fix(web): add custom-html widget support, fix secrets handling, hide …
5ymb01 Feb 23, 2026
050db58
Merge remote-tracking branch 'amd/claude/add-claude-documentation-zAB…
5ymb01 Feb 23, 2026
29bf97d
fix(api): replace fragile wrapper script with direct stdin for plugin…
5ymb01 Feb 23, 2026
f8dff98
fix(of-the-day): fix file upload button and edit modal closing immedi…
5ymb01 Feb 24, 2026
7e08547
fix(of-the-day): auto-register category in config on first toggle
5ymb01 Feb 24, 2026
d0ac077
Merge branch 'claude/fix-of-the-day-webui' into Dev
5ymb01 Feb 24, 2026
98ed732
fix(api): remove stale wrapper_path reference in timeout handler
5ymb01 Feb 24, 2026
62722c5
Merge upstream ChuckBuilds/LEDMatrix (12 commits) into Dev
5ymb01 Feb 24, 2026
0b75e29
fix(of-the-day): load data files for categories missing from categori…
5ymb01 Feb 24, 2026
a55fbd1
Merge remote-tracking branch 'origin/main' into Dev
5ymb01 Feb 25, 2026
debdfd4
chore: remove plugin files from LEDMatrix repo (belong in plugin repos)
5ymb01 Feb 25, 2026
3bdc398
fix(security): address CodeRabbit security findings
5ymb01 Feb 25, 2026
2e8ebf0
fix(bugs): address CodeRabbit major/minor bug findings
5ymb01 Feb 25, 2026
e8e4071
fix(minor): address CodeRabbit minor/nitpick findings
5ymb01 Feb 25, 2026
13fc310
fix(quality): code quality fixes and test infrastructure repairs
5ymb01 Feb 26, 2026
2ba009f
fix(quality): add CodeRabbit checklist to CLAUDE.md, fix whitespace s…
5ymb01 Feb 27, 2026
9ba8268
Merge remote-tracking branch 'origin/main' into Dev
5ymb01 Feb 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
624 changes: 601 additions & 23 deletions CLAUDE.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion config/config_secrets.template.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"weather": {
"ledmatrix-weather": {
"api_key": "YOUR_OPENWEATHERMAP_API_KEY"
},
"youtube": {
Expand Down
805 changes: 805 additions & 0 deletions plugin-repos/of-the-day/manager.py

Large diffs are not rendered by default.

95 changes: 95 additions & 0 deletions plugin-repos/of-the-day/scripts/save_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""
Save updated content to a JSON file in the of_the_day directory.
Validates the JSON structure before saving.
"""

import os
import json
import sys
from pathlib import Path

# Get plugin directory (scripts/ -> plugin root)
plugin_dir = Path(__file__).parent.parent
data_dir = plugin_dir / 'of_the_day'
data_dir.mkdir(parents=True, exist_ok=True)

try:
input_data = json.load(sys.stdin)
filename = input_data.get('filename', '')
content_str = input_data.get('content', '')

if not filename or not content_str:
print(json.dumps({
'status': 'error',
'message': 'Filename and content are required'
}))
sys.exit(1)

# Security: ensure filename doesn't contain path traversal
if '..' in filename or '/' in filename or '\\' in filename:
print(json.dumps({
'status': 'error',
'message': 'Invalid filename'
}))
sys.exit(1)

if not filename.endswith('.json'):
print(json.dumps({
'status': 'error',
'message': 'File must be a JSON file (.json)'
}))
sys.exit(1)

# Validate JSON
try:
content = json.loads(content_str)
except json.JSONDecodeError as e:
print(json.dumps({
'status': 'error',
'message': f'Invalid JSON: {str(e)}'
}))
sys.exit(1)

# Validate structure (must be an object with day number keys 1-365)
if not isinstance(content, dict):
print(json.dumps({
'status': 'error',
'message': 'JSON must be an object with day numbers (1-365) as keys'
}))
sys.exit(1)

# Check if keys are valid day numbers
for key in content.keys():
try:
day_num = int(key)
if day_num < 1 or day_num > 365:
print(json.dumps({
'status': 'error',
'message': f'Day number {day_num} is out of range (must be 1-365)'
}))
sys.exit(1)
except ValueError:
print(json.dumps({
'status': 'error',
'message': f'Invalid key "{key}": all keys must be day numbers (1-365)'
}))
sys.exit(1)

# Save file
file_path = data_dir / filename
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(content, f, indent=2, ensure_ascii=False)

print(json.dumps({
'status': 'success',
'message': f'File {filename} saved successfully'
}))

except Exception as e:
print(json.dumps({
'status': 'error',
'message': str(e)
}))
sys.exit(1)

120 changes: 120 additions & 0 deletions plugin-repos/of-the-day/scripts/toggle_category.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""
Toggle a category's enabled status.
Receives category_name and optional enabled state via stdin as JSON.
"""

import os
import re
import json
import sys
from pathlib import Path

LEDMATRIX_ROOT = os.environ.get('LEDMATRIX_ROOT', os.getcwd())
config_file = Path(LEDMATRIX_ROOT) / 'config' / 'config.json'

# Read params from stdin
try:
stdin_input = sys.stdin.read().strip()
if stdin_input:
params = json.loads(stdin_input)
else:
params = {}
except (json.JSONDecodeError, ValueError) as e:
print(json.dumps({
'status': 'error',
'message': f'Invalid JSON input: {str(e)}'
}))
sys.exit(1)

category_name = params.get('category_name')
if not category_name:
print(json.dumps({
'status': 'error',
'message': 'category_name is required'
}))
sys.exit(1)

if not re.fullmatch(r'[a-z0-9_-]+', category_name, flags=re.IGNORECASE):
print(json.dumps({
'status': 'error',
'message': 'category_name must contain only letters, numbers, "_" or "-"'
}))
sys.exit(1)

# Load current config
config = {}
try:
if config_file.exists():
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
except (json.JSONDecodeError, ValueError) as e:
print(json.dumps({
'status': 'error',
'message': f'Failed to load config: {str(e)}'
}))
sys.exit(1)
Comment on lines +47 to +56
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Include filesystem errors when loading config.json.

Read failures like permissions/IO issues currently bypass this handler and can return an unstructured traceback instead of the expected JSON error payload.

🛠️ Suggested fix
-except (json.JSONDecodeError, ValueError) as e:
+except (OSError, json.JSONDecodeError, ValueError) as e:
     print(json.dumps({
         'status': 'error',
         'message': f'Failed to load config: {str(e)}'
     }))
     sys.exit(1)
🧰 Tools
🪛 Ruff (0.15.2)

[warning] 54-54: Use explicit conversion flag

Replace with conversion flag

(RUF010)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin-repos/of-the-day/scripts/toggle_category.py` around lines 47 - 56, The
current try/except around opening and json.load(config_file) only catches JSON
decode errors and ValueError, so filesystem errors (e.g., permission denied, IO
errors) bubble up; update the exception handling for the block that opens and
loads config_file (the open(...) and json.load(...) calls) to also catch OSError
(or include IOError/OSError) and surface those errors in the same JSON error
payload (use the same process: print a JSON object with 'status':'error' and
message containing str(e) and exit), ensuring the except still references the
same variable name (e) so the printed message remains consistent.


# Get plugin config
plugin_config = config.get('of-the-day', {})
categories = plugin_config.get('categories', {})

# If category isn't in config yet (e.g. a manually-placed file), auto-register it
# so it can be toggled immediately without needing a re-upload.
if category_name not in categories:
plugin_dir = Path(__file__).parent.parent
data_file = f'of_the_day/{category_name}.json'
display_name = category_name.replace('_', ' ').title()
categories[category_name] = {
'enabled': True,
'data_file': data_file,
'display_name': display_name
}
# Also add to category_order if missing
category_order = plugin_config.get('category_order', [])
if category_name not in category_order:
category_order.append(category_name)
plugin_config['category_order'] = category_order

# Determine new enabled state
if 'enabled' in params:
# Explicit state provided — accept bool or "true"/"false" string
enabled_value = params['enabled']
if isinstance(enabled_value, bool):
new_enabled = enabled_value
elif isinstance(enabled_value, str) and enabled_value.lower() in ('true', 'false'):
new_enabled = enabled_value.lower() == 'true'
else:
print(json.dumps({
'status': 'error',
'message': 'enabled must be a boolean or "true"/"false" string'
}))
sys.exit(1)
else:
# Toggle current state
current_enabled = categories[category_name].get('enabled', True)
new_enabled = not current_enabled

# Update the category
categories[category_name]['enabled'] = new_enabled
plugin_config['categories'] = categories
config['of-the-day'] = plugin_config

# Save config
try:
config_file.parent.mkdir(parents=True, exist_ok=True)
with open(config_file, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
except (OSError, TypeError) as e:
print(json.dumps({
'status': 'error',
'message': f'Failed to save config: {str(e)}'
}))
sys.exit(1)

print(json.dumps({
'status': 'success',
'message': f'Category "{category_name}" {"enabled" if new_enabled else "disabled"}',
'category_name': category_name,
'enabled': new_enabled
}))
107 changes: 107 additions & 0 deletions plugin-repos/of-the-day/scripts/upload_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""
Upload a JSON file to the of_the_day directory.
Validates the file format and automatically adds category to config.
"""

import os
import json
import sys
from pathlib import Path

# Get plugin directory (scripts/ -> plugin root)
plugin_dir = Path(__file__).parent.parent
data_dir = plugin_dir / 'of_the_day'
data_dir.mkdir(parents=True, exist_ok=True)

# Read JSON from stdin
try:
input_data = json.load(sys.stdin)
filename = input_data.get('filename', '')
content = input_data.get('content', '')

if not filename or not content:
print(json.dumps({
'status': 'error',
'message': 'Filename and content are required'
}))
sys.exit(1)

# Validate filename
if not filename.endswith('.json'):
print(json.dumps({
'status': 'error',
'message': 'File must be a JSON file (.json)'
}))
sys.exit(1)

if '..' in filename or '/' in filename or '\\' in filename:
print(json.dumps({
'status': 'error',
'message': 'Invalid filename'
}))
sys.exit(1)

# Validate JSON content
try:
data = json.loads(content)
except json.JSONDecodeError as e:
print(json.dumps({
'status': 'error',
'message': f'Invalid JSON: {str(e)}'
}))
sys.exit(1)

# Validate structure (must be an object with day number keys 1-365)
if not isinstance(data, dict):
print(json.dumps({
'status': 'error',
'message': 'JSON must be an object with day numbers (1-365) as keys'
}))
sys.exit(1)

# Check if keys are valid day numbers
for key in data.keys():
try:
day_num = int(key)
if day_num < 1 or day_num > 365:
print(json.dumps({
'status': 'error',
'message': f'Day number {day_num} is out of range (must be 1-365)'
}))
sys.exit(1)
except ValueError:
print(json.dumps({
'status': 'error',
'message': f'Invalid key "{key}": all keys must be day numbers (1-365)'
}))
sys.exit(1)

# Save file
file_path = data_dir / filename
with open(file_path, 'w', encoding='utf-8') as f:
Comment on lines 30 to 82
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add path traversal protection for filename.

Currently, ../ or path separators can escape the of_the_day directory. This is a write-through security risk.

🛡️ Fix
     # Validate filename
     if not filename.endswith('.json'):
         print(json.dumps({
             'status': 'error',
             'message': 'File must be a JSON file (.json)'
         }))
         sys.exit(1)
+
+    if '..' in filename or '/' in filename or '\\' in filename:
+        print(json.dumps({
+            'status': 'error',
+            'message': 'Invalid filename'
+        }))
+        sys.exit(1)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Validate filename
if not filename.endswith('.json'):
print(json.dumps({
'status': 'error',
'message': 'File must be a JSON file (.json)'
}))
sys.exit(1)
# Validate JSON content
try:
data = json.loads(content)
except json.JSONDecodeError as e:
print(json.dumps({
'status': 'error',
'message': f'Invalid JSON: {str(e)}'
}))
sys.exit(1)
# Validate structure (must be an object with day number keys 1-365)
if not isinstance(data, dict):
print(json.dumps({
'status': 'error',
'message': 'JSON must be an object with day numbers (1-365) as keys'
}))
sys.exit(1)
# Check if keys are valid day numbers
for key in data.keys():
try:
day_num = int(key)
if day_num < 1 or day_num > 365:
print(json.dumps({
'status': 'error',
'message': f'Day number {day_num} is out of range (must be 1-365)'
}))
sys.exit(1)
except ValueError:
print(json.dumps({
'status': 'error',
'message': f'Invalid key "{key}": all keys must be day numbers (1-365)'
}))
sys.exit(1)
# Save file
file_path = data_dir / filename
with open(file_path, 'w', encoding='utf-8') as f:
# Validate filename
if not filename.endswith('.json'):
print(json.dumps({
'status': 'error',
'message': 'File must be a JSON file (.json)'
}))
sys.exit(1)
if '..' in filename or '/' in filename or '\\' in filename:
print(json.dumps({
'status': 'error',
'message': 'Invalid filename'
}))
sys.exit(1)
# Validate JSON content
try:
data = json.loads(content)
except json.JSONDecodeError as e:
print(json.dumps({
'status': 'error',
'message': f'Invalid JSON: {str(e)}'
}))
sys.exit(1)
# Validate structure (must be an object with day number keys 1-365)
if not isinstance(data, dict):
print(json.dumps({
'status': 'error',
'message': 'JSON must be an object with day numbers (1-365) as keys'
}))
sys.exit(1)
# Check if keys are valid day numbers
for key in data.keys():
try:
day_num = int(key)
if day_num < 1 or day_num > 365:
print(json.dumps({
'status': 'error',
'message': f'Day number {day_num} is out of range (must be 1-365)'
}))
sys.exit(1)
except ValueError:
print(json.dumps({
'status': 'error',
'message': f'Invalid key "{key}": all keys must be day numbers (1-365)'
}))
sys.exit(1)
# Save file
file_path = data_dir / filename
with open(file_path, 'w', encoding='utf-8') as f:
🧰 Tools
🪛 Ruff (0.15.2)

[warning] 44-44: Use explicit conversion flag

Replace with conversion flag

(RUF010)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin-repos/of-the-day/scripts/upload_file.py` around lines 30 - 75, The
filename is not validated against path traversal, allowing "../" or absolute
paths to escape data_dir; update the upload routine (around the filename,
data_dir, and file_path usage) to sanitize and restrict paths: ensure filename
is not absolute and contains no path separators or ".." (reject if
os.path.isabs(filename) or os.path.sep in filename or '..' in filename),
restrict to a safe pattern (e.g., regex like r'^[A-Za-z0-9_.-]+\.json$'), then
construct file_path = (data_dir / filename).resolve() and verify file_path is
inside data_dir by checking
file_path.resolve().is_relative_to(data_dir.resolve()) or by comparing prefixes
(reject and exit with an error if the check fails) before opening the file.

json.dump(data, f, indent=2, ensure_ascii=False)

# Extract category name and update config
category_name = filename.replace('.json', '')
display_name = input_data.get('display_name', category_name.replace('_', ' ').title())

# Update config
sys.path.insert(0, str(plugin_dir))
from scripts.update_config import add_category_to_config
add_category_to_config(category_name, f'of_the_day/{filename}', display_name)

print(json.dumps({
'status': 'success',
'message': f'File {filename} uploaded successfully',
'filename': filename,
'category_name': category_name
}))

except Exception as e:
print(json.dumps({
'status': 'error',
'message': str(e)
}))
sys.exit(1)

Loading