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
run: |
python scripts/generate_manifest_from_site.py \
--site-dir website/src/data/components \
--manifest manifest/vendor_manifest.json
python scripts/vendor_update.py --scan-only
- name: Check for updates
id: check-updates
@@ -116,7 +114,7 @@ jobs:
fi
git commit -m "chore: update vendored files
Updated $(echo "${{ steps.get-entries.outputs.entry_ids }}" | tr ',' ' ' | wc -w) vendored file(s):
$(echo "${{ steps.get-entries.outputs.entry_ids }}" | tr ',' '\n' | sed 's/^/ - /')

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
```
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.
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.
---
## 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:
- **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
## 📸 Guided Walkthrough
### 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/
├── manifest/
│ └── vendor_manifest.json # Canonical list of vendored files
├── vendor/ # Vendored file copies
│ └── owner-repo/ # Organized by repository
│ └── path/to/file.stl
├── scripts/
│ ├── generate_manifest_from_site.py # Generate manifest from component JSONs
│ ├── vendor_update.py # Download and pin files
│ └── 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
```
Access the app at `http://localhost:5173`.
### 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:**
### Local Development
1. **Clone the repo**
2. **Setup Website**:
```bash
cd website
npm install
npm run dev
```
3. **Setup Logic** (Optional, for vendor updates):
```bash
cd scripts
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:
- 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
## 🗺️ Project Roadmap
### 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
python scripts/generate_manifest_from_site.py \
--site-dir website/src/data/components \
--manifest manifest/vendor_manifest.json
```
Contributions are welcome! Whether you are adding a new remote, a toy mount, or a hardware mod, please refer to our detailed guide:
This script:
- 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`)
👉 **[Read the CONTRIBUTING.md](./CONTRIBUTING.md)**
#### 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
# Update all entries
python scripts/vendor_update.py \
--manifest manifest/vendor_manifest.json
## 📜 License
# Update a specific entry
python scripts/vendor_update.py \
--manifest manifest/vendor_manifest.json \
--entry ossm-actuator-body-bottom
This project is part of the **Open Source Sex Machine (OSSM)** project. Please refer to the main OSSM project for full license details.
# Dry run (see what would be done)
python scripts/vendor_update.py \
--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.
---
*Built with ❤️ by the OSSM Community.*

View File

@@ -4,495 +4,639 @@
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl",
"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_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",
"last_checked": "2026-01-07T01:20:58.324330+00:00",
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"status": "up-to-date",
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
"orig_site_json": "website/src/data/components/stand.json",
"orig_item_id": "handle-spacer"
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
},
{
"id": "ossm-24mm-clamping-thread-belt-clamp",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Actuator/OSSM - 24mm Clamping Thread - Belt Clamp.stl",
"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_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",
"last_checked": "2026-01-07T01:20:58.945151+00:00",
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"status": "up-to-date",
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
"orig_site_json": "website/src/data/components/actuator.json",
"orig_item_id": "ossm-24mm-clamping-thread-belt-clamp"
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
},
{
"id": "ossm-24mm-clamping-thread-end-effector",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Actuator/OSSM - 24mm Clamping Thread - End Effector.stl",
"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_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",
"last_checked": "2026-01-07T01:20:59.854476+00:00",
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"status": "up-to-date",
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
"orig_site_json": "website/src/data/components/actuator.json",
"orig_item_id": "ossm-24mm-clamping-thread-end-effector"
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
},
{
"id": "ossm-24mm-nut-5-sided",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Actuator/OSSM - 24mm Nut - 5 Sided.stl",
"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_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",
"last_checked": "2026-01-07T01:21:00.555525+00:00",
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"status": "up-to-date",
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
"orig_site_json": "website/src/data/components/actuator.json",
"orig_item_id": "ossm-24mm-nut-5-sided"
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
},
{
"id": "ossm-3030-cap",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Extrusion Cap.stl",
"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_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",
"last_checked": "2026-01-07T01:21:01.205246+00:00",
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"status": "up-to-date",
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
"orig_site_json": "website/src/data/components/stand.json",
"orig_item_id": "ossm-3030-cap"
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
},
{
"id": "ossm-actuator-body-bottom",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Actuator/OSSM - Actuator - Body - Bottom.stl",
"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_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",
"last_checked": "2026-01-07T01:21:02.027595+00:00",
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"status": "up-to-date",
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
"orig_site_json": "website/src/data/components/actuator.json",
"orig_item_id": "ossm-actuator-body-bottom"
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
},
{
"id": "ossm-actuator-body-cover",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Actuator/OSSM - Actuator - Body - Cover.stl",
"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_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",
"last_checked": "2026-01-07T01:21:02.767604+00:00",
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"status": "up-to-date",
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
"orig_site_json": "website/src/data/components/actuator.json",
"orig_item_id": "ossm-actuator-body-cover"
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
},
{
"id": "ossm-actuator-body-middle",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Actuator/OSSM - Actuator - Body - Middle.stl",
"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_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",
"last_checked": "2026-01-07T01:21:03.531342+00:00",
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"status": "up-to-date",
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
"orig_site_json": "website/src/data/components/actuator.json",
"orig_item_id": "ossm-actuator-body-middle"
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
},
{
"id": "ossm-actuator-body-middle-pivot",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Actuator/Non-standard/OSSM - Actuator - Body - Middle Pivot.stl",
"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_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",
"last_checked": "2026-01-07T01:21:04.528132+00:00",
"upstream_latest_sha": "ad39a03b628b8e38549b99036c8dfd4131948545",
"status": "up-to-date",
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/ad39a03b628b8e38549b99036c8dfd4131948545/LICENCE",
"orig_site_json": "website/src/data/components/mounting.json",
"orig_item_id": "ossm-actuator-body-middle-pivot"
"upstream_latest_sha": "ad39a03b628b8e38549b99036c8dfd4131948545"
},
{
"id": "ossm-belt-tensioner",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Actuator/OSSM - Belt Tensioner.stl",
"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_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",
"last_checked": "2026-01-07T01:21:05.499523+00:00",
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"status": "up-to-date",
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
"orig_site_json": "website/src/data/components/actuator.json",
"orig_item_id": "ossm-belt-tensioner"
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
},
{
"id": "ossm-handle-spacer",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl",
"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_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl",
"checksum_sha256": null,
"last_checked": null,
"upstream_latest_sha": null,
"status": "error",
"license": null,
"orig_site_json": "website/src/data/components/mounting.json",
"orig_item_id": "ossm-handle-spacer"
"upstream_latest_sha": null
},
{
"id": "ossm-pcb-3030-mount",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/PCB/OSSM - PCB - 3030 Mount.stl",
"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_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/PCB/OSSM - PCB - 3030 Mount.stl",
"checksum_sha256": null,
"last_checked": null,
"upstream_latest_sha": null,
"status": "error",
"license": null,
"orig_site_json": "website/src/data/components/pcb.json",
"orig_item_id": "ossm-pcb-3030-mount"
"upstream_latest_sha": null
},
{
"id": "ossm-pcb-3030-mount-cover",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/PCB/OSSM - PCB - 3030 Mount Cover.stl",
"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_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/PCB/OSSM - PCB - 3030 Mount Cover.stl",
"checksum_sha256": null,
"last_checked": null,
"upstream_latest_sha": null,
"status": "error",
"license": null,
"orig_site_json": "website/src/data/components/pcb.json",
"orig_item_id": "ossm-pcb-3030-mount-cover"
"upstream_latest_sha": null
},
{
"id": "ossm-pcb-aio-cover-mount",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/PCB/OSSM - PCB - AIO Cover Mount.stl",
"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_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/PCB/OSSM - PCB - AIO Cover Mount.stl",
"checksum_sha256": null,
"last_checked": null,
"upstream_latest_sha": null,
"status": "error",
"license": null,
"orig_site_json": "website/src/data/components/pcb.json",
"orig_item_id": "ossm-pcb-aio-cover-mount"
"upstream_latest_sha": null
},
{
"id": "ossm-pitclamp-mini-42AIM30",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Mounting/Non-standard/OSSM - Mounting Ring - PitClamp Mini - 42AIM V1.1.stl",
"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_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,
"last_checked": null,
"upstream_latest_sha": null,
"status": "error",
"license": null,
"orig_site_json": "website/src/data/components/mounting.json",
"orig_item_id": "ossm-pitclamp-mini-42AIM30"
"upstream_latest_sha": null
},
{
"id": "ossm-pitclamp-mini-57AIM30",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Mounting/OSSM - Mounting Ring - PitClamp Mini - 57AIM V1.1.stl",
"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_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Mounting Ring - PitClamp Mini - 57AIM V1.1.stl",
"checksum_sha256": null,
"last_checked": null,
"upstream_latest_sha": null,
"status": "error",
"license": null,
"orig_site_json": "website/src/data/components/mounting.json",
"orig_item_id": "ossm-pitclamp-mini-57AIM30"
"upstream_latest_sha": null
},
{
"id": "ossm-pitclamp-mini-dogbone-bolts ",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Dogbone Bolts.stl",
"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_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Dogbone Bolts.stl",
"checksum_sha256": null,
"last_checked": null,
"upstream_latest_sha": null,
"status": "error",
"license": null,
"orig_site_json": "website/src/data/components/mounting.json",
"orig_item_id": "ossm-pitclamp-mini-dogbone-bolts "
"upstream_latest_sha": null
},
{
"id": "ossm-pitclamp-mini-dogbone-nuts",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Dogbone Nuts.stl",
"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_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Dogbone Nuts.stl",
"checksum_sha256": null,
"last_checked": null,
"upstream_latest_sha": null,
"status": "error",
"license": null,
"orig_site_json": "website/src/data/components/mounting.json",
"orig_item_id": "ossm-pitclamp-mini-dogbone-nuts"
"upstream_latest_sha": null
},
{
"id": "ossm-pitclamp-mini-handle",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Handle.stl",
"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_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Handle.stl",
"checksum_sha256": null,
"last_checked": null,
"upstream_latest_sha": null,
"status": "error",
"license": null,
"orig_site_json": "website/src/data/components/mounting.json",
"orig_item_id": "ossm-pitclamp-mini-handle"
"upstream_latest_sha": null
},
{
"id": "ossm-pitclamp-mini-iHSV57",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Mounting/Non-standard/OSSM - Mounting Ring - PitClamp Mini - iHSV57.stl",
"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_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/Non-standard/OSSM - Mounting Ring - PitClamp Mini - iHSV57.stl",
"checksum_sha256": null,
"last_checked": null,
"upstream_latest_sha": null,
"status": "error",
"license": null,
"orig_site_json": "website/src/data/components/mounting.json",
"orig_item_id": "ossm-pitclamp-mini-iHSV57"
"upstream_latest_sha": null
},
{
"id": "ossm-pitclamp-mini-lower",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Lower V1.1.stl",
"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_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Lower V1.1.stl",
"checksum_sha256": null,
"last_checked": null,
"upstream_latest_sha": null,
"status": "error",
"license": null,
"orig_site_json": "website/src/data/components/mounting.json",
"orig_item_id": "ossm-pitclamp-mini-lower"
"upstream_latest_sha": null
},
{
"id": "ossm-pitclamp-mini-upper",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Upper V1.1.stl",
"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_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Upper V1.1.stl",
"checksum_sha256": null,
"last_checked": null,
"upstream_latest_sha": null,
"status": "error",
"license": null,
"orig_site_json": "website/src/data/components/mounting.json",
"orig_item_id": "ossm-pitclamp-mini-upper"
"upstream_latest_sha": null
},
{
"id": "ossm-remote-body",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Remote/OSSM - Remote - Body.stl",
"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_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/OSSM - Remote - Body.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_item_id": "ossm-remote-body"
"upstream_latest_sha": null
},
{
"id": "ossm-remote-knob",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Remote/OSSM - Remote - Knob - Rounded.stl",
"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_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/OSSM - Remote - Knob - Rounded.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_item_id": "ossm-remote-knob"
"upstream_latest_sha": null
},
{
"id": "ossm-remote-knob-knurled",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Knurled.stl",
"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_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Knurled.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_item_id": "ossm-remote-knob-knurled"
"upstream_latest_sha": null
},
{
"id": "ossm-remote-knob-knurled-with-position-indicator",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Knurled With Position Indicator.stl",
"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",
"checksum_sha256": null,
"last_checked": null,
"upstream_latest_sha": null,
"status": "error",
"license": null,
"orig_site_json": "website/src/data/components/remote.json",
"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",
"orig_item_id": "ossm-remote-knob-knurled-with-position-indicator",
"pinned_sha": null,
"pinned_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Simple.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_item_id": "ossm-remote-knob-simple"
"upstream_latest_sha": null
},
{
"id": "ossm-remote-knob-simple-with-position-indicator",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Simple With Position Indicator.stl",
"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_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,
"last_checked": null,
"upstream_latest_sha": null,
"status": "error",
"license": null,
"orig_site_json": "website/src/data/components/remote.json",
"orig_item_id": "ossm-remote-knob-simple-with-position-indicator"
"upstream_latest_sha": null
},
{
"id": "ossm-remote-top-cover",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Remote/OSSM - Remote - Top Cover.stl",
"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_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/OSSM - Remote - Top Cover.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_item_id": "ossm-remote-top-cover"
"upstream_latest_sha": null
},
{
"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",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Pivot Plate Left.stl",
"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_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Pivot Plate Left.stl",
"checksum_sha256": null,
"last_checked": null,
"upstream_latest_sha": null,
"status": "error",
"license": null,
"orig_site_json": "website/src/data/components/stand.json",
"orig_item_id": "pivot-plate"
"upstream_latest_sha": null
},
{
"id": "pivot-plate-right",
"source_repo": "KinkyMakers/OSSM-hardware",
"source_path": "Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Pivot Plate Right.stl",
"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_raw_url": null,
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Pivot Plate Right.stl",
"checksum_sha256": null,
"last_checked": null,
"upstream_latest_sha": null,
"status": "error",
"license": null,
"orig_site_json": "website/src/data/components/stand.json",
"orig_item_id": "pivot-plate-right"
"upstream_latest_sha": null
}
]

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.
Downloads files specified in manifest, pins them to commit SHAs,
computes checksums, and optionally syncs vendor metadata back to site JSON files.
Automatically scans website/src/data/components for parts with GitHub URLs,
updates the manifest, and then downloads/pins files.
"""
import argparse
@@ -14,8 +14,8 @@ import sys
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict, List, Optional
from urllib.parse import urlparse
from typing import Dict, List, Optional, Tuple, Generator, Any
from urllib.parse import urlparse, unquote, parse_qs
import requests
@@ -226,6 +226,182 @@ def download_file(url: str, dest_path: Path) -> bool:
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(
entry: Dict,
api: GitHubAPI,
@@ -254,6 +430,31 @@ def update_manifest_entry(
local_path = Path(entry['local_path'])
if not local_path.is_absolute():
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:
print(f" [DRY RUN] Would download to {local_path}")
@@ -309,45 +510,24 @@ def sync_to_site_json(entry: Dict, repo_root: Path) -> bool:
data = json.load(f)
# 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):
# Check if this is a printedParts array
if 'printedParts' in obj and isinstance(obj['printedParts'], list):
for part in obj['printedParts']:
if isinstance(part, dict) and part.get('id') == target_id:
# Update this part
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
# Check bodyParts, knobs, etc.
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
# If this object IS the part (has the ID)
if obj.get('id') == target_id:
if 'vendor' not in obj:
obj['vendor'] = {}
obj['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 values
for value in obj.values():
if find_and_update_part(value, target_id):
return True
@@ -396,9 +576,9 @@ def main():
help='Show what would be done without downloading files'
)
parser.add_argument(
'--sync-site',
'--no-sync',
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(
'--delay',
@@ -406,6 +586,16 @@ def main():
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()
@@ -414,13 +604,30 @@ def main():
manifest_path = (script_dir / args.manifest).resolve()
repo_root = script_dir
if not manifest_path.exists():
print(f"Error: Manifest file not found: {manifest_path}", file=sys.stderr)
sys.exit(1)
# Load manifest
with open(manifest_path, 'r', encoding='utf-8') as f:
manifest_data = json.load(f)
# 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():
print(f"Error: Manifest file not found: {manifest_path}", file=sys.stderr)
sys.exit(1)
with open(manifest_path, 'r', encoding='utf-8') as f:
manifest_data = json.load(f)
# Convert to dict if it's a list
if isinstance(manifest_data, list):
@@ -446,7 +653,7 @@ def main():
updated_entry = update_manifest_entry(entry, api, repo_root, dry_run=args.dry_run)
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)
updated_count += 1

View File

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

File diff suppressed because it is too large Load Diff

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 }) {
const handleSelect = (buildType) => {
@@ -6,8 +6,8 @@ export default function MainPage({ onSelectBuildType }) {
};
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
<div className="max-w-4xl mx-auto px-4">
<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 flex-grow">
{/* Header */}
<div className="mb-12 text-center">
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-2">
@@ -23,7 +23,7 @@ export default function MainPage({ onSelectBuildType }) {
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6 text-center">
Select Your Build Type
</h2>
<div className="grid md:grid-cols-3 gap-6">
{/* New Build - RAD Kit */}
<button
@@ -120,6 +120,7 @@ export default function MainPage({ onSelectBuildType }) {
</div>
</div>
</div>
<Footer />
</div>
);
}

View File

@@ -6,6 +6,7 @@ import OptionsStep from './steps/OptionsStep';
import RemoteStep from './steps/RemoteStep';
import ToyMountStep from './steps/ToyMountStep';
import BOMSummary from './BOMSummary';
import Footer from './Footer';
const steps = [
{ id: 'motor', name: 'Motor', component: MotorStep },
@@ -87,7 +88,7 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
// Power Supply step - require power supply selection
return;
}
if (currentStep < filteredSteps.length - 1) {
setCurrentStep(currentStep + 1);
}
@@ -104,17 +105,17 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
if (stepIndex <= currentStep) {
return true;
}
// In upgrade mode, no validation needed
if (buildType === 'upgrade') {
return true;
}
// In RAD Kit mode, all steps are pre-selected, so navigation is always allowed
if (buildType === 'rad-kit') {
return true;
}
// Check if required steps are completed before jumping ahead
if (stepIndex > 0 && !config.motor) {
return false; // Can't skip motor step
@@ -122,7 +123,7 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
if (stepIndex > 1 && !config.powerSupply) {
return false; // Can't skip power supply step
}
return true;
};
@@ -137,7 +138,7 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
if (buildType === 'upgrade') {
return true;
}
if (currentStep === 0 && !config.motor) {
return false; // Motor step - require motor selection
}
@@ -158,8 +159,8 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
}, [buildType, currentStep]);
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
<div className="max-w-4xl mx-auto px-4">
<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 flex-grow w-full">
{/* Back Button */}
{onBackToMain && (
<div className="mb-4">
@@ -191,7 +192,7 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
OSSM Configurator
</h1>
<p className="text-gray-600 dark:text-gray-300">
{buildType === 'upgrade'
{buildType === 'upgrade'
? 'Select upgrade components and modifications'
: 'Configure your Open Source Sex Machine'}
</p>
@@ -212,17 +213,15 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
{/* Circle */}
<button
onClick={() => goToStep(index)}
className={`w-10 h-10 rounded-full flex items-center justify-center font-semibold flex-shrink-0 z-10 ${
index === currentStep
? 'bg-blue-600 dark:bg-blue-500 text-white'
: index < currentStep
className={`w-10 h-10 rounded-full flex items-center justify-center font-semibold flex-shrink-0 z-10 ${index === currentStep
? 'bg-blue-600 dark:bg-blue-500 text-white'
: index < currentStep
? 'bg-green-500 dark:bg-green-600 text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-500 dark:text-gray-400'
} ${
index <= currentStep
} ${index <= currentStep
? 'cursor-pointer hover:opacity-80'
: 'cursor-not-allowed'
}`}
}`}
disabled={!canNavigateToStep(index)}
>
{index < currentStep ? '✓' : index + 1}
@@ -230,10 +229,9 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
{/* Connecting line to the right */}
{index < filteredSteps.length - 1 && (
<div
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'
}`}
style={{
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'
}`}
style={{
width: 'calc(100% - 40px)',
marginLeft: '20px'
}}
@@ -242,11 +240,10 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
{/* Text label */}
<button
onClick={() => goToStep(index)}
className={`mt-2 text-sm font-medium text-center ${
index <= currentStep
? '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'
}`}
className={`mt-2 text-sm font-medium text-center ${index <= currentStep
? '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'
}`}
disabled={!canNavigateToStep(index)}
>
{step.name}
@@ -273,11 +270,10 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
<button
onClick={prevStep}
disabled={currentStep === 0}
className={`px-6 py-2 rounded-lg font-medium ${
currentStep === 0
? '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'
}`}
className={`px-6 py-2 rounded-lg font-medium ${currentStep === 0
? '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'
}`}
>
Previous
</button>
@@ -285,11 +281,10 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
<button
onClick={nextStep}
disabled={!canProceedToNextStep()}
className={`px-6 py-2 rounded-lg font-medium ${
canProceedToNextStep()
? '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'
}`}
className={`px-6 py-2 rounded-lg font-medium ${canProceedToNextStep()
? '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'
}`}
>
Next
</button>
@@ -297,6 +292,7 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
</div>
)}
</div>
<Footer />
</div>
);
}

View File

@@ -33,7 +33,7 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
const updates = {
remoteType: remoteId,
};
// Reset PCB selection when switching remotes
if (remoteId === 'ossm-remote-radr') {
// RADR only available from RAD
@@ -41,10 +41,10 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
} else {
updates.remotePCB = null;
}
// Clear knob selection when switching remotes
updates.remoteKnob = null;
updateConfig(updates);
};
@@ -64,7 +64,7 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
const getAvailableKnobs = () => {
const remoteSystem = getSelectedRemoteSystem();
if (!remoteSystem || !remoteSystem.knobs) return [];
return remoteSystem.knobs.map((knob) => ({
id: knob.id,
name: knob.name,
@@ -88,11 +88,10 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
<button
key={remote.id}
onClick={() => handleRemoteSelect(remote.id)}
className={`p-4 border-2 rounded-lg text-left transition-all w-full ${
isSelected
className={`p-4 border-2 rounded-lg text-left transition-all w-full ${isSelected
? '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'
}`}
}`}
>
{imagePath && (
<div className="mb-3 flex justify-center">
@@ -147,21 +146,19 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
<div className="flex gap-4">
<button
onClick={() => handlePCBSelect('rad')}
className={`px-4 py-2 border-2 rounded-lg transition-all ${
selectedRemotePCB === 'rad'
className={`px-4 py-2 border-2 rounded-lg transition-all ${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-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
}`}
}`}
>
Purchase from RAD
</button>
<button
onClick={() => handlePCBSelect('pcbway')}
className={`px-4 py-2 border-2 rounded-lg transition-all ${
selectedRemotePCB === 'pcbway'
className={`px-4 py-2 border-2 rounded-lg transition-all ${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-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
}`}
}`}
>
Self-source with PCBWay
</button>
@@ -177,11 +174,10 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
<button
key={knob.id}
onClick={() => handleKnobSelect(knob)}
className={`p-4 border-2 rounded-lg text-left transition-all w-full ${
isSelected
className={`p-4 border-2 rounded-lg text-left transition-all w-full ${isSelected
? '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'
}`}
}`}
>
<div className="flex items-start justify-between mb-2">
<div className="flex-1">
@@ -243,7 +239,7 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
{/* Remote Selection */}
<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">
{availableRemotesFiltered.map((remote) => renderRemoteCard(remote))}
</div>
@@ -285,9 +281,8 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
)}
</div>
<svg
className={`w-4 h-4 text-gray-500 dark:text-gray-400 transition-transform ${
expandedKnobs ? 'transform rotate-180' : ''
}`}
className={`w-4 h-4 text-gray-500 dark:text-gray-400 transition-transform ${expandedKnobs ? 'transform rotate-180' : ''
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"

View File

@@ -19,7 +19,7 @@
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Bottom.stl",
"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"
}
},
@@ -39,7 +39,7 @@
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Middle.stl",
"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"
}
},
@@ -51,6 +51,9 @@
"timeEstimate": "1h3m",
"colour": "primary",
"required": true,
"Condition": {
"cover.id": "standard-cover"
},
"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",
"vendor": {
@@ -59,7 +62,7 @@
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Cover.stl",
"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"
}
},
@@ -99,7 +102,7 @@
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - Belt Clamp.stl",
"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"
}
},
@@ -119,7 +122,7 @@
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - End Effector.stl",
"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"
}
},
@@ -139,7 +142,7 @@
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Nut - 5 Sided.stl",
"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"
}
}

View File

@@ -51,6 +51,9 @@
"timeEstimate": "2h10m",
"colour": "primary",
"required": true,
"Condition": {
"motor.id": "57AIM30"
},
"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",
"vendor": {
@@ -71,6 +74,9 @@
"timeEstimate": "2h10m",
"colour": "primary",
"required": true,
"Condition": {
"motor.id": "42AIM30"
},
"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",
"vendor": {
@@ -91,6 +97,9 @@
"timeEstimate": "2h10m",
"colour": "primary",
"required": true,
"Condition": {
"motor.id": "iHSV57"
},
"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",
"vendor": {
@@ -185,6 +194,9 @@
"timeEstimate": "5h8m",
"colour": "primary",
"required": true,
"replaces": [
"ossm-actuator-body-middle"
],
"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",
"vendor": {

View File

@@ -66,7 +66,7 @@
"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",
"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"
}
}
@@ -187,7 +187,7 @@
"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",
"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"
}
}

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 powerSupplies from './powerSupplies.json';
import optionsData from './options.json';
import colors from './colors.json';
import hardwareData from './hardware.json';
import motors from './components/motors.json';
import powerSupplies from './components/powerSupplies.json';
import optionsData from './config/options.json';
import colors from './common/colors.json';
import hardwareData from './common/hardware.json';
import actuatorComponents from './components/actuator.json';
import standComponents from './components/stand.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 pcbComponents from './components/pcb.json';
@@ -21,10 +21,10 @@ Object.values(hardwareData).forEach((category) => {
// Function to resolve hardware references (IDs) to full hardware definitions
const resolveHardwareReferences = (components) => {
const resolvedComponents = {};
Object.entries(components).forEach(([componentKey, component]) => {
resolvedComponents[componentKey] = { ...component };
// Resolve hardwareParts if they exist
if (component.hardwareParts) {
resolvedComponents[componentKey].hardwareParts = component.hardwareParts.map((hw) => {
@@ -51,7 +51,7 @@ const resolveHardwareReferences = (components) => {
};
});
}
// Also resolve hardwareParts in systems
if (component.systems) {
resolvedComponents[componentKey].systems = {};
@@ -82,7 +82,7 @@ const resolveHardwareReferences = (components) => {
});
}
});
return resolvedComponents;
};
@@ -133,7 +133,7 @@ const convertComponentPartsToOptions = (componentIds, componentData) => {
});
return allKnobs;
}
// New structure: systems with printedParts and hardwareParts (for hinges, etc.)
return componentIds
.map((systemId) => {
@@ -142,12 +142,12 @@ const convertComponentPartsToOptions = (componentIds, componentData) => {
console.warn(`Component system not found: ${systemId}`);
return null;
}
// Calculate total filament estimate from printed parts
const totalFilament = (system.printedParts || system.bodyParts)?.reduce((sum, part) => {
return sum + (part.filamentEstimate || 0);
}, 0) || 0;
return {
id: systemId,
name: system.name,
@@ -176,7 +176,7 @@ const convertComponentPartsToOptions = (componentIds, componentData) => {
console.warn(`Component part not found: ${componentId}`);
return null;
}
return {
id: part.id,
name: part.name,
@@ -196,24 +196,24 @@ const convertComponentPartsToOptions = (componentIds, componentData) => {
// Merge component options into options
const processOptions = (options, componentsData) => {
const processedOptions = { ...options };
// Process each option category
Object.keys(processedOptions).forEach((optionKey) => {
const optionCategory = processedOptions[optionKey];
if (!optionCategory || !optionCategory.sections) return;
const sections = { ...optionCategory.sections };
const categoryUseComponents = optionCategory.useComponents;
// Convert component parts to options format for each section
Object.keys(sections).forEach((sectionKey) => {
const section = sections[sectionKey];
// Check if section has componentIds to process
if (section.componentIds !== undefined) {
// Determine which component category to use
const componentKey = section.useComponents || categoryUseComponents;
if (componentKey) {
const componentData = componentsData[componentKey];
const options = convertComponentPartsToOptions(section.componentIds, componentData);
@@ -224,19 +224,19 @@ const processOptions = (options, componentsData) => {
console.warn(`No useComponents specified for ${optionKey}.${sectionKey}`);
section.options = [];
}
// Clean up temporary properties
delete section.componentIds;
delete section.useComponents;
}
});
processedOptions[optionKey].sections = sections;
if (categoryUseComponents) {
delete processedOptions[optionKey].useComponents;
}
});
return processedOptions;
};

View File

@@ -3,10 +3,10 @@ import ExcelJS from 'exceljs';
// Generate markdown overview
export const generateMarkdownOverview = (config, printedParts, hardwareParts, filamentTotals, totalTime, total) => {
const md = [];
md.push('# OSSM Build Configuration');
md.push(`\n**Generated:** ${new Date().toLocaleString()}\n`);
// Motor
if (config.motor) {
md.push(`## Motor: ${config.motor.name}`);
@@ -15,38 +15,38 @@ export const generateMarkdownOverview = (config, printedParts, hardwareParts, fi
md.push(`- **Wattage:** ${config.motor.wattage}`);
md.push('');
}
// Power Supply
if (config.powerSupply) {
md.push(`## Power Supply: ${config.powerSupply.name}`);
md.push(`- **Price:** ${config.powerSupply.price}`);
md.push('');
}
// Colors
md.push(`## Colors`);
md.push(`- **Primary:** ${config.primaryColor || 'Not selected'}`);
md.push(`- **Accent:** ${config.accentColor || 'Not selected'}`);
md.push('');
// Mount
if (config.mount) {
md.push(`## Mount: ${config.mount.name}`);
md.push('');
}
// Cover
if (config.cover) {
md.push(`## Cover: ${config.cover.name}`);
md.push('');
}
// PCB Mount
if (config.pcbMount) {
md.push(`## PCB Mount: ${config.pcbMount.name}`);
md.push('');
}
// Stand Options
if (config.standHinge || config.standFeet || config.standCrossbarSupports?.length > 0) {
md.push(`## Stand Options`);
@@ -57,7 +57,7 @@ export const generateMarkdownOverview = (config, printedParts, hardwareParts, fi
}
md.push('');
}
// Remote
if (config.remote || config.remoteKnob) {
md.push(`## Remote`);
@@ -65,7 +65,7 @@ export const generateMarkdownOverview = (config, printedParts, hardwareParts, fi
if (config.remoteKnob) md.push(`- **Knob:** ${config.remoteKnob.name}`);
md.push('');
}
// Filament Summary
if (filamentTotals.total > 0) {
md.push(`## Filament Summary`);
@@ -75,12 +75,12 @@ export const generateMarkdownOverview = (config, printedParts, hardwareParts, fi
md.push(`- **Estimated Print Time:** ${totalTime}`);
md.push('');
}
// Print Parts Summary
if (printedParts.length > 0) {
md.push(`## Printed Parts Summary`);
md.push(`- **Total Parts:** ${printedParts.length}`);
// Group by category
const partsByCategory = {};
printedParts.forEach(part => {
@@ -90,7 +90,7 @@ export const generateMarkdownOverview = (config, printedParts, hardwareParts, fi
}
partsByCategory[category].push(part);
});
Object.entries(partsByCategory).forEach(([category, parts]) => {
md.push(`### ${category} (${parts.length} parts)`);
parts.forEach(part => {
@@ -99,31 +99,31 @@ export const generateMarkdownOverview = (config, printedParts, hardwareParts, fi
});
md.push('');
}
// Hardware Summary
if (hardwareParts.length > 0) {
md.push(`## Hardware Summary`);
md.push(`- **Total Items:** ${hardwareParts.length}`);
md.push('');
}
// Cost Summary
if (total > 0) {
md.push(`## Estimated Cost`);
md.push(`- **Total:** $${total.toFixed(2)}`);
md.push('');
}
return md.join('\n');
};
// Generate Excel BOM with purchase links
export const generateExcelBOM = (hardwareParts, printedParts, config) => {
const rows = [];
// Header
rows.push(['Item', 'Name', 'Quantity', 'Price', 'Link', 'Category', 'Type']);
// Add motor
if (config.motor) {
const motorLinks = config.motor.links || [];
@@ -138,7 +138,7 @@ export const generateExcelBOM = (hardwareParts, printedParts, config) => {
'Hardware'
]);
}
// Add power supply
if (config.powerSupply) {
const psuLinks = config.powerSupply.links || [];
@@ -153,7 +153,7 @@ export const generateExcelBOM = (hardwareParts, printedParts, config) => {
'Hardware'
]);
}
// Add hardware parts
hardwareParts.forEach(hw => {
const links = hw.links || [];
@@ -168,7 +168,7 @@ export const generateExcelBOM = (hardwareParts, printedParts, config) => {
'Hardware'
]);
});
// Add printed parts (for reference, not purchase)
printedParts.forEach(part => {
rows.push([
@@ -181,14 +181,14 @@ export const generateExcelBOM = (hardwareParts, printedParts, config) => {
'Printed Part'
]);
});
// Create workbook and worksheet
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('BOM');
// Add rows
worksheet.addRows(rows);
// Set column widths
worksheet.columns = [
{ width: 30 }, // Item
@@ -199,42 +199,42 @@ export const generateExcelBOM = (hardwareParts, printedParts, config) => {
{ width: 20 }, // Category
{ width: 15 } // Type
];
return workbook;
};
// Generate Excel Print List with completion tracker
export const generateExcelPrintList = (printedParts, filamentTotals) => {
const rows = [];
// Header
rows.push(['Part Name', 'Category', 'Color', 'Quantity', 'Filament (g)', 'Print Time', 'Status', 'Completed']);
// Group parts by category and color
const partsByCategoryColor = {};
printedParts.forEach(part => {
const category = part.category || 'Other';
const color = part.colour === 'primary' ? 'Primary' : part.colour === 'secondary' ? 'Accent' : 'Other';
const key = `${category}_${color}`;
if (!partsByCategoryColor[key]) {
partsByCategoryColor[key] = [];
}
partsByCategoryColor[key].push(part);
});
// Sort by category, then color
const sortedKeys = Object.keys(partsByCategoryColor).sort();
sortedKeys.forEach(key => {
const parts = partsByCategoryColor[key];
const [category, color] = key.split('_');
parts.forEach(part => {
const filament = typeof part.filamentEstimate === 'number'
? part.filamentEstimate
const filament = typeof part.filamentEstimate === 'number'
? part.filamentEstimate
: parseFloat(part.filamentEstimate?.replace('~', '').replace('g', '')) || 0;
rows.push([
part.name || part.id || '',
category,
@@ -247,18 +247,18 @@ export const generateExcelPrintList = (printedParts, filamentTotals) => {
]);
});
});
// Add summary row
rows.push([]);
rows.push(['TOTAL', '', '', printedParts.length, filamentTotals.total.toFixed(2), '', '', '']);
// Create workbook and worksheet
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Print List');
// Add rows
worksheet.addRows(rows);
// Set column widths
worksheet.columns = [
{ width: 40 }, // Part Name
@@ -270,18 +270,18 @@ export const generateExcelPrintList = (printedParts, filamentTotals) => {
{ width: 15 }, // Status
{ width: 12 } // Completed
];
// Create a summary sheet with progress calculation
const summaryWorksheet = workbook.addWorksheet('Summary');
// Add summary rows
summaryWorksheet.getCell('A1').value = 'Print Progress Summary';
summaryWorksheet.getCell('A3').value = 'Total Parts';
summaryWorksheet.getCell('B3').value = printedParts.length;
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('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('A8').value = 'Total Filament (g)';
summaryWorksheet.getCell('B8').value = filamentTotals.total.toFixed(2);
@@ -289,12 +289,12 @@ export const generateExcelPrintList = (printedParts, filamentTotals) => {
summaryWorksheet.getCell('B9').value = filamentTotals.primary.toFixed(2);
summaryWorksheet.getCell('A10').value = 'Accent Color (g)';
summaryWorksheet.getCell('B10').value = (filamentTotals.secondary || 0).toFixed(2);
// Set column widths for summary sheet
summaryWorksheet.columns = [
{ width: 25 },
{ width: 15 }
];
return workbook;
};