mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-11 05:13:01 +00:00
fix(starlark): code review fixes - security, robustness, and schema parsing
## Security Fixes - manager.py: Check _update_manifest_safe return values to prevent silent failures - manager.py: Improve temp file cleanup in _save_manifest to prevent leaks - manager.py: Fix uninstall order (manifest → memory → disk) for consistency - api_v3.py: Add path traversal validation in uninstall endpoint - api_v3.py: Implement atomic writes for manifest files with temp + rename - pixlet_renderer.py: Relax config validation to only block dangerous shell metacharacters ## Frontend Robustness - plugins_manager.js: Add safeLocalStorage wrapper for restricted contexts (private browsing) - starlark_config.html: Scope querySelector to container to prevent modal conflicts ## Schema Parsing Improvements - pixlet_renderer.py: Indentation-aware get_schema() extraction (handles nested functions) - pixlet_renderer.py: Handle quoted defaults with commas (e.g., "New York, NY") - tronbyte_repository.py: Validate file_name is string before path traversal checks ## Dependencies - requirements.txt: Update Pillow (10.4.0), PyYAML (6.0.2), requests (2.32.0) ## Documentation - docs/STARLARK_APPS_GUIDE.md: Comprehensive guide explaining: - How Starlark apps work - That apps come from Tronbyte (not LEDMatrix) - Installation, configuration, troubleshooting - Links to upstream projects All changes improve security, reliability, and user experience. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
500
docs/STARLARK_APPS_GUIDE.md
Normal file
500
docs/STARLARK_APPS_GUIDE.md
Normal file
@@ -0,0 +1,500 @@
|
|||||||
|
# Starlark Apps Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Starlark Apps plugin for LEDMatrix enables you to run **Tidbyt/Tronbyte community apps** on your LED matrix display without modification. This integration allows you to access hundreds of pre-built widgets and apps from the vibrant Tidbyt community ecosystem.
|
||||||
|
|
||||||
|
## Important: Third-Party Content
|
||||||
|
|
||||||
|
**⚠️ Apps are NOT managed by the LEDMatrix project**
|
||||||
|
|
||||||
|
- Starlark apps are developed and maintained by the **Tidbyt/Tronbyte community**
|
||||||
|
- LEDMatrix provides the runtime environment but does **not** create, maintain, or support these apps
|
||||||
|
- All apps originate from the [Tronbyte Apps Repository](https://github.com/tronbyt/apps)
|
||||||
|
- App quality, functionality, and security are the responsibility of individual app authors
|
||||||
|
- LEDMatrix is not affiliated with Tidbyt Inc. or the Tronbyte project
|
||||||
|
|
||||||
|
## What is Starlark?
|
||||||
|
|
||||||
|
[Starlark](https://github.com/bazelbuild/starlark) is a Python-like language originally developed by Google for the Bazel build system. Tidbyt adopted Starlark for building LED display apps because it's:
|
||||||
|
|
||||||
|
- **Sandboxed**: Apps run in a safe, restricted environment
|
||||||
|
- **Simple**: Python-like syntax that's easy to learn
|
||||||
|
- **Deterministic**: Apps produce consistent output
|
||||||
|
- **Fast**: Compiled and optimized for performance
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ LEDMatrix System │
|
||||||
|
│ ┌────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Starlark Apps Plugin (manager.py) │ │
|
||||||
|
│ │ • Manages app lifecycle (install/uninstall) │ │
|
||||||
|
│ │ • Handles app configuration │ │
|
||||||
|
│ │ • Schedules app rendering │ │
|
||||||
|
│ └─────────────────┬──────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌─────────────────▼──────────────────────────────────┐ │
|
||||||
|
│ │ Pixlet Renderer (pixlet_renderer.py) │ │
|
||||||
|
│ │ • Executes .star files using Pixlet CLI │ │
|
||||||
|
│ │ • Extracts configuration schemas │ │
|
||||||
|
│ │ • Outputs WebP animations │ │
|
||||||
|
│ └─────────────────┬──────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌─────────────────▼──────────────────────────────────┐ │
|
||||||
|
│ │ Frame Extractor (frame_extractor.py) │ │
|
||||||
|
│ │ • Decodes WebP animations into frames │ │
|
||||||
|
│ │ • Scales/centers output for display size │ │
|
||||||
|
│ │ • Manages frame timing │ │
|
||||||
|
│ └─────────────────┬──────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌─────────────────▼──────────────────────────────────┐ │
|
||||||
|
│ │ LED Matrix Display │ │
|
||||||
|
│ │ • Renders final output to physical display │ │
|
||||||
|
│ └────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
▲
|
||||||
|
│
|
||||||
|
Downloads apps from
|
||||||
|
│
|
||||||
|
┌───────────────────┴─────────────────────────────────────┐
|
||||||
|
│ Tronbyte Apps Repository (GitHub) │
|
||||||
|
│ • 974+ community-built apps │
|
||||||
|
│ • Weather, sports, stocks, games, clocks, etc. │
|
||||||
|
│ • https://github.com/tronbyt/apps │
|
||||||
|
└──────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rendering Pipeline
|
||||||
|
|
||||||
|
1. **User installs app** from the Tronbyte repository via web UI
|
||||||
|
2. **Plugin downloads** the `.star` file (and any assets like images/fonts)
|
||||||
|
3. **Schema extraction** parses configuration options from the `.star` source
|
||||||
|
4. **User configures** the app through the web UI (timezone, location, API keys, etc.)
|
||||||
|
5. **Pixlet renders** the app with user config → produces WebP animation
|
||||||
|
6. **Frame extraction** decodes WebP → individual PIL Image frames
|
||||||
|
7. **Display scaling** adapts 64x32 Tidbyt output to your matrix size
|
||||||
|
8. **Rotation** cycles through your installed apps based on schedule
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### 1. Install Pixlet
|
||||||
|
|
||||||
|
Pixlet is the rendering engine that executes Starlark apps. The plugin will attempt to use:
|
||||||
|
|
||||||
|
1. **Bundled binary** (recommended): Downloaded to `bin/pixlet/pixlet-{platform}-{arch}`
|
||||||
|
2. **System installation**: If `pixlet` is available in your PATH
|
||||||
|
|
||||||
|
#### Auto-Install via Web UI
|
||||||
|
|
||||||
|
Navigate to: **Plugins → Starlark Apps → Status → Install Pixlet**
|
||||||
|
|
||||||
|
This runs the bundled installation script which downloads the appropriate binary for your platform.
|
||||||
|
|
||||||
|
#### Manual Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/LEDMatrix
|
||||||
|
bash scripts/download_pixlet.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify installation:
|
||||||
|
```bash
|
||||||
|
./bin/pixlet/pixlet-linux-amd64 version
|
||||||
|
# Pixlet 0.50.2 (or later)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Enable the Starlark Apps Plugin
|
||||||
|
|
||||||
|
1. Open the web UI
|
||||||
|
2. Navigate to **Plugins**
|
||||||
|
3. Find **Starlark Apps** in the installed plugins list
|
||||||
|
4. Enable the plugin
|
||||||
|
5. Configure settings:
|
||||||
|
- **Magnify**: Auto-calculated based on your display size (or set manually)
|
||||||
|
- **Render Interval**: How often apps re-render (default: 300s)
|
||||||
|
- **Display Duration**: How long each app shows (default: 15s)
|
||||||
|
- **Cache Output**: Enable to reduce re-rendering (recommended)
|
||||||
|
|
||||||
|
### 3. Browse and Install Apps
|
||||||
|
|
||||||
|
1. Navigate to **Plugins → Starlark Apps → App Store**
|
||||||
|
2. Browse available apps (974+ options)
|
||||||
|
3. Filter by category: Weather, Sports, Finance, Games, Clocks, etc.
|
||||||
|
4. Click **Install** on desired apps
|
||||||
|
5. Configure each app:
|
||||||
|
- Set location/timezone
|
||||||
|
- Enter API keys if required
|
||||||
|
- Customize display preferences
|
||||||
|
|
||||||
|
### 4. Configure Apps
|
||||||
|
|
||||||
|
Each app may have different configuration options:
|
||||||
|
|
||||||
|
#### Common Configuration Types
|
||||||
|
|
||||||
|
- **Location** (lat/lng/timezone): For weather, clocks, transit
|
||||||
|
- **API Keys**: For services like weather, stocks, sports scores
|
||||||
|
- **Display Preferences**: Colors, units, layouts
|
||||||
|
- **Dropdown Options**: Team selections, language, themes
|
||||||
|
- **Toggles**: Enable/disable features
|
||||||
|
|
||||||
|
Configuration is stored in `starlark-apps/{app-id}/config.json` and persists across app updates.
|
||||||
|
|
||||||
|
## App Sources and Categories
|
||||||
|
|
||||||
|
All apps are sourced from the [Tronbyte Apps Repository](https://github.com/tronbyt/apps). Popular categories include:
|
||||||
|
|
||||||
|
### 🌤️ Weather
|
||||||
|
- Analog Clock (with weather)
|
||||||
|
- Current Weather
|
||||||
|
- Weather Forecast
|
||||||
|
- Air Quality Index
|
||||||
|
|
||||||
|
### 🏈 Sports
|
||||||
|
- NFL Scores
|
||||||
|
- NBA Scores
|
||||||
|
- MLB Scores
|
||||||
|
- NHL Scores
|
||||||
|
- Soccer/Football Scores
|
||||||
|
- Formula 1 Results
|
||||||
|
|
||||||
|
### 💰 Finance
|
||||||
|
- Stock Tickers
|
||||||
|
- Cryptocurrency Prices
|
||||||
|
- Market Indices
|
||||||
|
|
||||||
|
### 🎮 Games & Fun
|
||||||
|
- Conway's Game of Life
|
||||||
|
- Pong
|
||||||
|
- Nyan Cat
|
||||||
|
- Retro Animations
|
||||||
|
|
||||||
|
### 🕐 Clocks
|
||||||
|
- Analog Clock
|
||||||
|
- Fuzzy Clock
|
||||||
|
- Binary Clock
|
||||||
|
- Word Clock
|
||||||
|
|
||||||
|
### 📰 Information
|
||||||
|
- News Headlines
|
||||||
|
- RSS Feeds
|
||||||
|
- GitHub Activity
|
||||||
|
- Reddit Feed
|
||||||
|
|
||||||
|
### 🚌 Transit & Travel
|
||||||
|
- Transit Arrivals
|
||||||
|
- Flight Tracker
|
||||||
|
- Train Schedules
|
||||||
|
|
||||||
|
## Display Size Compatibility
|
||||||
|
|
||||||
|
Tronbyte/Tidbyt apps are designed for **64×32 displays**. LEDMatrix automatically adapts content for different display sizes:
|
||||||
|
|
||||||
|
### Magnification
|
||||||
|
|
||||||
|
The plugin calculates optimal magnification based on your display:
|
||||||
|
|
||||||
|
```
|
||||||
|
magnify = floor(min(display_width / 64, display_height / 32))
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- **64×32**: magnify = 1 (native, pixel-perfect)
|
||||||
|
- **128×64**: magnify = 2 (2x scaling, crisp)
|
||||||
|
- **192×64**: magnify = 2 (2x + horizontal centering)
|
||||||
|
- **256×64**: magnify = 2 (2x + centering)
|
||||||
|
|
||||||
|
### Scaling Modes
|
||||||
|
|
||||||
|
**Config → Starlark Apps → Scale Method:**
|
||||||
|
- `nearest` (default): Sharp pixels, retro look
|
||||||
|
- `bilinear`: Smooth scaling, slight blur
|
||||||
|
- `bicubic`: Higher quality smooth scaling
|
||||||
|
- `lanczos`: Best quality, most processing
|
||||||
|
|
||||||
|
**Center vs Scale:**
|
||||||
|
- `scale_output=true`: Stretch to fill display (may distort aspect ratio)
|
||||||
|
- `center_small_output=true`: Center output without stretching (preserves aspect ratio)
|
||||||
|
|
||||||
|
## Configuration Schema Extraction
|
||||||
|
|
||||||
|
LEDMatrix automatically extracts configuration schemas from Starlark apps by parsing the `get_schema()` function in the `.star` source code.
|
||||||
|
|
||||||
|
### Supported Field Types
|
||||||
|
|
||||||
|
| Starlark Type | Web UI Rendering |
|
||||||
|
|--------------|------------------|
|
||||||
|
| `schema.Location` | Lat/Lng/Timezone picker |
|
||||||
|
| `schema.Text` | Text input field |
|
||||||
|
| `schema.Toggle` | Checkbox/switch |
|
||||||
|
| `schema.Dropdown` | Select dropdown |
|
||||||
|
| `schema.Color` | Color picker |
|
||||||
|
| `schema.DateTime` | Date/time picker |
|
||||||
|
| `schema.OAuth2` | Warning message (not supported) |
|
||||||
|
| `schema.PhotoSelect` | Warning message (not supported) |
|
||||||
|
| `schema.LocationBased` | Text fallback with note |
|
||||||
|
| `schema.Typeahead` | Text fallback with note |
|
||||||
|
|
||||||
|
### Schema Coverage
|
||||||
|
|
||||||
|
- **90-95%** of apps: Full schema support
|
||||||
|
- **5%**: Partial extraction (complex/dynamic schemas)
|
||||||
|
- **<1%**: No schema (apps without configuration)
|
||||||
|
|
||||||
|
Apps without extracted schemas can still run with default settings.
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
LEDMatrix/
|
||||||
|
├── plugin-repos/starlark-apps/ # Plugin source code
|
||||||
|
│ ├── manager.py # Main plugin logic
|
||||||
|
│ ├── pixlet_renderer.py # Pixlet CLI wrapper
|
||||||
|
│ ├── frame_extractor.py # WebP decoder
|
||||||
|
│ ├── tronbyte_repository.py # GitHub API client
|
||||||
|
│ └── requirements.txt # Python dependencies
|
||||||
|
│
|
||||||
|
├── starlark-apps/ # Installed apps (user data)
|
||||||
|
│ ├── manifest.json # App registry
|
||||||
|
│ │
|
||||||
|
│ └── analogclock/ # Example app
|
||||||
|
│ ├── analogclock.star # Starlark source
|
||||||
|
│ ├── config.json # User configuration
|
||||||
|
│ ├── schema.json # Extracted schema
|
||||||
|
│ ├── cached_render.webp # Rendered output cache
|
||||||
|
│ └── images/ # App assets (if any)
|
||||||
|
│ ├── hour_hand.png
|
||||||
|
│ └── minute_hand.png
|
||||||
|
│
|
||||||
|
├── bin/pixlet/ # Pixlet binaries
|
||||||
|
│ ├── pixlet-linux-amd64
|
||||||
|
│ ├── pixlet-linux-arm64
|
||||||
|
│ └── pixlet-darwin-arm64
|
||||||
|
│
|
||||||
|
└── scripts/
|
||||||
|
└── download_pixlet.sh # Pixlet installer
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Keys and External Services
|
||||||
|
|
||||||
|
Many apps require API keys for external services:
|
||||||
|
|
||||||
|
### Common API Services
|
||||||
|
|
||||||
|
- **Weather**: OpenWeatherMap, Weather.gov, Dark Sky
|
||||||
|
- **Sports**: ESPN, The Sports DB, SportsData.io
|
||||||
|
- **Finance**: Alpha Vantage, CoinGecko, Yahoo Finance
|
||||||
|
- **Transit**: TransitLand, NextBus, local transit APIs
|
||||||
|
- **News**: NewsAPI, Reddit, RSS feeds
|
||||||
|
|
||||||
|
### Security Note
|
||||||
|
|
||||||
|
- API keys are stored in `config.json` files on disk
|
||||||
|
- The LEDMatrix web interface does NOT encrypt API keys
|
||||||
|
- Ensure your Raspberry Pi is on a trusted network
|
||||||
|
- Use read-only or limited-scope API keys when possible
|
||||||
|
- **Never commit `starlark-apps/*/config.json` to version control**
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Pixlet Not Found
|
||||||
|
|
||||||
|
**Symptom**: "Pixlet binary not found" error
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. Run auto-installer: **Plugins → Starlark Apps → Install Pixlet**
|
||||||
|
2. Manual install: `bash scripts/download_pixlet.sh`
|
||||||
|
3. Check permissions: `chmod +x bin/pixlet/pixlet-*`
|
||||||
|
4. Verify architecture: `uname -m` matches binary name
|
||||||
|
|
||||||
|
### App Fails to Render
|
||||||
|
|
||||||
|
**Symptom**: "Rendering failed" error in logs
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. Check logs: `journalctl -u ledmatrix | grep -i pixlet`
|
||||||
|
2. Verify config: Ensure all required fields are filled
|
||||||
|
3. Test manually: `./bin/pixlet/pixlet-linux-amd64 render starlark-apps/{app-id}/{app-id}.star`
|
||||||
|
4. Missing assets: Some apps need images/fonts that may fail to download
|
||||||
|
5. API issues: Check API keys and rate limits
|
||||||
|
|
||||||
|
### Schema Not Extracted
|
||||||
|
|
||||||
|
**Symptom**: App installs but shows no configuration options
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. App may not have a `get_schema()` function (normal for some apps)
|
||||||
|
2. Schema extraction failed: Check logs for parse errors
|
||||||
|
3. Manual config: Edit `starlark-apps/{app-id}/config.json` directly
|
||||||
|
4. Report issue: File bug with app details at LEDMatrix GitHub
|
||||||
|
|
||||||
|
### Apps Show Distorted/Wrong Size
|
||||||
|
|
||||||
|
**Symptom**: Content appears stretched, squished, or cropped
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. Check magnify setting: **Plugins → Starlark Apps → Config**
|
||||||
|
2. Try `center_small_output=true` to preserve aspect ratio
|
||||||
|
3. Adjust `magnify` manually (1-8) for your display size
|
||||||
|
4. Some apps assume 64×32 - may not scale perfectly to all sizes
|
||||||
|
|
||||||
|
### App Shows Outdated Data
|
||||||
|
|
||||||
|
**Symptom**: Weather, sports scores, etc. don't update
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. Check render interval: **App Config → Render Interval** (300s default)
|
||||||
|
2. Force re-render: **Plugins → Starlark Apps → {App} → Render Now**
|
||||||
|
3. Clear cache: Restart LEDMatrix service
|
||||||
|
4. API rate limits: Some services throttle requests
|
||||||
|
5. Check app logs for API errors
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
### Render Intervals
|
||||||
|
|
||||||
|
- Apps re-render on a schedule (default: 300s = 5 minutes)
|
||||||
|
- Lower intervals = more CPU/API usage
|
||||||
|
- Recommended minimums:
|
||||||
|
- Static content (clocks): 30-60s
|
||||||
|
- Weather: 300s (5min)
|
||||||
|
- Sports scores: 60-120s
|
||||||
|
- Stock tickers: 60s
|
||||||
|
|
||||||
|
### Caching
|
||||||
|
|
||||||
|
Enable caching to reduce CPU load:
|
||||||
|
- `cache_rendered_output=true` (recommended)
|
||||||
|
- `cache_ttl=300` (5 minutes)
|
||||||
|
|
||||||
|
Cached WebP files are stored in `starlark-apps/{app-id}/cached_render.webp`
|
||||||
|
|
||||||
|
### Display Rotation
|
||||||
|
|
||||||
|
Balance number of enabled apps with display duration:
|
||||||
|
- 5 apps × 15s = 75s full cycle
|
||||||
|
- 20 apps × 15s = 300s (5min) cycle
|
||||||
|
|
||||||
|
Long cycles may cause apps to render before being displayed.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
### Unsupported Features
|
||||||
|
|
||||||
|
- **OAuth2 Authentication**: Apps requiring OAuth login won't work
|
||||||
|
- **PhotoSelect**: Image upload from mobile device not supported
|
||||||
|
- **Push Notifications**: Apps can't receive real-time events
|
||||||
|
- **Background Jobs**: No persistent background tasks
|
||||||
|
|
||||||
|
### API Rate Limits
|
||||||
|
|
||||||
|
Many apps use free API tiers with rate limits:
|
||||||
|
- Rendering too frequently may exceed limits
|
||||||
|
- Use appropriate `render_interval` settings
|
||||||
|
- Consider paid API tiers for heavy usage
|
||||||
|
|
||||||
|
### Display Size Constraints
|
||||||
|
|
||||||
|
Apps designed for 64×32 may not utilize larger displays fully:
|
||||||
|
- Content may appear small on 128×64+ displays
|
||||||
|
- Magnification helps but doesn't add detail
|
||||||
|
- Some apps hard-code 64×32 dimensions
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
### Manual App Installation
|
||||||
|
|
||||||
|
Upload custom `.star` files:
|
||||||
|
1. Navigate to **Starlark Apps → Upload**
|
||||||
|
2. Select `.star` file from disk
|
||||||
|
3. Configure app ID and metadata
|
||||||
|
4. Set render/display timing
|
||||||
|
|
||||||
|
### Custom App Development
|
||||||
|
|
||||||
|
While LEDMatrix runs Tronbyte apps, you can also create your own:
|
||||||
|
|
||||||
|
1. **Learn Starlark**: [Tidbyt Developer Docs](https://tidbyt.dev/)
|
||||||
|
2. **Write `.star` file**: Use Pixlet APIs for rendering
|
||||||
|
3. **Test locally**: `pixlet render myapp.star`
|
||||||
|
4. **Upload**: Use LEDMatrix web UI to install
|
||||||
|
5. **Share**: Contribute to [Tronbyte Apps](https://github.com/tronbyt/apps) repo
|
||||||
|
|
||||||
|
### Configuration Reference
|
||||||
|
|
||||||
|
**Plugin Config** (`config/config.json` → `plugins.starlark-apps`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"magnify": 0, // 0 = auto, 1-8 = manual
|
||||||
|
"render_timeout": 30, // Max seconds for Pixlet render
|
||||||
|
"cache_rendered_output": true, // Cache WebP files
|
||||||
|
"cache_ttl": 300, // Cache duration (seconds)
|
||||||
|
"scale_output": true, // Scale to display size
|
||||||
|
"scale_method": "nearest", // nearest|bilinear|bicubic|lanczos
|
||||||
|
"center_small_output": false, // Center instead of scale
|
||||||
|
"default_frame_delay": 50, // Frame timing (ms)
|
||||||
|
"max_frames": null, // Limit frames (null = unlimited)
|
||||||
|
"auto_refresh_apps": true // Auto re-render on interval
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**App Config** (`starlark-apps/{app-id}/config.json`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"location": "{\"lat\":\"40.7128\",\"lng\":\"-74.0060\",\"timezone\":\"America/New_York\"}",
|
||||||
|
"units": "imperial",
|
||||||
|
"api_key": "your-api-key-here",
|
||||||
|
"render_interval": 300, // App-specific override
|
||||||
|
"display_duration": 15 // App-specific override
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
### Official Documentation
|
||||||
|
|
||||||
|
- **Tidbyt Developer Docs**: https://tidbyt.dev/
|
||||||
|
- **Starlark Language**: https://github.com/bazelbuild/starlark
|
||||||
|
- **Pixlet Repository**: https://github.com/tidbyt/pixlet
|
||||||
|
- **Tronbyte Apps**: https://github.com/tronbyt/apps
|
||||||
|
|
||||||
|
### LEDMatrix Documentation
|
||||||
|
|
||||||
|
- [Plugin Development Guide](PLUGIN_DEVELOPMENT_GUIDE.md)
|
||||||
|
- [REST API Reference](REST_API_REFERENCE.md)
|
||||||
|
- [Troubleshooting Guide](TROUBLESHOOTING.md)
|
||||||
|
|
||||||
|
### Community
|
||||||
|
|
||||||
|
- **Tidbyt Community**: https://discuss.tidbyt.com/
|
||||||
|
- **Tronbyte Apps Issues**: https://github.com/tronbyt/apps/issues
|
||||||
|
- **LEDMatrix Issues**: https://github.com/ChuckBuilds/LEDMatrix/issues
|
||||||
|
|
||||||
|
## License and Legal
|
||||||
|
|
||||||
|
- **LEDMatrix**: MIT License (see project root)
|
||||||
|
- **Starlark Apps Plugin**: MIT License (part of LEDMatrix)
|
||||||
|
- **Pixlet**: Apache 2.0 License (Tidbyt Inc.)
|
||||||
|
- **Tronbyte Apps**: Various licenses (see individual app headers)
|
||||||
|
- **Starlark Language**: Apache 2.0 License (Google/Bazel)
|
||||||
|
|
||||||
|
**Disclaimer**: LEDMatrix is an independent project and is not affiliated with, endorsed by, or sponsored by Tidbyt Inc. The Starlark Apps plugin enables interoperability with Tidbyt's open-source ecosystem but does not imply any official relationship.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues with:
|
||||||
|
- **LEDMatrix integration**: File issues at [LEDMatrix GitHub](https://github.com/ChuckBuilds/LEDMatrix/issues)
|
||||||
|
- **Specific apps**: File issues at [Tronbyte Apps](https://github.com/tronbyt/apps/issues)
|
||||||
|
- **Pixlet rendering**: File issues at [Pixlet Repository](https://github.com/tidbyt/pixlet/issues)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Ready to get started?** Install the Starlark Apps plugin and explore 974+ community apps! 🎨
|
||||||
@@ -553,6 +553,7 @@ class StarlarkAppsPlugin(BasePlugin):
|
|||||||
Save apps manifest to file with file locking to prevent race conditions.
|
Save apps manifest to file with file locking to prevent race conditions.
|
||||||
Uses exclusive lock during write to prevent concurrent modifications.
|
Uses exclusive lock during write to prevent concurrent modifications.
|
||||||
"""
|
"""
|
||||||
|
temp_file = None
|
||||||
try:
|
try:
|
||||||
# Use atomic write pattern: write to temp file, then rename
|
# Use atomic write pattern: write to temp file, then rename
|
||||||
temp_file = self.manifest_file.with_suffix('.tmp')
|
temp_file = self.manifest_file.with_suffix('.tmp')
|
||||||
@@ -573,10 +574,10 @@ class StarlarkAppsPlugin(BasePlugin):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error saving manifest: {e}")
|
self.logger.error(f"Error saving manifest: {e}")
|
||||||
# Clean up temp file if it exists
|
# Clean up temp file if it exists
|
||||||
if temp_file.exists():
|
if temp_file and temp_file.exists():
|
||||||
try:
|
try:
|
||||||
temp_file.unlink()
|
temp_file.unlink()
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -879,7 +880,9 @@ class StarlarkAppsPlugin(BasePlugin):
|
|||||||
def update_fn(manifest):
|
def update_fn(manifest):
|
||||||
manifest["apps"][safe_app_id] = app_manifest
|
manifest["apps"][safe_app_id] = app_manifest
|
||||||
|
|
||||||
self._update_manifest_safe(update_fn)
|
if not self._update_manifest_safe(update_fn):
|
||||||
|
self.logger.error(f"Failed to update manifest for {app_id}")
|
||||||
|
return False
|
||||||
|
|
||||||
# Create app instance (use safe_app_id for internal key, original for display)
|
# Create app instance (use safe_app_id for internal key, original for display)
|
||||||
app = StarlarkApp(safe_app_id, app_dir, app_manifest)
|
app = StarlarkApp(safe_app_id, app_dir, app_manifest)
|
||||||
@@ -913,19 +916,24 @@ class StarlarkAppsPlugin(BasePlugin):
|
|||||||
if self.current_app and self.current_app.app_id == app_id:
|
if self.current_app and self.current_app.app_id == app_id:
|
||||||
self.current_app = None
|
self.current_app = None
|
||||||
|
|
||||||
# Remove from apps dict
|
# Get app reference before removing from dict
|
||||||
app = self.apps.pop(app_id)
|
app = self.apps.get(app_id)
|
||||||
|
|
||||||
# Remove directory
|
# Update manifest FIRST (before modifying filesystem)
|
||||||
if app.app_dir.exists():
|
|
||||||
shutil.rmtree(app.app_dir)
|
|
||||||
|
|
||||||
# Update manifest
|
|
||||||
def update_fn(manifest):
|
def update_fn(manifest):
|
||||||
if app_id in manifest["apps"]:
|
if app_id in manifest["apps"]:
|
||||||
del manifest["apps"][app_id]
|
del manifest["apps"][app_id]
|
||||||
|
|
||||||
self._update_manifest_safe(update_fn)
|
if not self._update_manifest_safe(update_fn):
|
||||||
|
self.logger.error(f"Failed to update manifest when uninstalling {app_id}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Remove from apps dict
|
||||||
|
self.apps.pop(app_id)
|
||||||
|
|
||||||
|
# Remove directory (after manifest update succeeds)
|
||||||
|
if app and app.app_dir.exists():
|
||||||
|
shutil.rmtree(app.app_dir)
|
||||||
|
|
||||||
self.logger.info(f"Uninstalled Starlark app: {app_id}")
|
self.logger.info(f"Uninstalled Starlark app: {app_id}")
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -264,10 +264,11 @@ class PixletRenderer:
|
|||||||
else:
|
else:
|
||||||
value_str = str(value)
|
value_str = str(value)
|
||||||
|
|
||||||
# Validate value doesn't contain shell metacharacters
|
# Validate value doesn't contain dangerous shell metacharacters
|
||||||
# Allow alphanumeric, spaces, and common safe chars: .-_:/@#,
|
# Block: backticks, $(), pipes, redirects, semicolons, ampersands, null bytes
|
||||||
if not re.match(r'^[a-zA-Z0-9 .\-_:/@#,{}"\[\]]*$', value_str):
|
# Allow: most printable chars including spaces, quotes, brackets, braces
|
||||||
logger.warning(f"Skipping config value with unsafe characters for key {key}: {value_str}")
|
if re.search(r'[`$|<>&;\x00]|\$\(', value_str):
|
||||||
|
logger.warning(f"Skipping config value with unsafe shell characters for key {key}: {value_str}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Add as positional argument (not -c flag)
|
# Add as positional argument (not -c flag)
|
||||||
@@ -469,7 +470,7 @@ class PixletRenderer:
|
|||||||
|
|
||||||
def _extract_get_schema_body(self, content: str) -> Optional[str]:
|
def _extract_get_schema_body(self, content: str) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Extract get_schema() function body.
|
Extract get_schema() function body using indentation-aware parsing.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
content: .star file content
|
content: .star file content
|
||||||
@@ -477,12 +478,45 @@ class PixletRenderer:
|
|||||||
Returns:
|
Returns:
|
||||||
Function body text, or None if not found
|
Function body text, or None if not found
|
||||||
"""
|
"""
|
||||||
# Find def get_schema():
|
# Find def get_schema(): line
|
||||||
pattern = r'def\s+get_schema\s*\(\s*\)\s*:(.*?)(?=\ndef\s|\Z)'
|
pattern = r'^(\s*)def\s+get_schema\s*\(\s*\)\s*:'
|
||||||
match = re.search(pattern, content, re.DOTALL)
|
match = re.search(pattern, content, re.MULTILINE)
|
||||||
|
|
||||||
if match:
|
if not match:
|
||||||
return match.group(1)
|
return None
|
||||||
|
|
||||||
|
# Get the indentation level of the function definition
|
||||||
|
func_indent = len(match.group(1))
|
||||||
|
func_start = match.end()
|
||||||
|
|
||||||
|
# Split content into lines starting after the function definition
|
||||||
|
lines_after = content[func_start:].split('\n')
|
||||||
|
body_lines = []
|
||||||
|
|
||||||
|
for line in lines_after:
|
||||||
|
# Skip empty lines
|
||||||
|
if not line.strip():
|
||||||
|
body_lines.append(line)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Calculate indentation of current line
|
||||||
|
stripped = line.lstrip()
|
||||||
|
line_indent = len(line) - len(stripped)
|
||||||
|
|
||||||
|
# If line has same or less indentation than function def, check if it's a top-level def
|
||||||
|
if line_indent <= func_indent:
|
||||||
|
# This is a line at the same or outer level - check if it's a function
|
||||||
|
if re.match(r'def\s+\w+', stripped):
|
||||||
|
# Found next top-level function, stop here
|
||||||
|
break
|
||||||
|
# Otherwise it might be a comment or other top-level code, stop anyway
|
||||||
|
break
|
||||||
|
|
||||||
|
# Line is indented more than function def, so it's part of the body
|
||||||
|
body_lines.append(line)
|
||||||
|
|
||||||
|
if body_lines:
|
||||||
|
return '\n'.join(body_lines)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _parse_schema_field(self, field_type: str, params_text: str, var_table: Dict) -> Optional[Dict[str, Any]]:
|
def _parse_schema_field(self, field_type: str, params_text: str, var_table: Dict) -> Optional[Dict[str, Any]]:
|
||||||
@@ -545,15 +579,24 @@ class PixletRenderer:
|
|||||||
field_dict['icon'] = icon_match.group(1)
|
field_dict['icon'] = icon_match.group(1)
|
||||||
|
|
||||||
# default (can be string, bool, or variable reference)
|
# default (can be string, bool, or variable reference)
|
||||||
default_match = re.search(r'default\s*=\s*([^,\)]+)', params_text)
|
# First try to match quoted strings (which may contain commas)
|
||||||
|
default_match = re.search(r'default\s*=\s*"([^"]*)"', params_text)
|
||||||
|
if not default_match:
|
||||||
|
# Try single quotes
|
||||||
|
default_match = re.search(r"default\s*=\s*'([^']*)'", params_text)
|
||||||
|
if not default_match:
|
||||||
|
# Fall back to unquoted value (stop at comma or closing paren)
|
||||||
|
default_match = re.search(r'default\s*=\s*([^,\)]+)', params_text)
|
||||||
|
|
||||||
if default_match:
|
if default_match:
|
||||||
default_value = default_match.group(1).strip()
|
default_value = default_match.group(1).strip()
|
||||||
# Handle boolean
|
# Handle boolean
|
||||||
if default_value in ('True', 'False'):
|
if default_value in ('True', 'False'):
|
||||||
field_dict['default'] = default_value.lower()
|
field_dict['default'] = default_value.lower()
|
||||||
# Handle string literal
|
# Handle string literal from first two patterns (already extracted without quotes)
|
||||||
elif default_value.startswith('"') and default_value.endswith('"'):
|
elif re.search(r'default\s*=\s*["\']', params_text):
|
||||||
field_dict['default'] = default_value.strip('"')
|
# This was a quoted string, use the captured content directly
|
||||||
|
field_dict['default'] = default_value
|
||||||
# Handle variable reference (can't resolve, use as-is)
|
# Handle variable reference (can't resolve, use as-is)
|
||||||
else:
|
else:
|
||||||
# Try to extract just the value if it's like options[0].value
|
# Try to extract just the value if it's like options[0].value
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
Pillow>=10.0.0
|
Pillow>=10.4.0
|
||||||
PyYAML>=6.0
|
PyYAML>=6.0.2
|
||||||
requests>=2.31.0
|
requests>=2.32.0
|
||||||
|
|||||||
@@ -462,6 +462,12 @@ class TronbyteRepository:
|
|||||||
for file_item in dir_data:
|
for file_item in dir_data:
|
||||||
if file_item.get('type') == 'file':
|
if file_item.get('type') == 'file':
|
||||||
file_name = file_item.get('name')
|
file_name = file_item.get('name')
|
||||||
|
|
||||||
|
# Ensure file_name is a non-empty string before validation
|
||||||
|
if not file_name or not isinstance(file_name, str):
|
||||||
|
logger.warning(f"Skipping file with invalid name in {dir_name}: {file_item}")
|
||||||
|
continue
|
||||||
|
|
||||||
# Validate filename for path traversal
|
# Validate filename for path traversal
|
||||||
if '..' in file_name or '/' in file_name or '\\' in file_name:
|
if '..' in file_name or '/' in file_name or '\\' in file_name:
|
||||||
logger.warning(f"Skipping potentially unsafe file: {file_name}")
|
logger.warning(f"Skipping potentially unsafe file: {file_name}")
|
||||||
|
|||||||
@@ -7078,14 +7078,29 @@ def _read_starlark_manifest() -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def _write_starlark_manifest(manifest: dict) -> bool:
|
def _write_starlark_manifest(manifest: dict) -> bool:
|
||||||
"""Write the starlark-apps manifest.json to disk."""
|
"""Write the starlark-apps manifest.json to disk with atomic write."""
|
||||||
|
temp_file = None
|
||||||
try:
|
try:
|
||||||
_STARLARK_APPS_DIR.mkdir(parents=True, exist_ok=True)
|
_STARLARK_APPS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
with open(_STARLARK_MANIFEST_FILE, 'w') as f:
|
|
||||||
|
# Atomic write pattern: write to temp file, then rename
|
||||||
|
temp_file = _STARLARK_MANIFEST_FILE.with_suffix('.tmp')
|
||||||
|
with open(temp_file, 'w') as f:
|
||||||
json.dump(manifest, f, indent=2)
|
json.dump(manifest, f, indent=2)
|
||||||
|
f.flush()
|
||||||
|
os.fsync(f.fileno()) # Ensure data is written to disk
|
||||||
|
|
||||||
|
# Atomic rename (overwrites destination)
|
||||||
|
temp_file.replace(_STARLARK_MANIFEST_FILE)
|
||||||
return True
|
return True
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
logger.error(f"Error writing starlark manifest: {e}")
|
logger.error(f"Error writing starlark manifest: {e}")
|
||||||
|
# Clean up temp file if it exists
|
||||||
|
if temp_file and temp_file.exists():
|
||||||
|
try:
|
||||||
|
temp_file.unlink()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -7398,7 +7413,15 @@ def uninstall_starlark_app(app_id):
|
|||||||
else:
|
else:
|
||||||
# Standalone: remove app dir and manifest entry
|
# Standalone: remove app dir and manifest entry
|
||||||
import shutil
|
import shutil
|
||||||
app_dir = _STARLARK_APPS_DIR / app_id
|
app_dir = (_STARLARK_APPS_DIR / app_id).resolve()
|
||||||
|
|
||||||
|
# Path traversal check - ensure app_dir is within _STARLARK_APPS_DIR
|
||||||
|
try:
|
||||||
|
app_dir.relative_to(_STARLARK_APPS_DIR.resolve())
|
||||||
|
except ValueError:
|
||||||
|
logger.warning(f"Path traversal attempt in uninstall: {app_id}")
|
||||||
|
return jsonify({'status': 'error', 'message': 'Invalid app_id'}), 400
|
||||||
|
|
||||||
if app_dir.exists():
|
if app_dir.exists():
|
||||||
shutil.rmtree(app_dir)
|
shutil.rmtree(app_dir)
|
||||||
manifest = _read_starlark_manifest()
|
manifest = _read_starlark_manifest()
|
||||||
|
|||||||
@@ -1,6 +1,43 @@
|
|||||||
|
// ─── LocalStorage Safety Wrappers ────────────────────────────────────────────
|
||||||
|
// Handles environments where localStorage is unavailable or restricted (private browsing, etc.)
|
||||||
|
const safeLocalStorage = {
|
||||||
|
getItem(key) {
|
||||||
|
try {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
return safeLocalStorage.getItem(key);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`safeLocalStorage.getItem failed for key "${key}":`, e.message);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
setItem(key, value) {
|
||||||
|
try {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
safeLocalStorage.setItem(key, value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`safeLocalStorage.setItem failed for key "${key}":`, e.message);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
removeItem(key) {
|
||||||
|
try {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`localStorage.removeItem failed for key "${key}":`, e.message);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Define critical functions immediately so they're available before any HTML is rendered
|
// Define critical functions immediately so they're available before any HTML is rendered
|
||||||
// Debug logging controlled by localStorage.setItem('pluginDebug', 'true')
|
// Debug logging controlled by safeLocalStorage.setItem('pluginDebug', 'true')
|
||||||
const _PLUGIN_DEBUG_EARLY = typeof localStorage !== 'undefined' && localStorage.getItem('pluginDebug') === 'true';
|
const _PLUGIN_DEBUG_EARLY = safeLocalStorage.getItem('pluginDebug') === 'true';
|
||||||
if (_PLUGIN_DEBUG_EARLY) console.log('[PLUGINS SCRIPT] Defining configurePlugin and togglePlugin at top level...');
|
if (_PLUGIN_DEBUG_EARLY) console.log('[PLUGINS SCRIPT] Defining configurePlugin and togglePlugin at top level...');
|
||||||
|
|
||||||
// Expose on-demand functions early as stubs (will be replaced when IIFE runs)
|
// Expose on-demand functions early as stubs (will be replaced when IIFE runs)
|
||||||
@@ -865,7 +902,7 @@ window.currentPluginConfig = null;
|
|||||||
|
|
||||||
// Store filter/sort state
|
// Store filter/sort state
|
||||||
const storeFilterState = {
|
const storeFilterState = {
|
||||||
sort: localStorage.getItem('storeSort') || 'a-z',
|
sort: safeLocalStorage.getItem('storeSort') || 'a-z',
|
||||||
filterVerified: false,
|
filterVerified: false,
|
||||||
filterNew: false,
|
filterNew: false,
|
||||||
filterInstalled: null, // null = all, true = installed only, false = not installed only
|
filterInstalled: null, // null = all, true = installed only, false = not installed only
|
||||||
@@ -873,7 +910,7 @@ window.currentPluginConfig = null;
|
|||||||
filterCategories: [],
|
filterCategories: [],
|
||||||
|
|
||||||
persist() {
|
persist() {
|
||||||
localStorage.setItem('storeSort', this.sort);
|
safeLocalStorage.setItem('storeSort', this.sort);
|
||||||
},
|
},
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
@@ -898,7 +935,7 @@ window.currentPluginConfig = null;
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Installed plugins sort state
|
// Installed plugins sort state
|
||||||
let installedSort = localStorage.getItem('installedSort') || 'a-z';
|
let installedSort = safeLocalStorage.getItem('installedSort') || 'a-z';
|
||||||
|
|
||||||
// Shared on-demand status store (mirrors Alpine store when available)
|
// Shared on-demand status store (mirrors Alpine store when available)
|
||||||
window.__onDemandStore = window.__onDemandStore || {
|
window.__onDemandStore = window.__onDemandStore || {
|
||||||
@@ -1251,8 +1288,8 @@ const pluginLoadCache = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Debug flag - set via localStorage.setItem('pluginDebug', 'true')
|
// Debug flag - set via safeLocalStorage.setItem('pluginDebug', 'true')
|
||||||
const PLUGIN_DEBUG = typeof localStorage !== 'undefined' && localStorage.getItem('pluginDebug') === 'true';
|
const PLUGIN_DEBUG = typeof localStorage !== 'undefined' && safeLocalStorage.getItem('pluginDebug') === 'true';
|
||||||
function pluginLog(...args) {
|
function pluginLog(...args) {
|
||||||
if (PLUGIN_DEBUG) console.log(...args);
|
if (PLUGIN_DEBUG) console.log(...args);
|
||||||
}
|
}
|
||||||
@@ -5269,7 +5306,7 @@ function setupStoreFilterListeners() {
|
|||||||
installedSortSelect.value = installedSort;
|
installedSortSelect.value = installedSort;
|
||||||
installedSortSelect.addEventListener('change', () => {
|
installedSortSelect.addEventListener('change', () => {
|
||||||
installedSort = installedSortSelect.value;
|
installedSort = installedSortSelect.value;
|
||||||
localStorage.setItem('installedSort', installedSort);
|
safeLocalStorage.setItem('installedSort', installedSort);
|
||||||
const plugins = window.installedPlugins || [];
|
const plugins = window.installedPlugins || [];
|
||||||
if (plugins.length > 0) {
|
if (plugins.length > 0) {
|
||||||
sortAndRenderInstalledPlugins(plugins);
|
sortAndRenderInstalledPlugins(plugins);
|
||||||
|
|||||||
@@ -155,13 +155,13 @@
|
|||||||
class="w-10 h-10 rounded border border-gray-300 cursor-pointer p-0.5"
|
class="w-10 h-10 rounded border border-gray-300 cursor-pointer p-0.5"
|
||||||
value="{{ current_val or '#FFFFFF' }}"
|
value="{{ current_val or '#FFFFFF' }}"
|
||||||
data-starlark-color-picker="{{ field_id }}"
|
data-starlark-color-picker="{{ field_id }}"
|
||||||
oninput="document.querySelector('[data-starlark-config={{ field_id }}]').value = this.value">
|
oninput="this.closest('.space-y-6').querySelector('[data-starlark-config={{ field_id }}]').value = this.value">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
class="form-control flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm font-mono"
|
class="form-control flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm font-mono"
|
||||||
value="{{ current_val or '#FFFFFF' }}"
|
value="{{ current_val or '#FFFFFF' }}"
|
||||||
placeholder="#RRGGBB"
|
placeholder="#RRGGBB"
|
||||||
data-starlark-config="{{ field_id }}"
|
data-starlark-config="{{ field_id }}"
|
||||||
oninput="var cp = document.querySelector('[data-starlark-color-picker={{ field_id }}]'); if(this.value.match(/^#[0-9a-fA-F]{6}$/)) cp.value = this.value;">
|
oninput="var cp = this.closest('.space-y-6').querySelector('[data-starlark-color-picker={{ field_id }}]'); if(cp && this.value.match(/^#[0-9a-fA-F]{6}$/)) cp.value = this.value;">
|
||||||
</div>
|
</div>
|
||||||
{% if field_desc %}
|
{% if field_desc %}
|
||||||
<p class="text-xs text-gray-400 mt-1">{{ field_desc }}</p>
|
<p class="text-xs text-gray-400 mt-1">{{ field_desc }}</p>
|
||||||
@@ -375,8 +375,15 @@ function toggleStarlarkApp(appId, enabled) {
|
|||||||
function saveStarlarkConfig(appId) {
|
function saveStarlarkConfig(appId) {
|
||||||
var config = {};
|
var config = {};
|
||||||
|
|
||||||
|
// Get container to scope queries (prevents conflicts if multiple modals open)
|
||||||
|
var container = document.getElementById('plugin-config-starlark:' + appId);
|
||||||
|
if (!container) {
|
||||||
|
console.error('Container not found for appId:', appId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Collect standard inputs (text, number, select, datetime, color text companion)
|
// Collect standard inputs (text, number, select, datetime, color text companion)
|
||||||
document.querySelectorAll('[data-starlark-config]').forEach(function(input) {
|
container.querySelectorAll('[data-starlark-config]').forEach(function(input) {
|
||||||
var key = input.getAttribute('data-starlark-config');
|
var key = input.getAttribute('data-starlark-config');
|
||||||
var type = input.getAttribute('data-starlark-type');
|
var type = input.getAttribute('data-starlark-type');
|
||||||
|
|
||||||
@@ -390,7 +397,7 @@ function saveStarlarkConfig(appId) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Collect location mini-form groups
|
// Collect location mini-form groups
|
||||||
document.querySelectorAll('[data-starlark-location-group]').forEach(function(group) {
|
container.querySelectorAll('[data-starlark-location-group]').forEach(function(group) {
|
||||||
var fieldId = group.getAttribute('data-starlark-location-group');
|
var fieldId = group.getAttribute('data-starlark-location-group');
|
||||||
var loc = {};
|
var loc = {};
|
||||||
group.querySelectorAll('[data-starlark-location-field="' + fieldId + '"]').forEach(function(sub) {
|
group.querySelectorAll('[data-starlark-location-field="' + fieldId + '"]').forEach(function(sub) {
|
||||||
|
|||||||
Reference in New Issue
Block a user