refactor: Restructure data files into component-specific and common directories, add new UI components, and update project documentation.
This commit is contained in:
4
.github/workflows/check-vendor.yml
vendored
4
.github/workflows/check-vendor.yml
vendored
@@ -30,9 +30,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate manifest from site data
|
- name: Generate manifest from site data
|
||||||
run: |
|
run: |
|
||||||
python scripts/generate_manifest_from_site.py \
|
python scripts/vendor_update.py --scan-only
|
||||||
--site-dir website/src/data/components \
|
|
||||||
--manifest manifest/vendor_manifest.json
|
|
||||||
|
|
||||||
- name: Check for updates
|
- name: Check for updates
|
||||||
id: check-updates
|
id: check-updates
|
||||||
|
|||||||
152
CONTRIBUTING.md
Normal file
152
CONTRIBUTING.md
Normal 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
626
README.md
@@ -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.
|
[](https://github.com/KinkyMakers/OSSM-Configurator)
|
||||||
|
[](https://github.com/KinkyMakers/OSSM-Configurator)
|
||||||
|
|
||||||
## Project Structure
|
A professional web-based configuration tool for the **Open Source Sex Machine (OSSM)** project. This application provides a premium, intuitive wizard interface that guides users through the complex process of selecting, customizing, and validating components for their OSSM build.
|
||||||
|
|
||||||
```
|
|
||||||
OSSM-Configurator/
|
|
||||||
├── website/ # Main web application
|
|
||||||
│ ├── src/ # React source code
|
|
||||||
│ ├── public/ # Static assets (images, etc.)
|
|
||||||
│ ├── dist/ # Build output (generated)
|
|
||||||
│ ├── node_modules/ # Dependencies (generated)
|
|
||||||
│ └── ... # Configuration files
|
|
||||||
├── BOM.xlsx # Bill of Materials spreadsheet
|
|
||||||
├── Screen Shots/ # Application screenshots
|
|
||||||
└── README.md # This file
|
|
||||||
```
|
|
||||||
|
|
||||||
## Website Overview
|
|
||||||
|
|
||||||
The OSSM Configurator is a React-based single-page application built with Vite. It provides a step-by-step wizard interface that allows users to:
|
|
||||||
|
|
||||||
1. **Select Motor** - Choose from available motor options (42AIM30, 57AIM30, iHSV57)
|
|
||||||
2. **Choose Power Supply** - Select appropriate power supply (24V PSU, 24V USB-C PD)
|
|
||||||
3. **Customize Colors** - Pick primary and accent colors for 3D printed parts
|
|
||||||
4. **Configure Options** - Select mounting options, stands, toy mounts, actuators, and other components
|
|
||||||
5. **Review Summary** - View complete BOM with pricing, filament estimates, and export options
|
|
||||||
|
|
||||||
### Key Features
|
|
||||||
|
|
||||||
- **Interactive Wizard Interface**: Step-by-step configuration process with progress tracking
|
|
||||||
- **Component Compatibility**: Ensures selected components are compatible with each other
|
|
||||||
- **Real-time Pricing**: Calculates total cost including hardware and printed parts
|
|
||||||
- **Filament Estimates**: Provides 3D printing filament requirements for each component
|
|
||||||
- **BOM Export**: Generate and download a complete Bill of Materials
|
|
||||||
- **Visual Component Selection**: Image-based component selection for better user experience
|
|
||||||
|
|
||||||
### Technology Stack
|
|
||||||
|
|
||||||
- **React 18** - UI framework
|
|
||||||
- **Vite** - Build tool and dev server
|
|
||||||
- **Tailwind CSS** - Styling
|
|
||||||
- **JSZip** - For generating downloadable BOM packages
|
|
||||||
|
|
||||||
## TODO
|
|
||||||
- [X] Dark Mode [Completed]
|
|
||||||
- [ ] Finalize Actuator Components and mapping to BOM [In Progress]
|
|
||||||
- [ ] Finalize Stand Components and mapping to BOM
|
|
||||||
- [ ] Finalize PCB Components and mapping to BOM
|
|
||||||
- [ ] Finalize Toy Mounts Components and mapping to BOM
|
|
||||||
- [ ] Finalize Remote Control Components and mapping to BOM
|
|
||||||
- [ ] Finalize Mounting Components and mapping to BOM
|
|
||||||
- [ ] Finalize Other Components and mapping to BOM
|
|
||||||
- [ ] Finalize Colors and mapping to BOM
|
|
||||||
- [ ] Finalize Pricing and mapping to BOM
|
|
||||||
- [ ] Finalize BOM Export and mapping to BOM
|
|
||||||
- [ ] Finalize BOM Import and mapping to BOM
|
|
||||||
- [ ] Finalize Storage and sharing of BOMs
|
|
||||||
- [ ] Add references to original hardware files and designs
|
|
||||||
- [ ] Add Readme/assembly instructions for each component
|
|
||||||
- [ ] Add FAQ and troubleshooting guide
|
|
||||||
- [ ] Add support for multiple languages
|
|
||||||
- [ ] Add support for multiple currencies
|
|
||||||
- [ ] Add support for multiple payment methods
|
|
||||||
- [ ] Add support for multiple shipping methods
|
|
||||||
- [ ] Add support for multiple shipping countries
|
|
||||||
- [ ] Add support for multiple shipping regions
|
|
||||||
- [ ] Add support for multiple shipping cities
|
|
||||||
- [ ] Add 3D render of final product with all components and options selected and coloured [If possible]
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
**Option 1: Using Docker (Recommended)**
|
|
||||||
- Docker Desktop or Docker Engine
|
|
||||||
- Docker Compose
|
|
||||||
|
|
||||||
**Option 2: Local Development**
|
|
||||||
- Node.js (v16 or higher recommended)
|
|
||||||
- npm or yarn
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
1. Navigate to the website directory:
|
|
||||||
```bash
|
|
||||||
cd website
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Install dependencies:
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Development
|
|
||||||
|
|
||||||
Run the development server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
The application will be available at `http://localhost:5173` (or the port shown in the terminal).
|
|
||||||
|
|
||||||
### Building for Production
|
|
||||||
|
|
||||||
Create an optimized production build:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
The built files will be in the `website/dist/` directory.
|
|
||||||
|
|
||||||
### Preview Production Build
|
|
||||||
|
|
||||||
Preview the production build locally:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run preview
|
|
||||||
```
|
|
||||||
|
|
||||||
## Docker Deployment
|
|
||||||
|
|
||||||
### Development with Docker Compose
|
|
||||||
|
|
||||||
Run the application in development mode with hot reload:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose-dev.yml up
|
|
||||||
```
|
|
||||||
|
|
||||||
The application will be available at `http://localhost:5173` with hot module replacement enabled.
|
|
||||||
|
|
||||||
To run in detached mode (background):
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose-dev.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
To stop the development container:
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose-dev.yml down
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production with Docker Compose
|
|
||||||
|
|
||||||
Build and run the production image:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up --build -d
|
|
||||||
```
|
|
||||||
|
|
||||||
The application will be available at `http://localhost:80`
|
|
||||||
|
|
||||||
To run without rebuilding (if image already exists):
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
To stop the production container:
|
|
||||||
```bash
|
|
||||||
docker-compose down
|
|
||||||
```
|
|
||||||
|
|
||||||
To view logs:
|
|
||||||
```bash
|
|
||||||
docker-compose logs -f
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using Pre-built Docker Images
|
|
||||||
|
|
||||||
The project includes GitHub Actions workflows that automatically build and publish Docker images to GitHub Container Registry (ghcr.io) on releases. You can pull and run the latest release image:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker pull ghcr.io/<your-username>/<your-repo-name>:V0.0.1-BETA
|
|
||||||
docker run -d -p 80:80 ghcr.io/<your-username>/<your-repo-name>:V0.0.1-BETA
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration Data
|
|
||||||
|
|
||||||
The application uses JSON data files located in `website/src/data/`:
|
|
||||||
|
|
||||||
- `motors.json` - Available motor options
|
|
||||||
- `powerSupplies.json` - Power supply options
|
|
||||||
- `colors.json` - Available color options
|
|
||||||
- `options.json` - Main configuration options
|
|
||||||
- `components/` - Detailed component data:
|
|
||||||
- `actuator.json` - Actuator components
|
|
||||||
- `mounting.json` - Mounting options
|
|
||||||
- `remote.json` - Remote control components
|
|
||||||
- `stand.json` - Stand components
|
|
||||||
- `toyMounts.json` - Toy mount options
|
|
||||||
|
|
||||||
## Project Purpose
|
|
||||||
|
|
||||||
The OSSM Configurator serves as a comprehensive tool for users building their own Open Source Sex Machine. It simplifies the configuration process by:
|
|
||||||
|
|
||||||
- **Guiding Selection**: Step-by-step wizard prevents missing critical components
|
|
||||||
- **Ensuring Compatibility**: Validates component combinations
|
|
||||||
- **Providing Transparency**: Shows costs, filament requirements, and time estimates
|
|
||||||
- **Generating Documentation**: Creates exportable BOM for ordering parts and printing
|
|
||||||
|
|
||||||
This tool is essential for both beginners and experienced builders who want to ensure they have all necessary components and understand the full scope of their build before starting.
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
When adding new components or options:
|
|
||||||
|
|
||||||
1. Update the appropriate JSON data files in `website/src/data/`
|
|
||||||
2. Add corresponding images to `website/public/images/`
|
|
||||||
3. Test the configuration flow to ensure compatibility
|
|
||||||
4. Update component pricing and filament estimates as needed
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is part of the Open Source Sex Machine (OSSM) project. Please refer to the OSSM project license for usage terms.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Vendor System
|
## 🌟 Key Features
|
||||||
|
|
||||||
The OSSM Configurator includes a robust vendoring and monitoring system for external asset files (STL files, etc.) referenced in component JSON files. This system ensures reproducible builds by pinning external files to specific commit SHAs and automatically detecting when upstream changes occur.
|
- **Intuitive Wizard Flow**: Step-by-step guidance from motor selection to final export.
|
||||||
|
- **Dynamic 3D Visualization (Coming Soon)**: Preview your build with real-time color updates.
|
||||||
|
- **Smart Compatibility**: Ensures your PSU matches your Motor and your Mount fits your Actuator.
|
||||||
|
- **Rich BOM Export**: Download a complete ZIP package containing:
|
||||||
|
- 📄 **README.md** overview of your build.
|
||||||
|
- 📊 **Excel BOM** for hardware ordering.
|
||||||
|
- 🖨️ **Print List** with filament estimates.
|
||||||
|
- 📁 **Organized STL Files** categorized by component and color.
|
||||||
|
- **Vendor System**: Integrated tracking for external CAD files to ensure reproducible builds.
|
||||||
|
- **Dark Mode Support**: A premium aesthetic designed for builders, day or night.
|
||||||
|
|
||||||
### Overview
|
---
|
||||||
|
|
||||||
The vendor system:
|
## 📸 Guided Walkthrough
|
||||||
- **Vendors** external files from GitHub repositories into the `vendor/` directory
|
|
||||||
- **Pins** files to specific commit SHAs for reproducible builds
|
|
||||||
- **Monitors** upstream repositories for changes
|
|
||||||
- **Automatically updates** vendored files when upstream changes are detected
|
|
||||||
- **Preserves backward compatibility** by keeping original `url` fields in component JSON files
|
|
||||||
|
|
||||||
### Project Structure (Vendor System)
|
### 1. Component Selection
|
||||||
|
The wizard starts with core hardware. Select your motor and power supply with real-time feedback on compatibility and cost.
|
||||||
|
|
||||||
|

|
||||||
|
*Figure 1: Motor selection with clear technical specs and visual feedback.*
|
||||||
|
|
||||||
|

|
||||||
|
*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.
|
||||||
|
|
||||||
|

|
||||||
|
*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.
|
||||||
|
|
||||||
|

|
||||||
|
*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.
|
||||||
|
|
||||||
|

|
||||||
|
*Figure 5: The comprehensive BOM summary with automated ZIP generation.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Getting Started
|
||||||
|
|
||||||
|
### Quick Start (Docker)
|
||||||
|
The easiest way to run the configurator locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose-dev.yml up -d
|
||||||
```
|
```
|
||||||
OSSM-Configurator/
|
Access the app at `http://localhost:5173`.
|
||||||
├── manifest/
|
|
||||||
│ └── vendor_manifest.json # Canonical list of vendored files
|
|
||||||
├── 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
|
|
||||||
```
|
|
||||||
|
|
||||||
### Manifest Schema
|
### Local Development
|
||||||
|
1. **Clone the repo**
|
||||||
The `manifest/vendor_manifest.json` file contains metadata for each vendored file:
|
2. **Setup Website**:
|
||||||
|
```bash
|
||||||
```json
|
cd website
|
||||||
{
|
npm install
|
||||||
"id": "unique-id-or-slug",
|
npm run dev
|
||||||
"source_repo": "owner/repo",
|
```
|
||||||
"source_path": "path/in/repo/file.stl",
|
3. **Setup Logic** (Optional, for vendor updates):
|
||||||
"source_ref": "main",
|
|
||||||
"pinned_sha": "commit-sha-currently-pinned",
|
|
||||||
"pinned_raw_url": "https://raw.githubusercontent.com/owner/repo/<sha>/path/to/file.stl",
|
|
||||||
"local_path": "vendor/owner-repo/path/to/file.stl",
|
|
||||||
"checksum_sha256": "sha256-hex-digest",
|
|
||||||
"last_checked": "2024-01-05T12:00:00Z",
|
|
||||||
"upstream_latest_sha": "latest-commit-sha-observed",
|
|
||||||
"status": "up-to-date | out-of-date | unknown",
|
|
||||||
"license": "SPDX-identifier-or-URL",
|
|
||||||
"orig_site_json": "website/src/data/components/actuator.json",
|
|
||||||
"orig_item_id": "ossm-actuator-body-bottom"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Integration with Component JSON Files
|
|
||||||
|
|
||||||
The vendor system adds a `vendor` object to each `printedParts` entry in component JSON files, while preserving the original `url` field for backward compatibility:
|
|
||||||
|
|
||||||
**Before:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "ossm-actuator-body-bottom",
|
|
||||||
"name": "Actuator Bottom",
|
|
||||||
"filePath": "OSSM - Actuator Body Bottom.stl",
|
|
||||||
"url": "https://github.com/KinkyMakers/OSSM-hardware/.../file.stl?raw=true"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**After (with vendor metadata):**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "ossm-actuator-body-bottom",
|
|
||||||
"name": "Actuator Bottom",
|
|
||||||
"filePath": "OSSM - Actuator Body Bottom.stl",
|
|
||||||
"url": "https://github.com/KinkyMakers/OSSM-hardware/.../file.stl?raw=true",
|
|
||||||
"vendor": {
|
|
||||||
"manifest_id": "ossm-actuator-body-bottom",
|
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/file.stl",
|
|
||||||
"pinned_sha": "abc123...",
|
|
||||||
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/abc123/...",
|
|
||||||
"checksum_sha256": "deadbeef...",
|
|
||||||
"last_checked": "2024-01-05T12:00:00Z",
|
|
||||||
"status": "up-to-date"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The website continues to use `printedParts.url` by default. Site code can optionally prefer `vendor.*` fields when available for offline hosting or improved reliability.
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- Python 3.11 or higher
|
|
||||||
- GitHub API token (for authenticated requests and higher rate limits)
|
|
||||||
|
|
||||||
### Setup
|
|
||||||
|
|
||||||
1. **Install Python dependencies:**
|
|
||||||
```bash
|
```bash
|
||||||
cd scripts
|
cd scripts
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
python vendor_update.py
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Set up GitHub API token:**
|
---
|
||||||
```bash
|
|
||||||
export GITHUB_API_TOKEN=your_token_here
|
|
||||||
# Or use GITHUB_TOKEN as fallback
|
|
||||||
```
|
|
||||||
|
|
||||||
To create a GitHub token:
|
## 🗺️ Project Roadmap
|
||||||
- Go to GitHub Settings → Developer settings → Personal access tokens → Tokens (classic)
|
|
||||||
- Generate a new token with `public_repo` scope (or `repo` for private repos)
|
|
||||||
- Required permissions: Read access to repositories
|
|
||||||
|
|
||||||
### Local Usage
|
We are constantly improving the OSSM Configurator. Check out our **[detailed roadmap](./roadmap/ROADMAP.md)** for upcoming features, including:
|
||||||
|
- 🛠️ Interactive 3D Render/Preview
|
||||||
|
- 🌍 Multi-language & Multi-currency support
|
||||||
|
- 📦 Integrated assembly guides
|
||||||
|
|
||||||
#### 1. Generate Manifest from Site Data
|
---
|
||||||
|
|
||||||
Scan component JSON files and create/update the vendor manifest:
|
## 🤝 Contributing
|
||||||
|
|
||||||
```bash
|
Contributions are welcome! Whether you are adding a new remote, a toy mount, or a hardware mod, please refer to our detailed guide:
|
||||||
python scripts/generate_manifest_from_site.py \
|
|
||||||
--site-dir website/src/data/components \
|
|
||||||
--manifest manifest/vendor_manifest.json
|
|
||||||
```
|
|
||||||
|
|
||||||
This script:
|
👉 **[Read the CONTRIBUTING.md](./CONTRIBUTING.md)**
|
||||||
- Scans all JSON files in `website/src/data/components/`
|
|
||||||
- Extracts `printedParts` entries with GitHub URLs
|
|
||||||
- Creates manifest entries for each GitHub-hosted file
|
|
||||||
- Supports nested structures (e.g., `systems.printedParts`, `bodyParts`, `knobs`)
|
|
||||||
|
|
||||||
#### 2. Download and Pin Files
|
### Quick Summary:
|
||||||
|
1. Update JSON data in `website/src/data/`.
|
||||||
|
2. Add relevant images to `website/public/images/`.
|
||||||
|
3. Run `python scripts/vendor_update.py` for new files.
|
||||||
|
4. Submit a PR!
|
||||||
|
|
||||||
Download files from GitHub and pin them to commit SHAs:
|
---
|
||||||
|
|
||||||
```bash
|
## 📜 License
|
||||||
# Update all entries
|
|
||||||
python scripts/vendor_update.py \
|
|
||||||
--manifest manifest/vendor_manifest.json
|
|
||||||
|
|
||||||
# Update a specific entry
|
This project is part of the **Open Source Sex Machine (OSSM)** project. Please refer to the main OSSM project for full license details.
|
||||||
python scripts/vendor_update.py \
|
|
||||||
--manifest manifest/vendor_manifest.json \
|
|
||||||
--entry ossm-actuator-body-bottom
|
|
||||||
|
|
||||||
# Dry run (see what would be done)
|
---
|
||||||
python scripts/vendor_update.py \
|
*Built with ❤️ by the OSSM Community.*
|
||||||
--manifest manifest/vendor_manifest.json \
|
|
||||||
--dry-run
|
|
||||||
|
|
||||||
# Update and sync vendor metadata to site JSON files
|
|
||||||
python scripts/vendor_update.py \
|
|
||||||
--manifest manifest/vendor_manifest.json \
|
|
||||||
--sync-site
|
|
||||||
```
|
|
||||||
|
|
||||||
This script:
|
|
||||||
- Resolves commit SHAs for the specified ref (branch/tag)
|
|
||||||
- Downloads files from pinned URLs
|
|
||||||
- Computes SHA256 checksums
|
|
||||||
- Updates manifest with metadata
|
|
||||||
- Optionally syncs vendor metadata back to component JSON files
|
|
||||||
|
|
||||||
#### 3. Check for Upstream Updates
|
|
||||||
|
|
||||||
Monitor upstream repositories for changes:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python scripts/check_updates.py \
|
|
||||||
--manifest manifest/vendor_manifest.json \
|
|
||||||
--output report.json
|
|
||||||
```
|
|
||||||
|
|
||||||
This script:
|
|
||||||
- Queries GitHub API for latest commit SHAs
|
|
||||||
- Compares with pinned SHAs
|
|
||||||
- Generates a report of up-to-date and out-of-date entries
|
|
||||||
- Exits with non-zero code if any entries are out-of-date (useful for CI)
|
|
||||||
|
|
||||||
### GitHub Action Workflow
|
|
||||||
|
|
||||||
The `.github/workflows/check-vendor.yml` workflow automatically:
|
|
||||||
|
|
||||||
1. **Runs daily** (2 AM UTC) or can be triggered manually
|
|
||||||
2. **Generates manifest** from site data
|
|
||||||
3. **Checks for updates** in upstream repositories
|
|
||||||
4. **Creates a branch and PR** if updates are available
|
|
||||||
5. **Updates vendored files** and syncs metadata to component JSON files
|
|
||||||
|
|
||||||
#### Manual Trigger
|
|
||||||
|
|
||||||
To manually trigger the workflow:
|
|
||||||
1. Go to Actions tab in GitHub
|
|
||||||
2. Select "Check Vendor Updates" workflow
|
|
||||||
3. Click "Run workflow"
|
|
||||||
|
|
||||||
#### Required Secrets
|
|
||||||
|
|
||||||
Configure these secrets in GitHub repository settings:
|
|
||||||
|
|
||||||
- `GITHUB_API_TOKEN`: GitHub personal access token with `public_repo` scope
|
|
||||||
|
|
||||||
The workflow uses `GITHUB_TOKEN` for creating PRs (automatically provided by GitHub Actions).
|
|
||||||
|
|
||||||
### Webhook Configuration
|
|
||||||
|
|
||||||
The `api/github_webhook/index.py` provides a Flask-based webhook handler that can receive push events from upstream repositories.
|
|
||||||
|
|
||||||
#### Setup
|
|
||||||
|
|
||||||
1. **Deploy the webhook** (e.g., to a serverless platform, VPS, or local server)
|
|
||||||
|
|
||||||
2. **Configure webhook secret:**
|
|
||||||
```bash
|
|
||||||
export WEBHOOK_SECRET=your_webhook_secret_here
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Add webhook to upstream repository:**
|
|
||||||
- Go to repository Settings → Webhooks → Add webhook
|
|
||||||
- Payload URL: `https://your-domain.com/webhook`
|
|
||||||
- Content type: `application/json`
|
|
||||||
- Secret: (same as `WEBHOOK_SECRET`)
|
|
||||||
- Events: Select "Just the push event"
|
|
||||||
- Active: ✓
|
|
||||||
|
|
||||||
#### Local Testing
|
|
||||||
|
|
||||||
Run the webhook locally for testing:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export WEBHOOK_SECRET=test_secret
|
|
||||||
export FLASK_DEBUG=true
|
|
||||||
python api/github_webhook/index.py
|
|
||||||
```
|
|
||||||
|
|
||||||
The webhook will:
|
|
||||||
- Verify GitHub signature using `X-Hub-Signature-256`
|
|
||||||
- Process push events
|
|
||||||
- Identify affected manifest entries
|
|
||||||
- Trigger update checks for changed files
|
|
||||||
|
|
||||||
### Running Tests
|
|
||||||
|
|
||||||
Run the test suite:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install test dependencies
|
|
||||||
pip install -r scripts/requirements.txt
|
|
||||||
|
|
||||||
# Run all tests
|
|
||||||
pytest tests/
|
|
||||||
|
|
||||||
# Run specific test file
|
|
||||||
pytest tests/test_vendor_update.py -v
|
|
||||||
pytest tests/test_check_updates.py -v
|
|
||||||
```
|
|
||||||
|
|
||||||
Tests use `pytest` with `responses` for mocking HTTP requests to GitHub API.
|
|
||||||
|
|
||||||
### URL Format Support
|
|
||||||
|
|
||||||
The vendor system supports multiple GitHub URL formats:
|
|
||||||
|
|
||||||
- `https://github.com/owner/repo/blob/main/path/to/file.stl?raw=true`
|
|
||||||
- `https://github.com/owner/repo/raw/main/path/to/file.stl`
|
|
||||||
- `https://raw.githubusercontent.com/owner/repo/main/path/to/file.stl`
|
|
||||||
|
|
||||||
The system automatically:
|
|
||||||
- Extracts owner, repo, path, and ref from URLs
|
|
||||||
- Defaults to `main` branch if ref is missing
|
|
||||||
- Handles URL-encoded paths
|
|
||||||
|
|
||||||
### Reverting Vendor Changes
|
|
||||||
|
|
||||||
To revert vendor metadata from component JSON files:
|
|
||||||
|
|
||||||
1. **Remove vendor fields** from component JSON files manually, or
|
|
||||||
2. **Restore from git history:**
|
|
||||||
```bash
|
|
||||||
git checkout HEAD -- website/src/data/components/
|
|
||||||
```
|
|
||||||
|
|
||||||
The original `url` fields are never removed, so the site continues to work even if vendor metadata is removed.
|
|
||||||
|
|
||||||
### Inspecting Vendor Status
|
|
||||||
|
|
||||||
View the current vendor status:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check manifest
|
|
||||||
cat manifest/vendor_manifest.json | jq '.[] | {id, status, pinned_sha, upstream_latest_sha}'
|
|
||||||
|
|
||||||
# Check for updates
|
|
||||||
python scripts/check_updates.py --manifest manifest/vendor_manifest.json
|
|
||||||
```
|
|
||||||
|
|
||||||
### Troubleshooting
|
|
||||||
|
|
||||||
**Issue: Rate limit errors from GitHub API**
|
|
||||||
- Solution: Set `GITHUB_API_TOKEN` environment variable with a personal access token
|
|
||||||
|
|
||||||
**Issue: File not found at ref**
|
|
||||||
- Solution: The script will try the default branch if the specified ref doesn't exist
|
|
||||||
|
|
||||||
**Issue: Webhook signature verification fails**
|
|
||||||
- Solution: Ensure `WEBHOOK_SECRET` matches the secret configured in GitHub webhook settings
|
|
||||||
|
|
||||||
**Issue: Manifest entries not found**
|
|
||||||
- Solution: Run `generate_manifest_from_site.py` to create/update manifest entries
|
|
||||||
|
|
||||||
### Best Practices
|
|
||||||
|
|
||||||
1. **Always pin to commit SHAs** (not branch names) for reproducible builds
|
|
||||||
2. **Run `check_updates.py` regularly** to detect upstream changes
|
|
||||||
3. **Review PRs** created by the GitHub Action before merging
|
|
||||||
4. **Test site builds** after vendor updates to ensure compatibility
|
|
||||||
5. **Keep manifest in version control** for tracking vendored files
|
|
||||||
|
|
||||||
### Example Workflow
|
|
||||||
|
|
||||||
Complete workflow for adding a new component with external files:
|
|
||||||
|
|
||||||
1. **Add component to site JSON:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "new-part",
|
|
||||||
"url": "https://github.com/owner/repo/blob/main/file.stl?raw=true"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Generate manifest:**
|
|
||||||
```bash
|
|
||||||
python scripts/generate_manifest_from_site.py
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Vendor the file:**
|
|
||||||
```bash
|
|
||||||
python scripts/vendor_update.py --entry new-part --sync-site
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Verify:**
|
|
||||||
```bash
|
|
||||||
python scripts/check_updates.py
|
|
||||||
```
|
|
||||||
|
|
||||||
The GitHub Action will automatically monitor for future updates.
|
|
||||||
|
|||||||
@@ -4,495 +4,639 @@
|
|||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl",
|
"source_path": "Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/stand.json",
|
||||||
|
"orig_item_id": "handle-spacer",
|
||||||
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
||||||
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl",
|
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl",
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl",
|
|
||||||
"checksum_sha256": "55ede7dff60a31d68159b352b5f2c63792b7a0dbe9d543a43681c3e52d229115",
|
"checksum_sha256": "55ede7dff60a31d68159b352b5f2c63792b7a0dbe9d543a43681c3e52d229115",
|
||||||
"last_checked": "2026-01-07T01:20:58.324330+00:00",
|
"last_checked": "2026-01-07T01:20:58.324330+00:00",
|
||||||
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
|
||||||
"status": "up-to-date",
|
"status": "up-to-date",
|
||||||
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
|
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
|
||||||
"orig_site_json": "website/src/data/components/stand.json",
|
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
|
||||||
"orig_item_id": "handle-spacer"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-24mm-clamping-thread-belt-clamp",
|
"id": "ossm-24mm-clamping-thread-belt-clamp",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Actuator/OSSM - 24mm Clamping Thread - Belt Clamp.stl",
|
"source_path": "Printed Parts/Actuator/OSSM - 24mm Clamping Thread - Belt Clamp.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - Belt Clamp.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/actuator.json",
|
||||||
|
"orig_item_id": "ossm-24mm-clamping-thread-belt-clamp",
|
||||||
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
||||||
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - Belt Clamp.stl",
|
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - Belt Clamp.stl",
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - Belt Clamp.stl",
|
|
||||||
"checksum_sha256": "457a71bc09cb53f12026fd829bec8fa5b04fdead0788822935780f42c90b9a7a",
|
"checksum_sha256": "457a71bc09cb53f12026fd829bec8fa5b04fdead0788822935780f42c90b9a7a",
|
||||||
"last_checked": "2026-01-07T01:20:58.945151+00:00",
|
"last_checked": "2026-01-07T01:20:58.945151+00:00",
|
||||||
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
|
||||||
"status": "up-to-date",
|
"status": "up-to-date",
|
||||||
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
|
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
|
||||||
"orig_site_json": "website/src/data/components/actuator.json",
|
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
|
||||||
"orig_item_id": "ossm-24mm-clamping-thread-belt-clamp"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-24mm-clamping-thread-end-effector",
|
"id": "ossm-24mm-clamping-thread-end-effector",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Actuator/OSSM - 24mm Clamping Thread - End Effector.stl",
|
"source_path": "Printed Parts/Actuator/OSSM - 24mm Clamping Thread - End Effector.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - End Effector.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/actuator.json",
|
||||||
|
"orig_item_id": "ossm-24mm-clamping-thread-end-effector",
|
||||||
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
||||||
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - End Effector.stl",
|
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - End Effector.stl",
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - End Effector.stl",
|
|
||||||
"checksum_sha256": "4860947b201e2e773b295d33bba09423ae40b4adeef3605d62687f2d40277de1",
|
"checksum_sha256": "4860947b201e2e773b295d33bba09423ae40b4adeef3605d62687f2d40277de1",
|
||||||
"last_checked": "2026-01-07T01:20:59.854476+00:00",
|
"last_checked": "2026-01-07T01:20:59.854476+00:00",
|
||||||
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
|
||||||
"status": "up-to-date",
|
"status": "up-to-date",
|
||||||
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
|
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
|
||||||
"orig_site_json": "website/src/data/components/actuator.json",
|
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
|
||||||
"orig_item_id": "ossm-24mm-clamping-thread-end-effector"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-24mm-nut-5-sided",
|
"id": "ossm-24mm-nut-5-sided",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Actuator/OSSM - 24mm Nut - 5 Sided.stl",
|
"source_path": "Printed Parts/Actuator/OSSM - 24mm Nut - 5 Sided.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - 24mm Nut - 5 Sided.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/actuator.json",
|
||||||
|
"orig_item_id": "ossm-24mm-nut-5-sided",
|
||||||
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
||||||
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Nut - 5 Sided.stl",
|
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Nut - 5 Sided.stl",
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - 24mm Nut - 5 Sided.stl",
|
|
||||||
"checksum_sha256": "38630c70b2fb929bba9a705dabf5bbd7b49ec882963e042b7108dc74284dd6ff",
|
"checksum_sha256": "38630c70b2fb929bba9a705dabf5bbd7b49ec882963e042b7108dc74284dd6ff",
|
||||||
"last_checked": "2026-01-07T01:21:00.555525+00:00",
|
"last_checked": "2026-01-07T01:21:00.555525+00:00",
|
||||||
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
|
||||||
"status": "up-to-date",
|
"status": "up-to-date",
|
||||||
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
|
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
|
||||||
"orig_site_json": "website/src/data/components/actuator.json",
|
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
|
||||||
"orig_item_id": "ossm-24mm-nut-5-sided"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-3030-cap",
|
"id": "ossm-3030-cap",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Extrusion Cap.stl",
|
"source_path": "Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Extrusion Cap.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Extrusion Cap.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/stand.json",
|
||||||
|
"orig_item_id": "ossm-3030-cap",
|
||||||
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
||||||
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Extrusion Cap.stl",
|
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Extrusion Cap.stl",
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Extrusion Cap.stl",
|
|
||||||
"checksum_sha256": "56fa9bb318cdeadc6d1698a1e6cef9371e58b0bc9c7729985bf639d8da2f25da",
|
"checksum_sha256": "56fa9bb318cdeadc6d1698a1e6cef9371e58b0bc9c7729985bf639d8da2f25da",
|
||||||
"last_checked": "2026-01-07T01:21:01.205246+00:00",
|
"last_checked": "2026-01-07T01:21:01.205246+00:00",
|
||||||
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
|
||||||
"status": "up-to-date",
|
"status": "up-to-date",
|
||||||
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
|
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
|
||||||
"orig_site_json": "website/src/data/components/stand.json",
|
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
|
||||||
"orig_item_id": "ossm-3030-cap"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-actuator-body-bottom",
|
"id": "ossm-actuator-body-bottom",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Actuator/OSSM - Actuator - Body - Bottom.stl",
|
"source_path": "Printed Parts/Actuator/OSSM - Actuator - Body - Bottom.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - Actuator - Body - Bottom.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/actuator.json",
|
||||||
|
"orig_item_id": "ossm-actuator-body-bottom",
|
||||||
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
||||||
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Bottom.stl",
|
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Bottom.stl",
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - Actuator - Body - Bottom.stl",
|
|
||||||
"checksum_sha256": "e7abdb99a7e9b9e7408a7b04a7dd50e42cc74510ea2969016a45a2a1387dcde3",
|
"checksum_sha256": "e7abdb99a7e9b9e7408a7b04a7dd50e42cc74510ea2969016a45a2a1387dcde3",
|
||||||
"last_checked": "2026-01-07T01:21:02.027595+00:00",
|
"last_checked": "2026-01-07T01:21:02.027595+00:00",
|
||||||
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
|
||||||
"status": "up-to-date",
|
"status": "up-to-date",
|
||||||
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
|
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
|
||||||
"orig_site_json": "website/src/data/components/actuator.json",
|
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
|
||||||
"orig_item_id": "ossm-actuator-body-bottom"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-actuator-body-cover",
|
"id": "ossm-actuator-body-cover",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Actuator/OSSM - Actuator - Body - Cover.stl",
|
"source_path": "Printed Parts/Actuator/OSSM - Actuator - Body - Cover.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - Actuator - Body - Cover.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/actuator.json",
|
||||||
|
"orig_item_id": "ossm-actuator-body-cover",
|
||||||
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
||||||
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Cover.stl",
|
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Cover.stl",
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - Actuator - Body - Cover.stl",
|
|
||||||
"checksum_sha256": "bbabc742d2f1753d1b4e21e42c197aec31a4a083b5c634e6e825cec69d4e3258",
|
"checksum_sha256": "bbabc742d2f1753d1b4e21e42c197aec31a4a083b5c634e6e825cec69d4e3258",
|
||||||
"last_checked": "2026-01-07T01:21:02.767604+00:00",
|
"last_checked": "2026-01-07T01:21:02.767604+00:00",
|
||||||
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
|
||||||
"status": "up-to-date",
|
"status": "up-to-date",
|
||||||
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
|
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
|
||||||
"orig_site_json": "website/src/data/components/actuator.json",
|
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
|
||||||
"orig_item_id": "ossm-actuator-body-cover"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-actuator-body-middle",
|
"id": "ossm-actuator-body-middle",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Actuator/OSSM - Actuator - Body - Middle.stl",
|
"source_path": "Printed Parts/Actuator/OSSM - Actuator - Body - Middle.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - Actuator - Body - Middle.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/actuator.json",
|
||||||
|
"orig_item_id": "ossm-actuator-body-middle",
|
||||||
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
||||||
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Middle.stl",
|
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Middle.stl",
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - Actuator - Body - Middle.stl",
|
|
||||||
"checksum_sha256": "ce6fb769378636c287af788ce42bdab1f2185dcffba929a0c72598742793b48a",
|
"checksum_sha256": "ce6fb769378636c287af788ce42bdab1f2185dcffba929a0c72598742793b48a",
|
||||||
"last_checked": "2026-01-07T01:21:03.531342+00:00",
|
"last_checked": "2026-01-07T01:21:03.531342+00:00",
|
||||||
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
|
||||||
"status": "up-to-date",
|
"status": "up-to-date",
|
||||||
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
|
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
|
||||||
"orig_site_json": "website/src/data/components/actuator.json",
|
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
|
||||||
"orig_item_id": "ossm-actuator-body-middle"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-actuator-body-middle-pivot",
|
"id": "ossm-actuator-body-middle-pivot",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Actuator/Non-standard/OSSM - Actuator - Body - Middle Pivot.stl",
|
"source_path": "Printed Parts/Actuator/Non-standard/OSSM - Actuator - Body - Middle Pivot.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/Non-standard/OSSM - Actuator - Body - Middle Pivot.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/mounting.json",
|
||||||
|
"orig_item_id": "ossm-actuator-body-middle-pivot",
|
||||||
"pinned_sha": "ad39a03b628b8e38549b99036c8dfd4131948545",
|
"pinned_sha": "ad39a03b628b8e38549b99036c8dfd4131948545",
|
||||||
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/ad39a03b628b8e38549b99036c8dfd4131948545/Printed Parts/Actuator/Non-standard/OSSM - Actuator - Body - Middle Pivot.stl",
|
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/ad39a03b628b8e38549b99036c8dfd4131948545/Printed Parts/Actuator/Non-standard/OSSM - Actuator - Body - Middle Pivot.stl",
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/Non-standard/OSSM - Actuator - Body - Middle Pivot.stl",
|
|
||||||
"checksum_sha256": "f6403a3c53e0d8c8e63d48bf853ab17c9f283421b1665b5503dbb04d59d0f52d",
|
"checksum_sha256": "f6403a3c53e0d8c8e63d48bf853ab17c9f283421b1665b5503dbb04d59d0f52d",
|
||||||
"last_checked": "2026-01-07T01:21:04.528132+00:00",
|
"last_checked": "2026-01-07T01:21:04.528132+00:00",
|
||||||
"upstream_latest_sha": "ad39a03b628b8e38549b99036c8dfd4131948545",
|
|
||||||
"status": "up-to-date",
|
"status": "up-to-date",
|
||||||
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/ad39a03b628b8e38549b99036c8dfd4131948545/LICENCE",
|
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/ad39a03b628b8e38549b99036c8dfd4131948545/LICENCE",
|
||||||
"orig_site_json": "website/src/data/components/mounting.json",
|
"upstream_latest_sha": "ad39a03b628b8e38549b99036c8dfd4131948545"
|
||||||
"orig_item_id": "ossm-actuator-body-middle-pivot"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-belt-tensioner",
|
"id": "ossm-belt-tensioner",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Actuator/OSSM - Belt Tensioner.stl",
|
"source_path": "Printed Parts/Actuator/OSSM - Belt Tensioner.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - Belt Tensioner.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/actuator.json",
|
||||||
|
"orig_item_id": "ossm-belt-tensioner",
|
||||||
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
||||||
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Belt Tensioner.stl",
|
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Belt Tensioner.stl",
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Actuator/OSSM - Belt Tensioner.stl",
|
|
||||||
"checksum_sha256": "31c74250c237763b0013ff42cc714ce14c293382a726de363f1686a7559f525f",
|
"checksum_sha256": "31c74250c237763b0013ff42cc714ce14c293382a726de363f1686a7559f525f",
|
||||||
"last_checked": "2026-01-07T01:21:05.499523+00:00",
|
"last_checked": "2026-01-07T01:21:05.499523+00:00",
|
||||||
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
|
||||||
"status": "up-to-date",
|
"status": "up-to-date",
|
||||||
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
|
"license": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/LICENCE",
|
||||||
"orig_site_json": "website/src/data/components/actuator.json",
|
"upstream_latest_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab"
|
||||||
"orig_item_id": "ossm-belt-tensioner"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-handle-spacer",
|
"id": "ossm-handle-spacer",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl",
|
"source_path": "Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/mounting.json",
|
||||||
|
"orig_item_id": "ossm-handle-spacer",
|
||||||
"pinned_sha": null,
|
"pinned_sha": null,
|
||||||
"pinned_raw_url": null,
|
"pinned_raw_url": null,
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl",
|
|
||||||
"checksum_sha256": null,
|
"checksum_sha256": null,
|
||||||
"last_checked": null,
|
"last_checked": null,
|
||||||
"upstream_latest_sha": null,
|
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"license": null,
|
"license": null,
|
||||||
"orig_site_json": "website/src/data/components/mounting.json",
|
"upstream_latest_sha": null
|
||||||
"orig_item_id": "ossm-handle-spacer"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-pcb-3030-mount",
|
"id": "ossm-pcb-3030-mount",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/PCB/OSSM - PCB - 3030 Mount.stl",
|
"source_path": "Printed Parts/PCB/OSSM - PCB - 3030 Mount.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/PCB/OSSM - PCB - 3030 Mount.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/pcb.json",
|
||||||
|
"orig_item_id": "ossm-pcb-3030-mount",
|
||||||
"pinned_sha": null,
|
"pinned_sha": null,
|
||||||
"pinned_raw_url": null,
|
"pinned_raw_url": null,
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/PCB/OSSM - PCB - 3030 Mount.stl",
|
|
||||||
"checksum_sha256": null,
|
"checksum_sha256": null,
|
||||||
"last_checked": null,
|
"last_checked": null,
|
||||||
"upstream_latest_sha": null,
|
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"license": null,
|
"license": null,
|
||||||
"orig_site_json": "website/src/data/components/pcb.json",
|
"upstream_latest_sha": null
|
||||||
"orig_item_id": "ossm-pcb-3030-mount"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-pcb-3030-mount-cover",
|
"id": "ossm-pcb-3030-mount-cover",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/PCB/OSSM - PCB - 3030 Mount Cover.stl",
|
"source_path": "Printed Parts/PCB/OSSM - PCB - 3030 Mount Cover.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/PCB/OSSM - PCB - 3030 Mount Cover.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/pcb.json",
|
||||||
|
"orig_item_id": "ossm-pcb-3030-mount-cover",
|
||||||
"pinned_sha": null,
|
"pinned_sha": null,
|
||||||
"pinned_raw_url": null,
|
"pinned_raw_url": null,
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/PCB/OSSM - PCB - 3030 Mount Cover.stl",
|
|
||||||
"checksum_sha256": null,
|
"checksum_sha256": null,
|
||||||
"last_checked": null,
|
"last_checked": null,
|
||||||
"upstream_latest_sha": null,
|
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"license": null,
|
"license": null,
|
||||||
"orig_site_json": "website/src/data/components/pcb.json",
|
"upstream_latest_sha": null
|
||||||
"orig_item_id": "ossm-pcb-3030-mount-cover"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-pcb-aio-cover-mount",
|
"id": "ossm-pcb-aio-cover-mount",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/PCB/OSSM - PCB - AIO Cover Mount.stl",
|
"source_path": "Printed Parts/PCB/OSSM - PCB - AIO Cover Mount.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/PCB/OSSM - PCB - AIO Cover Mount.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/pcb.json",
|
||||||
|
"orig_item_id": "ossm-pcb-aio-cover-mount",
|
||||||
"pinned_sha": null,
|
"pinned_sha": null,
|
||||||
"pinned_raw_url": null,
|
"pinned_raw_url": null,
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/PCB/OSSM - PCB - AIO Cover Mount.stl",
|
|
||||||
"checksum_sha256": null,
|
"checksum_sha256": null,
|
||||||
"last_checked": null,
|
"last_checked": null,
|
||||||
"upstream_latest_sha": null,
|
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"license": null,
|
"license": null,
|
||||||
"orig_site_json": "website/src/data/components/pcb.json",
|
"upstream_latest_sha": null
|
||||||
"orig_item_id": "ossm-pcb-aio-cover-mount"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-pitclamp-mini-42AIM30",
|
"id": "ossm-pitclamp-mini-42AIM30",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Mounting/Non-standard/OSSM - Mounting Ring - PitClamp Mini - 42AIM V1.1.stl",
|
"source_path": "Printed Parts/Mounting/Non-standard/OSSM - Mounting Ring - PitClamp Mini - 42AIM V1.1.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/Non-standard/OSSM - Mounting Ring - PitClamp Mini - 42AIM V1.1.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/mounting.json",
|
||||||
|
"orig_item_id": "ossm-pitclamp-mini-42AIM30",
|
||||||
"pinned_sha": null,
|
"pinned_sha": null,
|
||||||
"pinned_raw_url": null,
|
"pinned_raw_url": null,
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/Non-standard/OSSM - Mounting Ring - PitClamp Mini - 42AIM V1.1.stl",
|
|
||||||
"checksum_sha256": null,
|
"checksum_sha256": null,
|
||||||
"last_checked": null,
|
"last_checked": null,
|
||||||
"upstream_latest_sha": null,
|
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"license": null,
|
"license": null,
|
||||||
"orig_site_json": "website/src/data/components/mounting.json",
|
"upstream_latest_sha": null
|
||||||
"orig_item_id": "ossm-pitclamp-mini-42AIM30"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-pitclamp-mini-57AIM30",
|
"id": "ossm-pitclamp-mini-57AIM30",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Mounting/OSSM - Mounting Ring - PitClamp Mini - 57AIM V1.1.stl",
|
"source_path": "Printed Parts/Mounting/OSSM - Mounting Ring - PitClamp Mini - 57AIM V1.1.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Mounting Ring - PitClamp Mini - 57AIM V1.1.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/mounting.json",
|
||||||
|
"orig_item_id": "ossm-pitclamp-mini-57AIM30",
|
||||||
"pinned_sha": null,
|
"pinned_sha": null,
|
||||||
"pinned_raw_url": null,
|
"pinned_raw_url": null,
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Mounting Ring - PitClamp Mini - 57AIM V1.1.stl",
|
|
||||||
"checksum_sha256": null,
|
"checksum_sha256": null,
|
||||||
"last_checked": null,
|
"last_checked": null,
|
||||||
"upstream_latest_sha": null,
|
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"license": null,
|
"license": null,
|
||||||
"orig_site_json": "website/src/data/components/mounting.json",
|
"upstream_latest_sha": null
|
||||||
"orig_item_id": "ossm-pitclamp-mini-57AIM30"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-pitclamp-mini-dogbone-bolts ",
|
"id": "ossm-pitclamp-mini-dogbone-bolts ",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Dogbone Bolts.stl",
|
"source_path": "Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Dogbone Bolts.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Dogbone Bolts.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/mounting.json",
|
||||||
|
"orig_item_id": "ossm-pitclamp-mini-dogbone-bolts ",
|
||||||
"pinned_sha": null,
|
"pinned_sha": null,
|
||||||
"pinned_raw_url": null,
|
"pinned_raw_url": null,
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Dogbone Bolts.stl",
|
|
||||||
"checksum_sha256": null,
|
"checksum_sha256": null,
|
||||||
"last_checked": null,
|
"last_checked": null,
|
||||||
"upstream_latest_sha": null,
|
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"license": null,
|
"license": null,
|
||||||
"orig_site_json": "website/src/data/components/mounting.json",
|
"upstream_latest_sha": null
|
||||||
"orig_item_id": "ossm-pitclamp-mini-dogbone-bolts "
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-pitclamp-mini-dogbone-nuts",
|
"id": "ossm-pitclamp-mini-dogbone-nuts",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Dogbone Nuts.stl",
|
"source_path": "Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Dogbone Nuts.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Dogbone Nuts.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/mounting.json",
|
||||||
|
"orig_item_id": "ossm-pitclamp-mini-dogbone-nuts",
|
||||||
"pinned_sha": null,
|
"pinned_sha": null,
|
||||||
"pinned_raw_url": null,
|
"pinned_raw_url": null,
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Dogbone Nuts.stl",
|
|
||||||
"checksum_sha256": null,
|
"checksum_sha256": null,
|
||||||
"last_checked": null,
|
"last_checked": null,
|
||||||
"upstream_latest_sha": null,
|
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"license": null,
|
"license": null,
|
||||||
"orig_site_json": "website/src/data/components/mounting.json",
|
"upstream_latest_sha": null
|
||||||
"orig_item_id": "ossm-pitclamp-mini-dogbone-nuts"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-pitclamp-mini-handle",
|
"id": "ossm-pitclamp-mini-handle",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Handle.stl",
|
"source_path": "Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Handle.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Handle.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/mounting.json",
|
||||||
|
"orig_item_id": "ossm-pitclamp-mini-handle",
|
||||||
"pinned_sha": null,
|
"pinned_sha": null,
|
||||||
"pinned_raw_url": null,
|
"pinned_raw_url": null,
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Handle.stl",
|
|
||||||
"checksum_sha256": null,
|
"checksum_sha256": null,
|
||||||
"last_checked": null,
|
"last_checked": null,
|
||||||
"upstream_latest_sha": null,
|
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"license": null,
|
"license": null,
|
||||||
"orig_site_json": "website/src/data/components/mounting.json",
|
"upstream_latest_sha": null
|
||||||
"orig_item_id": "ossm-pitclamp-mini-handle"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-pitclamp-mini-iHSV57",
|
"id": "ossm-pitclamp-mini-iHSV57",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Mounting/Non-standard/OSSM - Mounting Ring - PitClamp Mini - iHSV57.stl",
|
"source_path": "Printed Parts/Mounting/Non-standard/OSSM - Mounting Ring - PitClamp Mini - iHSV57.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/Non-standard/OSSM - Mounting Ring - PitClamp Mini - iHSV57.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/mounting.json",
|
||||||
|
"orig_item_id": "ossm-pitclamp-mini-iHSV57",
|
||||||
"pinned_sha": null,
|
"pinned_sha": null,
|
||||||
"pinned_raw_url": null,
|
"pinned_raw_url": null,
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/Non-standard/OSSM - Mounting Ring - PitClamp Mini - iHSV57.stl",
|
|
||||||
"checksum_sha256": null,
|
"checksum_sha256": null,
|
||||||
"last_checked": null,
|
"last_checked": null,
|
||||||
"upstream_latest_sha": null,
|
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"license": null,
|
"license": null,
|
||||||
"orig_site_json": "website/src/data/components/mounting.json",
|
"upstream_latest_sha": null
|
||||||
"orig_item_id": "ossm-pitclamp-mini-iHSV57"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-pitclamp-mini-lower",
|
"id": "ossm-pitclamp-mini-lower",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Lower V1.1.stl",
|
"source_path": "Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Lower V1.1.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Lower V1.1.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/mounting.json",
|
||||||
|
"orig_item_id": "ossm-pitclamp-mini-lower",
|
||||||
"pinned_sha": null,
|
"pinned_sha": null,
|
||||||
"pinned_raw_url": null,
|
"pinned_raw_url": null,
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Lower V1.1.stl",
|
|
||||||
"checksum_sha256": null,
|
"checksum_sha256": null,
|
||||||
"last_checked": null,
|
"last_checked": null,
|
||||||
"upstream_latest_sha": null,
|
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"license": null,
|
"license": null,
|
||||||
"orig_site_json": "website/src/data/components/mounting.json",
|
"upstream_latest_sha": null
|
||||||
"orig_item_id": "ossm-pitclamp-mini-lower"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-pitclamp-mini-upper",
|
"id": "ossm-pitclamp-mini-upper",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Upper V1.1.stl",
|
"source_path": "Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Upper V1.1.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Upper V1.1.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/mounting.json",
|
||||||
|
"orig_item_id": "ossm-pitclamp-mini-upper",
|
||||||
"pinned_sha": null,
|
"pinned_sha": null,
|
||||||
"pinned_raw_url": null,
|
"pinned_raw_url": null,
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Mounting/OSSM - Base - PitClamp Mini - Upper V1.1.stl",
|
|
||||||
"checksum_sha256": null,
|
"checksum_sha256": null,
|
||||||
"last_checked": null,
|
"last_checked": null,
|
||||||
"upstream_latest_sha": null,
|
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"license": null,
|
"license": null,
|
||||||
"orig_site_json": "website/src/data/components/mounting.json",
|
"upstream_latest_sha": null
|
||||||
"orig_item_id": "ossm-pitclamp-mini-upper"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-remote-body",
|
"id": "ossm-remote-body",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Remote/OSSM - Remote - Body.stl",
|
"source_path": "Printed Parts/Remote/OSSM - Remote - Body.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/OSSM - Remote - Body.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/remote.json",
|
||||||
|
"orig_item_id": "ossm-remote-body",
|
||||||
"pinned_sha": null,
|
"pinned_sha": null,
|
||||||
"pinned_raw_url": null,
|
"pinned_raw_url": null,
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/OSSM - Remote - Body.stl",
|
|
||||||
"checksum_sha256": null,
|
"checksum_sha256": null,
|
||||||
"last_checked": null,
|
"last_checked": null,
|
||||||
"upstream_latest_sha": null,
|
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"license": null,
|
"license": null,
|
||||||
"orig_site_json": "website/src/data/components/remote.json",
|
"upstream_latest_sha": null
|
||||||
"orig_item_id": "ossm-remote-body"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-remote-knob",
|
"id": "ossm-remote-knob",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Remote/OSSM - Remote - Knob - Rounded.stl",
|
"source_path": "Printed Parts/Remote/OSSM - Remote - Knob - Rounded.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/OSSM - Remote - Knob - Rounded.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/remote.json",
|
||||||
|
"orig_item_id": "ossm-remote-knob",
|
||||||
"pinned_sha": null,
|
"pinned_sha": null,
|
||||||
"pinned_raw_url": null,
|
"pinned_raw_url": null,
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/OSSM - Remote - Knob - Rounded.stl",
|
|
||||||
"checksum_sha256": null,
|
"checksum_sha256": null,
|
||||||
"last_checked": null,
|
"last_checked": null,
|
||||||
"upstream_latest_sha": null,
|
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"license": null,
|
"license": null,
|
||||||
"orig_site_json": "website/src/data/components/remote.json",
|
"upstream_latest_sha": null
|
||||||
"orig_item_id": "ossm-remote-knob"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-remote-knob-knurled",
|
"id": "ossm-remote-knob-knurled",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Knurled.stl",
|
"source_path": "Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Knurled.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Knurled.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/remote.json",
|
||||||
|
"orig_item_id": "ossm-remote-knob-knurled",
|
||||||
"pinned_sha": null,
|
"pinned_sha": null,
|
||||||
"pinned_raw_url": null,
|
"pinned_raw_url": null,
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Knurled.stl",
|
|
||||||
"checksum_sha256": null,
|
"checksum_sha256": null,
|
||||||
"last_checked": null,
|
"last_checked": null,
|
||||||
"upstream_latest_sha": null,
|
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"license": null,
|
"license": null,
|
||||||
"orig_site_json": "website/src/data/components/remote.json",
|
"upstream_latest_sha": null
|
||||||
"orig_item_id": "ossm-remote-knob-knurled"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-remote-knob-knurled-with-position-indicator",
|
"id": "ossm-remote-knob-knurled-with-position-indicator",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Knurled With Position Indicator.stl",
|
"source_path": "Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Knurled With Position Indicator.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
"pinned_sha": null,
|
|
||||||
"pinned_raw_url": null,
|
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Knurled With Position Indicator.stl",
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Knurled With Position Indicator.stl",
|
||||||
"checksum_sha256": null,
|
|
||||||
"last_checked": null,
|
|
||||||
"upstream_latest_sha": null,
|
|
||||||
"status": "error",
|
|
||||||
"license": null,
|
|
||||||
"orig_site_json": "website/src/data/components/remote.json",
|
"orig_site_json": "website/src/data/components/remote.json",
|
||||||
"orig_item_id": "ossm-remote-knob-knurled-with-position-indicator"
|
"orig_item_id": "ossm-remote-knob-knurled-with-position-indicator",
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "ossm-remote-knob-simple",
|
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
|
||||||
"source_path": "Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Simple.stl",
|
|
||||||
"source_ref": "main",
|
|
||||||
"pinned_sha": null,
|
"pinned_sha": null,
|
||||||
"pinned_raw_url": null,
|
"pinned_raw_url": null,
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Simple.stl",
|
|
||||||
"checksum_sha256": null,
|
"checksum_sha256": null,
|
||||||
"last_checked": null,
|
"last_checked": null,
|
||||||
"upstream_latest_sha": null,
|
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"license": null,
|
"license": null,
|
||||||
"orig_site_json": "website/src/data/components/remote.json",
|
"upstream_latest_sha": null
|
||||||
"orig_item_id": "ossm-remote-knob-simple"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-remote-knob-simple-with-position-indicator",
|
"id": "ossm-remote-knob-simple-with-position-indicator",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Simple With Position Indicator.stl",
|
"source_path": "Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Simple With Position Indicator.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Simple With Position Indicator.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/remote.json",
|
||||||
|
"orig_item_id": "ossm-remote-knob-simple-with-position-indicator",
|
||||||
"pinned_sha": null,
|
"pinned_sha": null,
|
||||||
"pinned_raw_url": null,
|
"pinned_raw_url": null,
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/Non-standard/OSSM - Remote - Knob - Simple With Position Indicator.stl",
|
|
||||||
"checksum_sha256": null,
|
"checksum_sha256": null,
|
||||||
"last_checked": null,
|
"last_checked": null,
|
||||||
"upstream_latest_sha": null,
|
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"license": null,
|
"license": null,
|
||||||
"orig_site_json": "website/src/data/components/remote.json",
|
"upstream_latest_sha": null
|
||||||
"orig_item_id": "ossm-remote-knob-simple-with-position-indicator"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ossm-remote-top-cover",
|
"id": "ossm-remote-top-cover",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Remote/OSSM - Remote - Top Cover.stl",
|
"source_path": "Printed Parts/Remote/OSSM - Remote - Top Cover.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/OSSM - Remote - Top Cover.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/remote.json",
|
||||||
|
"orig_item_id": "ossm-remote-top-cover",
|
||||||
"pinned_sha": null,
|
"pinned_sha": null,
|
||||||
"pinned_raw_url": null,
|
"pinned_raw_url": null,
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Remote/OSSM - Remote - Top Cover.stl",
|
|
||||||
"checksum_sha256": null,
|
"checksum_sha256": null,
|
||||||
"last_checked": null,
|
"last_checked": null,
|
||||||
"upstream_latest_sha": null,
|
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"license": null,
|
"license": null,
|
||||||
"orig_site_json": "website/src/data/components/remote.json",
|
"upstream_latest_sha": null
|
||||||
"orig_item_id": "ossm-remote-top-cover"
|
},
|
||||||
|
{
|
||||||
|
"id": "ossm-toy-mount-double-double-24mm-threaded",
|
||||||
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
|
"source_path": "Printed Parts/Toy Mounts/OSSM - Toy Mount Double Double 24mm Threaded.stl",
|
||||||
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Double Double 24mm Threaded.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/toyMounts/ossm.json",
|
||||||
|
"orig_item_id": "ossm-toy-mount-double-double-24mm-threaded",
|
||||||
|
"pinned_sha": null,
|
||||||
|
"pinned_raw_url": null,
|
||||||
|
"checksum_sha256": null,
|
||||||
|
"last_checked": null,
|
||||||
|
"status": "pending",
|
||||||
|
"license": null,
|
||||||
|
"upstream_latest_sha": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ossm-toy-mount-double-double-rail-mounted",
|
||||||
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
|
"source_path": "Printed Parts/Toy Mounts/OSSM - Toy Mount Double Double Rail Mounted.stl",
|
||||||
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Double Double Rail Mounted.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/toyMounts/ossm.json",
|
||||||
|
"orig_item_id": "ossm-toy-mount-double-double-rail-mounted",
|
||||||
|
"pinned_sha": null,
|
||||||
|
"pinned_raw_url": null,
|
||||||
|
"checksum_sha256": null,
|
||||||
|
"last_checked": null,
|
||||||
|
"status": "pending",
|
||||||
|
"license": null,
|
||||||
|
"upstream_latest_sha": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ossm-toy-mount-flange-base-24mm-threaded",
|
||||||
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
|
"source_path": "Printed Parts/Toy Mounts/OSSM - Toy Mount Flange Base 24mm Threaded.stl",
|
||||||
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Flange Base 24mm Threaded.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/toyMounts/ossm.json",
|
||||||
|
"orig_item_id": "ossm-toy-mount-flange-base-24mm-threaded",
|
||||||
|
"pinned_sha": null,
|
||||||
|
"pinned_raw_url": null,
|
||||||
|
"checksum_sha256": null,
|
||||||
|
"last_checked": null,
|
||||||
|
"status": "pending",
|
||||||
|
"license": null,
|
||||||
|
"upstream_latest_sha": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ossm-toy-mount-flange-base-dildo-ring-2.5in ",
|
||||||
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
|
"source_path": "Printed Parts/Toy Mounts/OSSM - Toy Mount Flange Base Dildo Ring 2.5in.stl",
|
||||||
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Flange Base Dildo Ring 2.5in.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/toyMounts/ossm.json",
|
||||||
|
"orig_item_id": "ossm-toy-mount-flange-base-dildo-ring-2.5in ",
|
||||||
|
"pinned_sha": null,
|
||||||
|
"pinned_raw_url": null,
|
||||||
|
"checksum_sha256": null,
|
||||||
|
"last_checked": null,
|
||||||
|
"status": "pending",
|
||||||
|
"license": null,
|
||||||
|
"upstream_latest_sha": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ossm-toy-mount-flange-base-dildo-ring-2in",
|
||||||
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
|
"source_path": "Printed Parts/Toy Mounts/OSSM - Toy Mount Flange Base Dildo Ring 2in.stl",
|
||||||
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Flange Base Dildo Ring 2in.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/toyMounts/ossm.json",
|
||||||
|
"orig_item_id": "ossm-toy-mount-flange-base-dildo-ring-2in",
|
||||||
|
"pinned_sha": null,
|
||||||
|
"pinned_raw_url": null,
|
||||||
|
"checksum_sha256": null,
|
||||||
|
"last_checked": null,
|
||||||
|
"status": "pending",
|
||||||
|
"license": null,
|
||||||
|
"upstream_latest_sha": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ossm-toy-mount-sucson-mount-base-plate-24mm-threaded",
|
||||||
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
|
"source_path": "Printed Parts/Toy Mounts/OSSM - Toy Mount Sucson Mount Base Plate 24mm Threaded.stl",
|
||||||
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Sucson Mount Base Plate 24mm Threaded.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/toyMounts/ossm.json",
|
||||||
|
"orig_item_id": "ossm-toy-mount-sucson-mount-base-plate-24mm-threaded",
|
||||||
|
"pinned_sha": null,
|
||||||
|
"pinned_raw_url": null,
|
||||||
|
"checksum_sha256": null,
|
||||||
|
"last_checked": null,
|
||||||
|
"status": "pending",
|
||||||
|
"license": null,
|
||||||
|
"upstream_latest_sha": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ossm-toy-mount-sucson-mount-ring-insert-55mm",
|
||||||
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
|
"source_path": "Printed Parts/Toy Mounts/OSSM - Toy Mount Sucson Mount Ring Insert 55mm.stl",
|
||||||
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Sucson Mount Ring Insert 55mm.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/toyMounts/ossm.json",
|
||||||
|
"orig_item_id": "ossm-toy-mount-sucson-mount-ring-insert-55mm",
|
||||||
|
"pinned_sha": null,
|
||||||
|
"pinned_raw_url": null,
|
||||||
|
"checksum_sha256": null,
|
||||||
|
"last_checked": null,
|
||||||
|
"status": "pending",
|
||||||
|
"license": null,
|
||||||
|
"upstream_latest_sha": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ossm-toy-mount-sucson-mount-threaded-ring",
|
||||||
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
|
"source_path": "Printed Parts/Toy Mounts/OSSM - Toy Mount Sucson Mount Threaded Ring.stl",
|
||||||
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Sucson Mount Threaded Ring.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/toyMounts/ossm.json",
|
||||||
|
"orig_item_id": "ossm-toy-mount-sucson-mount-threaded-ring",
|
||||||
|
"pinned_sha": null,
|
||||||
|
"pinned_raw_url": null,
|
||||||
|
"checksum_sha256": null,
|
||||||
|
"last_checked": null,
|
||||||
|
"status": "pending",
|
||||||
|
"license": null,
|
||||||
|
"upstream_latest_sha": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ossm-toy-mount-tie-down-and-suction-plate-110mm",
|
||||||
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
|
"source_path": "Printed Parts/Toy Mounts/OSSM - Toy Mount Tie Down and Suction Plate 110mm.stl",
|
||||||
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Tie Down and Suction Plate 110mm.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/toyMounts/ossm.json",
|
||||||
|
"orig_item_id": "ossm-toy-mount-tie-down-and-suction-plate-110mm",
|
||||||
|
"pinned_sha": null,
|
||||||
|
"pinned_raw_url": null,
|
||||||
|
"checksum_sha256": null,
|
||||||
|
"last_checked": null,
|
||||||
|
"status": "pending",
|
||||||
|
"license": null,
|
||||||
|
"upstream_latest_sha": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ossm-toy-mount-tie-down-and-suction-plate-135mm",
|
||||||
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
|
"source_path": "Printed Parts/Toy Mounts/OSSM - Toy Mount Tie Down and Suction Plate 135mm.stl",
|
||||||
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Toy Mounts/OSSM - Toy Mount Tie Down and Suction Plate 135mm.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/toyMounts/ossm.json",
|
||||||
|
"orig_item_id": "ossm-toy-mount-tie-down-and-suction-plate-135mm",
|
||||||
|
"pinned_sha": null,
|
||||||
|
"pinned_raw_url": null,
|
||||||
|
"checksum_sha256": null,
|
||||||
|
"last_checked": null,
|
||||||
|
"status": "pending",
|
||||||
|
"license": null,
|
||||||
|
"upstream_latest_sha": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "pivot-plate",
|
"id": "pivot-plate",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Pivot Plate Left.stl",
|
"source_path": "Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Pivot Plate Left.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Pivot Plate Left.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/stand.json",
|
||||||
|
"orig_item_id": "pivot-plate",
|
||||||
"pinned_sha": null,
|
"pinned_sha": null,
|
||||||
"pinned_raw_url": null,
|
"pinned_raw_url": null,
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Pivot Plate Left.stl",
|
|
||||||
"checksum_sha256": null,
|
"checksum_sha256": null,
|
||||||
"last_checked": null,
|
"last_checked": null,
|
||||||
"upstream_latest_sha": null,
|
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"license": null,
|
"license": null,
|
||||||
"orig_site_json": "website/src/data/components/stand.json",
|
"upstream_latest_sha": null
|
||||||
"orig_item_id": "pivot-plate"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "pivot-plate-right",
|
"id": "pivot-plate-right",
|
||||||
"source_repo": "KinkyMakers/OSSM-hardware",
|
"source_repo": "KinkyMakers/OSSM-hardware",
|
||||||
"source_path": "Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Pivot Plate Right.stl",
|
"source_path": "Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Pivot Plate Right.stl",
|
||||||
"source_ref": "main",
|
"source_ref": "main",
|
||||||
|
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Pivot Plate Right.stl",
|
||||||
|
"orig_site_json": "website/src/data/components/stand.json",
|
||||||
|
"orig_item_id": "pivot-plate-right",
|
||||||
"pinned_sha": null,
|
"pinned_sha": null,
|
||||||
"pinned_raw_url": null,
|
"pinned_raw_url": null,
|
||||||
"local_path": "vendor/KinkyMakers-OSSM-hardware/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Pivot Plate Right.stl",
|
|
||||||
"checksum_sha256": null,
|
"checksum_sha256": null,
|
||||||
"last_checked": null,
|
"last_checked": null,
|
||||||
"upstream_latest_sha": null,
|
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"license": null,
|
"license": null,
|
||||||
"orig_site_json": "website/src/data/components/stand.json",
|
"upstream_latest_sha": null
|
||||||
"orig_item_id": "pivot-plate-right"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
75
roadmap/ROADMAP.md
Normal file
75
roadmap/ROADMAP.md
Normal 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
|
||||||
BIN
scripts/__pycache__/vendor_update.cpython-311.pyc
Normal file
BIN
scripts/__pycache__/vendor_update.cpython-311.pyc
Normal file
Binary file not shown.
@@ -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()
|
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
"""
|
"""
|
||||||
Download and pin external asset files from GitHub.
|
Download and pin external asset files from GitHub.
|
||||||
|
|
||||||
Downloads files specified in manifest, pins them to commit SHAs,
|
Automatically scans website/src/data/components for parts with GitHub URLs,
|
||||||
computes checksums, and optionally syncs vendor metadata back to site JSON files.
|
updates the manifest, and then downloads/pins files.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,8 +14,8 @@ import sys
|
|||||||
import time
|
import time
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional, Tuple, Generator, Any
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse, unquote, parse_qs
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
@@ -226,6 +226,182 @@ def download_file(url: str, dest_path: Path) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def parse_github_url(url: str) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str]]:
|
||||||
|
"""
|
||||||
|
Parse GitHub URL to return (owner, repo, ref, path).
|
||||||
|
Supports:
|
||||||
|
- https://github.com/owner/repo/blob/<ref>/path/to/file
|
||||||
|
- https://github.com/owner/repo/raw/<ref>/path/to/file
|
||||||
|
- https://raw.githubusercontent.com/owner/repo/<ref>/path/to/file
|
||||||
|
"""
|
||||||
|
if not url or not isinstance(url, str):
|
||||||
|
return None, None, None, None
|
||||||
|
|
||||||
|
# Check if it's a GitHub URL
|
||||||
|
if 'github.com' not in url:
|
||||||
|
return None, None, None, None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Handle raw.githubusercontent.com
|
||||||
|
if 'raw.githubusercontent.com' in url:
|
||||||
|
match_parts = url.split('/')
|
||||||
|
# https://raw.githubusercontent.com/OWNER/REPO/REF/PATH...
|
||||||
|
# parts: [https:, , raw.githubusercontent.com, OWNER, REPO, REF, PATH...]
|
||||||
|
if len(match_parts) >= 6:
|
||||||
|
owner = match_parts[3]
|
||||||
|
repo = match_parts[4]
|
||||||
|
ref = match_parts[5]
|
||||||
|
path = '/'.join(match_parts[6:]).split('?')[0]
|
||||||
|
return owner, repo, ref, unquote(path)
|
||||||
|
|
||||||
|
# Handle github.com and action.github.com
|
||||||
|
parsed = urlparse(url)
|
||||||
|
path = parsed.path.strip('/')
|
||||||
|
path_parts = path.split('/')
|
||||||
|
|
||||||
|
if len(path_parts) >= 4:
|
||||||
|
owner = path_parts[0]
|
||||||
|
repo = path_parts[1]
|
||||||
|
mode = path_parts[2] # 'blob' or 'raw'
|
||||||
|
|
||||||
|
if mode in ('blob', 'raw'):
|
||||||
|
ref = path_parts[3]
|
||||||
|
file_path = '/'.join(path_parts[4:])
|
||||||
|
|
||||||
|
# Check query params for ?raw=true
|
||||||
|
query_params = parse_qs(parsed.query)
|
||||||
|
if 'raw' in query_params or mode == 'raw':
|
||||||
|
return owner, repo, ref, unquote(file_path)
|
||||||
|
|
||||||
|
# Also treat 'blob' as a valid source if we just want the path
|
||||||
|
return owner, repo, ref, unquote(file_path)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None, None, None, None
|
||||||
|
|
||||||
|
|
||||||
|
def scan_site_components(components_dir: Path) -> Generator[Dict[str, Any], None, None]:
|
||||||
|
"""Recursively scan JSON files for parts with GitHub URLs."""
|
||||||
|
for json_file in components_dir.rglob('*.json'):
|
||||||
|
try:
|
||||||
|
with open(json_file, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
# Helper to find parts
|
||||||
|
queue = [data]
|
||||||
|
while queue:
|
||||||
|
item = queue.pop(0)
|
||||||
|
if isinstance(item, dict):
|
||||||
|
# Check if this item is a part
|
||||||
|
if 'id' in item and 'url' in item and item['url']:
|
||||||
|
owner, repo, ref, source_path = parse_github_url(item['url'])
|
||||||
|
if owner and repo and source_path:
|
||||||
|
yield {
|
||||||
|
'id': item['id'],
|
||||||
|
'url': item['url'],
|
||||||
|
'owner': owner,
|
||||||
|
'repo': repo,
|
||||||
|
'ref': ref or 'main',
|
||||||
|
'source_path': source_path,
|
||||||
|
'orig_site_json': json_file
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add children to queue
|
||||||
|
queue.extend(item.values())
|
||||||
|
elif isinstance(item, list):
|
||||||
|
queue.extend(item)
|
||||||
|
|
||||||
|
except (json.JSONDecodeError, IOError) as e:
|
||||||
|
print(f"Warning: Could not read {json_file}: {e}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def regenerate_manifest(manifest_path: Path, repo_root: Path) -> Tuple[List[Dict], int]:
|
||||||
|
"""
|
||||||
|
Regenerate manifest from site data.
|
||||||
|
Preserves state of existing entries.
|
||||||
|
Returns (new_manifest_list, changes_count).
|
||||||
|
"""
|
||||||
|
print("Scanning website components to regenerate manifest...")
|
||||||
|
|
||||||
|
# Load existing manifest to preserve state
|
||||||
|
old_manifest = {}
|
||||||
|
if manifest_path.exists():
|
||||||
|
with open(manifest_path, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if isinstance(data, list):
|
||||||
|
old_manifest = {entry['id']: entry for entry in data}
|
||||||
|
|
||||||
|
new_manifest = {}
|
||||||
|
components_dir = repo_root / 'website/src/data/components'
|
||||||
|
changes_count = 0
|
||||||
|
|
||||||
|
if not components_dir.exists():
|
||||||
|
print(f"Warning: Components directory not found: {components_dir}", file=sys.stderr)
|
||||||
|
return list(old_manifest.values()), 0
|
||||||
|
|
||||||
|
for part in scan_site_components(components_dir):
|
||||||
|
part_id = part['id']
|
||||||
|
old_entry = old_manifest.get(part_id)
|
||||||
|
|
||||||
|
# Calculate local path
|
||||||
|
# vendor/{owner}-{repo}/{path}
|
||||||
|
local_path = f"vendor/{part['owner']}-{part['repo']}/{part['source_path']}"
|
||||||
|
source_repo = f"{part['owner']}/{part['repo']}"
|
||||||
|
orig_site_json = str(part['orig_site_json'].relative_to(repo_root))
|
||||||
|
|
||||||
|
entry = {
|
||||||
|
'id': part_id,
|
||||||
|
'source_repo': source_repo,
|
||||||
|
'source_path': part['source_path'],
|
||||||
|
'source_ref': part['ref'],
|
||||||
|
'local_path': local_path,
|
||||||
|
'orig_site_json': orig_site_json,
|
||||||
|
'orig_item_id': part_id
|
||||||
|
}
|
||||||
|
|
||||||
|
# Preserve state if exists and config matches
|
||||||
|
if old_entry:
|
||||||
|
# Check if source config changed
|
||||||
|
config_changed = (
|
||||||
|
old_entry.get('source_repo') != source_repo or
|
||||||
|
old_entry.get('source_path') != part['source_path'] or
|
||||||
|
old_entry.get('source_ref') != part['ref']
|
||||||
|
)
|
||||||
|
|
||||||
|
if not config_changed:
|
||||||
|
# Copy state
|
||||||
|
for key in ['pinned_sha', 'pinned_raw_url', 'checksum_sha256', 'last_checked', 'status', 'license', 'upstream_latest_sha']:
|
||||||
|
if key in old_entry:
|
||||||
|
entry[key] = old_entry[key]
|
||||||
|
else:
|
||||||
|
print(f" Config changed for {part_id}, resetting status.")
|
||||||
|
entry['status'] = 'pending'
|
||||||
|
entry['pinned_sha'] = None
|
||||||
|
changes_count += 1
|
||||||
|
|
||||||
|
# Check if we updated manifest info (like orig_site_json moved)
|
||||||
|
if (old_entry.get('orig_site_json') != orig_site_json or
|
||||||
|
old_entry.get('local_path') != local_path):
|
||||||
|
changes_count += 1
|
||||||
|
else:
|
||||||
|
print(f" New part found: {part_id}")
|
||||||
|
entry['status'] = 'pending'
|
||||||
|
entry['pinned_sha'] = None
|
||||||
|
changes_count += 1
|
||||||
|
|
||||||
|
new_manifest[part_id] = entry
|
||||||
|
|
||||||
|
# Check for removed items
|
||||||
|
removed_count = len(old_manifest) - len(new_manifest)
|
||||||
|
if removed_count > 0:
|
||||||
|
print(f" Removed {removed_count} parts that are no longer in site JSONs.")
|
||||||
|
changes_count += removed_count
|
||||||
|
|
||||||
|
return sorted(new_manifest.values(), key=lambda x: x['id']), changes_count
|
||||||
|
|
||||||
|
|
||||||
def update_manifest_entry(
|
def update_manifest_entry(
|
||||||
entry: Dict,
|
entry: Dict,
|
||||||
api: GitHubAPI,
|
api: GitHubAPI,
|
||||||
@@ -255,6 +431,31 @@ def update_manifest_entry(
|
|||||||
if not local_path.is_absolute():
|
if not local_path.is_absolute():
|
||||||
local_path = repo_root / local_path
|
local_path = repo_root / local_path
|
||||||
|
|
||||||
|
# Check if file exists and is already at the correct version
|
||||||
|
current_pinned_sha = entry.get('pinned_sha')
|
||||||
|
if current_pinned_sha == commit_sha and local_path.exists():
|
||||||
|
if dry_run:
|
||||||
|
print(f" [DRY RUN] File up to date ({commit_sha}), would skip download.")
|
||||||
|
else:
|
||||||
|
print(f" File up to date ({commit_sha}), skipping download.")
|
||||||
|
# Ensure checksum is present
|
||||||
|
if 'checksum_sha256' not in entry or not entry['checksum_sha256']:
|
||||||
|
entry['checksum_sha256'] = compute_sha256(local_path)
|
||||||
|
|
||||||
|
entry['pinned_sha'] = commit_sha
|
||||||
|
entry['pinned_raw_url'] = pinned_raw_url
|
||||||
|
entry['last_checked'] = datetime.now(timezone.utc).isoformat()
|
||||||
|
entry['upstream_latest_sha'] = commit_sha
|
||||||
|
entry['status'] = 'up-to-date'
|
||||||
|
|
||||||
|
# If license is missing, try to get it, otherwise keep existing
|
||||||
|
if 'license' not in entry and not dry_run:
|
||||||
|
license_info = api.get_license(owner, repo, commit_sha)
|
||||||
|
if license_info:
|
||||||
|
entry['license'] = license_info
|
||||||
|
|
||||||
|
return entry
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
print(f" [DRY RUN] Would download to {local_path}")
|
print(f" [DRY RUN] Would download to {local_path}")
|
||||||
print(f" [DRY RUN] Pinned SHA: {commit_sha}")
|
print(f" [DRY RUN] Pinned SHA: {commit_sha}")
|
||||||
@@ -309,16 +510,13 @@ def sync_to_site_json(entry: Dict, repo_root: Path) -> bool:
|
|||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
|
||||||
# Find the printed part in the nested structure
|
# Find the printed part in the nested structure
|
||||||
def find_and_update_part(obj, target_id, path=''):
|
def find_and_update_part(obj, target_id):
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
# Check if this is a printedParts array
|
# If this object IS the part (has the ID)
|
||||||
if 'printedParts' in obj and isinstance(obj['printedParts'], list):
|
if obj.get('id') == target_id:
|
||||||
for part in obj['printedParts']:
|
if 'vendor' not in obj:
|
||||||
if isinstance(part, dict) and part.get('id') == target_id:
|
obj['vendor'] = {}
|
||||||
# Update this part
|
obj['vendor'].update({
|
||||||
if 'vendor' not in part:
|
|
||||||
part['vendor'] = {}
|
|
||||||
part['vendor'].update({
|
|
||||||
'manifest_id': entry['id'],
|
'manifest_id': entry['id'],
|
||||||
'local_path': entry['local_path'],
|
'local_path': entry['local_path'],
|
||||||
'pinned_sha': entry['pinned_sha'],
|
'pinned_sha': entry['pinned_sha'],
|
||||||
@@ -329,25 +527,7 @@ def sync_to_site_json(entry: Dict, repo_root: Path) -> bool:
|
|||||||
})
|
})
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Check bodyParts, knobs, etc.
|
# Recursively search values
|
||||||
for key in ['bodyParts', 'knobs']:
|
|
||||||
if key in obj and isinstance(obj[key], list):
|
|
||||||
for part in obj[key]:
|
|
||||||
if isinstance(part, dict) and part.get('id') == target_id:
|
|
||||||
if 'vendor' not in part:
|
|
||||||
part['vendor'] = {}
|
|
||||||
part['vendor'].update({
|
|
||||||
'manifest_id': entry['id'],
|
|
||||||
'local_path': entry['local_path'],
|
|
||||||
'pinned_sha': entry['pinned_sha'],
|
|
||||||
'pinned_raw_url': entry['pinned_raw_url'],
|
|
||||||
'checksum_sha256': entry['checksum_sha256'],
|
|
||||||
'last_checked': entry['last_checked'],
|
|
||||||
'status': entry['status']
|
|
||||||
})
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Recursively search
|
|
||||||
for value in obj.values():
|
for value in obj.values():
|
||||||
if find_and_update_part(value, target_id):
|
if find_and_update_part(value, target_id):
|
||||||
return True
|
return True
|
||||||
@@ -396,9 +576,9 @@ def main():
|
|||||||
help='Show what would be done without downloading files'
|
help='Show what would be done without downloading files'
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--sync-site',
|
'--no-sync',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help='Sync vendor metadata back to site JSON files'
|
help='Skip syncing vendor metadata back to site JSON files'
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--delay',
|
'--delay',
|
||||||
@@ -406,6 +586,16 @@ def main():
|
|||||||
default=0.5,
|
default=0.5,
|
||||||
help='Delay between API requests in seconds (default: 0.5)'
|
help='Delay between API requests in seconds (default: 0.5)'
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--no-scan',
|
||||||
|
action='store_true',
|
||||||
|
help='Skip scanning website for new components'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--scan-only',
|
||||||
|
action='store_true',
|
||||||
|
help='Only scan website and update manifest, do not check/download files'
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@@ -414,11 +604,28 @@ def main():
|
|||||||
manifest_path = (script_dir / args.manifest).resolve()
|
manifest_path = (script_dir / args.manifest).resolve()
|
||||||
repo_root = script_dir
|
repo_root = script_dir
|
||||||
|
|
||||||
|
# Regenerate manifest from website scan (unless disabled)
|
||||||
|
if not args.no_scan and not args.entry:
|
||||||
|
manifest_list, changes = regenerate_manifest(manifest_path, repo_root)
|
||||||
|
if changes > 0:
|
||||||
|
print(f"Manifest regenerated with {changes} changes.")
|
||||||
|
if not args.dry_run:
|
||||||
|
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(manifest_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(manifest_list, f, indent=2, sort_keys=False)
|
||||||
|
else:
|
||||||
|
print("No changes in manifest structure detected.")
|
||||||
|
|
||||||
|
if args.scan_only:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Reload manifest data for processing
|
||||||
|
manifest_data = manifest_list
|
||||||
|
else:
|
||||||
if not manifest_path.exists():
|
if not manifest_path.exists():
|
||||||
print(f"Error: Manifest file not found: {manifest_path}", file=sys.stderr)
|
print(f"Error: Manifest file not found: {manifest_path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Load manifest
|
|
||||||
with open(manifest_path, 'r', encoding='utf-8') as f:
|
with open(manifest_path, 'r', encoding='utf-8') as f:
|
||||||
manifest_data = json.load(f)
|
manifest_data = json.load(f)
|
||||||
|
|
||||||
@@ -446,7 +653,7 @@ def main():
|
|||||||
updated_entry = update_manifest_entry(entry, api, repo_root, dry_run=args.dry_run)
|
updated_entry = update_manifest_entry(entry, api, repo_root, dry_run=args.dry_run)
|
||||||
manifest[entry_id] = updated_entry
|
manifest[entry_id] = updated_entry
|
||||||
|
|
||||||
if args.sync_site and not args.dry_run:
|
if not args.no_sync and not args.dry_run:
|
||||||
sync_to_site_json(updated_entry, repo_root)
|
sync_to_site_json(updated_entry, repo_root)
|
||||||
|
|
||||||
updated_count += 1
|
updated_count += 1
|
||||||
|
|||||||
19
website/package-lock.json
generated
19
website/package-lock.json
generated
@@ -69,7 +69,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
@@ -1427,7 +1426,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
|
||||||
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
@@ -1467,7 +1465,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -1932,7 +1929,6 @@
|
|||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -2696,7 +2692,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
||||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -3908,7 +3903,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
@@ -4673,7 +4667,6 @@
|
|||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -4889,7 +4882,6 @@
|
|||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
},
|
},
|
||||||
@@ -5699,7 +5691,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -5845,13 +5836,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
|
||||||
"version": "7.16.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
|
||||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"node_modules/unzipper": {
|
"node_modules/unzipper": {
|
||||||
"version": "0.10.14",
|
"version": "0.10.14",
|
||||||
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz",
|
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz",
|
||||||
@@ -5958,7 +5942,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
|
||||||
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -6050,7 +6033,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -6233,7 +6215,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
|
||||||
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
|
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,21 @@ export default function BOMSummary({ config }) {
|
|||||||
const [zipProgress, setZipProgress] = useState({ current: 0, total: 0, currentFile: '' });
|
const [zipProgress, setZipProgress] = useState({ current: 0, total: 0, currentFile: '' });
|
||||||
const [hardwareViewMode, setHardwareViewMode] = useState('unified'); // 'unified' or 'expanded'
|
const [hardwareViewMode, setHardwareViewMode] = useState('unified'); // 'unified' or 'expanded'
|
||||||
const [activeTab, setActiveTab] = useState('overview'); // 'overview', 'printed', 'hardware'
|
const [activeTab, setActiveTab] = useState('overview'); // 'overview', 'printed', 'hardware'
|
||||||
|
const evaluateCondition = (condition, config) => {
|
||||||
|
if (!condition) return true;
|
||||||
|
|
||||||
|
return Object.entries(condition).every(([key, value]) => {
|
||||||
|
// Handle dot notation for nested config (e.g., motor.id)
|
||||||
|
const keys = key.split('.');
|
||||||
|
let current = config;
|
||||||
|
for (const k of keys) {
|
||||||
|
if (current === null || current === undefined) return false;
|
||||||
|
current = current[k];
|
||||||
|
}
|
||||||
|
return current === value;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const calculateTotal = () => {
|
const calculateTotal = () => {
|
||||||
let total = 0;
|
let total = 0;
|
||||||
|
|
||||||
@@ -49,73 +64,61 @@ export default function BOMSummary({ config }) {
|
|||||||
|
|
||||||
const getRequiredPrintedParts = () => {
|
const getRequiredPrintedParts = () => {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
const isMiddlePivotSelected = config.mount?.id === 'middle-pivot';
|
|
||||||
|
// Always include components that are marked as required and meet their conditions
|
||||||
|
Object.entries(partsData.components || {}).forEach(([componentKey, component]) => {
|
||||||
|
const category = component.category || componentKey;
|
||||||
|
|
||||||
|
// Handle standard printedParts array
|
||||||
|
if (component.printedParts) {
|
||||||
|
component.printedParts.forEach((part) => {
|
||||||
|
if (part.required && evaluateCondition(part.Condition, config)) {
|
||||||
|
parts.push({ ...part, category });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle systems (for hinges, remotes, etc.)
|
||||||
|
if (component.systems) {
|
||||||
|
// If it's a selected system, include its printed parts
|
||||||
|
const selectedSystemId = config[componentKey] || config.standHinge; // Fallback for naming mismatches
|
||||||
|
const system = component.systems[selectedSystemId?.id || selectedSystemId];
|
||||||
|
|
||||||
|
if (system) {
|
||||||
|
const systemParts = system.printedParts || system.bodyParts || [];
|
||||||
|
systemParts.forEach((part) => {
|
||||||
|
if (part.required && evaluateCondition(part.Condition, config)) {
|
||||||
|
parts.push({ ...part, category });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remote knobs are handled by looking up the knob in the system
|
||||||
|
if (componentKey === 'remotes' && config.remoteKnob) {
|
||||||
|
const knobPart = system.knobs?.find(k => k.id === config.remoteKnob.id);
|
||||||
|
if (knobPart) {
|
||||||
|
parts.push({ ...knobPart, category: 'Remote Knobs' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle options that are not explicitly in the "components" top-level structure but represent printed parts
|
||||||
|
|
||||||
|
// Mount variations (if not already handled by required parts)
|
||||||
|
if (config.mount && partsData.components?.mounts?.printedParts) {
|
||||||
|
const mountPart = partsData.components.mounts.printedParts.find(p => p.id === config.mount.id);
|
||||||
|
if (mountPart) parts.push({ ...mountPart, category: 'Mount' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom Cover
|
||||||
const coverId = config.cover?.id;
|
const coverId = config.cover?.id;
|
||||||
const isStandardCover = coverId === 'standard-cover';
|
const isStandardCover = coverId === 'standard-cover';
|
||||||
const isBlankCover = coverId === 'blank-cover';
|
const isBlankCover = coverId === 'blank-cover';
|
||||||
const isCustomCover = config.cover !== null && !isStandardCover && !isBlankCover;
|
const isCustomCover = config.cover !== null && !isStandardCover && !isBlankCover;
|
||||||
|
|
||||||
// Always include actuator parts, but exclude ossm-actuator-body-middle if middlePivot is selected
|
|
||||||
// and exclude ossm-actuator-body-cover if a non-standard cover is selected
|
|
||||||
if (partsData.components?.actuator?.printedParts) {
|
|
||||||
partsData.components.actuator.printedParts.forEach((part) => {
|
|
||||||
if (part.required) {
|
|
||||||
// Skip the actuator body middle part if middle pivot is selected
|
|
||||||
if (isMiddlePivotSelected && part.id === 'ossm-actuator-body-middle') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Skip the default cover if a non-standard cover is selected (blank or custom)
|
|
||||||
if ((isBlankCover || isCustomCover) && part.id === 'ossm-actuator-body-cover') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
parts.push({ ...part, category: 'Actuator Body' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include mount-specific parts based on selection
|
|
||||||
if (config.mount) {
|
|
||||||
const mountId = config.mount.id;
|
|
||||||
if (mountId === 'middle-pivot' && partsData.components?.middlePivot?.printedParts) {
|
|
||||||
partsData.components.middlePivot.printedParts.forEach((part) => {
|
|
||||||
if (part.required) {
|
|
||||||
parts.push({ ...part, category: 'Mount', replacesActuatorMiddle: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (mountId === 'pitclamp' && partsData.components?.pitClamp?.printedParts) {
|
|
||||||
const selectedMotorId = config.motor?.id;
|
|
||||||
partsData.components.pitClamp.printedParts.forEach((part) => {
|
|
||||||
if (part.required) {
|
|
||||||
// Filter motor-specific parts - only include the one that matches the selected motor
|
|
||||||
const isMotorSpecificPart = part.id.includes('57AIM30') || part.id.includes('42AIM30') || part.id.includes('iHSV57');
|
|
||||||
if (isMotorSpecificPart) {
|
|
||||||
// Only include if it matches the selected motor
|
|
||||||
const expectedPartId = `ossm-pitclamp-mini-${selectedMotorId}`;
|
|
||||||
if (part.id === expectedPartId) {
|
|
||||||
parts.push({ ...part, category: 'Mount' });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Include non-motor-specific parts (lower, upper, handle, dogbone-nuts)
|
|
||||||
parts.push({ ...part, category: 'Mount' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include mount option part
|
|
||||||
if (partsData.components?.mounts?.printedParts) {
|
|
||||||
const mountPart = partsData.components.mounts.printedParts.find(p => p.id === mountId);
|
|
||||||
if (mountPart) {
|
|
||||||
parts.push({ ...mountPart, category: 'Mount' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include custom cover if selected (replaces default cover)
|
|
||||||
// Skip blank cover and standard cover (standard uses the component, blank uses nothing)
|
|
||||||
if (isCustomCover) {
|
if (isCustomCover) {
|
||||||
const coverOption = config.cover;
|
const coverOption = config.cover;
|
||||||
// Convert cover option to part format for BOM
|
|
||||||
parts.push({
|
parts.push({
|
||||||
id: coverOption.id,
|
id: coverOption.id,
|
||||||
name: coverOption.name,
|
name: coverOption.name,
|
||||||
@@ -128,22 +131,23 @@ export default function BOMSummary({ config }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include PCB mount parts if a PCB mount is selected
|
// Stand components (feet, supports)
|
||||||
if (config.pcbMount) {
|
if (config.standFeet && partsData.components?.feet?.printedParts) {
|
||||||
const pcbMountId = config.pcbMount.id;
|
const feetPart = partsData.components.feet.printedParts.find(p => p.id === config.standFeet.id);
|
||||||
const pcbMountComponent = partsData.components?.[pcbMountId];
|
if (feetPart) parts.push({ ...feetPart, category: 'Stand Feet' });
|
||||||
if (pcbMountComponent?.printedParts) {
|
}
|
||||||
pcbMountComponent.printedParts.forEach((part) => {
|
|
||||||
if (part.required) {
|
if (config.standCrossbarSupports && partsData.components?.crossbarSupports?.printedParts) {
|
||||||
parts.push({ ...part, category: 'PCB Mount' });
|
const selectedSupportIds = new Set(config.standCrossbarSupports.map(opt => opt.id));
|
||||||
|
partsData.components.crossbarSupports.printedParts.forEach((part) => {
|
||||||
|
if (selectedSupportIds.has(part.id) && !part.isHardwareOnly) {
|
||||||
|
parts.push({ ...part, category: 'Stand Crossbar Supports' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Include toy mount parts if any toy mounts are selected
|
// Toy Mounts
|
||||||
if (config.toyMountOptions && config.toyMountOptions.length > 0) {
|
if (config.toyMountOptions && config.toyMountOptions.length > 0 && partsData.components?.toyMounts?.printedParts) {
|
||||||
if (partsData.components?.toyMounts?.printedParts) {
|
|
||||||
const selectedToyMountIds = new Set(config.toyMountOptions.map(opt => opt.id));
|
const selectedToyMountIds = new Set(config.toyMountOptions.map(opt => opt.id));
|
||||||
partsData.components.toyMounts.printedParts.forEach((part) => {
|
partsData.components.toyMounts.printedParts.forEach((part) => {
|
||||||
if (selectedToyMountIds.has(part.id)) {
|
if (selectedToyMountIds.has(part.id)) {
|
||||||
@@ -151,83 +155,16 @@ export default function BOMSummary({ config }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Include stand parts if stand options are selected
|
// Handle 'replaces' logic
|
||||||
if (config.standHinge || config.standFeet || (config.standCrossbarSupports && config.standCrossbarSupports.length > 0)) {
|
const replacedIds = new Set();
|
||||||
if (partsData.components?.stand?.printedParts) {
|
parts.forEach(part => {
|
||||||
partsData.components.stand.printedParts.forEach((part) => {
|
if (part.replaces) {
|
||||||
if (part.required) {
|
part.replaces.forEach(id => replacedIds.add(id));
|
||||||
parts.push({ ...part, category: 'Stand' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include stand hinge system parts
|
|
||||||
if (config.standHinge && partsData.components?.hinges?.systems) {
|
|
||||||
const hingeSystem = partsData.components.hinges.systems[config.standHinge.id];
|
|
||||||
if (hingeSystem?.printedParts) {
|
|
||||||
hingeSystem.printedParts.forEach((part) => {
|
|
||||||
if (part.required) {
|
|
||||||
parts.push({ ...part, category: 'Stand Hinges' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include stand feet option part
|
|
||||||
if (config.standFeet && partsData.components?.feet?.printedParts) {
|
|
||||||
const feetPart = partsData.components.feet.printedParts.find(p => p.id === config.standFeet.id);
|
|
||||||
if (feetPart) {
|
|
||||||
parts.push({ ...feetPart, category: 'Stand Feet' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include stand crossbar support option parts
|
|
||||||
if (config.standCrossbarSupports && config.standCrossbarSupports.length > 0 && partsData.components?.crossbarSupports?.printedParts) {
|
|
||||||
const selectedSupportIds = new Set(config.standCrossbarSupports.map(opt => opt.id));
|
|
||||||
partsData.components.crossbarSupports.printedParts.forEach((part) => {
|
|
||||||
if (selectedSupportIds.has(part.id)) {
|
|
||||||
// Skip hardware-only parts (e.g., aluminum standard-90-degree-support)
|
|
||||||
// These should only appear in the hardware list
|
|
||||||
if (part.isHardwareOnly) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
parts.push({ ...part, category: 'Stand Crossbar Supports' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include remote system parts if a remote knob is selected
|
|
||||||
if (config.remoteKnob && partsData.components?.remotes?.systems) {
|
|
||||||
// Find which system contains this knob
|
|
||||||
let remoteSystem = null;
|
|
||||||
Object.values(partsData.components.remotes.systems).forEach((system) => {
|
|
||||||
if (system.knobs && system.knobs.find(k => k.id === config.remoteKnob.id)) {
|
|
||||||
remoteSystem = system;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (remoteSystem) {
|
return parts.filter(part => !replacedIds.has(part.id));
|
||||||
// Include body parts from the system
|
|
||||||
if (remoteSystem.bodyParts) {
|
|
||||||
remoteSystem.bodyParts.forEach((part) => {
|
|
||||||
if (part.required) {
|
|
||||||
parts.push({ ...part, category: 'Remote Body' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include the selected remote knob part
|
|
||||||
const knobPart = remoteSystem.knobs?.find(p => p.id === config.remoteKnob.id);
|
|
||||||
if (knobPart) {
|
|
||||||
parts.push({ ...knobPart, category: 'Remote Knobs' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to categorize hardware by type
|
// Helper function to categorize hardware by type
|
||||||
@@ -298,6 +235,10 @@ export default function BOMSummary({ config }) {
|
|||||||
if (hingeSystem?.hardwareParts) {
|
if (hingeSystem?.hardwareParts) {
|
||||||
hingeSystem.hardwareParts.forEach((hardware) => {
|
hingeSystem.hardwareParts.forEach((hardware) => {
|
||||||
if (!hardware.required) return;
|
if (!hardware.required) return;
|
||||||
|
|
||||||
|
// Evaluate condition for hardware
|
||||||
|
if (!evaluateCondition(hardware.Condition, config)) return;
|
||||||
|
|
||||||
const key = hardware.id;
|
const key = hardware.id;
|
||||||
if (hardwareMap.has(key)) {
|
if (hardwareMap.has(key)) {
|
||||||
const existing = hardwareMap.get(key);
|
const existing = hardwareMap.get(key);
|
||||||
@@ -327,6 +268,10 @@ export default function BOMSummary({ config }) {
|
|||||||
if (remoteSystem?.hardwareParts) {
|
if (remoteSystem?.hardwareParts) {
|
||||||
remoteSystem.hardwareParts.forEach((hardware) => {
|
remoteSystem.hardwareParts.forEach((hardware) => {
|
||||||
if (!hardware.required) return;
|
if (!hardware.required) return;
|
||||||
|
|
||||||
|
// Evaluate condition for hardware
|
||||||
|
if (!evaluateCondition(hardware.Condition, config)) return;
|
||||||
|
|
||||||
const key = hardware.id;
|
const key = hardware.id;
|
||||||
if (hardwareMap.has(key)) {
|
if (hardwareMap.has(key)) {
|
||||||
const existing = hardwareMap.get(key);
|
const existing = hardwareMap.get(key);
|
||||||
@@ -368,6 +313,9 @@ export default function BOMSummary({ config }) {
|
|||||||
component.hardwareParts.forEach((hardware) => {
|
component.hardwareParts.forEach((hardware) => {
|
||||||
if (!hardware.required) return;
|
if (!hardware.required) return;
|
||||||
|
|
||||||
|
// Evaluate condition for hardware
|
||||||
|
if (!evaluateCondition(hardware.Condition, config)) return;
|
||||||
|
|
||||||
// If component has selected parts, check if hardware should be included
|
// If component has selected parts, check if hardware should be included
|
||||||
let shouldInclude = false;
|
let shouldInclude = false;
|
||||||
|
|
||||||
@@ -408,7 +356,6 @@ export default function BOMSummary({ config }) {
|
|||||||
return hardwareParts;
|
return hardwareParts;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse time estimate string (e.g., "2h14m", "1h3m", "40m25s") to minutes
|
|
||||||
const parseTimeToMinutes = (timeStr) => {
|
const parseTimeToMinutes = (timeStr) => {
|
||||||
if (!timeStr || typeof timeStr !== 'string') return 0;
|
if (!timeStr || typeof timeStr !== 'string') return 0;
|
||||||
|
|
||||||
@@ -697,8 +644,7 @@ export default function BOMSummary({ config }) {
|
|||||||
onClick={() => setActiveTab(tab.id)}
|
onClick={() => setActiveTab(tab.id)}
|
||||||
className={`
|
className={`
|
||||||
py-4 px-1 border-b-2 font-medium text-sm transition-colors
|
py-4 px-1 border-b-2 font-medium text-sm transition-colors
|
||||||
${
|
${activeTab === tab.id
|
||||||
activeTab === tab.id
|
|
||||||
? 'border-blue-500 dark:border-blue-400 text-blue-600 dark:text-blue-400'
|
? 'border-blue-500 dark:border-blue-400 text-blue-600 dark:text-blue-400'
|
||||||
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
|
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
|
||||||
}
|
}
|
||||||
@@ -816,7 +762,7 @@ export default function BOMSummary({ config }) {
|
|||||||
<img
|
<img
|
||||||
src={config.mount.image}
|
src={config.mount.image}
|
||||||
alt={config.mount.name}
|
alt={config.mount.name}
|
||||||
className="h-32 w-32 object-contain rounded-lg bg-gray-100 mb-2"
|
className="h-32 w-32 object-contain rounded-lg bg-gray-100 dark:bg-gray-700 mb-2"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
e.target.style.display = 'none';
|
e.target.style.display = 'none';
|
||||||
}}
|
}}
|
||||||
@@ -831,7 +777,7 @@ export default function BOMSummary({ config }) {
|
|||||||
<img
|
<img
|
||||||
src={config.cover.image}
|
src={config.cover.image}
|
||||||
alt={config.cover.name}
|
alt={config.cover.name}
|
||||||
className="h-32 w-32 object-contain rounded-lg bg-gray-100 mb-2"
|
className="h-32 w-32 object-contain rounded-lg bg-gray-100 dark:bg-gray-700 mb-2"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
e.target.style.display = 'none';
|
e.target.style.display = 'none';
|
||||||
}}
|
}}
|
||||||
@@ -846,7 +792,7 @@ export default function BOMSummary({ config }) {
|
|||||||
<img
|
<img
|
||||||
src={config.pcbMount.image}
|
src={config.pcbMount.image}
|
||||||
alt={config.pcbMount.name}
|
alt={config.pcbMount.name}
|
||||||
className="h-32 w-32 object-contain rounded-lg bg-gray-100 mb-2"
|
className="h-32 w-32 object-contain rounded-lg bg-gray-100 dark:bg-gray-700 mb-2"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
e.target.style.display = 'none';
|
e.target.style.display = 'none';
|
||||||
}}
|
}}
|
||||||
@@ -861,7 +807,7 @@ export default function BOMSummary({ config }) {
|
|||||||
<img
|
<img
|
||||||
src={config.standHinge.image}
|
src={config.standHinge.image}
|
||||||
alt={config.standHinge.name}
|
alt={config.standHinge.name}
|
||||||
className="h-32 w-32 object-contain rounded-lg bg-gray-100 mb-2"
|
className="h-32 w-32 object-contain rounded-lg bg-gray-100 dark:bg-gray-700 mb-2"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
e.target.style.display = 'none';
|
e.target.style.display = 'none';
|
||||||
}}
|
}}
|
||||||
@@ -876,7 +822,7 @@ export default function BOMSummary({ config }) {
|
|||||||
<img
|
<img
|
||||||
src={config.standFeet.image}
|
src={config.standFeet.image}
|
||||||
alt={config.standFeet.name}
|
alt={config.standFeet.name}
|
||||||
className="h-32 w-32 object-contain rounded-lg bg-gray-100 mb-2"
|
className="h-32 w-32 object-contain rounded-lg bg-gray-100 dark:bg-gray-700 mb-2"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
e.target.style.display = 'none';
|
e.target.style.display = 'none';
|
||||||
}}
|
}}
|
||||||
@@ -913,7 +859,7 @@ export default function BOMSummary({ config }) {
|
|||||||
<img
|
<img
|
||||||
src={remoteSystem.image}
|
src={remoteSystem.image}
|
||||||
alt={remoteSystem.name}
|
alt={remoteSystem.name}
|
||||||
className="h-32 w-32 object-contain rounded-lg bg-gray-100 mb-2"
|
className="h-32 w-32 object-contain rounded-lg bg-gray-100 dark:bg-gray-700 mb-2"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
e.target.style.display = 'none';
|
e.target.style.display = 'none';
|
||||||
}}
|
}}
|
||||||
@@ -938,7 +884,7 @@ export default function BOMSummary({ config }) {
|
|||||||
<img
|
<img
|
||||||
src={toyMount.image}
|
src={toyMount.image}
|
||||||
alt={toyMount.name}
|
alt={toyMount.name}
|
||||||
className="h-32 w-32 object-contain rounded-lg bg-gray-100 mb-2"
|
className="h-32 w-32 object-contain rounded-lg bg-gray-100 dark:bg-gray-700 mb-2"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
e.target.style.display = 'none';
|
e.target.style.display = 'none';
|
||||||
}}
|
}}
|
||||||
@@ -1102,18 +1048,18 @@ export default function BOMSummary({ config }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="min-w-full divide-y divide-gray-200 border border-gray-200 rounded-lg">
|
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50 dark:bg-gray-800">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">Part Name</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Part Name</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">Color</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Color</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">Description</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Description</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">File Path</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">File Path</th>
|
||||||
<th className="px-4 py-3 text-right text-xs font-medium text-gray-700 uppercase tracking-wider">Quantity</th>
|
<th className="px-4 py-3 text-right text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Quantity</th>
|
||||||
<th className="px-4 py-3 text-right text-xs font-medium text-gray-700 uppercase tracking-wider">Filament</th>
|
<th className="px-4 py-3 text-right text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Filament</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
{parts.map((part) => {
|
{parts.map((part) => {
|
||||||
const partColour = part.colour || 'primary';
|
const partColour = part.colour || 'primary';
|
||||||
const colorHex = getColorHex(
|
const colorHex = getColorHex(
|
||||||
@@ -1126,38 +1072,38 @@ export default function BOMSummary({ config }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={part.id} className="hover:bg-gray-50">
|
<tr key={part.id} className="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
||||||
<td className="px-4 py-3 whitespace-nowrap">
|
<td className="px-4 py-3 whitespace-nowrap">
|
||||||
<p className="text-sm font-medium text-gray-900">{part.name}</p>
|
<p className="text-sm font-medium text-gray-900 dark:text-white">{part.name}</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 whitespace-nowrap">
|
<td className="px-4 py-3 whitespace-nowrap">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
className="w-4 h-4 rounded-full border border-gray-300"
|
className="w-4 h-4 rounded-full border border-gray-300 dark:border-gray-600"
|
||||||
style={{ backgroundColor: colorHex }}
|
style={{ backgroundColor: colorHex }}
|
||||||
title={`${partColour === 'primary' ? 'Primary' : 'Secondary'} color: ${colorName}`}
|
title={`${partColour === 'primary' ? 'Primary' : 'Secondary'} color: ${colorName}`}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-gray-600 capitalize">{partColour}</span>
|
<span className="text-xs text-gray-600 dark:text-gray-400 capitalize">{partColour}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<p className="text-sm text-gray-600">{part.description || '-'}</p>
|
<p className="text-sm text-gray-600 dark:text-gray-300">{part.description || '-'}</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
{part.isHardwareOnly ? (
|
{part.isHardwareOnly ? (
|
||||||
<span className="text-xs text-blue-600 italic">Hardware only</span>
|
<span className="text-xs text-blue-600 dark:text-blue-400 italic">Hardware only</span>
|
||||||
) : part.filePath ? (
|
) : part.filePath ? (
|
||||||
<p className="text-xs text-gray-500 font-mono">{part.filePath}</p>
|
<p className="text-xs text-gray-500 dark:text-gray-400 font-mono">{part.filePath}</p>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-gray-400 dark:text-gray-500">-</span>
|
<span className="text-gray-400 dark:text-gray-500">-</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 whitespace-nowrap text-right">
|
<td className="px-4 py-3 whitespace-nowrap text-right">
|
||||||
<p className="text-sm font-medium text-gray-700">{part.quantity || 1}</p>
|
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">{part.quantity || 1}</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 whitespace-nowrap text-right">
|
<td className="px-4 py-3 whitespace-nowrap text-right">
|
||||||
{part.filamentEstimate !== undefined && part.filamentEstimate > 0 ? (
|
{part.filamentEstimate !== undefined && part.filamentEstimate > 0 ? (
|
||||||
<p className="text-sm font-medium text-gray-700">
|
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
{typeof part.filamentEstimate === 'number'
|
{typeof part.filamentEstimate === 'number'
|
||||||
? (part.filamentEstimate * (part.quantity || 1)).toFixed(1)
|
? (part.filamentEstimate * (part.quantity || 1)).toFixed(1)
|
||||||
: part.filamentEstimate}g
|
: part.filamentEstimate}g
|
||||||
@@ -1231,8 +1177,7 @@ export default function BOMSummary({ config }) {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setHardwareViewMode('unified')}
|
onClick={() => setHardwareViewMode('unified')}
|
||||||
className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${
|
className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${hardwareViewMode === 'unified'
|
||||||
hardwareViewMode === 'unified'
|
|
||||||
? 'bg-blue-600 dark:bg-blue-500 text-white'
|
? 'bg-blue-600 dark:bg-blue-500 text-white'
|
||||||
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
|
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
|
||||||
}`}
|
}`}
|
||||||
@@ -1241,8 +1186,7 @@ export default function BOMSummary({ config }) {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setHardwareViewMode('expanded')}
|
onClick={() => setHardwareViewMode('expanded')}
|
||||||
className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${
|
className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${hardwareViewMode === 'expanded'
|
||||||
hardwareViewMode === 'expanded'
|
|
||||||
? 'bg-blue-600 dark:bg-blue-500 text-white'
|
? 'bg-blue-600 dark:bg-blue-500 text-white'
|
||||||
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
|
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
12
website/src/components/Footer.jsx
Normal file
12
website/src/components/Footer.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import partsData from '../data/index.js';
|
import Footer from './Footer';
|
||||||
|
|
||||||
export default function MainPage({ onSelectBuildType }) {
|
export default function MainPage({ onSelectBuildType }) {
|
||||||
const handleSelect = (buildType) => {
|
const handleSelect = (buildType) => {
|
||||||
@@ -6,8 +6,8 @@ export default function MainPage({ onSelectBuildType }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8 flex flex-col">
|
||||||
<div className="max-w-4xl mx-auto px-4">
|
<div className="max-w-4xl mx-auto px-4 flex-grow">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-12 text-center">
|
<div className="mb-12 text-center">
|
||||||
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-2">
|
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-2">
|
||||||
@@ -120,6 +120,7 @@ export default function MainPage({ onSelectBuildType }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import OptionsStep from './steps/OptionsStep';
|
|||||||
import RemoteStep from './steps/RemoteStep';
|
import RemoteStep from './steps/RemoteStep';
|
||||||
import ToyMountStep from './steps/ToyMountStep';
|
import ToyMountStep from './steps/ToyMountStep';
|
||||||
import BOMSummary from './BOMSummary';
|
import BOMSummary from './BOMSummary';
|
||||||
|
import Footer from './Footer';
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
{ id: 'motor', name: 'Motor', component: MotorStep },
|
{ id: 'motor', name: 'Motor', component: MotorStep },
|
||||||
@@ -158,8 +159,8 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
|
|||||||
}, [buildType, currentStep]);
|
}, [buildType, currentStep]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8 flex flex-col">
|
||||||
<div className="max-w-4xl mx-auto px-4">
|
<div className="max-w-4xl mx-auto px-4 flex-grow w-full">
|
||||||
{/* Back Button */}
|
{/* Back Button */}
|
||||||
{onBackToMain && (
|
{onBackToMain && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
@@ -212,14 +213,12 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
|
|||||||
{/* Circle */}
|
{/* Circle */}
|
||||||
<button
|
<button
|
||||||
onClick={() => goToStep(index)}
|
onClick={() => goToStep(index)}
|
||||||
className={`w-10 h-10 rounded-full flex items-center justify-center font-semibold flex-shrink-0 z-10 ${
|
className={`w-10 h-10 rounded-full flex items-center justify-center font-semibold flex-shrink-0 z-10 ${index === currentStep
|
||||||
index === currentStep
|
|
||||||
? 'bg-blue-600 dark:bg-blue-500 text-white'
|
? 'bg-blue-600 dark:bg-blue-500 text-white'
|
||||||
: index < currentStep
|
: index < currentStep
|
||||||
? 'bg-green-500 dark:bg-green-600 text-white'
|
? 'bg-green-500 dark:bg-green-600 text-white'
|
||||||
: 'bg-gray-200 dark:bg-gray-700 text-gray-500 dark:text-gray-400'
|
: 'bg-gray-200 dark:bg-gray-700 text-gray-500 dark:text-gray-400'
|
||||||
} ${
|
} ${index <= currentStep
|
||||||
index <= currentStep
|
|
||||||
? 'cursor-pointer hover:opacity-80'
|
? 'cursor-pointer hover:opacity-80'
|
||||||
: 'cursor-not-allowed'
|
: 'cursor-not-allowed'
|
||||||
}`}
|
}`}
|
||||||
@@ -230,8 +229,7 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
|
|||||||
{/* Connecting line to the right */}
|
{/* Connecting line to the right */}
|
||||||
{index < filteredSteps.length - 1 && (
|
{index < filteredSteps.length - 1 && (
|
||||||
<div
|
<div
|
||||||
className={`absolute top-5 left-1/2 h-1 ${
|
className={`absolute top-5 left-1/2 h-1 ${index < currentStep ? 'bg-green-500 dark:bg-green-600' : 'bg-gray-200 dark:bg-gray-700'
|
||||||
index < currentStep ? 'bg-green-500 dark:bg-green-600' : 'bg-gray-200 dark:bg-gray-700'
|
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
width: 'calc(100% - 40px)',
|
width: 'calc(100% - 40px)',
|
||||||
@@ -242,8 +240,7 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
|
|||||||
{/* Text label */}
|
{/* Text label */}
|
||||||
<button
|
<button
|
||||||
onClick={() => goToStep(index)}
|
onClick={() => goToStep(index)}
|
||||||
className={`mt-2 text-sm font-medium text-center ${
|
className={`mt-2 text-sm font-medium text-center ${index <= currentStep
|
||||||
index <= currentStep
|
|
||||||
? 'text-blue-600 dark:text-blue-400 cursor-pointer hover:text-blue-800 dark:hover:text-blue-300'
|
? 'text-blue-600 dark:text-blue-400 cursor-pointer hover:text-blue-800 dark:hover:text-blue-300'
|
||||||
: 'text-gray-400 dark:text-gray-500 cursor-not-allowed'
|
: 'text-gray-400 dark:text-gray-500 cursor-not-allowed'
|
||||||
}`}
|
}`}
|
||||||
@@ -273,8 +270,7 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
|
|||||||
<button
|
<button
|
||||||
onClick={prevStep}
|
onClick={prevStep}
|
||||||
disabled={currentStep === 0}
|
disabled={currentStep === 0}
|
||||||
className={`px-6 py-2 rounded-lg font-medium ${
|
className={`px-6 py-2 rounded-lg font-medium ${currentStep === 0
|
||||||
currentStep === 0
|
|
||||||
? 'bg-gray-200 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed'
|
? 'bg-gray-200 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed'
|
||||||
: 'bg-gray-600 dark:bg-gray-700 text-white hover:bg-gray-700 dark:hover:bg-gray-600'
|
: 'bg-gray-600 dark:bg-gray-700 text-white hover:bg-gray-700 dark:hover:bg-gray-600'
|
||||||
}`}
|
}`}
|
||||||
@@ -285,8 +281,7 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
|
|||||||
<button
|
<button
|
||||||
onClick={nextStep}
|
onClick={nextStep}
|
||||||
disabled={!canProceedToNextStep()}
|
disabled={!canProceedToNextStep()}
|
||||||
className={`px-6 py-2 rounded-lg font-medium ${
|
className={`px-6 py-2 rounded-lg font-medium ${canProceedToNextStep()
|
||||||
canProceedToNextStep()
|
|
||||||
? 'bg-blue-600 dark:bg-blue-500 text-white hover:bg-blue-700 dark:hover:bg-blue-600'
|
? 'bg-blue-600 dark:bg-blue-500 text-white hover:bg-blue-700 dark:hover:bg-blue-600'
|
||||||
: 'bg-gray-200 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed'
|
: 'bg-gray-200 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed'
|
||||||
}`}
|
}`}
|
||||||
@@ -297,6 +292,7 @@ export default function Wizard({ buildType = 'self-source', initialConfig, updat
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,8 +88,7 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
|
|||||||
<button
|
<button
|
||||||
key={remote.id}
|
key={remote.id}
|
||||||
onClick={() => handleRemoteSelect(remote.id)}
|
onClick={() => handleRemoteSelect(remote.id)}
|
||||||
className={`p-4 border-2 rounded-lg text-left transition-all w-full ${
|
className={`p-4 border-2 rounded-lg text-left transition-all w-full ${isSelected
|
||||||
isSelected
|
|
||||||
? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30'
|
? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30'
|
||||||
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/50'
|
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/50'
|
||||||
}`}
|
}`}
|
||||||
@@ -147,8 +146,7 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
|
|||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => handlePCBSelect('rad')}
|
onClick={() => handlePCBSelect('rad')}
|
||||||
className={`px-4 py-2 border-2 rounded-lg transition-all ${
|
className={`px-4 py-2 border-2 rounded-lg transition-all ${selectedRemotePCB === 'rad'
|
||||||
selectedRemotePCB === 'rad'
|
|
||||||
? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30 text-blue-900 dark:text-blue-300 font-medium'
|
? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30 text-blue-900 dark:text-blue-300 font-medium'
|
||||||
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||||
}`}
|
}`}
|
||||||
@@ -157,8 +155,7 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handlePCBSelect('pcbway')}
|
onClick={() => handlePCBSelect('pcbway')}
|
||||||
className={`px-4 py-2 border-2 rounded-lg transition-all ${
|
className={`px-4 py-2 border-2 rounded-lg transition-all ${selectedRemotePCB === 'pcbway'
|
||||||
selectedRemotePCB === 'pcbway'
|
|
||||||
? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30 text-blue-900 dark:text-blue-300 font-medium'
|
? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30 text-blue-900 dark:text-blue-300 font-medium'
|
||||||
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||||
}`}
|
}`}
|
||||||
@@ -177,8 +174,7 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
|
|||||||
<button
|
<button
|
||||||
key={knob.id}
|
key={knob.id}
|
||||||
onClick={() => handleKnobSelect(knob)}
|
onClick={() => handleKnobSelect(knob)}
|
||||||
className={`p-4 border-2 rounded-lg text-left transition-all w-full ${
|
className={`p-4 border-2 rounded-lg text-left transition-all w-full ${isSelected
|
||||||
isSelected
|
|
||||||
? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30'
|
? 'border-blue-600 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/30'
|
||||||
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/50'
|
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/50'
|
||||||
}`}
|
}`}
|
||||||
@@ -243,7 +239,7 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
|
|||||||
|
|
||||||
{/* Remote Selection */}
|
{/* Remote Selection */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h3 className="text-lg font-semibold mb-3">Remote System</h3>
|
<h3 className="text-lg font-semibold mb-3 text-gray-900 dark:text-white">Remote System</h3>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{availableRemotesFiltered.map((remote) => renderRemoteCard(remote))}
|
{availableRemotesFiltered.map((remote) => renderRemoteCard(remote))}
|
||||||
</div>
|
</div>
|
||||||
@@ -285,8 +281,7 @@ export default function RemoteStep({ config, updateConfig, buildType }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<svg
|
<svg
|
||||||
className={`w-4 h-4 text-gray-500 dark:text-gray-400 transition-transform ${
|
className={`w-4 h-4 text-gray-500 dark:text-gray-400 transition-transform ${expandedKnobs ? 'transform rotate-180' : ''
|
||||||
expandedKnobs ? 'transform rotate-180' : ''
|
|
||||||
}`}
|
}`}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
||||||
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Bottom.stl",
|
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Bottom.stl",
|
||||||
"checksum_sha256": "e7abdb99a7e9b9e7408a7b04a7dd50e42cc74510ea2969016a45a2a1387dcde3",
|
"checksum_sha256": "e7abdb99a7e9b9e7408a7b04a7dd50e42cc74510ea2969016a45a2a1387dcde3",
|
||||||
"last_checked": "2026-01-07T01:21:02.027595+00:00",
|
"last_checked": "2026-01-07T06:30:14.604915+00:00",
|
||||||
"status": "up-to-date"
|
"status": "up-to-date"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
||||||
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Middle.stl",
|
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Middle.stl",
|
||||||
"checksum_sha256": "ce6fb769378636c287af788ce42bdab1f2185dcffba929a0c72598742793b48a",
|
"checksum_sha256": "ce6fb769378636c287af788ce42bdab1f2185dcffba929a0c72598742793b48a",
|
||||||
"last_checked": "2026-01-07T01:21:03.531342+00:00",
|
"last_checked": "2026-01-07T06:30:22.906540+00:00",
|
||||||
"status": "up-to-date"
|
"status": "up-to-date"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -51,6 +51,9 @@
|
|||||||
"timeEstimate": "1h3m",
|
"timeEstimate": "1h3m",
|
||||||
"colour": "primary",
|
"colour": "primary",
|
||||||
"required": true,
|
"required": true,
|
||||||
|
"Condition": {
|
||||||
|
"cover.id": "standard-cover"
|
||||||
|
},
|
||||||
"filePath": "OSSM - Actuator Body Cover.stl",
|
"filePath": "OSSM - Actuator Body Cover.stl",
|
||||||
"url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Actuator/OSSM%20-%20Actuator%20-%20Body%20-%20Cover.stl?raw=true",
|
"url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Actuator/OSSM%20-%20Actuator%20-%20Body%20-%20Cover.stl?raw=true",
|
||||||
"vendor": {
|
"vendor": {
|
||||||
@@ -59,7 +62,7 @@
|
|||||||
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
||||||
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Cover.stl",
|
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - Actuator - Body - Cover.stl",
|
||||||
"checksum_sha256": "bbabc742d2f1753d1b4e21e42c197aec31a4a083b5c634e6e825cec69d4e3258",
|
"checksum_sha256": "bbabc742d2f1753d1b4e21e42c197aec31a4a083b5c634e6e825cec69d4e3258",
|
||||||
"last_checked": "2026-01-07T01:21:02.767604+00:00",
|
"last_checked": "2026-01-07T06:30:18.689516+00:00",
|
||||||
"status": "up-to-date"
|
"status": "up-to-date"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -99,7 +102,7 @@
|
|||||||
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
||||||
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - Belt Clamp.stl",
|
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - Belt Clamp.stl",
|
||||||
"checksum_sha256": "457a71bc09cb53f12026fd829bec8fa5b04fdead0788822935780f42c90b9a7a",
|
"checksum_sha256": "457a71bc09cb53f12026fd829bec8fa5b04fdead0788822935780f42c90b9a7a",
|
||||||
"last_checked": "2026-01-07T01:20:58.945151+00:00",
|
"last_checked": "2026-01-07T06:30:08.525159+00:00",
|
||||||
"status": "up-to-date"
|
"status": "up-to-date"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -119,7 +122,7 @@
|
|||||||
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
||||||
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - End Effector.stl",
|
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Clamping Thread - End Effector.stl",
|
||||||
"checksum_sha256": "4860947b201e2e773b295d33bba09423ae40b4adeef3605d62687f2d40277de1",
|
"checksum_sha256": "4860947b201e2e773b295d33bba09423ae40b4adeef3605d62687f2d40277de1",
|
||||||
"last_checked": "2026-01-07T01:20:59.854476+00:00",
|
"last_checked": "2026-01-07T06:30:09.547007+00:00",
|
||||||
"status": "up-to-date"
|
"status": "up-to-date"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -139,7 +142,7 @@
|
|||||||
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
||||||
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Nut - 5 Sided.stl",
|
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Actuator/OSSM - 24mm Nut - 5 Sided.stl",
|
||||||
"checksum_sha256": "38630c70b2fb929bba9a705dabf5bbd7b49ec882963e042b7108dc74284dd6ff",
|
"checksum_sha256": "38630c70b2fb929bba9a705dabf5bbd7b49ec882963e042b7108dc74284dd6ff",
|
||||||
"last_checked": "2026-01-07T01:21:00.555525+00:00",
|
"last_checked": "2026-01-07T06:30:10.564924+00:00",
|
||||||
"status": "up-to-date"
|
"status": "up-to-date"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,9 @@
|
|||||||
"timeEstimate": "2h10m",
|
"timeEstimate": "2h10m",
|
||||||
"colour": "primary",
|
"colour": "primary",
|
||||||
"required": true,
|
"required": true,
|
||||||
|
"Condition": {
|
||||||
|
"motor.id": "57AIM30"
|
||||||
|
},
|
||||||
"filePath": "OSSM - Base - PitClamp Mini - 57AIM30 V1.1.stl",
|
"filePath": "OSSM - Base - PitClamp Mini - 57AIM30 V1.1.stl",
|
||||||
"url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Mounting/OSSM%20-%20Mounting%20Ring%20-%20PitClamp%20Mini%20-%2057AIM%20V1.1.stl?raw=true",
|
"url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Mounting/OSSM%20-%20Mounting%20Ring%20-%20PitClamp%20Mini%20-%2057AIM%20V1.1.stl?raw=true",
|
||||||
"vendor": {
|
"vendor": {
|
||||||
@@ -71,6 +74,9 @@
|
|||||||
"timeEstimate": "2h10m",
|
"timeEstimate": "2h10m",
|
||||||
"colour": "primary",
|
"colour": "primary",
|
||||||
"required": true,
|
"required": true,
|
||||||
|
"Condition": {
|
||||||
|
"motor.id": "42AIM30"
|
||||||
|
},
|
||||||
"filePath": "OSSM - Base - PitClamp Mini - 42AIM30 V1.1.stl",
|
"filePath": "OSSM - Base - PitClamp Mini - 42AIM30 V1.1.stl",
|
||||||
"url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Mounting/Non-standard/OSSM%20-%20Mounting%20Ring%20-%20PitClamp%20Mini%20-%2042AIM%20V1.1.stl?raw=true",
|
"url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Mounting/Non-standard/OSSM%20-%20Mounting%20Ring%20-%20PitClamp%20Mini%20-%2042AIM%20V1.1.stl?raw=true",
|
||||||
"vendor": {
|
"vendor": {
|
||||||
@@ -91,6 +97,9 @@
|
|||||||
"timeEstimate": "2h10m",
|
"timeEstimate": "2h10m",
|
||||||
"colour": "primary",
|
"colour": "primary",
|
||||||
"required": true,
|
"required": true,
|
||||||
|
"Condition": {
|
||||||
|
"motor.id": "iHSV57"
|
||||||
|
},
|
||||||
"filePath": "OSSM - Base - PitClamp Mini - iHSV57 V1.1.stl",
|
"filePath": "OSSM - Base - PitClamp Mini - iHSV57 V1.1.stl",
|
||||||
"url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Mounting/Non-standard/OSSM%20-%20Mounting%20Ring%20-%20PitClamp%20Mini%20-%20iHSV57.stl?raw=true",
|
"url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Mounting/Non-standard/OSSM%20-%20Mounting%20Ring%20-%20PitClamp%20Mini%20-%20iHSV57.stl?raw=true",
|
||||||
"vendor": {
|
"vendor": {
|
||||||
@@ -185,6 +194,9 @@
|
|||||||
"timeEstimate": "5h8m",
|
"timeEstimate": "5h8m",
|
||||||
"colour": "primary",
|
"colour": "primary",
|
||||||
"required": true,
|
"required": true,
|
||||||
|
"replaces": [
|
||||||
|
"ossm-actuator-body-middle"
|
||||||
|
],
|
||||||
"filePath": "OSSM - Actuator Body Middle Pivot.stl",
|
"filePath": "OSSM - Actuator Body Middle Pivot.stl",
|
||||||
"url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Actuator/Non-standard/OSSM%20-%20Actuator%20-%20Body%20-%20Middle%20Pivot.stl?raw=true",
|
"url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Actuator/Non-standard/OSSM%20-%20Actuator%20-%20Body%20-%20Middle%20Pivot.stl?raw=true",
|
||||||
"vendor": {
|
"vendor": {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
||||||
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl",
|
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Handle Spacer.stl",
|
||||||
"checksum_sha256": "55ede7dff60a31d68159b352b5f2c63792b7a0dbe9d543a43681c3e52d229115",
|
"checksum_sha256": "55ede7dff60a31d68159b352b5f2c63792b7a0dbe9d543a43681c3e52d229115",
|
||||||
"last_checked": "2026-01-07T01:20:58.324330+00:00",
|
"last_checked": "2026-01-07T06:30:07.525364+00:00",
|
||||||
"status": "up-to-date"
|
"status": "up-to-date"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,7 +187,7 @@
|
|||||||
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
"pinned_sha": "52537c0896eaef83fd9771dcc633903c7aa6a8ab",
|
||||||
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Extrusion Cap.stl",
|
"pinned_raw_url": "https://raw.githubusercontent.com/KinkyMakers/OSSM-hardware/52537c0896eaef83fd9771dcc633903c7aa6a8ab/Printed Parts/Stand/OSSM - Stand - 3030 Extrusion Base - Extrusion Cap.stl",
|
||||||
"checksum_sha256": "56fa9bb318cdeadc6d1698a1e6cef9371e58b0bc9c7729985bf639d8da2f25da",
|
"checksum_sha256": "56fa9bb318cdeadc6d1698a1e6cef9371e58b0bc9c7729985bf639d8da2f25da",
|
||||||
"last_checked": "2026-01-07T01:21:01.205246+00:00",
|
"last_checked": "2026-01-07T06:30:11.578686+00:00",
|
||||||
"status": "up-to-date"
|
"status": "up-to-date"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
14
website/src/data/components/toyMounts/index.js
Normal file
14
website/src/data/components/toyMounts/index.js
Normal 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
|
||||||
|
};
|
||||||
211
website/src/data/components/toyMounts/ossm.json
Normal file
211
website/src/data/components/toyMounts/ossm.json
Normal 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": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import motors from './motors.json';
|
import motors from './components/motors.json';
|
||||||
import powerSupplies from './powerSupplies.json';
|
import powerSupplies from './components/powerSupplies.json';
|
||||||
import optionsData from './options.json';
|
import optionsData from './config/options.json';
|
||||||
import colors from './colors.json';
|
import colors from './common/colors.json';
|
||||||
import hardwareData from './hardware.json';
|
import hardwareData from './common/hardware.json';
|
||||||
import actuatorComponents from './components/actuator.json';
|
import actuatorComponents from './components/actuator.json';
|
||||||
import standComponents from './components/stand.json';
|
import standComponents from './components/stand.json';
|
||||||
import mountingComponents from './components/mounting.json';
|
import mountingComponents from './components/mounting.json';
|
||||||
import toyMountsComponents from './components/toyMounts.json';
|
import toyMountsComponents from './components/toyMounts/index.js';
|
||||||
import remoteComponents from './components/remote.json';
|
import remoteComponents from './components/remote.json';
|
||||||
import pcbComponents from './components/pcb.json';
|
import pcbComponents from './components/pcb.json';
|
||||||
|
|
||||||
|
|||||||
@@ -279,9 +279,9 @@ export const generateExcelPrintList = (printedParts, filamentTotals) => {
|
|||||||
summaryWorksheet.getCell('A3').value = 'Total Parts';
|
summaryWorksheet.getCell('A3').value = 'Total Parts';
|
||||||
summaryWorksheet.getCell('B3').value = printedParts.length;
|
summaryWorksheet.getCell('B3').value = printedParts.length;
|
||||||
summaryWorksheet.getCell('A4').value = 'Completed Parts';
|
summaryWorksheet.getCell('A4').value = 'Completed Parts';
|
||||||
summaryWorksheet.getCell('B4').formula = `COUNTIF('Print List'.H:H,"✓")`;
|
summaryWorksheet.getCell('B4').value = { formula: `COUNTIF('Print List'.H:H,"✓")` };
|
||||||
summaryWorksheet.getCell('A5').value = 'Progress %';
|
summaryWorksheet.getCell('A5').value = 'Progress %';
|
||||||
summaryWorksheet.getCell('B5').formula = `IF(B3>0, (B4/B3)*100, 0)`;
|
summaryWorksheet.getCell('B5').value = { formula: `IF(B3>0, (B4/B3)*100, 0)` };
|
||||||
summaryWorksheet.getCell('A7').value = 'Filament Summary';
|
summaryWorksheet.getCell('A7').value = 'Filament Summary';
|
||||||
summaryWorksheet.getCell('A8').value = 'Total Filament (g)';
|
summaryWorksheet.getCell('A8').value = 'Total Filament (g)';
|
||||||
summaryWorksheet.getCell('B8').value = filamentTotals.total.toFixed(2);
|
summaryWorksheet.getCell('B8').value = filamentTotals.total.toFixed(2);
|
||||||
|
|||||||
Reference in New Issue
Block a user