refactor: Restructure data files into component-specific and common directories, add new UI components, and update project documentation.

This commit is contained in:
MunchDev-oss
2026-01-07 02:06:43 -05:00
parent 97d2b66f02
commit 5366865b4b
28 changed files with 1894 additions and 2051 deletions

View File

@@ -30,9 +30,7 @@ jobs:
- name: Generate manifest from site data - name: Generate manifest from site data
run: | run: |
python scripts/generate_manifest_from_site.py \ python scripts/vendor_update.py --scan-only
--site-dir website/src/data/components \
--manifest manifest/vendor_manifest.json
- name: Check for updates - name: Check for updates
id: check-updates id: check-updates

152
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,152 @@
# 🛠️ Contributing to OSSM Configurator
Thank you for your interest in contributing to the OSSM Configurator! This document provides a detailed guide on how to add new components, mods, remotes, and hardware to the system.
---
## 🏗️ Data Architecture Overview
The configurator's data is organized into several key directories within `website/src/data/`:
- `common/`: Shared data like `colors.json` and `hardware.json`.
- `components/`: Detailed definitions of physical parts (STLs, quantities, dependencies).
- `config/`: Wizard configuration, including `options.json` which defines the UI structure.
---
## Adding a New Component (Mod, Remote, or Part)
Adding a component involves three main steps:
1. Defining the physical parts in `components/`.
2. Ensuring all required hardware exists in `common/hardware.json`.
3. Linking the component into the wizard UI via `config/options.json`.
### Step 1: Define the Component
Components are defined as JSON objects. A component can use one of two structures:
#### A. Standard Component (e.g., `actuator.json`)
Used for fixed assemblies where all selected parts are gathered into a single list.
```json
{
"my-new-mod": {
"category": "Mods",
"type": "mod",
"printedParts": [
{
"id": "mod-part-a",
"name": "Mod Part A",
"filamentEstimate": 45.2,
"timeEstimate": "1h30m",
"colour": "primary",
"required": true,
"url": "https://github.com/Owner/Repo/blob/main/part-a.stl?raw=true"
}
],
"hardwareParts": [
{
"id": "hardware-fasteners-m3x8-shcs",
"required": true,
"quantity": 4,
"relatedParts": ["mod-part-a"]
}
]
}
}
```
#### B. System-based Component (e.g., `remote.json`, `hinges.json`)
Used when a category has multiple distinct "systems" that a user chooses between.
```json
{
"remotes": {
"category": "Remote",
"systems": {
"my-custom-remote": {
"name": "Custom Remote v1",
"description": "A high-performance custom remote",
"image": "/images/options/custom-remote.png",
"bodyParts": [
{
"id": "remote-shell",
"name": "Remote Shell",
"url": "...",
"required": true
}
],
"knobs": [
{ "id": "knob-standard", "name": "Standard Knob", "url": "..." }
],
"hardwareParts": [
{ "id": "remote-hardware", "required": true }
]
}
}
}
}
```
### Step 2: Register Hardware
If your component requires hardware not already in the system, add it to `website/src/data/common/hardware.json`:
```json
"fasteners": {
"M3x12 SHCS": {
"id": "hardware-fasteners-m3x12-shcs",
"name": "M3x12 SHCS",
"price": 0.15
}
}
```
### Step 3: Add to the Wizard (options.json)
To make your part selectable, add its ID to the `sections` in `website/src/data/config/options.json`.
```json
"toyMounts": {
"sections": {
"myNewCategory": {
"title": "My New Category",
"componentIds": ["my-new-part-id"],
"isMultiSelect": true
}
}
}
```
---
## 📦 The Vendor System
The configurator uses a script to "vendor" external files. This ensures that even if an upstream GitHub repo changes, our builds remain stable.
After adding a new component with a `url` field:
1. Run the vendor script:
```bash
python scripts/vendor_update.py
```
2. The script will:
- Download the file to the `vendor/` directory.
- Calculate a checksum.
- Pin it to the current commit SHA.
- Update your component JSON with a `vendor` metadata block.
---
## 🖼️ Images & Assets
- **Component Images**: Place images in `website/public/images/options/`.
- **Naming**: Use the component `id` as the filename (e.g., `my-part-id.png`).
- **Specs**: Transparent PNGs are preferred for a premium "floating" look.
---
## ✅ Contribution Checklist
1. [ ] Component JSON added to `website/src/data/components/`.
2. [ ] Hardware added to `common/hardware.json` (if new).
3. [ ] ID added to `config/options.json`.
4. [ ] `python scripts/vendor_update.py` executed.
5. [ ] Verified that the part appears in the summary and correctly calculates hardware.
6. [ ] (Optional) High-quality image added to `/public/images/options/`.

626
README.md
View File

@@ -1,571 +1,111 @@
# OSSM Configurator # 🛠️ OSSM Configurator
A web-based configuration tool for the Open Source Sex Machine (OSSM) project. This application provides an intuitive wizard interface that guides users through selecting and customizing components for their OSSM build, generating a complete Bill of Materials (BOM) and configuration summary. [![Project Version](https://img.shields.io/badge/version-0.0.1--beta-blue.svg)](https://github.com/KinkyMakers/OSSM-Configurator)
[![License](https://img.shields.io/badge/License-OSSM-green.svg)](https://github.com/KinkyMakers/OSSM-Configurator)
## Project Structure A professional web-based configuration tool for the **Open Source Sex Machine (OSSM)** project. This application provides a premium, intuitive wizard interface that guides users through the complex process of selecting, customizing, and validating components for their OSSM build.
```
OSSM-Configurator/
├── website/ # Main web application
│ ├── src/ # React source code
│ ├── public/ # Static assets (images, etc.)
│ ├── dist/ # Build output (generated)
│ ├── node_modules/ # Dependencies (generated)
│ └── ... # Configuration files
├── BOM.xlsx # Bill of Materials spreadsheet
├── Screen Shots/ # Application screenshots
└── README.md # This file
```
## Website Overview
The OSSM Configurator is a React-based single-page application built with Vite. It provides a step-by-step wizard interface that allows users to:
1. **Select Motor** - Choose from available motor options (42AIM30, 57AIM30, iHSV57)
2. **Choose Power Supply** - Select appropriate power supply (24V PSU, 24V USB-C PD)
3. **Customize Colors** - Pick primary and accent colors for 3D printed parts
4. **Configure Options** - Select mounting options, stands, toy mounts, actuators, and other components
5. **Review Summary** - View complete BOM with pricing, filament estimates, and export options
### Key Features
- **Interactive Wizard Interface**: Step-by-step configuration process with progress tracking
- **Component Compatibility**: Ensures selected components are compatible with each other
- **Real-time Pricing**: Calculates total cost including hardware and printed parts
- **Filament Estimates**: Provides 3D printing filament requirements for each component
- **BOM Export**: Generate and download a complete Bill of Materials
- **Visual Component Selection**: Image-based component selection for better user experience
### Technology Stack
- **React 18** - UI framework
- **Vite** - Build tool and dev server
- **Tailwind CSS** - Styling
- **JSZip** - For generating downloadable BOM packages
## TODO
- [X] Dark Mode [Completed]
- [ ] Finalize Actuator Components and mapping to BOM [In Progress]
- [ ] Finalize Stand Components and mapping to BOM
- [ ] Finalize PCB Components and mapping to BOM
- [ ] Finalize Toy Mounts Components and mapping to BOM
- [ ] Finalize Remote Control Components and mapping to BOM
- [ ] Finalize Mounting Components and mapping to BOM
- [ ] Finalize Other Components and mapping to BOM
- [ ] Finalize Colors and mapping to BOM
- [ ] Finalize Pricing and mapping to BOM
- [ ] Finalize BOM Export and mapping to BOM
- [ ] Finalize BOM Import and mapping to BOM
- [ ] Finalize Storage and sharing of BOMs
- [ ] Add references to original hardware files and designs
- [ ] Add Readme/assembly instructions for each component
- [ ] Add FAQ and troubleshooting guide
- [ ] Add support for multiple languages
- [ ] Add support for multiple currencies
- [ ] Add support for multiple payment methods
- [ ] Add support for multiple shipping methods
- [ ] Add support for multiple shipping countries
- [ ] Add support for multiple shipping regions
- [ ] Add support for multiple shipping cities
- [ ] Add 3D render of final product with all components and options selected and coloured [If possible]
## Getting Started
### Prerequisites
**Option 1: Using Docker (Recommended)**
- Docker Desktop or Docker Engine
- Docker Compose
**Option 2: Local Development**
- Node.js (v16 or higher recommended)
- npm or yarn
### Installation
1. Navigate to the website directory:
```bash
cd website
```
2. Install dependencies:
```bash
npm install
```
### Development
Run the development server:
```bash
npm run dev
```
The application will be available at `http://localhost:5173` (or the port shown in the terminal).
### Building for Production
Create an optimized production build:
```bash
npm run build
```
The built files will be in the `website/dist/` directory.
### Preview Production Build
Preview the production build locally:
```bash
npm run preview
```
## Docker Deployment
### Development with Docker Compose
Run the application in development mode with hot reload:
```bash
docker-compose -f docker-compose-dev.yml up
```
The application will be available at `http://localhost:5173` with hot module replacement enabled.
To run in detached mode (background):
```bash
docker-compose -f docker-compose-dev.yml up -d
```
To stop the development container:
```bash
docker-compose -f docker-compose-dev.yml down
```
### Production with Docker Compose
Build and run the production image:
```bash
docker-compose up --build -d
```
The application will be available at `http://localhost:80`
To run without rebuilding (if image already exists):
```bash
docker-compose up -d
```
To stop the production container:
```bash
docker-compose down
```
To view logs:
```bash
docker-compose logs -f
```
### Using Pre-built Docker Images
The project includes GitHub Actions workflows that automatically build and publish Docker images to GitHub Container Registry (ghcr.io) on releases. You can pull and run the latest release image:
```bash
docker pull ghcr.io/<your-username>/<your-repo-name>:V0.0.1-BETA
docker run -d -p 80:80 ghcr.io/<your-username>/<your-repo-name>:V0.0.1-BETA
```
## Configuration Data
The application uses JSON data files located in `website/src/data/`:
- `motors.json` - Available motor options
- `powerSupplies.json` - Power supply options
- `colors.json` - Available color options
- `options.json` - Main configuration options
- `components/` - Detailed component data:
- `actuator.json` - Actuator components
- `mounting.json` - Mounting options
- `remote.json` - Remote control components
- `stand.json` - Stand components
- `toyMounts.json` - Toy mount options
## Project Purpose
The OSSM Configurator serves as a comprehensive tool for users building their own Open Source Sex Machine. It simplifies the configuration process by:
- **Guiding Selection**: Step-by-step wizard prevents missing critical components
- **Ensuring Compatibility**: Validates component combinations
- **Providing Transparency**: Shows costs, filament requirements, and time estimates
- **Generating Documentation**: Creates exportable BOM for ordering parts and printing
This tool is essential for both beginners and experienced builders who want to ensure they have all necessary components and understand the full scope of their build before starting.
## Contributing
When adding new components or options:
1. Update the appropriate JSON data files in `website/src/data/`
2. Add corresponding images to `website/public/images/`
3. Test the configuration flow to ensure compatibility
4. Update component pricing and filament estimates as needed
## License
This project is part of the Open Source Sex Machine (OSSM) project. Please refer to the OSSM project license for usage terms.
--- ---
## Vendor System ## 🌟 Key Features
The OSSM Configurator includes a robust vendoring and monitoring system for external asset files (STL files, etc.) referenced in component JSON files. This system ensures reproducible builds by pinning external files to specific commit SHAs and automatically detecting when upstream changes occur. - **Intuitive Wizard Flow**: Step-by-step guidance from motor selection to final export.
- **Dynamic 3D Visualization (Coming Soon)**: Preview your build with real-time color updates.
- **Smart Compatibility**: Ensures your PSU matches your Motor and your Mount fits your Actuator.
- **Rich BOM Export**: Download a complete ZIP package containing:
- 📄 **README.md** overview of your build.
- 📊 **Excel BOM** for hardware ordering.
- 🖨️ **Print List** with filament estimates.
- 📁 **Organized STL Files** categorized by component and color.
- **Vendor System**: Integrated tracking for external CAD files to ensure reproducible builds.
- **Dark Mode Support**: A premium aesthetic designed for builders, day or night.
### Overview ---
The vendor system: ## 📸 Guided Walkthrough
- **Vendors** external files from GitHub repositories into the `vendor/` directory
- **Pins** files to specific commit SHAs for reproducible builds
- **Monitors** upstream repositories for changes
- **Automatically updates** vendored files when upstream changes are detected
- **Preserves backward compatibility** by keeping original `url` fields in component JSON files
### Project Structure (Vendor System) ### 1. Component Selection
The wizard starts with core hardware. Select your motor and power supply with real-time feedback on compatibility and cost.
![Motor Selection](./Screen%20Shots/Motor-Selected.png)
*Figure 1: Motor selection with clear technical specs and visual feedback.*
![PSU Selection](./Screen%20Shots/PSU%20selector.png)
*Figure 2: Choosing a compatible power supply (24V or USB-C PD).*
### 2. Aesthetic Customization
Choose your Primary and Accent colors. These selections automatically update the filament estimates in your final BOM.
![Color Selection](./Screen%20Shots/Color%20Selector.png)
*Figure 3: Interactive color picker for 3D printed components.*
### 3. Detailed Options
Configure every aspect of your machine, from the stand type to specialized toy mounts.
![Options Selection](./Screen%20Shots/Options%20List.png)
*Figure 4: Browsing the extensive list of compatible add-ons and variations.*
### 4. Final Summary & Export
Review your entire build, total cost, and total filament weight before exporting your build package.
![BOM Summary](./Screen%20Shots/Summary.png)
*Figure 5: The comprehensive BOM summary with automated ZIP generation.*
---
## 🚀 Getting Started
### Quick Start (Docker)
The easiest way to run the configurator locally:
```bash
docker-compose -f docker-compose-dev.yml up -d
``` ```
OSSM-Configurator/ Access the app at `http://localhost:5173`.
├── manifest/
│ └── vendor_manifest.json # Canonical list of vendored files ### Local Development
├── vendor/ # Vendored file copies 1. **Clone the repo**
│ └── owner-repo/ # Organized by repository 2. **Setup Website**:
│ └── path/to/file.stl ```bash
├── scripts/ cd website
├── generate_manifest_from_site.py # Generate manifest from component JSONs npm install
├── vendor_update.py # Download and pin files npm run dev
│ └── check_updates.py # Monitor upstream changes
├── api/
│ └── github_webhook/
│ └── index.py # Webhook handler for push events
├── tests/
│ ├── test_vendor_update.py # Tests for vendor_update.py
│ └── test_check_updates.py # Tests for check_updates.py
└── .github/
└── workflows/
└── check-vendor.yml # Automated monitoring workflow
``` ```
3. **Setup Logic** (Optional, for vendor updates):
### Manifest Schema
The `manifest/vendor_manifest.json` file contains metadata for each vendored file:
```json
{
"id": "unique-id-or-slug",
"source_repo": "owner/repo",
"source_path": "path/in/repo/file.stl",
"source_ref": "main",
"pinned_sha": "commit-sha-currently-pinned",
"pinned_raw_url": "https://raw.githubusercontent.com/owner/repo/<sha>/path/to/file.stl",
"local_path": "vendor/owner-repo/path/to/file.stl",
"checksum_sha256": "sha256-hex-digest",
"last_checked": "2024-01-05T12:00:00Z",
"upstream_latest_sha": "latest-commit-sha-observed",
"status": "up-to-date | out-of-date | unknown",
"license": "SPDX-identifier-or-URL",
"orig_site_json": "website/src/data/components/actuator.json",
"orig_item_id": "ossm-actuator-body-bottom"
}
```
### Integration with Component JSON Files
The vendor system adds a `vendor` object to each `printedParts` entry in component JSON files, while preserving the original `url` field for backward compatibility:
**Before:**
```json
{
"id": "ossm-actuator-body-bottom",
"name": "Actuator Bottom",
"filePath": "OSSM - Actuator Body Bottom.stl",
"url": "https://github.com/KinkyMakers/OSSM-hardware/.../file.stl?raw=true"
}
```
**After (with vendor metadata):**
```json
{
"id": "ossm-actuator-body-bottom",
"name": "Actuator Bottom",
"filePath": "OSSM - Actuator Body Bottom.stl",
"url": "https://github.com/KinkyMakers/OSSM-hardware/.../file.stl?raw=true",
"vendor": {
"manifest_id": "ossm-actuator-body-bottom",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/file.stl",
"pinned_sha": "abc123...",
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/abc123/...",
"checksum_sha256": "deadbeef...",
"last_checked": "2024-01-05T12:00:00Z",
"status": "up-to-date"
}
}
```
The website continues to use `printedParts.url` by default. Site code can optionally prefer `vendor.*` fields when available for offline hosting or improved reliability.
### Prerequisites
- Python 3.11 or higher
- GitHub API token (for authenticated requests and higher rate limits)
### Setup
1. **Install Python dependencies:**
```bash ```bash
cd scripts cd scripts
pip install -r requirements.txt pip install -r requirements.txt
python vendor_update.py
``` ```
2. **Set up GitHub API token:** ---
```bash
export GITHUB_API_TOKEN=your_token_here
# Or use GITHUB_TOKEN as fallback
```
To create a GitHub token: ## 🗺️ Project Roadmap
- Go to GitHub Settings → Developer settings → Personal access tokens → Tokens (classic)
- Generate a new token with `public_repo` scope (or `repo` for private repos)
- Required permissions: Read access to repositories
### Local Usage We are constantly improving the OSSM Configurator. Check out our **[detailed roadmap](./roadmap/ROADMAP.md)** for upcoming features, including:
- 🛠️ Interactive 3D Render/Preview
- 🌍 Multi-language & Multi-currency support
- 📦 Integrated assembly guides
#### 1. Generate Manifest from Site Data ---
Scan component JSON files and create/update the vendor manifest: ## 🤝 Contributing
```bash Contributions are welcome! Whether you are adding a new remote, a toy mount, or a hardware mod, please refer to our detailed guide:
python scripts/generate_manifest_from_site.py \
--site-dir website/src/data/components \
--manifest manifest/vendor_manifest.json
```
This script: 👉 **[Read the CONTRIBUTING.md](./CONTRIBUTING.md)**
- Scans all JSON files in `website/src/data/components/`
- Extracts `printedParts` entries with GitHub URLs
- Creates manifest entries for each GitHub-hosted file
- Supports nested structures (e.g., `systems.printedParts`, `bodyParts`, `knobs`)
#### 2. Download and Pin Files ### Quick Summary:
1. Update JSON data in `website/src/data/`.
2. Add relevant images to `website/public/images/`.
3. Run `python scripts/vendor_update.py` for new files.
4. Submit a PR!
Download files from GitHub and pin them to commit SHAs: ---
```bash ## 📜 License
# Update all entries
python scripts/vendor_update.py \
--manifest manifest/vendor_manifest.json
# Update a specific entry This project is part of the **Open Source Sex Machine (OSSM)** project. Please refer to the main OSSM project for full license details.
python scripts/vendor_update.py \
--manifest manifest/vendor_manifest.json \
--entry ossm-actuator-body-bottom
# Dry run (see what would be done) ---
python scripts/vendor_update.py \ *Built with ❤️ by the OSSM Community.*
--manifest manifest/vendor_manifest.json \
--dry-run
# Update and sync vendor metadata to site JSON files
python scripts/vendor_update.py \
--manifest manifest/vendor_manifest.json \
--sync-site
```
This script:
- Resolves commit SHAs for the specified ref (branch/tag)
- Downloads files from pinned URLs
- Computes SHA256 checksums
- Updates manifest with metadata
- Optionally syncs vendor metadata back to component JSON files
#### 3. Check for Upstream Updates
Monitor upstream repositories for changes:
```bash
python scripts/check_updates.py \
--manifest manifest/vendor_manifest.json \
--output report.json
```
This script:
- Queries GitHub API for latest commit SHAs
- Compares with pinned SHAs
- Generates a report of up-to-date and out-of-date entries
- Exits with non-zero code if any entries are out-of-date (useful for CI)
### GitHub Action Workflow
The `.github/workflows/check-vendor.yml` workflow automatically:
1. **Runs daily** (2 AM UTC) or can be triggered manually
2. **Generates manifest** from site data
3. **Checks for updates** in upstream repositories
4. **Creates a branch and PR** if updates are available
5. **Updates vendored files** and syncs metadata to component JSON files
#### Manual Trigger
To manually trigger the workflow:
1. Go to Actions tab in GitHub
2. Select "Check Vendor Updates" workflow
3. Click "Run workflow"
#### Required Secrets
Configure these secrets in GitHub repository settings:
- `GITHUB_API_TOKEN`: GitHub personal access token with `public_repo` scope
The workflow uses `GITHUB_TOKEN` for creating PRs (automatically provided by GitHub Actions).
### Webhook Configuration
The `api/github_webhook/index.py` provides a Flask-based webhook handler that can receive push events from upstream repositories.
#### Setup
1. **Deploy the webhook** (e.g., to a serverless platform, VPS, or local server)
2. **Configure webhook secret:**
```bash
export WEBHOOK_SECRET=your_webhook_secret_here
```
3. **Add webhook to upstream repository:**
- Go to repository Settings → Webhooks → Add webhook
- Payload URL: `https://your-domain.com/webhook`
- Content type: `application/json`
- Secret: (same as `WEBHOOK_SECRET`)
- Events: Select "Just the push event"
- Active: ✓
#### Local Testing
Run the webhook locally for testing:
```bash
export WEBHOOK_SECRET=test_secret
export FLASK_DEBUG=true
python api/github_webhook/index.py
```
The webhook will:
- Verify GitHub signature using `X-Hub-Signature-256`
- Process push events
- Identify affected manifest entries
- Trigger update checks for changed files
### Running Tests
Run the test suite:
```bash
# Install test dependencies
pip install -r scripts/requirements.txt
# Run all tests
pytest tests/
# Run specific test file
pytest tests/test_vendor_update.py -v
pytest tests/test_check_updates.py -v
```
Tests use `pytest` with `responses` for mocking HTTP requests to GitHub API.
### URL Format Support
The vendor system supports multiple GitHub URL formats:
- `https://github.com/owner/repo/blob/main/path/to/file.stl?raw=true`
- `https://github.com/owner/repo/raw/main/path/to/file.stl`
- `https://raw.githubusercontent.com/owner/repo/main/path/to/file.stl`
The system automatically:
- Extracts owner, repo, path, and ref from URLs
- Defaults to `main` branch if ref is missing
- Handles URL-encoded paths
### Reverting Vendor Changes
To revert vendor metadata from component JSON files:
1. **Remove vendor fields** from component JSON files manually, or
2. **Restore from git history:**
```bash
git checkout HEAD -- website/src/data/components/
```
The original `url` fields are never removed, so the site continues to work even if vendor metadata is removed.
### Inspecting Vendor Status
View the current vendor status:
```bash
# Check manifest
cat manifest/vendor_manifest.json | jq '.[] | {id, status, pinned_sha, upstream_latest_sha}'
# Check for updates
python scripts/check_updates.py --manifest manifest/vendor_manifest.json
```
### Troubleshooting
**Issue: Rate limit errors from GitHub API**
- Solution: Set `GITHUB_API_TOKEN` environment variable with a personal access token
**Issue: File not found at ref**
- Solution: The script will try the default branch if the specified ref doesn't exist
**Issue: Webhook signature verification fails**
- Solution: Ensure `WEBHOOK_SECRET` matches the secret configured in GitHub webhook settings
**Issue: Manifest entries not found**
- Solution: Run `generate_manifest_from_site.py` to create/update manifest entries
### Best Practices
1. **Always pin to commit SHAs** (not branch names) for reproducible builds
2. **Run `check_updates.py` regularly** to detect upstream changes
3. **Review PRs** created by the GitHub Action before merging
4. **Test site builds** after vendor updates to ensure compatibility
5. **Keep manifest in version control** for tracking vendored files
### Example Workflow
Complete workflow for adding a new component with external files:
1. **Add component to site JSON:**
```json
{
"id": "new-part",
"url": "https://github.com/owner/repo/blob/main/file.stl?raw=true"
}
```
2. **Generate manifest:**
```bash
python scripts/generate_manifest_from_site.py
```
3. **Vendor the file:**
```bash
python scripts/vendor_update.py --entry new-part --sync-site
```
4. **Verify:**
```bash
python scripts/check_updates.py
```
The GitHub Action will automatically monitor for future updates.

View File

@@ -4,495 +4,639 @@
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl", "source_path": "Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl",
"orig_site_json": "website/src/data/components/stand.json",
"orig_item_id": "handle-spacer",
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab", "pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl", "pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl",
"checksum_sha256": "55ede7dff60a31d68159b352b5f2c63792b7a0dbe9d543a43681c3e52d229115", "checksum_sha256": "55ede7dff60a31d68159b352b5f2c63792b7a0dbe9d543a43681c3e52d229115",
"last_checked": "2026-01-07T01:20:58.324330+00:00", "last_checked": "2026-01-07T01:20:58.324330+00:00",
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"status": "up-to-date", "status": "up-to-date",
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE", "license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
"orig_site_json": "website/src/data/components/stand.json", "upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
"orig_item_id": "handle-spacer"
}, },
{ {
"id": "ossm-24mm-clamping-thread-belt-clamp", "id": "ossm-24mm-clamping-thread-belt-clamp",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Actuator/OSSM - 24mm Clamping Thread - Belt Clamp.stl", "source_path": "Printed Parts/Actuator/OSSM - 24mm Clamping Thread - Belt Clamp.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - Belt Clamp.stl",
"orig_site_json": "website/src/data/components/actuator.json",
"orig_item_id": "ossm-24mm-clamping-thread-belt-clamp",
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab", "pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - Belt Clamp.stl", "pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - Belt Clamp.stl",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - Belt Clamp.stl",
"checksum_sha256": "457a71bc09cb53f12026fd829bec8fa5b04fdead0788822935780f42c90b9a7a", "checksum_sha256": "457a71bc09cb53f12026fd829bec8fa5b04fdead0788822935780f42c90b9a7a",
"last_checked": "2026-01-07T01:20:58.945151+00:00", "last_checked": "2026-01-07T01:20:58.945151+00:00",
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"status": "up-to-date", "status": "up-to-date",
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE", "license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
"orig_site_json": "website/src/data/components/actuator.json", "upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
"orig_item_id": "ossm-24mm-clamping-thread-belt-clamp"
}, },
{ {
"id": "ossm-24mm-clamping-thread-end-effector", "id": "ossm-24mm-clamping-thread-end-effector",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Actuator/OSSM - 24mm Clamping Thread - End Effector.stl", "source_path": "Printed Parts/Actuator/OSSM - 24mm Clamping Thread - End Effector.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - End Effector.stl",
"orig_site_json": "website/src/data/components/actuator.json",
"orig_item_id": "ossm-24mm-clamping-thread-end-effector",
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab", "pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - End Effector.stl", "pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - End Effector.stl",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - End Effector.stl",
"checksum_sha256": "4860947b201e2e773b295d33bba09423ae40b4adeef3605d62687f2d40277de1", "checksum_sha256": "4860947b201e2e773b295d33bba09423ae40b4adeef3605d62687f2d40277de1",
"last_checked": "2026-01-07T01:20:59.854476+00:00", "last_checked": "2026-01-07T01:20:59.854476+00:00",
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"status": "up-to-date", "status": "up-to-date",
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE", "license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
"orig_site_json": "website/src/data/components/actuator.json", "upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
"orig_item_id": "ossm-24mm-clamping-thread-end-effector"
}, },
{ {
"id": "ossm-24mm-nut-5-sided", "id": "ossm-24mm-nut-5-sided",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Actuator/OSSM - 24mm Nut - 5 Sided.stl", "source_path": "Printed Parts/Actuator/OSSM - 24mm Nut - 5 Sided.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - 24mm Nut - 5 Sided.stl",
"orig_site_json": "website/src/data/components/actuator.json",
"orig_item_id": "ossm-24mm-nut-5-sided",
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab", "pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Nut - 5 Sided.stl", "pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Nut - 5 Sided.stl",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - 24mm Nut - 5 Sided.stl",
"checksum_sha256": "38630c70b2fb929bba9a705dabf5bbd7b49ec882963e042b7108dc74284dd6ff", "checksum_sha256": "38630c70b2fb929bba9a705dabf5bbd7b49ec882963e042b7108dc74284dd6ff",
"last_checked": "2026-01-07T01:21:00.555525+00:00", "last_checked": "2026-01-07T01:21:00.555525+00:00",
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"status": "up-to-date", "status": "up-to-date",
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE", "license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
"orig_site_json": "website/src/data/components/actuator.json", "upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
"orig_item_id": "ossm-24mm-nut-5-sided"
}, },
{ {
"id": "ossm-3030-cap", "id": "ossm-3030-cap",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Extrusion Cap.stl", "source_path": "Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Extrusion Cap.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Extrusion Cap.stl",
"orig_site_json": "website/src/data/components/stand.json",
"orig_item_id": "ossm-3030-cap",
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab", "pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Extrusion Cap.stl", "pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Extrusion Cap.stl",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Extrusion Cap.stl",
"checksum_sha256": "56fa9bb318cdeadc6d1698a1e6cef9371e58b0bc9c7729985bf639d8da2f25da", "checksum_sha256": "56fa9bb318cdeadc6d1698a1e6cef9371e58b0bc9c7729985bf639d8da2f25da",
"last_checked": "2026-01-07T01:21:01.205246+00:00", "last_checked": "2026-01-07T01:21:01.205246+00:00",
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"status": "up-to-date", "status": "up-to-date",
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE", "license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
"orig_site_json": "website/src/data/components/stand.json", "upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
"orig_item_id": "ossm-3030-cap"
}, },
{ {
"id": "ossm-actuator-body-bottom", "id": "ossm-actuator-body-bottom",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Actuator/OSSM - Actuator - Body - Bottom.stl", "source_path": "Printed Parts/Actuator/OSSM - Actuator - Body - Bottom.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - Actuator - Body - Bottom.stl",
"orig_site_json": "website/src/data/components/actuator.json",
"orig_item_id": "ossm-actuator-body-bottom",
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab", "pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Bottom.stl", "pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Bottom.stl",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - Actuator - Body - Bottom.stl",
"checksum_sha256": "e7abdb99a7e9b9e7408a7b04a7dd50e42cc74510ea2969016a45a2a1387dcde3", "checksum_sha256": "e7abdb99a7e9b9e7408a7b04a7dd50e42cc74510ea2969016a45a2a1387dcde3",
"last_checked": "2026-01-07T01:21:02.027595+00:00", "last_checked": "2026-01-07T01:21:02.027595+00:00",
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"status": "up-to-date", "status": "up-to-date",
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE", "license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
"orig_site_json": "website/src/data/components/actuator.json", "upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
"orig_item_id": "ossm-actuator-body-bottom"
}, },
{ {
"id": "ossm-actuator-body-cover", "id": "ossm-actuator-body-cover",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Actuator/OSSM - Actuator - Body - Cover.stl", "source_path": "Printed Parts/Actuator/OSSM - Actuator - Body - Cover.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - Actuator - Body - Cover.stl",
"orig_site_json": "website/src/data/components/actuator.json",
"orig_item_id": "ossm-actuator-body-cover",
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab", "pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Cover.stl", "pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Cover.stl",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - Actuator - Body - Cover.stl",
"checksum_sha256": "bbabc742d2f1753d1b4e21e42c197aec31a4a083b5c634e6e825cec69d4e3258", "checksum_sha256": "bbabc742d2f1753d1b4e21e42c197aec31a4a083b5c634e6e825cec69d4e3258",
"last_checked": "2026-01-07T01:21:02.767604+00:00", "last_checked": "2026-01-07T01:21:02.767604+00:00",
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"status": "up-to-date", "status": "up-to-date",
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE", "license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
"orig_site_json": "website/src/data/components/actuator.json", "upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
"orig_item_id": "ossm-actuator-body-cover"
}, },
{ {
"id": "ossm-actuator-body-middle", "id": "ossm-actuator-body-middle",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Actuator/OSSM - Actuator - Body - Middle.stl", "source_path": "Printed Parts/Actuator/OSSM - Actuator - Body - Middle.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - Actuator - Body - Middle.stl",
"orig_site_json": "website/src/data/components/actuator.json",
"orig_item_id": "ossm-actuator-body-middle",
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab", "pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Middle.stl", "pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Middle.stl",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - Actuator - Body - Middle.stl",
"checksum_sha256": "ce6fb769378636c287af788ce42bdab1f2185dcffba929a0c72598742793b48a", "checksum_sha256": "ce6fb769378636c287af788ce42bdab1f2185dcffba929a0c72598742793b48a",
"last_checked": "2026-01-07T01:21:03.531342+00:00", "last_checked": "2026-01-07T01:21:03.531342+00:00",
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"status": "up-to-date", "status": "up-to-date",
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE", "license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
"orig_site_json": "website/src/data/components/actuator.json", "upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
"orig_item_id": "ossm-actuator-body-middle"
}, },
{ {
"id": "ossm-actuator-body-middle-pivot", "id": "ossm-actuator-body-middle-pivot",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Actuator/Non-standard/OSSM - Actuator - Body - Middle Pivot.stl", "source_path": "Printed Parts/Actuator/Non-standard/OSSM - Actuator - Body - Middle Pivot.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/Non-standard/OSSM - Actuator - Body - Middle Pivot.stl",
"orig_site_json": "website/src/data/components/mounting.json",
"orig_item_id": "ossm-actuator-body-middle-pivot",
"pinned_sha": "ad39a03b628b8e38549b99036c8dfd4131948545", "pinned_sha": "ad39a03b628b8e38549b99036c8dfd4131948545",
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/ad39a03b628b8e38549b99036c8dfd4131948545/Printed Parts/Actuator/Non-standard/OSSM - Actuator - Body - Middle Pivot.stl", "pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/ad39a03b628b8e38549b99036c8dfd4131948545/Printed Parts/Actuator/Non-standard/OSSM - Actuator - Body - Middle Pivot.stl",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/Non-standard/OSSM - Actuator - Body - Middle Pivot.stl",
"checksum_sha256": "f6403a3c53e0d8c8e63d48bf853ab17c9f283421b1665b5503dbb04d59d0f52d", "checksum_sha256": "f6403a3c53e0d8c8e63d48bf853ab17c9f283421b1665b5503dbb04d59d0f52d",
"last_checked": "2026-01-07T01:21:04.528132+00:00", "last_checked": "2026-01-07T01:21:04.528132+00:00",
"upstream_latest_sha": "ad39a03b628b8e38549b99036c8dfd4131948545",
"status": "up-to-date", "status": "up-to-date",
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/ad39a03b628b8e38549b99036c8dfd4131948545/LICENCE", "license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/ad39a03b628b8e38549b99036c8dfd4131948545/LICENCE",
"orig_site_json": "website/src/data/components/mounting.json", "upstream_latest_sha": "ad39a03b628b8e38549b99036c8dfd4131948545"
"orig_item_id": "ossm-actuator-body-middle-pivot"
}, },
{ {
"id": "ossm-belt-tensioner", "id": "ossm-belt-tensioner",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Actuator/OSSM - Belt Tensioner.stl", "source_path": "Printed Parts/Actuator/OSSM - Belt Tensioner.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - Belt Tensioner.stl",
"orig_site_json": "website/src/data/components/actuator.json",
"orig_item_id": "ossm-belt-tensioner",
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab", "pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Belt Tensioner.stl", "pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Belt Tensioner.stl",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - Belt Tensioner.stl",
"checksum_sha256": "31c74250c237763b0013ff42cc714ce14c293382a726de363f1686a7559f525f", "checksum_sha256": "31c74250c237763b0013ff42cc714ce14c293382a726de363f1686a7559f525f",
"last_checked": "2026-01-07T01:21:05.499523+00:00", "last_checked": "2026-01-07T01:21:05.499523+00:00",
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"status": "up-to-date", "status": "up-to-date",
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE", "license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
"orig_site_json": "website/src/data/components/actuator.json", "upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
"orig_item_id": "ossm-belt-tensioner"
}, },
{ {
"id": "ossm-handle-spacer", "id": "ossm-handle-spacer",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl", "source_path": "Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl",
"orig_site_json": "website/src/data/components/mounting.json",
"orig_item_id": "ossm-handle-spacer",
"pinned_sha": null, "pinned_sha": null,
"pinned_raw_url": null, "pinned_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl",
"checksum_sha256": null, "checksum_sha256": null,
"last_checked": null, "last_checked": null,
"upstream_latest_sha": null,
"status": "error", "status": "error",
"license": null, "license": null,
"orig_site_json": "website/src/data/components/mounting.json", "upstream_latest_sha": null
"orig_item_id": "ossm-handle-spacer"
}, },
{ {
"id": "ossm-pcb-3030-mount", "id": "ossm-pcb-3030-mount",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/PCB/OSSM - PCB - 3030 Mount.stl", "source_path": "Printed Parts/PCB/OSSM - PCB - 3030 Mount.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/PCB/OSSM - PCB - 3030 Mount.stl",
"orig_site_json": "website/src/data/components/pcb.json",
"orig_item_id": "ossm-pcb-3030-mount",
"pinned_sha": null, "pinned_sha": null,
"pinned_raw_url": null, "pinned_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/PCB/OSSM - PCB - 3030 Mount.stl",
"checksum_sha256": null, "checksum_sha256": null,
"last_checked": null, "last_checked": null,
"upstream_latest_sha": null,
"status": "error", "status": "error",
"license": null, "license": null,
"orig_site_json": "website/src/data/components/pcb.json", "upstream_latest_sha": null
"orig_item_id": "ossm-pcb-3030-mount"
}, },
{ {
"id": "ossm-pcb-3030-mount-cover", "id": "ossm-pcb-3030-mount-cover",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/PCB/OSSM - PCB - 3030 Mount Cover.stl", "source_path": "Printed Parts/PCB/OSSM - PCB - 3030 Mount Cover.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/PCB/OSSM - PCB - 3030 Mount Cover.stl",
"orig_site_json": "website/src/data/components/pcb.json",
"orig_item_id": "ossm-pcb-3030-mount-cover",
"pinned_sha": null, "pinned_sha": null,
"pinned_raw_url": null, "pinned_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/PCB/OSSM - PCB - 3030 Mount Cover.stl",
"checksum_sha256": null, "checksum_sha256": null,
"last_checked": null, "last_checked": null,
"upstream_latest_sha": null,
"status": "error", "status": "error",
"license": null, "license": null,
"orig_site_json": "website/src/data/components/pcb.json", "upstream_latest_sha": null
"orig_item_id": "ossm-pcb-3030-mount-cover"
}, },
{ {
"id": "ossm-pcb-aio-cover-mount", "id": "ossm-pcb-aio-cover-mount",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/PCB/OSSM - PCB - AIO Cover Mount.stl", "source_path": "Printed Parts/PCB/OSSM - PCB - AIO Cover Mount.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/PCB/OSSM - PCB - AIO Cover Mount.stl",
"orig_site_json": "website/src/data/components/pcb.json",
"orig_item_id": "ossm-pcb-aio-cover-mount",
"pinned_sha": null, "pinned_sha": null,
"pinned_raw_url": null, "pinned_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/PCB/OSSM - PCB - AIO Cover Mount.stl",
"checksum_sha256": null, "checksum_sha256": null,
"last_checked": null, "last_checked": null,
"upstream_latest_sha": null,
"status": "error", "status": "error",
"license": null, "license": null,
"orig_site_json": "website/src/data/components/pcb.json", "upstream_latest_sha": null
"orig_item_id": "ossm-pcb-aio-cover-mount"
}, },
{ {
"id": "ossm-pitclamp-mini-42AIM30", "id": "ossm-pitclamp-mini-42AIM30",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Mounting/Non-standard/OSSM - Mounting Ring - PitClamp Mini - 42AIM V1.1.stl", "source_path": "Printed Parts/Mounting/Non-standard/OSSM - Mounting Ring - PitClamp Mini - 42AIM V1.1.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/Non-standard/OSSM - Mounting Ring - PitClamp Mini - 42AIM V1.1.stl",
"orig_site_json": "website/src/data/components/mounting.json",
"orig_item_id": "ossm-pitclamp-mini-42AIM30",
"pinned_sha": null, "pinned_sha": null,
"pinned_raw_url": null, "pinned_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/Non-standard/OSSM - Mounting Ring - PitClamp Mini - 42AIM V1.1.stl",
"checksum_sha256": null, "checksum_sha256": null,
"last_checked": null, "last_checked": null,
"upstream_latest_sha": null,
"status": "error", "status": "error",
"license": null, "license": null,
"orig_site_json": "website/src/data/components/mounting.json", "upstream_latest_sha": null
"orig_item_id": "ossm-pitclamp-mini-42AIM30"
}, },
{ {
"id": "ossm-pitclamp-mini-57AIM30", "id": "ossm-pitclamp-mini-57AIM30",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Mounting/OSSM - Mounting Ring - PitClamp Mini - 57AIM V1.1.stl", "source_path": "Printed Parts/Mounting/OSSM - Mounting Ring - PitClamp Mini - 57AIM V1.1.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Mounting Ring - PitClamp Mini - 57AIM V1.1.stl",
"orig_site_json": "website/src/data/components/mounting.json",
"orig_item_id": "ossm-pitclamp-mini-57AIM30",
"pinned_sha": null, "pinned_sha": null,
"pinned_raw_url": null, "pinned_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Mounting Ring - PitClamp Mini - 57AIM V1.1.stl",
"checksum_sha256": null, "checksum_sha256": null,
"last_checked": null, "last_checked": null,
"upstream_latest_sha": null,
"status": "error", "status": "error",
"license": null, "license": null,
"orig_site_json": "website/src/data/components/mounting.json", "upstream_latest_sha": null
"orig_item_id": "ossm-pitclamp-mini-57AIM30"
}, },
{ {
"id": "ossm-pitclamp-mini-dogbone-bolts ", "id": "ossm-pitclamp-mini-dogbone-bolts ",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Dogbone Bolts.stl", "source_path": "Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Dogbone Bolts.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Dogbone Bolts.stl",
"orig_site_json": "website/src/data/components/mounting.json",
"orig_item_id": "ossm-pitclamp-mini-dogbone-bolts ",
"pinned_sha": null, "pinned_sha": null,
"pinned_raw_url": null, "pinned_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Dogbone Bolts.stl",
"checksum_sha256": null, "checksum_sha256": null,
"last_checked": null, "last_checked": null,
"upstream_latest_sha": null,
"status": "error", "status": "error",
"license": null, "license": null,
"orig_site_json": "website/src/data/components/mounting.json", "upstream_latest_sha": null
"orig_item_id": "ossm-pitclamp-mini-dogbone-bolts "
}, },
{ {
"id": "ossm-pitclamp-mini-dogbone-nuts", "id": "ossm-pitclamp-mini-dogbone-nuts",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Dogbone Nuts.stl", "source_path": "Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Dogbone Nuts.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Dogbone Nuts.stl",
"orig_site_json": "website/src/data/components/mounting.json",
"orig_item_id": "ossm-pitclamp-mini-dogbone-nuts",
"pinned_sha": null, "pinned_sha": null,
"pinned_raw_url": null, "pinned_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Dogbone Nuts.stl",
"checksum_sha256": null, "checksum_sha256": null,
"last_checked": null, "last_checked": null,
"upstream_latest_sha": null,
"status": "error", "status": "error",
"license": null, "license": null,
"orig_site_json": "website/src/data/components/mounting.json", "upstream_latest_sha": null
"orig_item_id": "ossm-pitclamp-mini-dogbone-nuts"
}, },
{ {
"id": "ossm-pitclamp-mini-handle", "id": "ossm-pitclamp-mini-handle",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Handle.stl", "source_path": "Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Handle.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Handle.stl",
"orig_site_json": "website/src/data/components/mounting.json",
"orig_item_id": "ossm-pitclamp-mini-handle",
"pinned_sha": null, "pinned_sha": null,
"pinned_raw_url": null, "pinned_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Handle.stl",
"checksum_sha256": null, "checksum_sha256": null,
"last_checked": null, "last_checked": null,
"upstream_latest_sha": null,
"status": "error", "status": "error",
"license": null, "license": null,
"orig_site_json": "website/src/data/components/mounting.json", "upstream_latest_sha": null
"orig_item_id": "ossm-pitclamp-mini-handle"
}, },
{ {
"id": "ossm-pitclamp-mini-iHSV57", "id": "ossm-pitclamp-mini-iHSV57",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Mounting/Non-standard/OSSM - Mounting Ring - PitClamp Mini - iHSV57.stl", "source_path": "Printed Parts/Mounting/Non-standard/OSSM - Mounting Ring - PitClamp Mini - iHSV57.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/Non-standard/OSSM - Mounting Ring - PitClamp Mini - iHSV57.stl",
"orig_site_json": "website/src/data/components/mounting.json",
"orig_item_id": "ossm-pitclamp-mini-iHSV57",
"pinned_sha": null, "pinned_sha": null,
"pinned_raw_url": null, "pinned_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/Non-standard/OSSM - Mounting Ring - PitClamp Mini - iHSV57.stl",
"checksum_sha256": null, "checksum_sha256": null,
"last_checked": null, "last_checked": null,
"upstream_latest_sha": null,
"status": "error", "status": "error",
"license": null, "license": null,
"orig_site_json": "website/src/data/components/mounting.json", "upstream_latest_sha": null
"orig_item_id": "ossm-pitclamp-mini-iHSV57"
}, },
{ {
"id": "ossm-pitclamp-mini-lower", "id": "ossm-pitclamp-mini-lower",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Lower V1.1.stl", "source_path": "Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Lower V1.1.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Lower V1.1.stl",
"orig_site_json": "website/src/data/components/mounting.json",
"orig_item_id": "ossm-pitclamp-mini-lower",
"pinned_sha": null, "pinned_sha": null,
"pinned_raw_url": null, "pinned_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Lower V1.1.stl",
"checksum_sha256": null, "checksum_sha256": null,
"last_checked": null, "last_checked": null,
"upstream_latest_sha": null,
"status": "error", "status": "error",
"license": null, "license": null,
"orig_site_json": "website/src/data/components/mounting.json", "upstream_latest_sha": null
"orig_item_id": "ossm-pitclamp-mini-lower"
}, },
{ {
"id": "ossm-pitclamp-mini-upper", "id": "ossm-pitclamp-mini-upper",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Upper V1.1.stl", "source_path": "Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Upper V1.1.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Upper V1.1.stl",
"orig_site_json": "website/src/data/components/mounting.json",
"orig_item_id": "ossm-pitclamp-mini-upper",
"pinned_sha": null, "pinned_sha": null,
"pinned_raw_url": null, "pinned_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Upper V1.1.stl",
"checksum_sha256": null, "checksum_sha256": null,
"last_checked": null, "last_checked": null,
"upstream_latest_sha": null,
"status": "error", "status": "error",
"license": null, "license": null,
"orig_site_json": "website/src/data/components/mounting.json", "upstream_latest_sha": null
"orig_item_id": "ossm-pitclamp-mini-upper"
}, },
{ {
"id": "ossm-remote-body", "id": "ossm-remote-body",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Remote/OSSM - Remote - Body.stl", "source_path": "Printed Parts/Remote/OSSM - Remote - Body.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/OSSM - Remote - Body.stl",
"orig_site_json": "website/src/data/components/remote.json",
"orig_item_id": "ossm-remote-body",
"pinned_sha": null, "pinned_sha": null,
"pinned_raw_url": null, "pinned_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/OSSM - Remote - Body.stl",
"checksum_sha256": null, "checksum_sha256": null,
"last_checked": null, "last_checked": null,
"upstream_latest_sha": null,
"status": "error", "status": "error",
"license": null, "license": null,
"orig_site_json": "website/src/data/components/remote.json", "upstream_latest_sha": null
"orig_item_id": "ossm-remote-body"
}, },
{ {
"id": "ossm-remote-knob", "id": "ossm-remote-knob",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Remote/OSSM - Remote - Knob - Rounded.stl", "source_path": "Printed Parts/Remote/OSSM - Remote - Knob - Rounded.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/OSSM - Remote - Knob - Rounded.stl",
"orig_site_json": "website/src/data/components/remote.json",
"orig_item_id": "ossm-remote-knob",
"pinned_sha": null, "pinned_sha": null,
"pinned_raw_url": null, "pinned_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/OSSM - Remote - Knob - Rounded.stl",
"checksum_sha256": null, "checksum_sha256": null,
"last_checked": null, "last_checked": null,
"upstream_latest_sha": null,
"status": "error", "status": "error",
"license": null, "license": null,
"orig_site_json": "website/src/data/components/remote.json", "upstream_latest_sha": null
"orig_item_id": "ossm-remote-knob"
}, },
{ {
"id": "ossm-remote-knob-knurled", "id": "ossm-remote-knob-knurled",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Knurled.stl", "source_path": "Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Knurled.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Knurled.stl",
"orig_site_json": "website/src/data/components/remote.json",
"orig_item_id": "ossm-remote-knob-knurled",
"pinned_sha": null, "pinned_sha": null,
"pinned_raw_url": null, "pinned_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Knurled.stl",
"checksum_sha256": null, "checksum_sha256": null,
"last_checked": null, "last_checked": null,
"upstream_latest_sha": null,
"status": "error", "status": "error",
"license": null, "license": null,
"orig_site_json": "website/src/data/components/remote.json", "upstream_latest_sha": null
"orig_item_id": "ossm-remote-knob-knurled"
}, },
{ {
"id": "ossm-remote-knob-knurled-with-position-indicator", "id": "ossm-remote-knob-knurled-with-position-indicator",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Knurled With Position Indicator.stl", "source_path": "Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Knurled With Position Indicator.stl",
"source_ref": "main", "source_ref": "main",
"pinned_sha": null,
"pinned_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Knurled With Position Indicator.stl", "local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Knurled With Position Indicator.stl",
"checksum_sha256": null,
"last_checked": null,
"upstream_latest_sha": null,
"status": "error",
"license": null,
"orig_site_json": "website/src/data/components/remote.json", "orig_site_json": "website/src/data/components/remote.json",
"orig_item_id": "ossm-remote-knob-knurled-with-position-indicator" "orig_item_id": "ossm-remote-knob-knurled-with-position-indicator",
},
{
"id": "ossm-remote-knob-simple",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Simple.stl",
"source_ref": "main",
"pinned_sha": null, "pinned_sha": null,
"pinned_raw_url": null, "pinned_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Simple.stl",
"checksum_sha256": null, "checksum_sha256": null,
"last_checked": null, "last_checked": null,
"upstream_latest_sha": null,
"status": "error", "status": "error",
"license": null, "license": null,
"orig_site_json": "website/src/data/components/remote.json", "upstream_latest_sha": null
"orig_item_id": "ossm-remote-knob-simple"
}, },
{ {
"id": "ossm-remote-knob-simple-with-position-indicator", "id": "ossm-remote-knob-simple-with-position-indicator",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Simple With Position Indicator.stl", "source_path": "Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Simple With Position Indicator.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Simple With Position Indicator.stl",
"orig_site_json": "website/src/data/components/remote.json",
"orig_item_id": "ossm-remote-knob-simple-with-position-indicator",
"pinned_sha": null, "pinned_sha": null,
"pinned_raw_url": null, "pinned_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Simple With Position Indicator.stl",
"checksum_sha256": null, "checksum_sha256": null,
"last_checked": null, "last_checked": null,
"upstream_latest_sha": null,
"status": "error", "status": "error",
"license": null, "license": null,
"orig_site_json": "website/src/data/components/remote.json", "upstream_latest_sha": null
"orig_item_id": "ossm-remote-knob-simple-with-position-indicator"
}, },
{ {
"id": "ossm-remote-top-cover", "id": "ossm-remote-top-cover",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Remote/OSSM - Remote - Top Cover.stl", "source_path": "Printed Parts/Remote/OSSM - Remote - Top Cover.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/OSSM - Remote - Top Cover.stl",
"orig_site_json": "website/src/data/components/remote.json",
"orig_item_id": "ossm-remote-top-cover",
"pinned_sha": null, "pinned_sha": null,
"pinned_raw_url": null, "pinned_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/OSSM - Remote - Top Cover.stl",
"checksum_sha256": null, "checksum_sha256": null,
"last_checked": null, "last_checked": null,
"upstream_latest_sha": null,
"status": "error", "status": "error",
"license": null, "license": null,
"orig_site_json": "website/src/data/components/remote.json", "upstream_latest_sha": null
"orig_item_id": "ossm-remote-top-cover" },
{
"id": "ossm-toy-mount-double-double-24mm-threaded",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Toy Mounts/OSSM - Toy Mount Double Double 24mm Threaded.stl",
"source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Double Double 24mm Threaded.stl",
"orig_site_json": "website/src/data/components/toyMounts/ossm.json",
"orig_item_id": "ossm-toy-mount-double-double-24mm-threaded",
"pinned_sha": null,
"pinned_raw_url": null,
"checksum_sha256": null,
"last_checked": null,
"status": "pending",
"license": null,
"upstream_latest_sha": null
},
{
"id": "ossm-toy-mount-double-double-rail-mounted",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Toy Mounts/OSSM - Toy Mount Double Double Rail Mounted.stl",
"source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Double Double Rail Mounted.stl",
"orig_site_json": "website/src/data/components/toyMounts/ossm.json",
"orig_item_id": "ossm-toy-mount-double-double-rail-mounted",
"pinned_sha": null,
"pinned_raw_url": null,
"checksum_sha256": null,
"last_checked": null,
"status": "pending",
"license": null,
"upstream_latest_sha": null
},
{
"id": "ossm-toy-mount-flange-base-24mm-threaded",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Toy Mounts/OSSM - Toy Mount Flange Base 24mm Threaded.stl",
"source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Flange Base 24mm Threaded.stl",
"orig_site_json": "website/src/data/components/toyMounts/ossm.json",
"orig_item_id": "ossm-toy-mount-flange-base-24mm-threaded",
"pinned_sha": null,
"pinned_raw_url": null,
"checksum_sha256": null,
"last_checked": null,
"status": "pending",
"license": null,
"upstream_latest_sha": null
},
{
"id": "ossm-toy-mount-flange-base-dildo-ring-2.5in ",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Toy Mounts/OSSM - Toy Mount Flange Base Dildo Ring 2.5in.stl",
"source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Flange Base Dildo Ring 2.5in.stl",
"orig_site_json": "website/src/data/components/toyMounts/ossm.json",
"orig_item_id": "ossm-toy-mount-flange-base-dildo-ring-2.5in ",
"pinned_sha": null,
"pinned_raw_url": null,
"checksum_sha256": null,
"last_checked": null,
"status": "pending",
"license": null,
"upstream_latest_sha": null
},
{
"id": "ossm-toy-mount-flange-base-dildo-ring-2in",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Toy Mounts/OSSM - Toy Mount Flange Base Dildo Ring 2in.stl",
"source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Flange Base Dildo Ring 2in.stl",
"orig_site_json": "website/src/data/components/toyMounts/ossm.json",
"orig_item_id": "ossm-toy-mount-flange-base-dildo-ring-2in",
"pinned_sha": null,
"pinned_raw_url": null,
"checksum_sha256": null,
"last_checked": null,
"status": "pending",
"license": null,
"upstream_latest_sha": null
},
{
"id": "ossm-toy-mount-sucson-mount-base-plate-24mm-threaded",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Toy Mounts/OSSM - Toy Mount Sucson Mount Base Plate 24mm Threaded.stl",
"source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Sucson Mount Base Plate 24mm Threaded.stl",
"orig_site_json": "website/src/data/components/toyMounts/ossm.json",
"orig_item_id": "ossm-toy-mount-sucson-mount-base-plate-24mm-threaded",
"pinned_sha": null,
"pinned_raw_url": null,
"checksum_sha256": null,
"last_checked": null,
"status": "pending",
"license": null,
"upstream_latest_sha": null
},
{
"id": "ossm-toy-mount-sucson-mount-ring-insert-55mm",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Toy Mounts/OSSM - Toy Mount Sucson Mount Ring Insert 55mm.stl",
"source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Sucson Mount Ring Insert 55mm.stl",
"orig_site_json": "website/src/data/components/toyMounts/ossm.json",
"orig_item_id": "ossm-toy-mount-sucson-mount-ring-insert-55mm",
"pinned_sha": null,
"pinned_raw_url": null,
"checksum_sha256": null,
"last_checked": null,
"status": "pending",
"license": null,
"upstream_latest_sha": null
},
{
"id": "ossm-toy-mount-sucson-mount-threaded-ring",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Toy Mounts/OSSM - Toy Mount Sucson Mount Threaded Ring.stl",
"source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Sucson Mount Threaded Ring.stl",
"orig_site_json": "website/src/data/components/toyMounts/ossm.json",
"orig_item_id": "ossm-toy-mount-sucson-mount-threaded-ring",
"pinned_sha": null,
"pinned_raw_url": null,
"checksum_sha256": null,
"last_checked": null,
"status": "pending",
"license": null,
"upstream_latest_sha": null
},
{
"id": "ossm-toy-mount-tie-down-and-suction-plate-110mm",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Toy Mounts/OSSM - Toy Mount Tie Down and Suction Plate 110mm.stl",
"source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Tie Down and Suction Plate 110mm.stl",
"orig_site_json": "website/src/data/components/toyMounts/ossm.json",
"orig_item_id": "ossm-toy-mount-tie-down-and-suction-plate-110mm",
"pinned_sha": null,
"pinned_raw_url": null,
"checksum_sha256": null,
"last_checked": null,
"status": "pending",
"license": null,
"upstream_latest_sha": null
},
{
"id": "ossm-toy-mount-tie-down-and-suction-plate-135mm",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Toy Mounts/OSSM - Toy Mount Tie Down and Suction Plate 135mm.stl",
"source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Tie Down and Suction Plate 135mm.stl",
"orig_site_json": "website/src/data/components/toyMounts/ossm.json",
"orig_item_id": "ossm-toy-mount-tie-down-and-suction-plate-135mm",
"pinned_sha": null,
"pinned_raw_url": null,
"checksum_sha256": null,
"last_checked": null,
"status": "pending",
"license": null,
"upstream_latest_sha": null
}, },
{ {
"id": "pivot-plate", "id": "pivot-plate",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Pivot Plate Left.stl", "source_path": "Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Pivot Plate Left.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Pivot Plate Left.stl",
"orig_site_json": "website/src/data/components/stand.json",
"orig_item_id": "pivot-plate",
"pinned_sha": null, "pinned_sha": null,
"pinned_raw_url": null, "pinned_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Pivot Plate Left.stl",
"checksum_sha256": null, "checksum_sha256": null,
"last_checked": null, "last_checked": null,
"upstream_latest_sha": null,
"status": "error", "status": "error",
"license": null, "license": null,
"orig_site_json": "website/src/data/components/stand.json", "upstream_latest_sha": null
"orig_item_id": "pivot-plate"
}, },
{ {
"id": "pivot-plate-right", "id": "pivot-plate-right",
"source_repo": "KinkyMakers/OSSM-hardware", "source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Pivot Plate Right.stl", "source_path": "Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Pivot Plate Right.stl",
"source_ref": "main", "source_ref": "main",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Pivot Plate Right.stl",
"orig_site_json": "website/src/data/components/stand.json",
"orig_item_id": "pivot-plate-right",
"pinned_sha": null, "pinned_sha": null,
"pinned_raw_url": null, "pinned_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Pivot Plate Right.stl",
"checksum_sha256": null, "checksum_sha256": null,
"last_checked": null, "last_checked": null,
"upstream_latest_sha": null,
"status": "error", "status": "error",
"license": null, "license": null,
"orig_site_json": "website/src/data/components/stand.json", "upstream_latest_sha": null
"orig_item_id": "pivot-plate-right"
} }
] ]

75
roadmap/ROADMAP.md Normal file
View File

@@ -0,0 +1,75 @@
# OSSM Configurator Roadmap
This document outlines the planned features, improvements, and milestones for the OSSM Configurator project.
## 🚀 Phase 1: Core Configuration & Data Integrity (Current Focus)
The primary goal is to ensure the configurator accurately represents all available OSSM components and generates a perfect Bill of Materials.
* **Actuator Components**: Complete the mapping for all actuator body variations (Standard, AIM, iHSV, etc.).
* **Stand & Mounting**: Finalize the hardware requirements for all stand types and mounting solutions.
* **BOM Logic**: Refine the dependency logic (e.g., "replaces" field) to ensure no duplicate or missing parts.
* **Price Accuracy**: Implement precise pricing for all hardware components.
* **Import/Export**: Finalize the BOM export/import cycle for saving and resuming builds.
## 🎨 Phase 2: User Experience & Aesthetics
Moving beyond functionality to create a premium, intuitive configuration experience.
* [x] **Dark Mode**: High-quality dark mode support across all components.
* **Micro-animations**: Smooth transitions between wizard steps and hover effects on components.
* **Loading States**: Add skeleton loaders and progress indicators for the Zip export process.
* **Mobile Optimization**: Ensure the configurator is fully responsive for mobile builders.
## 🧊 Phase 3: Advanced Visualization (The Next Frontier)
Introducing 3D visualization to help users see their build before they print.
* **Interactive 3D Preview**: Implement a real-time 3D renderer using React Three Fiber.
* **Dynamic Customization**: Update the 3D model in real-time based on selected Primary and Accent colors.
* **Compatibility Highlighting**: Visually show how components fit together in the 3D space.
* **Exploded View**: Create an interactive exploded view component to help with assembly visualization.
## 🌐 Phase 4: Localization & Exporting
Making the tool accessible to the global OSSM community.
* **Multi-language Support**: Translate the interface into major languages (German, Spanish, French, etc.).
* **Multi-currency**: Dynamic currency conversion for the BOM summary.
* **Imperial/Metric Toggle**: Support both systems for hardware and measurements.
## 🛠️ Phase 5: Build Guides & Integration
Becoming the central hub for starting an OSSM build.
* **Integrated Assembly Guides**: Show assembly steps/READMEs directly within the configurator.
* **FAQ & Troubleshooting**: A built-in guide for common build issues.
* **Community Preset Gallery**: Allow users to share their configurations as presets for others to use.
---
## 📋 TODO List
### High Priority
- [ ] Finalize Actuator Components and mapping to BOM [In Progress]
- [ ] Finalize Stand Components and mapping to BOM
- [ ] Finalize PCB Components and mapping to BOM
- [ ] Finalize Toy Mounts Components and mapping to BOM
- [ ] Finalize Remote Control Components and mapping to BOM
- [ ] Finalize Mounting Components and mapping to BOM
- [ ] Finalize Other Components and mapping to BOM
- [ ] Finalize Colors and mapping to BOM
- [ ] Finalize Pricing and mapping to BOM
- [ ] Finalize BOM Export and mapping to BOM
- [ ] Finalize BOM Import and mapping to BOM
### Features & Infrastructure
- [ ] Finalize Storage and sharing of BOMs
- [ ] Add references to original hardware files and designs
- [ ] Add Readme/assembly instructions for each component
- [ ] Add FAQ and troubleshooting guide
- [ ] Add support for multiple languages
- [ ] Add support for multiple currencies
- [ ] Add 3D render of final product with all components and options selected and colored [Planned - Phase 3]
### Future Considerations
- [ ] Add support for multiple payment methods
- [ ] Add support for multiple shipping methods
- [ ] Add support for multiple shipping countries
- [ ] Add support for multiple shipping regions
- [ ] Add support for multiple shipping cities

Binary file not shown.

View File

@@ -1,327 +0,0 @@
#!/usr/bin/env python3
"""
Generate vendor manifest from site component JSON files.
Scans /src/data/components/*.json for printedParts entries with GitHub URLs
and creates or updates manifest/vendor_manifest.json.
"""
import argparse
import json
import os
import re
import sys
from pathlib import Path
from typing import Dict, List, Optional, Any
from urllib.parse import urlparse, parse_qs, unquote
def parse_github_url(url: str) -> Optional[Dict[str, str]]:
"""
Parse GitHub URL to extract owner, repo, path, and ref.
Supports:
- https://github.com/owner/repo/blob/<ref>/path/to/file
- https://github.com/owner/repo/raw/<ref>/path/to/file
- https://raw.githubusercontent.com/owner/repo/<ref>/path/to/file
"""
if not url or not isinstance(url, str):
return None
# Check if it's a GitHub URL
if 'github.com' not in url:
return None
# Handle raw.githubusercontent.com
if 'raw.githubusercontent.com' in url:
match = re.match(r'https://raw\.githubusercontent\.com/([^/]+)/([^/]+)/([^/]+)/(.+)', url)
if match:
owner, repo, ref, path = match.groups()
return {
'owner': owner,
'repo': repo,
'ref': ref,
'path': unquote(path).split('?')[0] # Remove query params
}
# Handle github.com URLs
parsed = urlparse(url)
path_parts = parsed.path.strip('/').split('/')
if len(path_parts) < 5:
return None
owner = path_parts[0]
repo = path_parts[1]
mode = path_parts[2] # 'blob' or 'raw'
ref = path_parts[3]
# Get file path (everything after ref)
file_path = '/'.join(path_parts[4:])
# Remove query params from path
file_path = unquote(file_path).split('?')[0]
# Handle ?raw=true in query params (sometimes used with blob URLs)
query_params = parse_qs(parsed.query)
if 'raw' in query_params or mode == 'raw':
return {
'owner': owner,
'repo': repo,
'ref': ref,
'path': file_path
}
return None
def find_printed_parts(data: Any, path: str = '') -> List[Dict[str, Any]]:
"""
Recursively find all printedParts entries in nested JSON structure.
Returns list of (part_dict, json_file_path, part_id) tuples.
"""
parts = []
if isinstance(data, dict):
# Check if this dict has a 'printedParts' key
if 'printedParts' in data:
for part in data['printedParts']:
if isinstance(part, dict) and 'id' in part:
parts.append({
'part': part,
'json_path': path,
'part_id': part.get('id')
})
# Also check for 'bodyParts', 'knobs', etc. that might contain parts
for key in ['bodyParts', 'knobs']:
if key in data and isinstance(data[key], list):
for part in data[key]:
if isinstance(part, dict) and 'id' in part:
parts.append({
'part': part,
'json_path': path,
'part_id': part.get('id')
})
# Recursively search nested structures
for key, value in data.items():
if isinstance(value, (dict, list)):
parts.extend(find_printed_parts(value, path))
elif isinstance(data, list):
for item in data:
parts.extend(find_printed_parts(item, path))
return parts
def generate_manifest_id(part_id: str, owner: str, repo: str, path: str) -> str:
"""Generate a manifest ID from part ID or create one from repo/path."""
if part_id:
return part_id
# Generate slug from owner-repo-path
slug = f"{owner}-{repo}-{path.replace('/', '-').replace(' ', '-')}"
# Remove special chars
slug = re.sub(r'[^a-zA-Z0-9_-]', '', slug)
return slug[:100] # Limit length
def generate_local_path(owner: str, repo: str, path: str) -> str:
"""Generate local vendor path from owner, repo, and file path."""
repo_dir = f"{owner}-{repo}"
return f"vendor/{repo_dir}/{path}"
def load_existing_manifest(manifest_path: Path) -> Dict[str, Dict]:
"""Load existing manifest or return empty dict."""
if manifest_path.exists():
try:
with open(manifest_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# Convert list to dict keyed by id
if isinstance(data, list):
return {entry['id']: entry for entry in data}
elif isinstance(data, dict) and 'entries' in data:
return {entry['id']: entry for entry in data['entries']}
elif isinstance(data, dict):
# Assume it's already keyed by id
return data
except (json.JSONDecodeError, KeyError) as e:
print(f"Warning: Could not parse existing manifest: {e}", file=sys.stderr)
return {}
def scan_component_files(site_dir: Path, repo_root: Path) -> List[Dict[str, Any]]:
"""Scan all component JSON files and extract printedParts with GitHub URLs."""
entries = []
if not site_dir.exists():
print(f"Error: Site directory does not exist: {site_dir}", file=sys.stderr)
return entries
for json_file in site_dir.glob('*.json'):
try:
with open(json_file, 'r', encoding='utf-8') as f:
data = json.load(f)
parts = find_printed_parts(data, str(json_file))
for item in parts:
part = item['part']
url = part.get('url')
if not url:
continue
github_info = parse_github_url(url)
if not github_info:
print(f"Warning: Skipping non-GitHub URL in {json_file}: {url}", file=sys.stderr)
continue
part_id = item['part_id']
manifest_id = generate_manifest_id(
part_id,
github_info['owner'],
github_info['repo'],
github_info['path']
)
local_path = generate_local_path(
github_info['owner'],
github_info['repo'],
github_info['path']
)
# Store relative path from repo root
try:
json_file_rel = json_file.relative_to(repo_root)
except ValueError:
# If not relative, use absolute path
json_file_rel = json_file
entries.append({
'manifest_id': manifest_id,
'part_id': part_id,
'part': part,
'json_file': str(json_file_rel),
'github_info': github_info,
'local_path': local_path
})
except (json.JSONDecodeError, IOError) as e:
print(f"Warning: Could not read {json_file}: {e}", file=sys.stderr)
continue
return entries
def create_or_update_manifest_entry(
existing_entry: Optional[Dict],
new_data: Dict[str, Any]
) -> Dict[str, Any]:
"""Create new manifest entry or merge with existing."""
github_info = new_data['github_info']
manifest_id = new_data['manifest_id']
if existing_entry:
# Merge: keep existing pinned data, update source info if changed
entry = existing_entry.copy()
entry['source_repo'] = f"{github_info['owner']}/{github_info['repo']}"
entry['source_path'] = github_info['path']
entry['source_ref'] = github_info.get('ref', 'main')
entry['local_path'] = new_data['local_path']
entry['orig_site_json'] = new_data['json_file']
entry['orig_item_id'] = new_data['part_id']
# Don't overwrite pinned_sha, checksum, etc. if they exist
return entry
# Create new entry
return {
'id': manifest_id,
'source_repo': f"{github_info['owner']}/{github_info['repo']}",
'source_path': github_info['path'],
'source_ref': github_info.get('ref', 'main'),
'pinned_sha': None,
'pinned_raw_url': None,
'local_path': new_data['local_path'],
'checksum_sha256': None,
'last_checked': None,
'upstream_latest_sha': None,
'status': 'unknown',
'license': None,
'orig_site_json': new_data['json_file'],
'orig_item_id': new_data['part_id']
}
def main():
parser = argparse.ArgumentParser(
description='Generate vendor manifest from site component JSON files'
)
parser.add_argument(
'--site-dir',
type=Path,
default=Path('website/src/data/components'),
help='Directory containing component JSON files (default: website/src/data/components)'
)
parser.add_argument(
'--manifest',
type=Path,
default=Path('manifest/vendor_manifest.json'),
help='Path to manifest file (default: manifest/vendor_manifest.json)'
)
args = parser.parse_args()
# Resolve paths relative to script location or current directory
script_dir = Path(__file__).parent.parent
site_dir = (script_dir / args.site_dir).resolve()
manifest_path = (script_dir / args.manifest).resolve()
# Ensure manifest directory exists
manifest_path.parent.mkdir(parents=True, exist_ok=True)
# Load existing manifest
existing_manifest = load_existing_manifest(manifest_path)
# Scan component files
print(f"Scanning component files in {site_dir}...")
entries = scan_component_files(site_dir, repo_root=script_dir)
if not entries:
print("No GitHub URLs found in component files.", file=sys.stderr)
sys.exit(1)
# Create or update manifest entries
updated_manifest = existing_manifest.copy()
for entry_data in entries:
manifest_id = entry_data['manifest_id']
existing_entry = updated_manifest.get(manifest_id)
new_entry = create_or_update_manifest_entry(existing_entry, entry_data)
updated_manifest[manifest_id] = new_entry
# Convert to sorted list for deterministic output
manifest_list = sorted(updated_manifest.values(), key=lambda x: x['id'])
# Write manifest
print(f"Writing manifest to {manifest_path}...")
with open(manifest_path, 'w', encoding='utf-8') as f:
json.dump(manifest_list, f, indent=2, sort_keys=False)
print(f"Generated {len(manifest_list)} manifest entries.")
# Show summary
new_entries = len(manifest_list) - len(existing_manifest)
if new_entries > 0:
print(f"Added {new_entries} new entries.")
if len(existing_manifest) > 0:
print(f"Updated {len(existing_manifest)} existing entries.")
if __name__ == '__main__':
main()

View File

@@ -2,8 +2,8 @@
""" """
Download and pin external asset files from GitHub. Download and pin external asset files from GitHub.
Downloads files specified in manifest, pins them to commit SHAs, Automatically scans website/src/data/components for parts with GitHub URLs,
computes checksums, and optionally syncs vendor metadata back to site JSON files. updates the manifest, and then downloads/pins files.
""" """
import argparse import argparse
@@ -14,8 +14,8 @@ import sys
import time import time
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional from typing import Dict, List, Optional, Tuple, Generator, Any
from urllib.parse import urlparse from urllib.parse import urlparse, unquote, parse_qs
import requests import requests
@@ -226,6 +226,182 @@ def download_file(url: str, dest_path: Path) -> bool:
return False return False
def parse_github_url(url: str) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str]]:
"""
Parse GitHub URL to return (owner, repo, ref, path).
Supports:
- https://github.com/owner/repo/blob/<ref>/path/to/file
- https://github.com/owner/repo/raw/<ref>/path/to/file
- https://raw.githubusercontent.com/owner/repo/<ref>/path/to/file
"""
if not url or not isinstance(url, str):
return None, None, None, None
# Check if it's a GitHub URL
if 'github.com' not in url:
return None, None, None, None
try:
# Handle raw.githubusercontent.com
if 'raw.githubusercontent.com' in url:
match_parts = url.split('/')
# https://raw.githubusercontent.com/OWNER/REPO/REF/PATH...
# parts: [https:, , raw.githubusercontent.com, OWNER, REPO, REF, PATH...]
if len(match_parts) >= 6:
owner = match_parts[3]
repo = match_parts[4]
ref = match_parts[5]
path = '/'.join(match_parts[6:]).split('?')[0]
return owner, repo, ref, unquote(path)
# Handle github.com and action.github.com
parsed = urlparse(url)
path = parsed.path.strip('/')
path_parts = path.split('/')
if len(path_parts) >= 4:
owner = path_parts[0]
repo = path_parts[1]
mode = path_parts[2] # 'blob' or 'raw'
if mode in ('blob', 'raw'):
ref = path_parts[3]
file_path = '/'.join(path_parts[4:])
# Check query params for ?raw=true
query_params = parse_qs(parsed.query)
if 'raw' in query_params or mode == 'raw':
return owner, repo, ref, unquote(file_path)
# Also treat 'blob' as a valid source if we just want the path
return owner, repo, ref, unquote(file_path)
except Exception:
pass
return None, None, None, None
def scan_site_components(components_dir: Path) -> Generator[Dict[str, Any], None, None]:
"""Recursively scan JSON files for parts with GitHub URLs."""
for json_file in components_dir.rglob('*.json'):
try:
with open(json_file, 'r', encoding='utf-8') as f:
data = json.load(f)
# Helper to find parts
queue = [data]
while queue:
item = queue.pop(0)
if isinstance(item, dict):
# Check if this item is a part
if 'id' in item and 'url' in item and item['url']:
owner, repo, ref, source_path = parse_github_url(item['url'])
if owner and repo and source_path:
yield {
'id': item['id'],
'url': item['url'],
'owner': owner,
'repo': repo,
'ref': ref or 'main',
'source_path': source_path,
'orig_site_json': json_file
}
# Add children to queue
queue.extend(item.values())
elif isinstance(item, list):
queue.extend(item)
except (json.JSONDecodeError, IOError) as e:
print(f"Warning: Could not read {json_file}: {e}", file=sys.stderr)
def regenerate_manifest(manifest_path: Path, repo_root: Path) -> Tuple[List[Dict], int]:
"""
Regenerate manifest from site data.
Preserves state of existing entries.
Returns (new_manifest_list, changes_count).
"""
print("Scanning website components to regenerate manifest...")
# Load existing manifest to preserve state
old_manifest = {}
if manifest_path.exists():
with open(manifest_path, 'r', encoding='utf-8') as f:
data = json.load(f)
if isinstance(data, list):
old_manifest = {entry['id']: entry for entry in data}
new_manifest = {}
components_dir = repo_root / 'website/src/data/components'
changes_count = 0
if not components_dir.exists():
print(f"Warning: Components directory not found: {components_dir}", file=sys.stderr)
return list(old_manifest.values()), 0
for part in scan_site_components(components_dir):
part_id = part['id']
old_entry = old_manifest.get(part_id)
# Calculate local path
# vendor/{owner}-{repo}/{path}
local_path = f"vendor/{part['owner']}-{part['repo']}/{part['source_path']}"
source_repo = f"{part['owner']}/{part['repo']}"
orig_site_json = str(part['orig_site_json'].relative_to(repo_root))
entry = {
'id': part_id,
'source_repo': source_repo,
'source_path': part['source_path'],
'source_ref': part['ref'],
'local_path': local_path,
'orig_site_json': orig_site_json,
'orig_item_id': part_id
}
# Preserve state if exists and config matches
if old_entry:
# Check if source config changed
config_changed = (
old_entry.get('source_repo') != source_repo or
old_entry.get('source_path') != part['source_path'] or
old_entry.get('source_ref') != part['ref']
)
if not config_changed:
# Copy state
for key in ['pinned_sha', 'pinned_raw_url', 'checksum_sha256', 'last_checked', 'status', 'license', 'upstream_latest_sha']:
if key in old_entry:
entry[key] = old_entry[key]
else:
print(f" Config changed for {part_id}, resetting status.")
entry['status'] = 'pending'
entry['pinned_sha'] = None
changes_count += 1
# Check if we updated manifest info (like orig_site_json moved)
if (old_entry.get('orig_site_json') != orig_site_json or
old_entry.get('local_path') != local_path):
changes_count += 1
else:
print(f" New part found: {part_id}")
entry['status'] = 'pending'
entry['pinned_sha'] = None
changes_count += 1
new_manifest[part_id] = entry
# Check for removed items
removed_count = len(old_manifest) - len(new_manifest)
if removed_count > 0:
print(f" Removed {removed_count} parts that are no longer in site JSONs.")
changes_count += removed_count
return sorted(new_manifest.values(), key=lambda x: x['id']), changes_count
def update_manifest_entry( def update_manifest_entry(
entry: Dict, entry: Dict,
api: GitHubAPI, api: GitHubAPI,
@@ -255,6 +431,31 @@ def update_manifest_entry(
if not local_path.is_absolute(): if not local_path.is_absolute():
local_path = repo_root / local_path local_path = repo_root / local_path
# Check if file exists and is already at the correct version
current_pinned_sha = entry.get('pinned_sha')
if current_pinned_sha == commit_sha and local_path.exists():
if dry_run:
print(f" [DRY RUN] File up to date ({commit_sha}), would skip download.")
else:
print(f" File up to date ({commit_sha}), skipping download.")
# Ensure checksum is present
if 'checksum_sha256' not in entry or not entry['checksum_sha256']:
entry['checksum_sha256'] = compute_sha256(local_path)
entry['pinned_sha'] = commit_sha
entry['pinned_raw_url'] = pinned_raw_url
entry['last_checked'] = datetime.now(timezone.utc).isoformat()
entry['upstream_latest_sha'] = commit_sha
entry['status'] = 'up-to-date'
# If license is missing, try to get it, otherwise keep existing
if 'license' not in entry and not dry_run:
license_info = api.get_license(owner, repo, commit_sha)
if license_info:
entry['license'] = license_info
return entry
if dry_run: if dry_run:
print(f" [DRY RUN] Would download to {local_path}") print(f" [DRY RUN] Would download to {local_path}")
print(f" [DRY RUN] Pinned SHA: {commit_sha}") print(f" [DRY RUN] Pinned SHA: {commit_sha}")
@@ -309,16 +510,13 @@ def sync_to_site_json(entry: Dict, repo_root: Path) -> bool:
data = json.load(f) data = json.load(f)
# Find the printed part in the nested structure # Find the printed part in the nested structure
def find_and_update_part(obj, target_id, path=''): def find_and_update_part(obj, target_id):
if isinstance(obj, dict): if isinstance(obj, dict):
# Check if this is a printedParts array # If this object IS the part (has the ID)
if 'printedParts' in obj and isinstance(obj['printedParts'], list): if obj.get('id') == target_id:
for part in obj['printedParts']: if 'vendor' not in obj:
if isinstance(part, dict) and part.get('id') == target_id: obj['vendor'] = {}
# Update this part obj['vendor'].update({
if 'vendor' not in part:
part['vendor'] = {}
part['vendor'].update({
'manifest_id': entry['id'], 'manifest_id': entry['id'],
'local_path': entry['local_path'], 'local_path': entry['local_path'],
'pinned_sha': entry['pinned_sha'], 'pinned_sha': entry['pinned_sha'],
@@ -329,25 +527,7 @@ def sync_to_site_json(entry: Dict, repo_root: Path) -> bool:
}) })
return True return True
# Check bodyParts, knobs, etc. # Recursively search values
for key in ['bodyParts', 'knobs']:
if key in obj and isinstance(obj[key], list):
for part in obj[key]:
if isinstance(part, dict) and part.get('id') == target_id:
if 'vendor' not in part:
part['vendor'] = {}
part['vendor'].update({
'manifest_id': entry['id'],
'local_path': entry['local_path'],
'pinned_sha': entry['pinned_sha'],
'pinned_raw_url': entry['pinned_raw_url'],
'checksum_sha256': entry['checksum_sha256'],
'last_checked': entry['last_checked'],
'status': entry['status']
})
return True
# Recursively search
for value in obj.values(): for value in obj.values():
if find_and_update_part(value, target_id): if find_and_update_part(value, target_id):
return True return True
@@ -396,9 +576,9 @@ def main():
help='Show what would be done without downloading files' help='Show what would be done without downloading files'
) )
parser.add_argument( parser.add_argument(
'--sync-site', '--no-sync',
action='store_true', action='store_true',
help='Sync vendor metadata back to site JSON files' help='Skip syncing vendor metadata back to site JSON files'
) )
parser.add_argument( parser.add_argument(
'--delay', '--delay',
@@ -406,6 +586,16 @@ def main():
default=0.5, default=0.5,
help='Delay between API requests in seconds (default: 0.5)' help='Delay between API requests in seconds (default: 0.5)'
) )
parser.add_argument(
'--no-scan',
action='store_true',
help='Skip scanning website for new components'
)
parser.add_argument(
'--scan-only',
action='store_true',
help='Only scan website and update manifest, do not check/download files'
)
args = parser.parse_args() args = parser.parse_args()
@@ -414,11 +604,28 @@ def main():
manifest_path = (script_dir / args.manifest).resolve() manifest_path = (script_dir / args.manifest).resolve()
repo_root = script_dir repo_root = script_dir
# Regenerate manifest from website scan (unless disabled)
if not args.no_scan and not args.entry:
manifest_list, changes = regenerate_manifest(manifest_path, repo_root)
if changes > 0:
print(f"Manifest regenerated with {changes} changes.")
if not args.dry_run:
manifest_path.parent.mkdir(parents=True, exist_ok=True)
with open(manifest_path, 'w', encoding='utf-8') as f:
json.dump(manifest_list, f, indent=2, sort_keys=False)
else:
print("No changes in manifest structure detected.")
if args.scan_only:
return
# Reload manifest data for processing
manifest_data = manifest_list
else:
if not manifest_path.exists(): if not manifest_path.exists():
print(f"Error: Manifest file not found: {manifest_path}", file=sys.stderr) print(f"Error: Manifest file not found: {manifest_path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
# Load manifest
with open(manifest_path, 'r', encoding='utf-8') as f: with open(manifest_path, 'r', encoding='utf-8') as f:
manifest_data = json.load(f) manifest_data = json.load(f)
@@ -446,7 +653,7 @@ def main():
updated_entry = update_manifest_entry(entry, api, repo_root, dry_run=args.dry_run) updated_entry = update_manifest_entry(entry, api, repo_root, dry_run=args.dry_run)
manifest[entry_id] = updated_entry manifest[entry_id] = updated_entry
if args.sync_site and not args.dry_run: if not args.no_sync and not args.dry_run:
sync_to_site_json(updated_entry, repo_root) sync_to_site_json(updated_entry, repo_root)
updated_count += 1 updated_count += 1

View File

@@ -69,7 +69,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5", "@babel/generator": "^7.28.5",
@@ -1427,7 +1426,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"csstype": "^3.2.2" "csstype": "^3.2.2"
@@ -1467,7 +1465,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -1932,7 +1929,6 @@
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ],
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -2696,7 +2692,6 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -3908,7 +3903,6 @@
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true, "dev": true,
"peer": true,
"bin": { "bin": {
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
} }
@@ -4673,7 +4667,6 @@
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ],
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -4889,7 +4882,6 @@
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0" "loose-envify": "^1.1.0"
}, },
@@ -5699,7 +5691,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -5845,13 +5836,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"optional": true
},
"node_modules/unzipper": { "node_modules/unzipper": {
"version": "0.10.14", "version": "0.10.14",
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz",
@@ -5958,7 +5942,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.27.0", "esbuild": "^0.27.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -6050,7 +6033,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -6233,7 +6215,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
"dev": true, "dev": true,
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }

View File

@@ -10,6 +10,21 @@ export default function BOMSummary({ config }) {
const [zipProgress, setZipProgress] = useState({ current: 0, total: 0, currentFile: '' }); const [zipProgress, setZipProgress] = useState({ current: 0, total: 0, currentFile: '' });
const [hardwareViewMode, setHardwareViewMode] = useState('unified'); // 'unified' or 'expanded' const [hardwareViewMode, setHardwareViewMode] = useState('unified'); // 'unified' or 'expanded'
const [activeTab, setActiveTab] = useState('overview'); // 'overview', 'printed', 'hardware' const [activeTab, setActiveTab] = useState('overview'); // 'overview', 'printed', 'hardware'
const evaluateCondition = (condition, config) => {
if (!condition) return true;
return Object.entries(condition).every(([key, value]) => {
// Handle dot notation for nested config (e.g., motor.id)
const keys = key.split('.');
let current = config;
for (const k of keys) {
if (current === null || current === undefined) return false;
current = current[k];
}
return current === value;
});
};
const calculateTotal = () => { const calculateTotal = () => {
let total = 0; let total = 0;
@@ -49,73 +64,61 @@ export default function BOMSummary({ config }) {
const getRequiredPrintedParts = () => { const getRequiredPrintedParts = () => {
const parts = []; const parts = [];
const isMiddlePivotSelected = config.mount?.id === 'middle-pivot';
// Always include components that are marked as required and meet their conditions
Object.entries(partsData.components || {}).forEach(([componentKey, component]) => {
const category = component.category || componentKey;
// Handle standard printedParts array
if (component.printedParts) {
component.printedParts.forEach((part) => {
if (part.required && evaluateCondition(part.Condition, config)) {
parts.push({ ...part, category });
}
});
}
// Handle systems (for hinges, remotes, etc.)
if (component.systems) {
// If it's a selected system, include its printed parts
const selectedSystemId = config[componentKey] || config.standHinge; // Fallback for naming mismatches
const system = component.systems[selectedSystemId?.id || selectedSystemId];
if (system) {
const systemParts = system.printedParts || system.bodyParts || [];
systemParts.forEach((part) => {
if (part.required && evaluateCondition(part.Condition, config)) {
parts.push({ ...part, category });
}
});
// Remote knobs are handled by looking up the knob in the system
if (componentKey === 'remotes' && config.remoteKnob) {
const knobPart = system.knobs?.find(k => k.id === config.remoteKnob.id);
if (knobPart) {
parts.push({ ...knobPart, category: 'Remote Knobs' });
}
}
}
}
});
// Handle options that are not explicitly in the "components" top-level structure but represent printed parts
// Mount variations (if not already handled by required parts)
if (config.mount && partsData.components?.mounts?.printedParts) {
const mountPart = partsData.components.mounts.printedParts.find(p => p.id === config.mount.id);
if (mountPart) parts.push({ ...mountPart, category: 'Mount' });
}
// Custom Cover
const coverId = config.cover?.id; const coverId = config.cover?.id;
const isStandardCover = coverId === 'standard-cover'; const isStandardCover = coverId === 'standard-cover';
const isBlankCover = coverId === 'blank-cover'; const isBlankCover = coverId === 'blank-cover';
const isCustomCover = config.cover !== null && !isStandardCover && !isBlankCover; const isCustomCover = config.cover !== null && !isStandardCover && !isBlankCover;
// Always include actuator parts, but exclude ossm-actuator-body-middle if middlePivot is selected
// and exclude ossm-actuator-body-cover if a non-standard cover is selected
if (partsData.components?.actuator?.printedParts) {
partsData.components.actuator.printedParts.forEach((part) => {
if (part.required) {
// Skip the actuator body middle part if middle pivot is selected
if (isMiddlePivotSelected && part.id === 'ossm-actuator-body-middle') {
return;
}
// Skip the default cover if a non-standard cover is selected (blank or custom)
if ((isBlankCover || isCustomCover) && part.id === 'ossm-actuator-body-cover') {
return;
}
parts.push({ ...part, category: 'Actuator Body' });
}
});
}
// Include mount-specific parts based on selection
if (config.mount) {
const mountId = config.mount.id;
if (mountId === 'middle-pivot' && partsData.components?.middlePivot?.printedParts) {
partsData.components.middlePivot.printedParts.forEach((part) => {
if (part.required) {
parts.push({ ...part, category: 'Mount', replacesActuatorMiddle: true });
}
});
} else if (mountId === 'pitclamp' && partsData.components?.pitClamp?.printedParts) {
const selectedMotorId = config.motor?.id;
partsData.components.pitClamp.printedParts.forEach((part) => {
if (part.required) {
// Filter motor-specific parts - only include the one that matches the selected motor
const isMotorSpecificPart = part.id.includes('57AIM30') || part.id.includes('42AIM30') || part.id.includes('iHSV57');
if (isMotorSpecificPart) {
// Only include if it matches the selected motor
const expectedPartId = `ossm-pitclamp-mini-${selectedMotorId}`;
if (part.id === expectedPartId) {
parts.push({ ...part, category: 'Mount' });
}
} else {
// Include non-motor-specific parts (lower, upper, handle, dogbone-nuts)
parts.push({ ...part, category: 'Mount' });
}
}
});
}
// Include mount option part
if (partsData.components?.mounts?.printedParts) {
const mountPart = partsData.components.mounts.printedParts.find(p => p.id === mountId);
if (mountPart) {
parts.push({ ...mountPart, category: 'Mount' });
}
}
}
// Include custom cover if selected (replaces default cover)
// Skip blank cover and standard cover (standard uses the component, blank uses nothing)
if (isCustomCover) { if (isCustomCover) {
const coverOption = config.cover; const coverOption = config.cover;
// Convert cover option to part format for BOM
parts.push({ parts.push({
id: coverOption.id, id: coverOption.id,
name: coverOption.name, name: coverOption.name,
@@ -128,22 +131,23 @@ export default function BOMSummary({ config }) {
}); });
} }
// Include PCB mount parts if a PCB mount is selected // Stand components (feet, supports)
if (config.pcbMount) { if (config.standFeet && partsData.components?.feet?.printedParts) {
const pcbMountId = config.pcbMount.id; const feetPart = partsData.components.feet.printedParts.find(p => p.id === config.standFeet.id);
const pcbMountComponent = partsData.components?.[pcbMountId]; if (feetPart) parts.push({ ...feetPart, category: 'Stand Feet' });
if (pcbMountComponent?.printedParts) { }
pcbMountComponent.printedParts.forEach((part) => {
if (part.required) { if (config.standCrossbarSupports && partsData.components?.crossbarSupports?.printedParts) {
parts.push({ ...part, category: 'PCB Mount' }); const selectedSupportIds = new Set(config.standCrossbarSupports.map(opt => opt.id));
partsData.components.crossbarSupports.printedParts.forEach((part) => {
if (selectedSupportIds.has(part.id) && !part.isHardwareOnly) {
parts.push({ ...part, category: 'Stand Crossbar Supports' });
} }
}); });
} }
}
// Include toy mount parts if any toy mounts are selected // Toy Mounts
if (config.toyMountOptions && config.toyMountOptions.length > 0) { if (config.toyMountOptions && config.toyMountOptions.length > 0 && partsData.components?.toyMounts?.printedParts) {
if (partsData.components?.toyMounts?.printedParts) {
const selectedToyMountIds = new Set(config.toyMountOptions.map(opt => opt.id)); const selectedToyMountIds = new Set(config.toyMountOptions.map(opt => opt.id));
partsData.components.toyMounts.printedParts.forEach((part) => { partsData.components.toyMounts.printedParts.forEach((part) => {
if (selectedToyMountIds.has(part.id)) { if (selectedToyMountIds.has(part.id)) {
@@ -151,83 +155,16 @@ export default function BOMSummary({ config }) {
} }
}); });
} }
}
// Include stand parts if stand options are selected // Handle 'replaces' logic
if (config.standHinge || config.standFeet || (config.standCrossbarSupports && config.standCrossbarSupports.length > 0)) { const replacedIds = new Set();
if (partsData.components?.stand?.printedParts) { parts.forEach(part => {
partsData.components.stand.printedParts.forEach((part) => { if (part.replaces) {
if (part.required) { part.replaces.forEach(id => replacedIds.add(id));
parts.push({ ...part, category: 'Stand' });
}
});
}
}
// Include stand hinge system parts
if (config.standHinge && partsData.components?.hinges?.systems) {
const hingeSystem = partsData.components.hinges.systems[config.standHinge.id];
if (hingeSystem?.printedParts) {
hingeSystem.printedParts.forEach((part) => {
if (part.required) {
parts.push({ ...part, category: 'Stand Hinges' });
}
});
}
}
// Include stand feet option part
if (config.standFeet && partsData.components?.feet?.printedParts) {
const feetPart = partsData.components.feet.printedParts.find(p => p.id === config.standFeet.id);
if (feetPart) {
parts.push({ ...feetPart, category: 'Stand Feet' });
}
}
// Include stand crossbar support option parts
if (config.standCrossbarSupports && config.standCrossbarSupports.length > 0 && partsData.components?.crossbarSupports?.printedParts) {
const selectedSupportIds = new Set(config.standCrossbarSupports.map(opt => opt.id));
partsData.components.crossbarSupports.printedParts.forEach((part) => {
if (selectedSupportIds.has(part.id)) {
// Skip hardware-only parts (e.g., aluminum standard-90-degree-support)
// These should only appear in the hardware list
if (part.isHardwareOnly) {
return;
}
parts.push({ ...part, category: 'Stand Crossbar Supports' });
}
});
}
// Include remote system parts if a remote knob is selected
if (config.remoteKnob && partsData.components?.remotes?.systems) {
// Find which system contains this knob
let remoteSystem = null;
Object.values(partsData.components.remotes.systems).forEach((system) => {
if (system.knobs && system.knobs.find(k => k.id === config.remoteKnob.id)) {
remoteSystem = system;
} }
}); });
if (remoteSystem) { return parts.filter(part => !replacedIds.has(part.id));
// Include body parts from the system
if (remoteSystem.bodyParts) {
remoteSystem.bodyParts.forEach((part) => {
if (part.required) {
parts.push({ ...part, category: 'Remote Body' });
}
});
}
// Include the selected remote knob part
const knobPart = remoteSystem.knobs?.find(p => p.id === config.remoteKnob.id);
if (knobPart) {
parts.push({ ...knobPart, category: 'Remote Knobs' });
}
}
}
return parts;
}; };
// Helper function to categorize hardware by type // Helper function to categorize hardware by type
@@ -298,6 +235,10 @@ export default function BOMSummary({ config }) {
if (hingeSystem?.hardwareParts) { if (hingeSystem?.hardwareParts) {
hingeSystem.hardwareParts.forEach((hardware) => { hingeSystem.hardwareParts.forEach((hardware) => {
if (!hardware.required) return; if (!hardware.required) return;
// Evaluate condition for hardware
if (!evaluateCondition(hardware.Condition, config)) return;
const key = hardware.id; const key = hardware.id;
if (hardwareMap.has(key)) { if (hardwareMap.has(key)) {
const existing = hardwareMap.get(key); const existing = hardwareMap.get(key);
@@ -327,6 +268,10 @@ export default function BOMSummary({ config }) {
if (remoteSystem?.hardwareParts) { if (remoteSystem?.hardwareParts) {
remoteSystem.hardwareParts.forEach((hardware) => { remoteSystem.hardwareParts.forEach((hardware) => {
if (!hardware.required) return; if (!hardware.required) return;
// Evaluate condition for hardware
if (!evaluateCondition(hardware.Condition, config)) return;
const key = hardware.id; const key = hardware.id;
if (hardwareMap.has(key)) { if (hardwareMap.has(key)) {
const existing = hardwareMap.get(key); const existing = hardwareMap.get(key);
@@ -368,6 +313,9 @@ export default function BOMSummary({ config }) {
component.hardwareParts.forEach((hardware) => { component.hardwareParts.forEach((hardware) => {
if (!hardware.required) return; if (!hardware.required) return;
// Evaluate condition for hardware
if (!evaluateCondition(hardware.Condition, config)) return;
// If component has selected parts, check if hardware should be included // If component has selected parts, check if hardware should be included
let shouldInclude = false; let shouldInclude = false;
@@ -408,7 +356,6 @@ export default function BOMSummary({ config }) {
return hardwareParts; return hardwareParts;
}; };
// Parse time estimate string (e.g., "2h14m", "1h3m", "40m25s") to minutes
const parseTimeToMinutes = (timeStr) => { const parseTimeToMinutes = (timeStr) => {
if (!timeStr || typeof timeStr !== 'string') return 0; if (!timeStr || typeof timeStr !== 'string') return 0;
@@ -697,8 +644,7 @@ export default function BOMSummary({ config }) {
onClick={() => setActiveTab(tab.id)} onClick={() => setActiveTab(tab.id)}
className={` className={`
py-4 px-1 border-b-2 font-medium text-sm transition-colors py-4 px-1 border-b-2 font-medium text-sm transition-colors
${ ${activeTab === tab.id
activeTab === tab.id
? 'border-blue-500 dark:border-blue-400 text-blue-600 dark:text-blue-400' ? 'border-blue-500 dark:border-blue-400 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600' : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
} }
@@ -816,7 +762,7 @@ export default function BOMSummary({ config }) {
<img <img
src={config.mount.image} src={config.mount.image}
alt={config.mount.name} alt={config.mount.name}
className="h-32 w-32 object-contain rounded-lg bg-gray-100 mb-2" className="h-32 w-32 object-contain rounded-lg bg-gray-100 dark:bg-gray-700 mb-2"
onError={(e) => { onError={(e) => {
e.target.style.display = 'none'; e.target.style.display = 'none';
}} }}
@@ -831,7 +777,7 @@ export default function BOMSummary({ config }) {
<img <img
src={config.cover.image} src={config.cover.image}
alt={config.cover.name} alt={config.cover.name}
className="h-32 w-32 object-contain rounded-lg bg-gray-100 mb-2" className="h-32 w-32 object-contain rounded-lg bg-gray-100 dark:bg-gray-700 mb-2"
onError={(e) => { onError={(e) => {
e.target.style.display = 'none'; e.target.style.display = 'none';
}} }}
@@ -846,7 +792,7 @@ export default function BOMSummary({ config }) {
<img <img
src={config.pcbMount.image} src={config.pcbMount.image}
alt={config.pcbMount.name} alt={config.pcbMount.name}
className="h-32 w-32 object-contain rounded-lg bg-gray-100 mb-2" className="h-32 w-32 object-contain rounded-lg bg-gray-100 dark:bg-gray-700 mb-2"
onError={(e) => { onError={(e) => {
e.target.style.display = 'none'; e.target.style.display = 'none';
}} }}
@@ -861,7 +807,7 @@ export default function BOMSummary({ config }) {
<img <img
src={config.standHinge.image} src={config.standHinge.image}
alt={config.standHinge.name} alt={config.standHinge.name}
className="h-32 w-32 object-contain rounded-lg bg-gray-100 mb-2" className="h-32 w-32 object-contain rounded-lg bg-gray-100 dark:bg-gray-700 mb-2"
onError={(e) => { onError={(e) => {
e.target.style.display = 'none'; e.target.style.display = 'none';
}} }}
@@ -876,7 +822,7 @@ export default function BOMSummary({ config }) {
<img <img
src={config.standFeet.image} src={config.standFeet.image}
alt={config.standFeet.name} alt={config.standFeet.name}
className="h-32 w-32 object-contain rounded-lg bg-gray-100 mb-2" className="h-32 w-32 object-contain rounded-lg bg-gray-100 dark:bg-gray-700 mb-2"
onError={(e) => { onError={(e) => {
e.target.style.display = 'none'; e.target.style.display = 'none';
}} }}
@@ -913,7 +859,7 @@ export default function BOMSummary({ config }) {
<img <img
src={remoteSystem.image} src={remoteSystem.image}
alt={remoteSystem.name} alt={remoteSystem.name}
className="h-32 w-32 object-contain rounded-lg bg-gray-100 mb-2" className="h-32 w-32 object-contain rounded-lg bg-gray-100 dark:bg-gray-700 mb-2"
onError={(e) => { onError={(e) => {
e.target.style.display = 'none'; e.target.style.display = 'none';
}} }}
@@ -938,7 +884,7 @@ export default function BOMSummary({ config }) {
<img <img
src={toyMount.image} src={toyMount.image}
alt={toyMount.name} alt={toyMount.name}
className="h-32 w-32 object-contain rounded-lg bg-gray-100 mb-2" className="h-32 w-32 object-contain rounded-lg bg-gray-100 dark:bg-gray-700 mb-2"
onError={(e) => { onError={(e) => {
e.target.style.display = 'none'; e.target.style.display = 'none';
}} }}
@@ -1102,18 +1048,18 @@ export default function BOMSummary({ config }) {
)} )}
</div> </div>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 border border-gray-200 rounded-lg"> <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-lg">
<thead className="bg-gray-50"> <thead className="bg-gray-50 dark:bg-gray-800">
<tr> <tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">Part Name</th> <th className="px-4 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Part Name</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">Color</th> <th className="px-4 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Color</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">Description</th> <th className="px-4 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Description</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">File Path</th> <th className="px-4 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">File Path</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-700 uppercase tracking-wider">Quantity</th> <th className="px-4 py-3 text-right text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Quantity</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-700 uppercase tracking-wider">Filament</th> <th className="px-4 py-3 text-right text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Filament</th>
</tr> </tr>
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{parts.map((part) => { {parts.map((part) => {
const partColour = part.colour || 'primary'; const partColour = part.colour || 'primary';
const colorHex = getColorHex( const colorHex = getColorHex(
@@ -1126,38 +1072,38 @@ export default function BOMSummary({ config }) {
); );
return ( return (
<tr key={part.id} className="hover:bg-gray-50"> <tr key={part.id} className="hover:bg-gray-50 dark:hover:bg-gray-700/50">
<td className="px-4 py-3 whitespace-nowrap"> <td className="px-4 py-3 whitespace-nowrap">
<p className="text-sm font-medium text-gray-900">{part.name}</p> <p className="text-sm font-medium text-gray-900 dark:text-white">{part.name}</p>
</td> </td>
<td className="px-4 py-3 whitespace-nowrap"> <td className="px-4 py-3 whitespace-nowrap">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div <div
className="w-4 h-4 rounded-full border border-gray-300" className="w-4 h-4 rounded-full border border-gray-300 dark:border-gray-600"
style={{ backgroundColor: colorHex }} style={{ backgroundColor: colorHex }}
title={`${partColour === 'primary' ? 'Primary' : 'Secondary'} color: ${colorName}`} title={`${partColour === 'primary' ? 'Primary' : 'Secondary'} color: ${colorName}`}
/> />
<span className="text-xs text-gray-600 capitalize">{partColour}</span> <span className="text-xs text-gray-600 dark:text-gray-400 capitalize">{partColour}</span>
</div> </div>
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<p className="text-sm text-gray-600">{part.description || '-'}</p> <p className="text-sm text-gray-600 dark:text-gray-300">{part.description || '-'}</p>
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
{part.isHardwareOnly ? ( {part.isHardwareOnly ? (
<span className="text-xs text-blue-600 italic">Hardware only</span> <span className="text-xs text-blue-600 dark:text-blue-400 italic">Hardware only</span>
) : part.filePath ? ( ) : part.filePath ? (
<p className="text-xs text-gray-500 font-mono">{part.filePath}</p> <p className="text-xs text-gray-500 dark:text-gray-400 font-mono">{part.filePath}</p>
) : ( ) : (
<span className="text-gray-400 dark:text-gray-500">-</span> <span className="text-gray-400 dark:text-gray-500">-</span>
)} )}
</td> </td>
<td className="px-4 py-3 whitespace-nowrap text-right"> <td className="px-4 py-3 whitespace-nowrap text-right">
<p className="text-sm font-medium text-gray-700">{part.quantity || 1}</p> <p className="text-sm font-medium text-gray-700 dark:text-gray-300">{part.quantity || 1}</p>
</td> </td>
<td className="px-4 py-3 whitespace-nowrap text-right"> <td className="px-4 py-3 whitespace-nowrap text-right">
{part.filamentEstimate !== undefined && part.filamentEstimate > 0 ? ( {part.filamentEstimate !== undefined && part.filamentEstimate > 0 ? (
<p className="text-sm font-medium text-gray-700"> <p className="text-sm font-medium text-gray-700 dark:text-gray-300">
{typeof part.filamentEstimate === 'number' {typeof part.filamentEstimate === 'number'
? (part.filamentEstimate * (part.quantity || 1)).toFixed(1) ? (part.filamentEstimate * (part.quantity || 1)).toFixed(1)
: part.filamentEstimate}g : part.filamentEstimate}g
@@ -1231,8 +1177,7 @@ export default function BOMSummary({ config }) {
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={() => setHardwareViewMode('unified')} onClick={() => setHardwareViewMode('unified')}
className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${ className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${hardwareViewMode === 'unified'
hardwareViewMode === 'unified'
? 'bg-blue-600 dark:bg-blue-500 text-white' ? 'bg-blue-600 dark:bg-blue-500 text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
}`} }`}
@@ -1241,8 +1186,7 @@ export default function BOMSummary({ config }) {
</button> </button>
<button <button
onClick={() => setHardwareViewMode('expanded')} onClick={() => setHardwareViewMode('expanded')}
className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${ className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${hardwareViewMode === 'expanded'
hardwareViewMode === 'expanded'
? 'bg-blue-600 dark:bg-blue-500 text-white' ? 'bg-blue-600 dark:bg-blue-500 text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
}`} }`}

View File

@@ -0,0 +1,12 @@
export default function Footer() {
return (
<footer className="w-full py-8 mt-12 border-t border-gray-200 dark:border-gray-700">
<div className="max-w-4xl mx-auto px-4 text-center">
<p className="text-sm text-gray-500 dark:text-gray-400">
Disclaimer: The OSSM Configurator is an independent open-source project.
We are not directly associated with the vendors listed, but their contributions to open source are deeply appreciated and reciprocated.
</p>
</div>
</footer>
);
}

View File

@@ -1,4 +1,4 @@
import partsData from '../data/index.js'; import Footer from './Footer';
export default function MainPage({ onSelectBuildType }) { export default function MainPage({ onSelectBuildType }) {
const handleSelect = (buildType) => { const handleSelect = (buildType) => {
@@ -6,8 +6,8 @@ export default function MainPage({ onSelectBuildType }) {
}; };
return ( return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8"> <div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8 flex flex-col">
<div className="max-w-4xl mx-auto px-4"> <div className="max-w-4xl mx-auto px-4 flex-grow">
{/* Header */} {/* Header */}
<div className="mb-12 text-center"> <div className="mb-12 text-center">
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-2"> <h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-2">
@@ -120,6 +120,7 @@ export default function MainPage({ onSelectBuildType }) {
</div> </div>
</div> </div>
</div> </div>
<Footer />
</div> </div>
); );
} }

View File

@@ -6,6 +6,7 @@ import OptionsStep from './steps/OptionsStep';
import RemoteStep from './steps/RemoteStep'; import RemoteStep from './steps/RemoteStep';
import ToyMountStep from './steps/ToyMountStep'; import ToyMountStep from './steps/ToyMountStep';
import BOMSummary from './BOMSummary'; import BOMSummary from './BOMSummary';
import Footer from './Footer';
const steps = [ const steps = [
{ id: 'motor', name: 'Motor', component: MotorStep }, { id: 'motor', name: 'Motor', component: MotorStep },
@@ -158,8 +159,8 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
}, [buildType, currentStep]); }, [buildType, currentStep]);
return ( return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8"> <div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8 flex flex-col">
<div className="max-w-4xl mx-auto px-4"> <div className="max-w-4xl mx-auto px-4 flex-grow w-full">
{/* Back Button */} {/* Back Button */}
{onBackToMain && ( {onBackToMain && (
<div className="mb-4"> <div className="mb-4">
@@ -212,14 +213,12 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
{/* Circle */} {/* Circle */}
<button <button
onClick={() => goToStep(index)} onClick={() => goToStep(index)}
className={`w-10 h-10 rounded-full flex items-center justify-center font-semibold flex-shrink-0 z-10 ${ className={`w-10 h-10 rounded-full flex items-center justify-center font-semibold flex-shrink-0 z-10 ${index === currentStep
index === currentStep
? 'bg-blue-600 dark:bg-blue-500 text-white' ? 'bg-blue-600 dark:bg-blue-500 text-white'
: index < currentStep : index < currentStep
? 'bg-green-500 dark:bg-green-600 text-white' ? 'bg-green-500 dark:bg-green-600 text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-500 dark:text-gray-400' : 'bg-gray-200 dark:bg-gray-700 text-gray-500 dark:text-gray-400'
} ${ } ${index <= currentStep
index <= currentStep
? 'cursor-pointer hover:opacity-80' ? 'cursor-pointer hover:opacity-80'
: 'cursor-not-allowed' : 'cursor-not-allowed'
}`} }`}
@@ -230,8 +229,7 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
{/* Connecting line to the right */} {/* Connecting line to the right */}
{index < filteredSteps.length - 1 && ( {index < filteredSteps.length - 1 && (
<div <div
className={`absolute top-5 left-1/2 h-1 ${ className={`absolute top-5 left-1/2 h-1 ${index < currentStep ? 'bg-green-500 dark:bg-green-600' : 'bg-gray-200 dark:bg-gray-700'
index < currentStep ? 'bg-green-500 dark:bg-green-600' : 'bg-gray-200 dark:bg-gray-700'
}`} }`}
style={{ style={{
width: 'calc(100% - 40px)', width: 'calc(100% - 40px)',
@@ -242,8 +240,7 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
{/* Text label */} {/* Text label */}
<button <button
onClick={() => goToStep(index)} onClick={() => goToStep(index)}
className={`mt-2 text-sm font-medium text-center ${ className={`mt-2 text-sm font-medium text-center ${index <= currentStep
index <= currentStep
? 'text-blue-600 dark:text-blue-400 cursor-pointer hover:text-blue-800 dark:hover:text-blue-300' ? 'text-blue-600 dark:text-blue-400 cursor-pointer hover:text-blue-800 dark:hover:text-blue-300'
: 'text-gray-400 dark:text-gray-500 cursor-not-allowed' : 'text-gray-400 dark:text-gray-500 cursor-not-allowed'
}`} }`}
@@ -273,8 +270,7 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
<button <button
onClick={prevStep} onClick={prevStep}
disabled={currentStep === 0} disabled={currentStep === 0}
className={`px-6 py-2 rounded-lg font-medium ${ className={`px-6 py-2 rounded-lg font-medium ${currentStep === 0
currentStep === 0
? 'bg-gray-200 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed' ? 'bg-gray-200 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed'
: 'bg-gray-600 dark:bg-gray-700 text-white hover:bg-gray-700 dark:hover:bg-gray-600' : 'bg-gray-600 dark:bg-gray-700 text-white hover:bg-gray-700 dark:hover:bg-gray-600'
}`} }`}
@@ -285,8 +281,7 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
<button <button
onClick={nextStep} onClick={nextStep}
disabled={!canProceedToNextStep()} disabled={!canProceedToNextStep()}
className={`px-6 py-2 rounded-lg font-medium ${ className={`px-6 py-2 rounded-lg font-medium ${canProceedToNextStep()
canProceedToNextStep()
? 'bg-blue-600 dark:bg-blue-500 text-white hover:bg-blue-700 dark:hover:bg-blue-600' ? 'bg-blue-600 dark:bg-blue-500 text-white hover:bg-blue-700 dark:hover:bg-blue-600'
: 'bg-gray-200 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed' : 'bg-gray-200 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed'
}`} }`}
@@ -297,6 +292,7 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
</div> </div>
)} )}
</div> </div>
<Footer />
</div> </div>
); );
} }

View File

@@ -88,8 +88,7 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
<button <button
key={remote.id} key={remote.id}
onClick={() => handleRemoteSelect(remote.id)} onClick={() => handleRemoteSelect(remote.id)}
className={`p-4 border-2 rounded-lg text-left transition-all w-full ${ className={`p-4 border-2 rounded-lg text-left transition-all w-full ${isSelected
isSelected
? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30' ? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/50' : 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/50'
}`} }`}
@@ -147,8 +146,7 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
<div className="flex gap-4"> <div className="flex gap-4">
<button <button
onClick={() => handlePCBSelect('rad')} onClick={() => handlePCBSelect('rad')}
className={`px-4 py-2 border-2 rounded-lg transition-all ${ className={`px-4 py-2 border-2 rounded-lg transition-all ${selectedRemotePCB === 'rad'
selectedRemotePCB === 'rad'
? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30 text-blue-900 dark:text-blue-300 font-medium' ? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30 text-blue-900 dark:text-blue-300 font-medium'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600' : 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
}`} }`}
@@ -157,8 +155,7 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
</button> </button>
<button <button
onClick={() => handlePCBSelect('pcbway')} onClick={() => handlePCBSelect('pcbway')}
className={`px-4 py-2 border-2 rounded-lg transition-all ${ className={`px-4 py-2 border-2 rounded-lg transition-all ${selectedRemotePCB === 'pcbway'
selectedRemotePCB === 'pcbway'
? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30 text-blue-900 dark:text-blue-300 font-medium' ? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30 text-blue-900 dark:text-blue-300 font-medium'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600' : 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
}`} }`}
@@ -177,8 +174,7 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
<button <button
key={knob.id} key={knob.id}
onClick={() => handleKnobSelect(knob)} onClick={() => handleKnobSelect(knob)}
className={`p-4 border-2 rounded-lg text-left transition-all w-full ${ className={`p-4 border-2 rounded-lg text-left transition-all w-full ${isSelected
isSelected
? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30' ? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/50' : 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/50'
}`} }`}
@@ -243,7 +239,7 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
{/* Remote Selection */} {/* Remote Selection */}
<div className="mb-6"> <div className="mb-6">
<h3 className="text-lg font-semibold mb-3">Remote System</h3> <h3 className="text-lg font-semibold mb-3 text-gray-900 dark:text-white">Remote System</h3>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
{availableRemotesFiltered.map((remote) => renderRemoteCard(remote))} {availableRemotesFiltered.map((remote) => renderRemoteCard(remote))}
</div> </div>
@@ -285,8 +281,7 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
)} )}
</div> </div>
<svg <svg
className={`w-4 h-4 text-gray-500 dark:text-gray-400 transition-transform ${ className={`w-4 h-4 text-gray-500 dark:text-gray-400 transition-transform ${expandedKnobs ? 'transform rotate-180' : ''
expandedKnobs ? 'transform rotate-180' : ''
}`} }`}
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"

View File

@@ -19,7 +19,7 @@
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab", "pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Bottom.stl", "pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Bottom.stl",
"checksum_sha256": "e7abdb99a7e9b9e7408a7b04a7dd50e42cc74510ea2969016a45a2a1387dcde3", "checksum_sha256": "e7abdb99a7e9b9e7408a7b04a7dd50e42cc74510ea2969016a45a2a1387dcde3",
"last_checked": "2026-01-07T01:21:02.027595+00:00", "last_checked": "2026-01-07T06:30:14.604915+00:00",
"status": "up-to-date" "status": "up-to-date"
} }
}, },
@@ -39,7 +39,7 @@
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab", "pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Middle.stl", "pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Middle.stl",
"checksum_sha256": "ce6fb769378636c287af788ce42bdab1f2185dcffba929a0c72598742793b48a", "checksum_sha256": "ce6fb769378636c287af788ce42bdab1f2185dcffba929a0c72598742793b48a",
"last_checked": "2026-01-07T01:21:03.531342+00:00", "last_checked": "2026-01-07T06:30:22.906540+00:00",
"status": "up-to-date" "status": "up-to-date"
} }
}, },
@@ -51,6 +51,9 @@
"timeEstimate": "1h3m", "timeEstimate": "1h3m",
"colour": "primary", "colour": "primary",
"required": true, "required": true,
"Condition": {
"cover.id": "standard-cover"
},
"filePath": "OSSM - Actuator Body Cover.stl", "filePath": "OSSM - Actuator Body Cover.stl",
"url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Actuator/OSSM%20-%20Actuator%20-%20Body%20-%20Cover.stl?raw=true", "url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Actuator/OSSM%20-%20Actuator%20-%20Body%20-%20Cover.stl?raw=true",
"vendor": { "vendor": {
@@ -59,7 +62,7 @@
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab", "pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Cover.stl", "pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Cover.stl",
"checksum_sha256": "bbabc742d2f1753d1b4e21e42c197aec31a4a083b5c634e6e825cec69d4e3258", "checksum_sha256": "bbabc742d2f1753d1b4e21e42c197aec31a4a083b5c634e6e825cec69d4e3258",
"last_checked": "2026-01-07T01:21:02.767604+00:00", "last_checked": "2026-01-07T06:30:18.689516+00:00",
"status": "up-to-date" "status": "up-to-date"
} }
}, },
@@ -99,7 +102,7 @@
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab", "pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - Belt Clamp.stl", "pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - Belt Clamp.stl",
"checksum_sha256": "457a71bc09cb53f12026fd829bec8fa5b04fdead0788822935780f42c90b9a7a", "checksum_sha256": "457a71bc09cb53f12026fd829bec8fa5b04fdead0788822935780f42c90b9a7a",
"last_checked": "2026-01-07T01:20:58.945151+00:00", "last_checked": "2026-01-07T06:30:08.525159+00:00",
"status": "up-to-date" "status": "up-to-date"
} }
}, },
@@ -119,7 +122,7 @@
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab", "pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - End Effector.stl", "pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - End Effector.stl",
"checksum_sha256": "4860947b201e2e773b295d33bba09423ae40b4adeef3605d62687f2d40277de1", "checksum_sha256": "4860947b201e2e773b295d33bba09423ae40b4adeef3605d62687f2d40277de1",
"last_checked": "2026-01-07T01:20:59.854476+00:00", "last_checked": "2026-01-07T06:30:09.547007+00:00",
"status": "up-to-date" "status": "up-to-date"
} }
}, },
@@ -139,7 +142,7 @@
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab", "pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Nut - 5 Sided.stl", "pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Nut - 5 Sided.stl",
"checksum_sha256": "38630c70b2fb929bba9a705dabf5bbd7b49ec882963e042b7108dc74284dd6ff", "checksum_sha256": "38630c70b2fb929bba9a705dabf5bbd7b49ec882963e042b7108dc74284dd6ff",
"last_checked": "2026-01-07T01:21:00.555525+00:00", "last_checked": "2026-01-07T06:30:10.564924+00:00",
"status": "up-to-date" "status": "up-to-date"
} }
} }

View File

@@ -51,6 +51,9 @@
"timeEstimate": "2h10m", "timeEstimate": "2h10m",
"colour": "primary", "colour": "primary",
"required": true, "required": true,
"Condition": {
"motor.id": "57AIM30"
},
"filePath": "OSSM - Base - PitClamp Mini - 57AIM30 V1.1.stl", "filePath": "OSSM - Base - PitClamp Mini - 57AIM30 V1.1.stl",
"url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Mounting/OSSM%20-%20Mounting%20Ring%20-%20PitClamp%20Mini%20-%2057AIM%20V1.1.stl?raw=true", "url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Mounting/OSSM%20-%20Mounting%20Ring%20-%20PitClamp%20Mini%20-%2057AIM%20V1.1.stl?raw=true",
"vendor": { "vendor": {
@@ -71,6 +74,9 @@
"timeEstimate": "2h10m", "timeEstimate": "2h10m",
"colour": "primary", "colour": "primary",
"required": true, "required": true,
"Condition": {
"motor.id": "42AIM30"
},
"filePath": "OSSM - Base - PitClamp Mini - 42AIM30 V1.1.stl", "filePath": "OSSM - Base - PitClamp Mini - 42AIM30 V1.1.stl",
"url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Mounting/Non-standard/OSSM%20-%20Mounting%20Ring%20-%20PitClamp%20Mini%20-%2042AIM%20V1.1.stl?raw=true", "url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Mounting/Non-standard/OSSM%20-%20Mounting%20Ring%20-%20PitClamp%20Mini%20-%2042AIM%20V1.1.stl?raw=true",
"vendor": { "vendor": {
@@ -91,6 +97,9 @@
"timeEstimate": "2h10m", "timeEstimate": "2h10m",
"colour": "primary", "colour": "primary",
"required": true, "required": true,
"Condition": {
"motor.id": "iHSV57"
},
"filePath": "OSSM - Base - PitClamp Mini - iHSV57 V1.1.stl", "filePath": "OSSM - Base - PitClamp Mini - iHSV57 V1.1.stl",
"url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Mounting/Non-standard/OSSM%20-%20Mounting%20Ring%20-%20PitClamp%20Mini%20-%20iHSV57.stl?raw=true", "url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Mounting/Non-standard/OSSM%20-%20Mounting%20Ring%20-%20PitClamp%20Mini%20-%20iHSV57.stl?raw=true",
"vendor": { "vendor": {
@@ -185,6 +194,9 @@
"timeEstimate": "5h8m", "timeEstimate": "5h8m",
"colour": "primary", "colour": "primary",
"required": true, "required": true,
"replaces": [
"ossm-actuator-body-middle"
],
"filePath": "OSSM - Actuator Body Middle Pivot.stl", "filePath": "OSSM - Actuator Body Middle Pivot.stl",
"url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Actuator/Non-standard/OSSM%20-%20Actuator%20-%20Body%20-%20Middle%20Pivot.stl?raw=true", "url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Actuator/Non-standard/OSSM%20-%20Actuator%20-%20Body%20-%20Middle%20Pivot.stl?raw=true",
"vendor": { "vendor": {

View File

@@ -66,7 +66,7 @@
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab", "pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl", "pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl",
"checksum_sha256": "55ede7dff60a31d68159b352b5f2c63792b7a0dbe9d543a43681c3e52d229115", "checksum_sha256": "55ede7dff60a31d68159b352b5f2c63792b7a0dbe9d543a43681c3e52d229115",
"last_checked": "2026-01-07T01:20:58.324330+00:00", "last_checked": "2026-01-07T06:30:07.525364+00:00",
"status": "up-to-date" "status": "up-to-date"
} }
} }
@@ -187,7 +187,7 @@
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab", "pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Extrusion Cap.stl", "pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Extrusion Cap.stl",
"checksum_sha256": "56fa9bb318cdeadc6d1698a1e6cef9371e58b0bc9c7729985bf639d8da2f25da", "checksum_sha256": "56fa9bb318cdeadc6d1698a1e6cef9371e58b0bc9c7729985bf639d8da2f25da",
"last_checked": "2026-01-07T01:21:01.205246+00:00", "last_checked": "2026-01-07T06:30:11.578686+00:00",
"status": "up-to-date" "status": "up-to-date"
} }
} }

View File

@@ -1,115 +0,0 @@
{
"toyMounts": {
"category": "Toy Mounts",
"type": "mod",
"printedParts": [
{
"id": "ossm-toy-mount-flange-base-24mm-threaded",
"name": "Toy Mount Flange Base 24mm Threaded",
"description": "Toy mount system",
"filamentEstimate": 46.26,
"timeEstimate": "1h48m",
"colour": "secondary",
"required": true,
"filePath": "ossm-toy-mount-flange-base-24mm-threaded.stl"
},
{
"id": "ossm-toy-mount-flange-base-dildo-ring-2.5in ",
"name": "Toy Mount Flange Base Dildo Ring 2.5in",
"description": "Toy mount system",
"filamentEstimate": 15.24,
"timeEstimate": "55m",
"colour": "secondary",
"required": true,
"filePath": "ossm-toy-mount-flange-base-dildo-ring-2_5in.stl"
},
{
"id": "ossm-toy-mount-flange-base-dildo-ring-2in",
"name": "Toy Mount Flange Base Dildo Ring 2in",
"description": "Toy mount system",
"filamentEstimate": 15.24,
"timeEstimate": "55m",
"colour": "secondary",
"required": true,
"filePath": "ossm-toy-mount-flange-base-dildo-ring-2in.stl"
},
{
"id": "ossm-toy-mount-double-double-24mm-threaded",
"name": "Toy Mount Double Double 24mm Threaded",
"description": "Toy mount system",
"filamentEstimate": 15.24,
"timeEstimate": "55m",
"colour": "secondary",
"required": true,
"filePath": "ossm-toy-mount-double-double-24mm-threaded.stl"
},
{
"id": "ossm-toy-mount-double-double-rail-mounted",
"name": "Toy Mount Double Double Rail Mounted",
"description": "Toy mount system",
"filamentEstimate": 15.24,
"timeEstimate": "55m",
"colour": "primary",
"required": true,
"filePath": "ossm-toy-mount-double-double-rail-mounted.stl"
},
{
"id": "ossm-toy-mount-sucson-mount-base-plate-24mm-threaded",
"name": "Toy Mount Sucson Mount Base Plate 24mm Threaded",
"description": "Toy mount system",
"filamentEstimate": 15.24,
"timeEstimate": "55m",
"colour": "secondary",
"required": true,
"filePath": "ossm-toy-mount-sucson-mount-base-plate-24mm-threaded.stl"
},
{
"id": "ossm-toy-mount-sucson-mount-ring-insert-55mm",
"name": "Toy Mount Sucson Mount Ring Insert 55mm",
"description": "Toy mount system",
"filamentEstimate": 15.24,
"timeEstimate": "55m",
"colour": "primary",
"required": true,
"filePath": "ossm-toy-mount-sucson-mount-ring-insert-55mm.stl"
},
{
"id": "ossm-toy-mount-sucson-mount-threaded-ring",
"name": "Toy Mount Sucson Mount Threaded Ring",
"description": "Toy mount system",
"filamentEstimate": 15.24,
"timeEstimate": "55m",
"colour": "secondary",
"required": true,
"filePath": "ossm-toy-mount-sucson-mount-threaded-ring.stl"
},
{
"id": "ossm-toy-mount-tie-down-and-suction-plate-110mm",
"name": "Toy Mount Tie Down and Suction Plate 110mm",
"description": "Toy mount system",
"filamentEstimate": 15.24,
"timeEstimate": "55m",
"colour": "secondary",
"required": true,
"filePath": "ossm-toy-mount-tie-down-and-suction-plate-110mm.stl"
},
{
"id": "ossm-toy-mount-tie-down-and-suction-plate-135mm",
"name": "Toy Mount Tie Down and Suction Plate 135mm",
"description": "Toy mount system",
"filamentEstimate": 15.24,
"timeEstimate": "55m",
"colour": "secondary",
"required": true,
"filePath": "ossm-toy-mount-tie-down-and-suction-plate-135mm.stl"
}
],
"hardwareParts": [
{
"id": "toy-mount-hardware",
"required": true,
"relatedParts": []
}
]
}
}

View File

@@ -0,0 +1,14 @@
import ossm from './ossm.json';
const rawParts = [ossm];
const mergedToyMounts = {
category: "Toy Mounts",
type: "mod",
printedParts: rawParts.flatMap(v => v.printedParts || []),
hardwareParts: rawParts.flatMap(v => v.hardwareParts || [])
};
export default {
toyMounts: mergedToyMounts
};

View File

@@ -0,0 +1,211 @@
{
"printedParts": [
{
"id": "ossm-toy-mount-flange-base-24mm-threaded",
"name": "Toy Mount Flange Base 24mm Threaded",
"description": "Toy mount system",
"filamentEstimate": 46.26,
"timeEstimate": "1h48m",
"colour": "secondary",
"required": true,
"filePath": "ossm-toy-mount-flange-base-24mm-threaded.stl",
"url": "https://action.github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Toy%20Mounts/OSSM%20-%20Toy%20Mount%20Flange%20Base%2024mm%20Threaded.stl?raw=true",
"vendor": {
"manifest_id": "ossm-toy-mount-flange-base-24mm-threaded",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Flange Base 24mm Threaded.stl",
"pinned_sha": null,
"pinned_raw_url": null,
"checksum_sha256": null,
"last_checked": null,
"status": "pending"
}
},
{
"id": "ossm-toy-mount-flange-base-dildo-ring-2.5in ",
"name": "Toy Mount Flange Base Dildo Ring 2.5in",
"description": "Toy mount system",
"filamentEstimate": 15.24,
"timeEstimate": "55m",
"colour": "secondary",
"required": true,
"filePath": "ossm-toy-mount-flange-base-dildo-ring-2_5in.stl",
"url": "https://action.github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Toy%20Mounts/OSSM%20-%20Toy%20Mount%20Flange%20Base%20Dildo%20Ring%202.5in.stl?raw=true",
"vendor": {
"manifest_id": "ossm-toy-mount-flange-base-dildo-ring-2.5in ",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Flange Base Dildo Ring 2.5in.stl",
"pinned_sha": null,
"pinned_raw_url": null,
"checksum_sha256": null,
"last_checked": null,
"status": "pending"
}
},
{
"id": "ossm-toy-mount-flange-base-dildo-ring-2in",
"name": "Toy Mount Flange Base Dildo Ring 2in",
"description": "Toy mount system",
"filamentEstimate": 15.24,
"timeEstimate": "55m",
"colour": "secondary",
"required": true,
"filePath": "ossm-toy-mount-flange-base-dildo-ring-2in.stl",
"url": "https://action.github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Toy%20Mounts/OSSM%20-%20Toy%20Mount%20Flange%20Base%20Dildo%20Ring%202in.stl?raw=true",
"vendor": {
"manifest_id": "ossm-toy-mount-flange-base-dildo-ring-2in",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Flange Base Dildo Ring 2in.stl",
"pinned_sha": null,
"pinned_raw_url": null,
"checksum_sha256": null,
"last_checked": null,
"status": "pending"
}
},
{
"id": "ossm-toy-mount-double-double-24mm-threaded",
"name": "Toy Mount Double Double 24mm Threaded",
"description": "Toy mount system",
"filamentEstimate": 15.24,
"timeEstimate": "55m",
"colour": "secondary",
"required": true,
"filePath": "ossm-toy-mount-double-double-24mm-threaded.stl",
"url": "https://action.github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Toy%20Mounts/OSSM%20-%20Toy%20Mount%20Double%20Double%2024mm%20Threaded.stl?raw=true",
"vendor": {
"manifest_id": "ossm-toy-mount-double-double-24mm-threaded",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Double Double 24mm Threaded.stl",
"pinned_sha": null,
"pinned_raw_url": null,
"checksum_sha256": null,
"last_checked": null,
"status": "pending"
}
},
{
"id": "ossm-toy-mount-double-double-rail-mounted",
"name": "Toy Mount Double Double Rail Mounted",
"description": "Toy mount system",
"filamentEstimate": 15.24,
"timeEstimate": "55m",
"colour": "primary",
"required": true,
"filePath": "ossm-toy-mount-double-double-rail-mounted.stl",
"url": "https://action.github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Toy%20Mounts/OSSM%20-%20Toy%20Mount%20Double%20Double%20Rail%20Mounted.stl?raw=true",
"vendor": {
"manifest_id": "ossm-toy-mount-double-double-rail-mounted",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Double Double Rail Mounted.stl",
"pinned_sha": null,
"pinned_raw_url": null,
"checksum_sha256": null,
"last_checked": null,
"status": "pending"
}
},
{
"id": "ossm-toy-mount-sucson-mount-base-plate-24mm-threaded",
"name": "Toy Mount Sucson Mount Base Plate 24mm Threaded",
"description": "Toy mount system",
"filamentEstimate": 15.24,
"timeEstimate": "55m",
"colour": "secondary",
"required": true,
"filePath": "ossm-toy-mount-sucson-mount-base-plate-24mm-threaded.stl",
"url": "https://action.github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Toy%20Mounts/OSSM%20-%20Toy%20Mount%20Sucson%20Mount%20Base%20Plate%2024mm%20Threaded.stl?raw=true",
"vendor": {
"manifest_id": "ossm-toy-mount-sucson-mount-base-plate-24mm-threaded",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Sucson Mount Base Plate 24mm Threaded.stl",
"pinned_sha": null,
"pinned_raw_url": null,
"checksum_sha256": null,
"last_checked": null,
"status": "pending"
}
},
{
"id": "ossm-toy-mount-sucson-mount-ring-insert-55mm",
"name": "Toy Mount Sucson Mount Ring Insert 55mm",
"description": "Toy mount system",
"filamentEstimate": 15.24,
"timeEstimate": "55m",
"colour": "primary",
"required": true,
"filePath": "ossm-toy-mount-sucson-mount-ring-insert-55mm.stl",
"url": "https://action.github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Toy%20Mounts/OSSM%20-%20Toy%20Mount%20Sucson%20Mount%20Ring%20Insert%2055mm.stl?raw=true",
"vendor": {
"manifest_id": "ossm-toy-mount-sucson-mount-ring-insert-55mm",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Sucson Mount Ring Insert 55mm.stl",
"pinned_sha": null,
"pinned_raw_url": null,
"checksum_sha256": null,
"last_checked": null,
"status": "pending"
}
},
{
"id": "ossm-toy-mount-sucson-mount-threaded-ring",
"name": "Toy Mount Sucson Mount Threaded Ring",
"description": "Toy mount system",
"filamentEstimate": 15.24,
"timeEstimate": "55m",
"colour": "secondary",
"required": true,
"filePath": "ossm-toy-mount-sucson-mount-threaded-ring.stl",
"url": "https://action.github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Toy%20Mounts/OSSM%20-%20Toy%20Mount%20Sucson%20Mount%20Threaded%20Ring.stl?raw=true",
"vendor": {
"manifest_id": "ossm-toy-mount-sucson-mount-threaded-ring",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Sucson Mount Threaded Ring.stl",
"pinned_sha": null,
"pinned_raw_url": null,
"checksum_sha256": null,
"last_checked": null,
"status": "pending"
}
},
{
"id": "ossm-toy-mount-tie-down-and-suction-plate-110mm",
"name": "Toy Mount Tie Down and Suction Plate 110mm",
"description": "Toy mount system",
"filamentEstimate": 15.24,
"timeEstimate": "55m",
"colour": "secondary",
"required": true,
"filePath": "ossm-toy-mount-tie-down-and-suction-plate-110mm.stl",
"url": "https://action.github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Toy%20Mounts/OSSM%20-%20Toy%20Mount%20Tie%20Down%20and%20Suction%20Plate%20110mm.stl?raw=true",
"vendor": {
"manifest_id": "ossm-toy-mount-tie-down-and-suction-plate-110mm",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Tie Down and Suction Plate 110mm.stl",
"pinned_sha": null,
"pinned_raw_url": null,
"checksum_sha256": null,
"last_checked": null,
"status": "pending"
}
},
{
"id": "ossm-toy-mount-tie-down-and-suction-plate-135mm",
"name": "Toy Mount Tie Down and Suction Plate 135mm",
"description": "Toy mount system",
"filamentEstimate": 15.24,
"timeEstimate": "55m",
"colour": "secondary",
"required": true,
"filePath": "ossm-toy-mount-tie-down-and-suction-plate-135mm.stl",
"url": "https://action.github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Toy%20Mounts/OSSM%20-%20Toy%20Mount%20Tie%20Down%20and%20Suction%20Plate%20135mm.stl?raw=true",
"vendor": {
"manifest_id": "ossm-toy-mount-tie-down-and-suction-plate-135mm",
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Tie Down and Suction Plate 135mm.stl",
"pinned_sha": null,
"pinned_raw_url": null,
"checksum_sha256": null,
"last_checked": null,
"status": "pending"
}
}
],
"hardwareParts": [
{
"id": "toy-mount-hardware",
"required": true,
"relatedParts": []
}
]
}

View File

@@ -1,12 +1,12 @@
import motors from './motors.json'; import motors from './components/motors.json';
import powerSupplies from './powerSupplies.json'; import powerSupplies from './components/powerSupplies.json';
import optionsData from './options.json'; import optionsData from './config/options.json';
import colors from './colors.json'; import colors from './common/colors.json';
import hardwareData from './hardware.json'; import hardwareData from './common/hardware.json';
import actuatorComponents from './components/actuator.json'; import actuatorComponents from './components/actuator.json';
import standComponents from './components/stand.json'; import standComponents from './components/stand.json';
import mountingComponents from './components/mounting.json'; import mountingComponents from './components/mounting.json';
import toyMountsComponents from './components/toyMounts.json'; import toyMountsComponents from './components/toyMounts/index.js';
import remoteComponents from './components/remote.json'; import remoteComponents from './components/remote.json';
import pcbComponents from './components/pcb.json'; import pcbComponents from './components/pcb.json';

View File

@@ -279,9 +279,9 @@ export const generateExcelPrintList = (printedParts, filamentTotals) => {
summaryWorksheet.getCell('A3').value = 'Total Parts'; summaryWorksheet.getCell('A3').value = 'Total Parts';
summaryWorksheet.getCell('B3').value = printedParts.length; summaryWorksheet.getCell('B3').value = printedParts.length;
summaryWorksheet.getCell('A4').value = 'Completed Parts'; summaryWorksheet.getCell('A4').value = 'Completed Parts';
summaryWorksheet.getCell('B4').formula = `COUNTIF('Print List'.H:H,"✓")`; summaryWorksheet.getCell('B4').value = { formula: `COUNTIF('Print List'.H:H,"✓")` };
summaryWorksheet.getCell('A5').value = 'Progress %'; summaryWorksheet.getCell('A5').value = 'Progress %';
summaryWorksheet.getCell('B5').formula = `IF(B3>0, (B4/B3)*100, 0)`; summaryWorksheet.getCell('B5').value = { formula: `IF(B3>0, (B4/B3)*100, 0)` };
summaryWorksheet.getCell('A7').value = 'Filament Summary'; summaryWorksheet.getCell('A7').value = 'Filament Summary';
summaryWorksheet.getCell('A8').value = 'Total Filament (g)'; summaryWorksheet.getCell('A8').value = 'Total Filament (g)';
summaryWorksheet.getCell('B8').value = filamentTotals.total.toFixed(2); summaryWorksheet.getCell('B8').value = filamentTotals.total.toFixed(2);