Initial infrastructure documentation - comprehensive homelab reference

This commit is contained in:
Funky (OpenClaw)
2026-02-23 03:42:22 +00:00
commit 0682c79580
169 changed files with 63913 additions and 0 deletions

280
.claude-context.md Normal file
View File

@@ -0,0 +1,280 @@
# Fred's Projects - Claude Code Context
**Source of Truth**: `C:\Users\Fred\projects`
This file provides context to Claude Code about all of Fred's active projects. When Fred starts a Claude Code session in VS Code Insiders, Claude should be aware of this ecosystem.
---
## Active Projects
### 1. claude-workflows
**Path**: `C:\Users\Fred\projects\claude-workflows`
**Purpose**: Shared slash commands and ADHD-friendly productivity tools for Claude Code
**Status**: Active development
**Key Features**:
- `/push` - Quick commit and push
- `/eod` - End of day workflow
- ADHD assistant system with proactive interventions
- Auto-discovery scripts for cross-project setup
- Personality-driven assistant behavior (`personality.md`)
**Context Files**:
- `.assistant/personality.md` - Defines ADHD-friendly behavior rules
- `.assistant/state.json.template` - Session state tracking template
---
### 2. VA-Strategy
**Path**: `C:\Users\Fred\projects\VA-Strategy`
**Purpose**: Personal VA disability claims management system
**Status**: Active - in progress
**Goal**: 100% VA disability rating via TDIU
**Current Rating**: 60% combined, 30% highest single (PTSD)
**Context Files**:
- `CLAUDE.md` - Project-specific guidance for Claude Code
- `Gemini.md` - Master strategic roadmap
- `tracking/master-tracking.md` - Claim status tracker
- `tracking/immediate-action-checklist.md` - Priority to-do list
**Key Workflows**:
- Headache tracking for migraine claim (50% target)
- PTSD statement preparation (70% target)
- Sleep apnea evidence collection (50% target)
- Git-based document versioning
---
### 3. infrastructure
**Path**: `C:\Users\Fred\projects\infrastructure`
**Purpose**: Home network, Home Assistant, smart home automation
**Status**: Active maintenance + active projects
**Active Subprojects**:
- **Voice Assistant**: Local GPU-accelerated voice system (Gaming PC + Surface Go)
- **Furnace Control**: ESP32-based smart furnace controller (planning phase)
- **Home Assistant**: Main HA configuration
- **ESPHome**: Device configurations (garage controller, planned furnace)
**Context Files**:
- `README.md` - Infrastructure overview
- `docs/FURNACE-PROJECT.md` - ESP32 furnace project
- `voice-assistant/CLAUDE.md` - Voice system context
**Tech Stack**: Home Assistant, ESPHome, MQTT, Docker, Ollama, Whisper, Piper TTS
---
### 4. claude-code-history
**Path**: `C:\Users\Fred\projects\claude-code-history`
**Purpose**: Session history and state persistence for Claude Code
**Status**: Background system
Contains:
- Session transcripts
- State files
- Project history
- Stats cache
---
### 5. config
**Path**: `C:\Users\Fred\projects\config`
**Purpose**: Shared configuration files
**Status**: Minimal/placeholder
---
## ADHD Assistant Behavior
Claude Code should operate with ADHD-friendly principles when working with Fred:
### Core Principles
1. **Proactive, Not Reactive** - Notice patterns and intervene
2. **Gentle Nudging** - Suggest, don't command
3. **Celebrate Wins** - Acknowledge all completions
4. **Context Preservation** - Remember across sessions
5. **No Judgment** - Side quests are valid exploration
### Side Quest Detection
**When Fred starts working on something unrelated to the current project**, Claude should:
```
🤔 I notice we've shifted focus:
Current project: [X]
New work: [Y]
This looks like a side quest. Would you like to:
1. Continue (I'll track it)
2. Switch to the [Y] project
3. Make this a new project
4. Park it and return to [X]
```
### Project-Specific Context Loading
When Fred opens a project in VS Code, Claude should:
1. Check for project-specific `CLAUDE.md` file
2. Load project context and current status
3. Greet with relevant session info
4. Track scope drift across project boundaries
---
## Shared Resources
### Claude Shared Directory
**Path**: `C:\Users\Fred\claude-shared\` (symlinked from claude-workflows)
**Contains**:
- Shared slash commands
- ADHD assistant state file (`~/.claude-assistant/state.json`)
- Setup scripts for auto-discovery
### Slash Commands (Available Everywhere)
- `/push` - Auto-commit and push
- `/eod` - End of day workflow
- `/focus` - Check current goals *(in development)*
- `/sidequest` - Log tangents *(in development)*
- `/stuck` - Get unstuck *(in development)*
- `/reflect` - Session review *(in development)*
---
## Cross-Project Workflows
### When Fred Starts a Side Quest
Example: Working on VA-Strategy, starts researching ESPHome for furnace
**Claude should**:
1. Detect context shift (VA → infrastructure)
2. Offer to switch projects
3. If continuing, track as side quest in state file
4. Set timer for check-in (30 min default)
5. Preserve VA-Strategy context for return
### When Fred Opens VS Code in a Project
**Claude should**:
1. Read `.claude-context.md` (this file) for ecosystem awareness
2. Read project-specific `CLAUDE.md` if exists
3. Check state file for active session
4. Greet with context:
```
📋 Welcome back, Fred!
Project: [name]
Last session: [time ago]
Status: [brief summary]
Ready to continue?
```
---
## State Management
### Session State File
**Location**: `~/.claude-assistant/state.json` (Windows: `C:\Users\Fred\.claude-assistant\state.json`)
**Tracks**:
- Current project and goal
- Active side quests
- Stuck signals
- Session history
- Cross-project context
### State File Schema
```json
{
"current_session": {
"project": "VA-Strategy",
"started_at": "2025-12-13T10:00:00Z",
"primary_goal": "Complete headache log entries",
"side_quests": [
{
"topic": "Research ESP32 temperature sensors",
"original_project": "VA-Strategy",
"target_project": "infrastructure",
"started_at": "2025-12-13T10:30:00Z",
"status": "in_progress"
}
]
}
}
```
---
## VS Code Insiders Setup
### Recommended Settings
To enable full context awareness in VS Code Insiders:
1. **Multi-root Workspace** - Open all projects simultaneously
2. **Workspace-Specific Settings** - Per-project `.vscode/settings.json`
3. **Claude Context Files** - This file + project-specific `CLAUDE.md` files
### Creating Multi-Root Workspace
```json
{
"folders": [
{
"path": "C:\\Users\\Fred\\projects\\claude-workflows",
"name": "🎯 Claude Workflows"
},
{
"path": "C:\\Users\\Fred\\projects\\VA-Strategy",
"name": "🏥 VA Strategy"
},
{
"path": "C:\\Users\\Fred\\projects\\infrastructure",
"name": "🏠 Infrastructure"
}
],
"settings": {
"claude.contextFiles": [
"C:\\Users\\Fred\\projects\\.claude-context.md"
]
}
}
```
Save as: `C:\Users\Fred\projects\fred-workspace.code-workspace`
---
## Quick Reference
### Project Quick IDs
- **claude-workflows**: Productivity tools, ADHD assistant
- **VA-Strategy**: VA claims, documentation, tracking
- **infrastructure**: Home automation, voice assistant, ESPHome
- **claude-code-history**: Session history (background)
- **config**: Shared configs (minimal)
### When to Switch Projects
| You're Talking About... | Project | Action |
|------------------------|---------|--------|
| Slash commands, ADHD features | claude-workflows | Switch or side quest |
| VA claims, medical evidence | VA-Strategy | Switch or side quest |
| Home Assistant, ESP32, voice assistant | infrastructure | Switch or side quest |
| New idea unrelated to current work | TBD | Offer to create new project |
---
## For Claude Code: Session Start Checklist
When Fred starts VS Code Insiders:
- [ ] Read `.claude-context.md` (this file)
- [ ] Identify current project from workspace/folder
- [ ] Read project-specific `CLAUDE.md` if exists
- [ ] Check `~/.claude-assistant/state.json` for active session
- [ ] Greet with relevant context
- [ ] Be ready to detect and manage side quests
- [ ] Apply ADHD-friendly behavior from `personality.md`
---
**Last Updated**: 2025-12-13
**Maintained By**: Fred with Claude Code assistance
**Purpose**: Provide ecosystem-level context for intelligent, ADHD-friendly Claude Code sessions

17
.claude/index.html Normal file
View File

@@ -0,0 +1,17 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Directory listing for /.claude/</title>
</head>
<body>
<h1>Directory listing for /.claude/</h1>
<hr>
<ul>
<li><a href="commands/">commands/</a></li>
<li><a href="docs/">docs/</a></li>
<li><a href="settings.local.json">settings.local.json</a></li>
</ul>
<hr>
</body>
</html>

15
.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
# Ignore embedded git repositories (manage separately)
VA-Strategy/
claude-code-history/
claude-workflows/
config/
# Windows
Thumbs.db
Desktop.ini
# Temp files
*.tmp
*.temp
nul
.aider*

174
AIDER-QUICKSTART.md Normal file
View File

@@ -0,0 +1,174 @@
# Aider Quick Start Guide
## What is Aider?
Aider is your **local, free AI coding assistant** that works with Ollama. It saves Claude API tokens for routine coding tasks.
## Setup Complete! ✓
- Aider installed
- Ollama configured with two models:
- `qwen2.5-coder:7b-instruct` (fast, 8GB VRAM)
- `qwen2.5-coder:14b-instruct` (smart, uses some RAM)
- Config file: `~/.aider.conf.yml`
- Helper functions: `C:\Users\Fred\projects\aider-helpers.ps1`
## Quick Start
### 1. Load Helper Functions (One Time Setup)
Add this line to your PowerShell profile:
```powershell
. C:\Users\Fred\projects\aider-helpers.ps1
```
To find your profile location:
```powershell
$PROFILE
```
Or run the helpers manually each session:
```powershell
. C:\Users\Fred\projects\aider-helpers.ps1
```
### 2. Basic Usage
```powershell
# Navigate to your project
cd C:\Users\Fred\projects\VA-Strategy
# Start Aider (uses 7b model by default)
aider-fast
# Or use the smarter model for complex tasks
aider-smart
# Or use plain aider (reads .aider.conf.yml)
aider
```
### 3. Inside Aider
```
> Add a function to parse headache log entries
> Refactor the PTSD statement generator
> Add error handling to the tracking module
> /help # see all commands
> /exit # quit
```
## When to Use What
### Use Aider (Local) for:
- ✅ Refactoring code
- ✅ Writing boilerplate
- ✅ Adding simple features
- ✅ Code reviews
- ✅ Fixing simple bugs
- ✅ Documentation
- ✅ Test writing
Cost: **$0** (completely free, uses your GPU)
### Use Claude Code for:
- 🎯 Complex architectural decisions
- 🎯 Multi-file refactors requiring deep understanding
- 🎯 Difficult debugging
- 🎯 Planning new features
- 🎯 Strategic code design
Cost: ~$3-15 per million tokens (monthly limit)
## Helper Commands
```powershell
aider-fast # Fast 7b model (everyday coding)
aider-smart # Powerful 14b model (complex tasks)
aider-plan # Architect mode (planning)
aider-commit # Generate git commit messages
aider-watch # Auto-reload on file changes
aider-status # Show available models and commands
aider-estimate # Compare token costs
```
## Examples
### Example 1: Simple Refactor
```powershell
cd C:\Users\Fred\projects\VA-Strategy
aider-fast tracking/headache-log.py
> Refactor this to use dataclasses instead of dictionaries
```
### Example 2: Add Feature
```powershell
cd C:\Users\Fred\projects\infrastructure\voice-assistant
aider-fast
> Add a new command to check system temperature
> /add sensors.py utils.py
> Make sure it logs to the debug file
```
### Example 3: Git Commit
```powershell
git add .
aider-commit
# Aider will analyze changes and suggest a commit message
```
## Tips
1. **Start Small**: Try Aider on simple tasks first to get comfortable
2. **Use Git**: Aider works best with git repos (can auto-commit)
3. **Be Specific**: Clear prompts get better results ("Add error handling for missing files" vs "make it better")
4. **Switch Models**: Use 7b for speed, 14b for quality
5. **Save Claude Tokens**: Use Aider for 80% of tasks, Claude for the 20% that need genius-level reasoning
## Token Savings Example
**Typical Day:**
- 10 simple refactors: Aider (free) instead of Claude ($0.30)
- 5 feature additions: Aider (free) instead of Claude ($0.75)
- 3 bug fixes: Aider (free) instead of Claude ($0.45)
- 2 complex architecture tasks: Claude ($0.60)
**Total saved: $1.50/day = $45/month**
## Troubleshooting
### Ollama not running
```powershell
# Check Ollama status
ollama list
# Restart Ollama if needed (it should auto-start)
```
### Model too slow
```powershell
# Switch to faster 7b model
aider --model ollama/qwen2.5-coder:7b-instruct
```
### Need better quality
```powershell
# Switch to smarter 14b model
aider --model ollama/qwen2.5-coder:14b-instruct
```
## Next Steps
1. Load the helper functions in your PowerShell profile
2. Try `aider-status` to verify everything works
3. Navigate to a project and run `aider-fast`
4. Start with a simple task like "Add a docstring to this function"
---
**Happy coding!** You're now set up to save Claude tokens while maintaining productivity.
For full Aider documentation: https://aider.chat

190
ALERT-REDUCTION-SUMMARY.md Normal file
View File

@@ -0,0 +1,190 @@
# Alert Reduction Summary
## What Changed
Your Prometheus alerting has been updated to **drastically reduce notification noise** while still catching critical issues.
### Before (Your Current Inbox)
- 🔥 **All warnings** → Email inbox spam
- Multiple alerts per minute
- Drowning out important messages
### After (New Configuration)
-**Only CRITICAL alerts** → Discord notification
- **WARNING alerts** → Logged in Prometheus UI (no notification)
- Clean inbox, important alerts still get through
---
## Updated Alert Thresholds
### CPU Monitoring
| Severity | Threshold | Duration | Action |
|----------|-----------|----------|--------|
| **WARNING** | 80%+ | 5 minutes | Logged only |
| **CRITICAL** | 95%+ | 5 minutes | **Discord notification** |
### Memory Monitoring
| Severity | Threshold | Duration | Action |
|----------|-----------|----------|--------|
| **WARNING** | 85%+ | 10 minutes | Logged only |
| **CRITICAL** | 95%+ | 5 minutes | **Discord notification** |
### Disk Space
| Severity | Threshold | Duration | Action |
|----------|-----------|----------|--------|
| **WARNING** | <15% free | 5 minutes | Logged only |
| **CRITICAL** | <5% free | 2 minutes | **Discord notification** |
---
## CRITICAL Alerts (Discord Notifications)
You will **ONLY** receive Discord notifications for:
### Infrastructure
-**HostDown** - Any host completely unreachable for 2+ minutes
-**ProxmoxNodeDown** - Proxmox host down for 2+ minutes
-**VPSDown** - VPS (66.63.182.168) unreachable for 2+ minutes
### Performance
- 🔥 **CriticalCPUUsage** - CPU >95% sustained for 5+ minutes
- 🧠 **CriticalMemoryUsage** - Memory >95% sustained for 5+ minutes
### Storage
- 💾 **DiskSpaceCritical** - Disk <5% free space for 2+ minutes
### Services
- 🗄️ **PostgreSQLDown** - Database down for 2+ minutes
- ⚙️ **PrometheusConfigReloadFailed** - Monitoring system config broken
---
## WARNING Alerts (Logged Only)
These alerts are **visible in Prometheus UI** but **do NOT trigger notifications**:
- CPU 80-95% (warning threshold)
- Memory 85-95%
- Disk 5-15% free
- Network interface down
- High disk I/O wait
- Home Assistant down
- n8n automation down
- Prometheus scrape failures
- and more...
**You can check them anytime at:** http://10.0.10.25:9090/alerts
---
## Deployment
### Quick Deploy
```bash
cd /root/.openclaw/workspace/fred-infrastructure
./deploy-reduced-alerts.sh
```
### Manual Steps (if needed)
```bash
# 1. Backup existing configs
ssh root@10.0.10.25 'mkdir -p /etc/prometheus/backups'
ssh root@10.0.10.25 'cp /etc/prometheus/alertmanager.yml /etc/prometheus/backups/alertmanager.yml.backup'
ssh root@10.0.10.25 'cp /etc/prometheus/rules/homelab-alerts.yml /etc/prometheus/backups/homelab-alerts.yml.backup'
# 2. Upload new configs
scp alertmanager-config-updated.yml root@10.0.10.25:/etc/prometheus/alertmanager.yml
scp prometheus-alert-rules-updated.yml root@10.0.10.25:/etc/prometheus/rules/homelab-alerts.yml
# 3. Reload services
ssh root@10.0.10.25 'systemctl reload prometheus'
ssh root@10.0.10.25 'systemctl reload prometheus-alertmanager'
# 4. Verify
curl http://10.0.10.25:9090/api/v1/rules
curl http://10.0.10.25:9093/api/v1/status
```
### Test Critical Alert
Send a test alert to verify Discord webhook works:
```bash
curl -X POST http://10.0.10.25:9093/api/v1/alerts -d '[
{
"labels": {
"alertname": "TestCriticalAlert",
"severity": "critical",
"instance": "test:9100"
},
"annotations": {
"summary": "Test alert - please ignore"
}
}
]'
```
You should see this appear in Discord within ~30 seconds.
---
## Expected Results
### Your Email Inbox
- **Before:** 50+ Prometheus alerts per day
- **After:** **ZERO** (all notifications moved to Discord)
### Your Discord Server
- **Only critical issues** that need immediate attention
- Estimated: 0-2 alerts per day (unless something is actually broken)
### Prometheus UI
- **All alerts still visible** at http://10.0.10.25:9090/alerts
- Use this to check warnings when convenient
---
## Monitoring Your Alerts
### Check Active Alerts
```bash
# View current alerts
curl http://10.0.10.25:9090/api/v1/alerts | python3 -m json.tool
# View Alertmanager status
curl http://10.0.10.25:9093/api/v1/status | python3 -m json.tool
```
### Web Interfaces
- **Prometheus:** http://10.0.10.25:9090/alerts
- **Alertmanager:** http://10.0.10.25:9093/#/alerts
---
## Rollback (If Needed)
If something goes wrong, restore the backup:
```bash
ssh root@10.0.10.25 'cp /etc/prometheus/backups/alertmanager.yml.* /etc/prometheus/alertmanager.yml'
ssh root@10.0.10.25 'cp /etc/prometheus/backups/homelab-alerts.yml.* /etc/prometheus/rules/homelab-alerts.yml'
ssh root@10.0.10.25 'systemctl reload prometheus prometheus-alertmanager'
```
---
## Files Created
- `prometheus-alert-rules-updated.yml` - Updated alert rules (CPU 80%+, warnings logged)
- `alertmanager-config-updated.yml` - Only critical → Discord, warnings → null
- `deploy-reduced-alerts.sh` - Automated deployment script
- `ALERT-REDUCTION-SUMMARY.md` - This file
---
## Questions?
- **"I want to see warnings occasionally"** → Check http://10.0.10.25:9090/alerts daily
- **"I need email back"** → We can add it back for critical-only
- **"80% CPU is too low"** → We can adjust the threshold up/down
- **"I want alerts in Slack instead"** → Easy to add another webhook
Let me know if you want to tweak anything! 🎯

59
CLAUDE.md Normal file
View File

@@ -0,0 +1,59 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Quick Reference
This repository manages Fred's homelab infrastructure - a mix of deployment scripts, monitoring tools, and service integrations for a Proxmox-based network.
**When you need specific information, refer to these specialized docs:**
### Core Architecture
- [.claude/docs/ARCHITECTURE.md](.claude/docs/ARCHITECTURE.md) - Infrastructure components, network topology, WireGuard VPN
- [.claude/docs/SERVICES.md](.claude/docs/SERVICES.md) - Deployed services, container IDs, service-specific details
- [.claude/docs/SCRIPTS.md](.claude/docs/SCRIPTS.md) - Deployment scripts and automation tools
### Operations
- [.claude/docs/COMMON-TASKS.md](.claude/docs/COMMON-TASKS.md) - SSH access, Caddy management, container operations
- [.claude/docs/CERTIFICATES.md](.claude/docs/CERTIFICATES.md) - Step-CA setup, ACME provisioner, SSL configuration
### Detailed References (load when needed)
- Subdirectories have their own CLAUDE.md files (infrastructure/, n8n-workflows/, etc.)
- Service-specific deployment guides in respective directories
- Submodule documentation in submodule directories
## Essential Quick Facts
**Primary Infrastructure:**
- VPS: fred@66.63.182.168 (vps.nianticbooks.com) - Caddy reverse proxy
- Proxmox: root@10.0.10.3 (main-pve) - LXC container host
- Network: 10.0.10.0/24 (WireGuard tunnel: 10.0.8.0/24)
**Critical Services:**
- Uptime Kuma: 10.0.10.26 (CT 128)
- Home Assistant: 10.0.10.24
- CA Server: 10.0.10.15 (CT 115)
## Repository Structure
**Subdirectories:**
- `infrastructure/` - Core infrastructure docs and automation
- `n8n-workflows/` - n8n workflow JSON definitions
- `bible-reading-plan/` - Submodule (separate repo)
**Slash Commands:**
- `/eod`, `/push`, `/focus`, `/sidequest`, `/stuck`, `/reflect` (in `.claude/commands/`)
## Key Constraints
- Proxmox storage: `local` only (never `local-lvm`)
- SSH: Always use key-based auth
- Test on target system before committing
- VPS: 2 CPU / 4GB RAM - lightweight services only

View File

@@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBozCCAUmgAwIBAgIQDhQK0OV98/81eUmxrmQDODAKBggqhkjOPQQDAjAwMS4w
LAYDVQQDEyVDYWRkeSBMb2NhbCBBdXRob3JpdHkgLSAyMDI2IEVDQyBSb290MB4X
DTI2MDEyNTE5Mjg0MloXDTM1MTIwNDE5Mjg0MlowMDEuMCwGA1UEAxMlQ2FkZHkg
TG9jYWwgQXV0aG9yaXR5IC0gMjAyNiBFQ0MgUm9vdDBZMBMGByqGSM49AgEGCCqG
SM49AwEHA0IABK+03Zxz5H6c6/5NPoRsDbR50mSiYWitxjSgu8WVHi9QKrhRUDcP
9xulfLgnDEGKzg6PHP8uSKmKjJ+S94c633+jRTBDMA4GA1UdDwEB/wQEAwIBBjAS
BgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBTa7eOocuWd3U3qq0Upi55X/VrC
PjAKBggqhkjOPQQDAgNIADBFAiA0aW0FfQ3KRZ5ojp3oK5wwBSmjaRNQko7qtXw1
PZrlEgIhANpvOrrYzoBCEm/anqkOmTL3qf+wFNFyg+hAl3L70O/f
-----END CERTIFICATE-----

138
DEPLOY-NOW.md Normal file
View File

@@ -0,0 +1,138 @@
# 🚀 DEPLOY NOW - Stop the Alert Spam!
## Quick Deploy (Copy/Paste This)
Run these commands from **your terminal** (PC, not OpenClaw):
### 1. Access the Prometheus container
```bash
# SSH to Proxmox
ssh root@10.0.10.3
# Enter the Prometheus container
pct enter 125
```
### 2. Backup existing configs
```bash
# Inside the container
mkdir -p /etc/prometheus/backups
cp /etc/prometheus/alertmanager.yml /etc/prometheus/backups/alertmanager.yml.backup
cp /etc/prometheus/rules/homelab-alerts.yml /etc/prometheus/backups/homelab-alerts.yml.backup
```
### 3. Download the new configs
From your **PC**, download the updated files I created:
```bash
# On your PC
scp root@10.0.10.28:/root/.openclaw/workspace/fred-infrastructure/alertmanager-config-updated.yml ~/
scp root@10.0.10.28:/root/.openclaw/workspace/fred-infrastructure/prometheus-alert-rules-updated.yml ~/
```
### 4. Upload to Prometheus container
```bash
# On your PC
scp ~/alertmanager-config-updated.yml root@10.0.10.3:/tmp/
scp ~/prometheus-alert-rules-updated.yml root@10.0.10.3:/tmp/
```
Then back in Proxmox SSH:
```bash
# Copy into container
pct push 125 /tmp/alertmanager-config-updated.yml /etc/prometheus/alertmanager.yml
pct push 125 /tmp/prometheus-alert-rules-updated.yml /etc/prometheus/rules/homelab-alerts.yml
```
### 5. Reload Prometheus
```bash
# Inside the container (pct enter 125)
systemctl reload prometheus
systemctl reload prometheus-alertmanager
# Verify services reloaded
systemctl status prometheus
systemctl status prometheus-alertmanager
```
### 6. Test it works
```bash
# Should see the new config
curl http://10.0.10.25:9090/api/v1/rules
# Send test alert to Discord
curl -X POST http://10.0.10.25:9093/api/v1/alerts -d '[
{
"labels": {
"alertname": "TestCriticalAlert",
"severity": "critical",
"instance": "test:9100"
},
"annotations": {
"summary": "Test alert - please ignore"
}
}
]'
```
You should see the test alert appear in **Discord** within 30 seconds!
---
## ⚡ EVEN FASTER: One-Liner (Advanced)
If you want to do it all in one go:
```bash
ssh root@10.0.10.3 "pct exec 125 -- bash -c '
mkdir -p /etc/prometheus/backups && \
cp /etc/prometheus/alertmanager.yml /etc/prometheus/backups/alertmanager.yml.backup && \
cp /etc/prometheus/rules/homelab-alerts.yml /etc/prometheus/backups/homelab-alerts.yml.backup && \
curl -o /etc/prometheus/alertmanager.yml http://10.0.10.28:PORT/workspace/fred-infrastructure/alertmanager-config-updated.yml && \
curl -o /etc/prometheus/rules/homelab-alerts.yml http://10.0.10.28:PORT/workspace/fred-infrastructure/prometheus-alert-rules-updated.yml && \
systemctl reload prometheus prometheus-alertmanager
'"
```
(Replace `PORT` with OpenClaw web port if you have file serving enabled)
---
## 🎯 What This Does
**Before:**
- ⚠️ WARNING alerts every 2 minutes → Email
- "CPU changed 61.5% rapidly (5.04 → 1.94)" ← WTF is this even
**After:**
- ✅ CRITICAL alerts only → Discord
- ❌ WARNING alerts → Logged, not sent
- 📧 Email inbox → CLEAN
**Result:**
- Your inbox goes from 50+ alerts/day to ZERO
- Discord gets only real emergencies (host down, disk full, etc.)
- Warnings still logged in Prometheus UI if you want to check
---
## 🚨 Current Alert Spam Example
That alert you just got:
```
⚠️ WARNING: 10.0.10.3:9100
cpu_usage on 10.0.10.3:9100 changed 61.5% rapidly (5.04 → 1.94)
```
This is **literally complaining that CPU usage went DOWN**. Pure noise.
After deployment: **This will NEVER notify you again.** It'll be logged if you want to see it, but won't spam your inbox.
---
## Questions?
- **"I can't SSH to Proxmox"** → Let me know, I'll help
- **"It broke something"** → Restore backup: `cp /etc/prometheus/backups/* /etc/prometheus/`
- **"I want it even quieter"** → We can tune thresholds further
- **"I want email back for criticals"** → Easy to add
**Just deploy this and watch your inbox become peaceful again.** 🧘‍♂️✨

317
N8N-UPTIME-KUMA-GUIDE.md Normal file
View File

@@ -0,0 +1,317 @@
# n8n + Uptime Kuma Integration Guide
**n8n for Beginners:** This guide will walk you through setting up smart notifications for your infrastructure monitoring.
---
## What This Does
This n8n workflow receives alerts from Uptime Kuma and routes them intelligently:
- **CRITICAL alerts** → Email + Discord/Slack (immediate)
- **PUBLIC alerts** → Discord (after retry threshold)
- **INTERNAL/METRICS/UTILITY** → Logged only (no spam)
All alerts are logged for debugging and historical tracking.
---
## Step 1: Import Workflow into n8n
### 1.1 Access n8n
Go to http://10.0.10.22:5678
### 1.2 Import the Workflow
1. Click **Workflows** (left sidebar)
2. Click **Add Workflow** (top right)
3. Click the **⋮** menu (top right) → **Import from File**
4. Select: `C:\Users\Fred\projects\n8n-uptime-kuma-workflow.json`
5. Click **Import**
### 1.3 Activate the Workflow
1. The workflow editor will open
2. Toggle the **Active** switch (top right) to **ON**
3. You should see "Workflow activated" message
---
## Step 2: Get Your Webhook URL
### 2.1 Find the Webhook URL
1. In the workflow editor, click on the **"Webhook - Uptime Kuma"** node (first blue node)
2. Look for **Test URL** or **Production URL**
3. Copy the production URL - it should look like:
```
http://10.0.10.22:5678/webhook/uptime-kuma
```
**Save this URL** - you'll need it for Uptime Kuma!
---
## Step 3: Configure Uptime Kuma Notifications
### 3.1 Create n8n Notification Channel
1. Go to Uptime Kuma: http://10.0.10.26:3001
2. Click **Settings** (left sidebar)
3. Click **Notifications** tab
4. Click **Add New Notification**
### 3.2 Configure Webhook
- **Notification Type:** Select **Webhook**
- **Friendly Name:** `n8n Alert Router`
- **POST URL:** `http://10.0.10.22:5678/webhook/uptime-kuma`
- **Content Type:** `application/json`
- **Notification Sound:** (your preference)
- Click **Test** to verify
- Click **Save**
### 3.3 Assign to Monitors
Now assign this notification to your monitors:
1. Go to **Dashboard**
2. Click on a **CRITICAL** monitor (e.g., "Proxmox - main-pve")
3. Click **Edit**
4. Scroll to **Notifications**
5. Select **n8n Alert Router**
6. **Apply on:** Select **When status is DOWN**
7. Click **Save**
**Repeat for all CRITICAL monitors** (you can select multiple monitors and edit in bulk)
---
## Step 4: Enable Notification Channels
By default, all notification nodes are **disabled** (to prevent errors). Enable the ones you want to use:
### Option A: Discord (Recommended for Beginners)
**4.1 Create Discord Webhook:**
1. Open Discord
2. Go to your server → Settings → Integrations
3. Click **Webhooks** → **New Webhook**
4. Name it "Uptime Kuma Alerts"
5. Select channel (e.g., #monitoring)
6. Copy the **Webhook URL**
**4.2 Configure in n8n:**
1. In the workflow, click **"Discord - CRITICAL Alert"** node
2. Click **Parameters**
3. Paste your Discord webhook URL
4. Toggle the node **Enabled** (remove checkmark from "Disabled")
5. Click **Save** (top right)
**Repeat for "Discord - PUBLIC Alert"** if you want public service alerts too.
### Option B: Email (SMTP)
**4.1 Configure in n8n:**
1. Click **"Email - CRITICAL Alert"** node
2. Click **Credentials** → **Create New**
3. Choose your email provider:
- **Gmail:** Use App Password (not your main password)
- **Outlook/Office365:** Use SMTP settings
- **Custom SMTP:** Enter your SMTP details
**Example: Gmail Setup**
- **From Email:** your-gmail@gmail.com
- **To Email:** your-alert-email@gmail.com
- **SMTP Host:** smtp.gmail.com
- **SMTP Port:** 587
- **Username:** your-gmail@gmail.com
- **Password:** (App Password from Google Account settings)
- **Secure:** TLS
4. Click **Test** to verify
5. Click **Save**
6. Enable the node (uncheck "Disabled")
### Option C: Slack
**4.1 Create Slack App:**
1. Go to https://api.slack.com/apps
2. Click **Create New App** → **From scratch**
3. Name: "Uptime Kuma"
4. Select your workspace
5. Go to **OAuth & Permissions**
6. Add Bot Token Scopes: `chat:write`, `chat:write.public`
7. Click **Install to Workspace**
8. Copy the **Bot User OAuth Token**
**4.2 Configure in n8n:**
1. Click **"Slack - CRITICAL Alert"** node
2. Click **Credentials** → **Create New**
3. Paste your Bot Token
4. Click **Save**
5. Enable the node
---
## Step 5: Test Your Setup
### 5.1 Trigger a Test Alert
Let's test with a non-critical monitor:
1. Go to Uptime Kuma
2. Find **"[UTILITY] CA Server"** monitor
3. Click **Edit**
4. Temporarily change port from `8443` to `9999` (invalid port)
5. Click **Save**
6. Wait 30-60 seconds for the monitor to check
### 5.2 Check n8n Execution
1. Go to n8n: http://10.0.10.22:5678
2. Click **Executions** (left sidebar)
3. You should see a new execution
4. Click it to view details
### 5.3 Verify Alert Received
- **Discord:** Check your #monitoring channel
- **Email:** Check your inbox
- **Slack:** Check your channel
### 5.4 Fix the Monitor
1. Go back to Uptime Kuma
2. Edit the CA Server monitor
3. Change port back to `8443`
4. Save
---
## Workflow Overview
Here's what happens when a monitor goes down:
```
Uptime Kuma Alert
n8n Webhook Receives
Format Alert Data
(Parses monitor info, status, error)
┌──────────────┬──────────────┐
↓ ↓ ↓
Is CRITICAL? Is PUBLIC? Log All
↓ ↓ Alerts
↓ ↓
Email + Discord
Discord + (Optional)
Slack
(Optional)
```
**Smart Routing:**
- CRITICAL monitors (VPS, Proxmox, PostgreSQL) → Immediate multi-channel alerts
- PUBLIC monitors (websites) → Discord only
- Everything else → Logged for review
---
## Customization Ideas
### Add SMS Alerts (via Twilio)
1. Add **Twilio** node after "Is CRITICAL?"
2. Configure with your Twilio account
3. Send SMS for CRITICAL alerts only
### Add Phone Call Alerts
1. Use **Twilio** or **VoIP.ms** node
2. Trigger phone calls for critical infrastructure failures
3. Only for CRITICAL monitors to avoid spam
### Create Alert Batching
1. Add **Wait** node before Discord/Email
2. Collect multiple alerts for 5 minutes
3. Send one consolidated message
### Add Status Page Integration
1. After "Format Alert Data", add **HTTP Request** node
2. POST to your status page API
3. Auto-update public status page
### Store Alerts in Database
1. Add **PostgreSQL** node (connect to 10.0.10.20)
2. Create `alerts` table
3. Log all alerts for historical analysis
---
## Troubleshooting
### Webhook Not Receiving Data
- Check workflow is **Active** (toggle at top right)
- Verify webhook URL is correct in Uptime Kuma
- Check n8n executions for errors
### Discord/Email Not Sending
- Make sure node is **Enabled** (not grayed out)
- Check credentials are configured
- Look at n8n execution details for error messages
### Too Many Alerts
- Increase retry counts in Uptime Kuma monitors
- Change "Apply on" to only DOWN (not also UP)
- Add filtering logic in n8n (only alert after X failures)
### Not Enough Alerts
- Check monitor has notification assigned
- Verify "Apply on" includes DOWN status
- Check n8n workflow is active
---
## Advanced: Alert Suppression (Prevent Spam)
Add this after "Format Alert Data" to suppress duplicate alerts:
1. Add **Function** node
2. Code:
```javascript
// Suppress duplicate alerts within 15 minutes
const alertKey = `${$json.fullName}-${$json.status}`;
const lastAlert = $workflow.staticData[alertKey] || 0;
const now = Date.now();
if (now - lastAlert < 15 * 60 * 1000) {
// Alert sent less than 15 min ago - suppress
return [];
}
// Send alert and remember timestamp
$workflow.staticData[alertKey] = now;
return [$input.item];
```
This prevents getting the same alert repeatedly.
---
## Next Steps
1. ✅ Import workflow into n8n
2. ✅ Activate workflow and get webhook URL
3. ✅ Configure Uptime Kuma notification
4. ✅ Enable Discord/Email/Slack notifications
5. ✅ Test with a non-critical monitor
6. ✅ Assign notifications to all CRITICAL monitors
7. ⏸️ Customize alert routing (optional)
8. ⏸️ Add SMS/phone call alerts (optional)
9. ⏸️ Create alert dashboard in Grafana (optional)
---
**n8n Dashboard:** http://10.0.10.22:5678
**Uptime Kuma:** http://10.0.10.26:3001
**Workflow File:** `C:\Users\Fred\projects\n8n-uptime-kuma-workflow.json`
---
**Pro Tip:** Once you're comfortable with n8n, you can create workflows for:
- Auto-restarting failed services
- Creating GitHub issues when monitors fail
- Sending daily/weekly uptime reports
- Integration with your homelab automation
Welcome to n8n! 🎉

View File

@@ -0,0 +1,15 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Directory listing for /Printing/.claude/</title>
</head>
<body>
<h1>Directory listing for /Printing/.claude/</h1>
<hr>
<ul>
<li><a href="settings.local.json">settings.local.json</a></li>
</ul>
<hr>
</body>
</html>

View File

@@ -0,0 +1,27 @@
{
"permissions": {
"allow": [
"Bash(python generate_nameplates.py:*)",
"Bash(git add:*)",
"Bash(git init:*)",
"Bash(git remote add:*)",
"Bash(git fetch:*)",
"Bash(git commit:*)",
"Bash(git push:*)",
"Bash(git ls-remote:*)",
"Bash(done)",
"Bash(git pull:*)",
"Bash(python:*)",
"Bash(\"C:\\Program Files\\OpenSCAD\\openscad.exe\" -D 'name=\"\"Fred\"\"' -o \"TEST_Fred_raised_text.stl\" \"zipper_pull_raised_text_template.scad\")",
"Bash(ls:*)",
"Bash(head:*)",
"Bash(\"C:\\Program Files\\OpenSCAD\\openscad.exe\" \"zipper-pulls-raised-text/Ford-Fred (2).stl\" --export-format echo --o -)",
"Bash(\"C:\\Program Files\\OpenSCAD\\openscad.exe\" \"zipper-pulls-raised-text/111.stl\" --export-format echo --o -)",
"Bash(dir \"C:\\Users\\Fred\\.claude-worktrees\\Printing\\eager-bose\\Nameplates\\zipper-pulls-raised-text\" /T:W)",
"Bash(findstr:*)",
"Bash(powershell:*)"
],
"deny": [],
"ask": []
}
}

41
Printing/.gitignore vendored Normal file
View File

@@ -0,0 +1,41 @@
# Generated STL files
*.stl
# Build artifacts
*.o
*.obj
# Python cache
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
*.so
# Virtual environments
venv/
env/
ENV/
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS files
.DS_Store
Thumbs.db
desktop.ini
# Node.js (if accidentally included)
node_modules/
*.tar.xz
*.tar.gz
# Temporary files
*~
*.tmp
*.temp
test_output.stl

453
Printing/CLAUDE.md Normal file
View File

@@ -0,0 +1,453 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
A collection of 3D printing utilities for generating personalized STL files using **Python + OpenSCAD**. The repository contains two main subprojects:
1. **Nameplates** - Batch nameplate and zipper pull generator
2. **Key caps** - Mechanical keyboard keycap scaling utilities
All projects use OpenSCAD as the 3D modeling engine, invoked via Python scripts or standalone SCAD files.
## Repository Structure
```
Printing/
├── Nameplates/ # Nameplate and zipper pull generators
│ ├── generate_nameplates.py # Batch nameplate generator
│ ├── generate_zipper_pulls.py # Batch zipper pull generator
│ ├── nameplate_template.scad # Nameplate template (38mm height, no hole)
│ ├── zipper_pull_template.scad # Zipper pull template (34mm height, 4mm hole)
│ ├── names.txt # Input names (one per line)
│ ├── output_stl/ # Generated nameplate STLs
│ ├── zipper-pulls/ # Generated zipper pull STLs
│ ├── Fordscript.ttf # Custom cursive font (must be installed)
│ └── CLAUDE.md # Detailed nameplate documentation
└── Key caps/ # Keyboard keycap scaling
├── Body5_scaled_FIXED.scad # Primary keycap scaling script
├── Body5.stl # Original keycap model
├── CLAUDE.md # Detailed keycap documentation
└── README_scaling.md # User guide for scaling workflow
```
## Critical Dependencies
### OpenSCAD Installation
- **Path**: `C:\Program Files\OpenSCAD\openscad.exe` (hardcoded in Python scripts)
- **Download**: https://openscad.org/downloads.html
- **Update path** in scripts if installed elsewhere (see `generate_nameplates.py:36`, `generate_zipper_pulls.py:36`)
### Font Requirements (Nameplates/Zipper Pulls Only)
- **Font**: Fordscript cursive font (`Nameplates/Fordscript.ttf`)
- **Installation**: Must be installed in Windows (right-click → Install for all users)
- **Critical**: OpenSCAD references fonts by system name, not file path
- **Verification**: After install, restart OpenSCAD before generating files
## Key Commands
### Nameplates
```bash
# Generate all nameplates (38mm height, no hole)
cd Nameplates
python generate_nameplates.py
# Output: output_stl/*.stl
```
### Zipper Pulls
```bash
# Generate all zipper pulls (100mm × 34mm total, 4mm hole with 1mm clearance)
cd Nameplates
python generate_zipper_pulls.py
# Output: zipper-pulls/*.stl
```
### Key Caps
```powershell
# Render scaled keycap (command line)
& "C:\Program Files\OpenSCAD\openscad.exe" -o "output.stl" "Body5_scaled_FIXED.scad"
# Or: Open in OpenSCAD GUI, adjust parameters, press F6 to render, export STL
& "C:\Program Files\OpenSCAD\openscad.exe" "Body5_scaled_FIXED.scad"
```
### Manual OpenSCAD Testing
```powershell
# Test single nameplate
& "C:\Program Files\OpenSCAD\openscad.exe" -D 'name="TestName"' -o test_output.stl nameplate_template.scad
# Test single zipper pull
& "C:\Program Files\OpenSCAD\openscad.exe" -D 'name="TestName"' -o test_output.stl zipper_pull_template.scad
# Preview in GUI (edit parameters at top of file)
& "C:\Program Files\OpenSCAD\openscad.exe" nameplate_template.scad
```
## Architecture Overview
### Nameplates vs Zipper Pulls
Both use the same two-layer oval design (white base + blue top with engraved text) but differ in dimensions and features:
| Feature | Nameplates | Zipper Pulls |
|---------|-----------|-------------|
| Template | `nameplate_template.scad` | `zipper_pull_template.scad` |
| Generator | `generate_nameplates.py` | `generate_zipper_pulls.py` |
| Total Size (W×H) | 100mm × 38mm | 100mm × 34mm |
| Blue Layer Size | 96mm × 34mm | 96mm × 30mm |
| White Base Offset | 2mm all sides | 2mm all sides |
| Font Size | 30mm | 13mm |
| Hole | None | 4mm diameter, left side |
| Hole Clearance | N/A | 1mm from white base edge |
| Output Directory | `output_stl/` | `zipper-pulls/` |
| Use Case | Display nameplates | Zipper/bag attachments |
**Important**: These are separate templates and scripts. Modifying one does not affect the other.
### Pipeline Architecture
```
names.txt
Python Generator Script (reads names, iterates)
OpenSCAD CLI (renders each name via template)
STL Files (output to respective directories)
```
**Key Pattern**: Python script calls OpenSCAD subprocess for each name:
```python
cmd = [
r'C:\Program Files\OpenSCAD\openscad.exe',
'-D', f'name="{escaped_name}"', # Override parameter
'-o', output_file, # Output STL path
template_file # SCAD template
]
subprocess.run(cmd, capture_output=True, text=True, check=True)
```
### OpenSCAD Template Pattern
All SCAD templates follow this structure:
```scad
// 1. Parameters (overridable via -D flag)
name = "NAME";
oval_width = 100;
// ... more parameters
// 2. Modules (reusable geometry functions)
module oval(width, height, depth) { ... }
module base_layer() { ... }
module top_layer() { ... }
// 3. Assembly module
module nameplate() {
base_layer();
top_layer();
}
// 4. Execution (builds the model)
nameplate();
```
## Project-Specific Conventions
### Name Sanitization
Python scripts convert names to safe filenames:
- Removes special characters (keeps alphanumeric, spaces, hyphens, underscores)
- Replaces spaces with underscores
- Escapes quotes for shell: `name.replace('"', '\\"')`
- Example: `"John Doe"``John_Doe.stl`
### Two-Layer Design Philosophy
- **White base layer**: Structural foundation, extends beyond blue layer by `base_offset` (2mm)
- **Blue top layer**: Visual layer with engraved text
- **Text engraving**: Cuts completely through blue layer (`text_depth = top_thickness = 1mm`) to expose white base underneath
- **Result**: White text on blue background
### Hole Positioning (Zipper Pulls)
Hole is positioned on left side with clearance from white base edge:
```scad
white_width = oval_width + base_offset*2;
hole_x = -(white_width/2) + (hole_diameter/2) + hole_clearance;
// For 96mm blue + 2mm offset = 100mm white, 4mm hole, 1mm clearance:
// hole_x = -50 + 2 + 1 = -47mm from center
```
**Critical**:
- `hole_clearance` must be sufficient to prevent hole from breaking through oval edge. Current value (1mm) tested and verified.
- Hole is positioned relative to **white base width**, not blue layer width, to ensure it stays within the white outline.
## Common Workflows
### Add New Names to Both Systems
1. Edit `Nameplates/names.txt`, add name on new line
2. Run both generators:
```bash
cd Nameplates
python generate_nameplates.py # Creates nameplate
python generate_zipper_pulls.py # Creates zipper pull
```
3. Find STLs in `output_stl/` and `zipper-pulls/` respectively
### Adjust Zipper Pull Hole Clearance
If hole is too close to edge or breaking through:
1. Edit `zipper_pull_template.scad`, line 17:
```scad
hole_clearance = 1; // Increase this value
```
2. Regenerate all zipper pulls:
```bash
python generate_zipper_pulls.py
```
**Note**: Hole clearance is measured from the **white base edge**, not the blue layer edge.
### Create Custom Dimension Variant
To create a new variant (e.g., smaller nameplates):
1. Copy existing template: `nameplate_template.scad` → `nameplate_small_template.scad`
2. Modify dimensions at top of new file
3. Copy generator script: `generate_nameplates.py` → `generate_nameplates_small.py`
4. Update generator to use new template and output directory:
```python
template_file = "nameplate_small_template.scad"
output_dir = "output_stl_small"
```
### Scale Keycaps for Different Tolerances
1. Open `Key caps/Body5_scaled_FIXED.scad` in OpenSCAD GUI
2. Adjust `body_scale_xy` parameter at top (default: 0.98 = 98%)
- Too tight: decrease to 0.97 (3% reduction)
- Too loose: increase to 0.99 (1% reduction)
3. Press `F5` for preview, `F6` for full render
4. Export via `File > Export > Export as STL`
5. Test print and iterate
See `Key caps/README_scaling.md` for detailed keycap scaling workflow.
## Troubleshooting
### Font Not Found (Nameplates/Zipper Pulls)
**Symptom**: Text is missing or uses default font in generated STLs.
**Fix**:
1. Install `Fordscript.ttf`: Right-click → "Install for all users"
2. Restart OpenSCAD if running
3. Test with manual command:
```powershell
& "C:\Program Files\OpenSCAD\openscad.exe" -D 'name="Test"' -o test.stl nameplate_template.scad
```
4. Open `test.stl` in slicer and verify text appears
### OpenSCAD Path Not Found
**Symptom**: `FileNotFoundError: OpenSCAD not found`
**Fix**: Update hardcoded path in generator scripts:
```python
# In generate_nameplates.py and generate_zipper_pulls.py, line 36:
cmd = [
r'C:\Program Files\OpenSCAD\openscad.exe', # ← Update this path
# ...
]
```
### Zipper Pull Hole Breaking Through Edge
**Symptom**: Hole intersects with oval edge, causing weak point or break.
**Fix**: Increase `hole_clearance` in `zipper_pull_template.scad`:
```scad
hole_clearance = 1.5; // Increase from 1mm to 1.5mm
```
Then regenerate all zipper pulls.
**Note**: Ensure hole clearance is measured from the white base edge (100mm total width), not the blue layer edge (96mm).
### Keycap Stem Scaled Incorrectly
**Symptom**: Cherry MX stem doesn't fit switch after scaling.
**Cause**: Using wrong SCAD file (non-FIXED version requires manual stem positioning).
**Fix**: Always use `Body5_scaled_FIXED.scad` which uses centered import approach. The stem preservation region is automatically centered at origin.
## Advanced Customization
### Add Custom Parameter to Templates
To add a new parameter (e.g., border thickness):
1. Add parameter to template:
```scad
border_thickness = 0.5; // New parameter
```
2. Use in geometry:
```scad
module base_layer() {
difference() {
oval(oval_width, oval_height, base_thickness);
// Cut border groove
translate([0, 0, base_thickness - border_thickness])
oval(oval_width - 4, oval_height - 4, border_thickness + 0.1);
}
}
```
3. Pass from Python script (optional):
```python
cmd = [
r'C:\Program Files\OpenSCAD\openscad.exe',
'-D', f'name="{escaped_name}"',
'-D', f'border_thickness={thickness}', # Override parameter
'-o', output_file,
template_file
]
```
### Batch Generate Multiple Sizes
Modify generator script to loop over multiple configurations:
```python
configs = [
{"template": "nameplate_template.scad", "width": 100, "height": 38},
{"template": "nameplate_template.scad", "width": 80, "height": 30},
{"template": "nameplate_template.scad", "width": 60, "height": 23},
]
for name in names:
for config in configs:
output_file = f"{config['width']}x{config['height']}_{safe_name}.stl"
cmd = [
r'C:\Program Files\OpenSCAD\openscad.exe',
'-D', f'name="{escaped_name}"',
'-D', f'oval_width={config["width"]}',
'-D', f'oval_height={config["height"]}',
'-o', output_file,
config["template"]
]
subprocess.run(cmd, ...)
```
## Development Reference
### OpenSCAD Command Line Flags
```powershell
# Override parameter
-D 'name="value"'
# Output file
-o output.stl
# Check syntax (no output)
--check
# Verbose output
--verbose
# Clear cache (useful when changing imports)
--clear-cache
# Version info
--version
```
### Python Subprocess Pattern
All generators use the same pattern:
```python
result = subprocess.run(
cmd,
capture_output=True, # Capture stdout/stderr
text=True, # Return strings, not bytes
check=True # Raise CalledProcessError on non-zero exit
)
```
Error handling:
```python
try:
subprocess.run(cmd, capture_output=True, text=True, check=True)
except subprocess.CalledProcessError as e:
print(f"Error: {e.stderr}")
except FileNotFoundError:
print("OpenSCAD not found. Install from https://openscad.org")
```
## File Organization Guidelines
### When to Create New Template vs Modify Existing
**Create New Template** if:
- Fundamentally different design (e.g., adding holes, changing shape)
- Different use case (nameplates vs zipper pulls)
- Need to maintain both versions simultaneously
**Modify Existing Template** if:
- Adjusting dimensions only
- Tweaking existing features
- Single-use or temporary change
### Output Directory Naming
- Use descriptive names: `output_stl/`, `zipper-pulls/`
- Keep separate output directories for different variants
- Never mix different variants in the same directory (prevents confusion)
### Generator Script Naming
- Match template name: `generate_nameplates.py` uses `nameplate_template.scad`
- Be explicit: `generate_zipper_pulls.py` not `generate_zippers.py`
- One template per generator for clarity
## 3D Printing Notes
### Recommended Slicer Settings
- **Material**: Two colors (white + blue PLA recommended)
- **Layer Height**: 0.1-0.2mm for clean text details
- **Infill**: 20-30% (thin parts don't need much)
- **Supports**: Not needed (flat design, print as modeled)
- **Orientation**: Print flat on bed
- **Bed Adhesion**: Brim or raft recommended for thin parts
- **First Layer**: Critical for thin 1.5mm base - ensure good adhesion
### Multi-Color Printing
Two approaches:
1. **Filament swap**: Print white base, pause, swap to blue, continue
2. **Two prints**: Print white base separately, print blue top, glue together
**Important**: Text must be fully engraved through blue layer to show white underneath.
#### M600 Filament Change Height
When using M600 command for automatic filament change in slicer:
- **Base layer thickness**: 1.5mm
- **Actual M600 height**: 1.5mm + one layer height
- **Reason**: M600 triggers BEFORE the specified layer is printed. If set to exactly 1.5mm, the slicer will print one solid white layer at 1.5mm, then trigger M600, then start the blue outline and text.
- **Example**: With 0.2mm layer height, set M600 at 1.7mm (1.5 + 0.2) to ensure filament change happens at the correct layer.
- **Verification**: After slicing, verify that the layer immediately after M600 begins the blue top layer, not another white layer.
## Project-Specific Quirks
### Why Two Separate Templates for Nameplates/Zipper Pulls?
- Different dimensions (38mm vs 34mm height)
- Zipper pulls need hole, nameplates don't
- Maintains backward compatibility with existing nameplate output
- Allows independent evolution of each variant
### Why Hardcoded OpenSCAD Path?
- Windows standard installation location is consistent
- Avoids PATH environment complexity
- Easy to update in one place per script
- Future: Consider environment variable or config file
### Why Manual Font Installation?
- OpenSCAD uses system font registry, not file paths
- Ensures consistent font rendering across machines
- Font must be available to OpenSCAD process
- No programmatic workaround exists
### Keycap Stem Preservation Algorithm
- Circular region around stem preserved at 100% scale
- Rest of keycap scaled down for better fit
- Cherry MX stem is exactly 4mm × 4mm by spec (must not scale)
- Circular preservation accommodates off-brand switch tolerances
- See `Key caps/CLAUDE.md` for detailed algorithm explanation

View File

@@ -0,0 +1 @@
C:/Users/Fred/claude-shared/commands

View File

@@ -0,0 +1,16 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Directory listing for /Printing/Key caps/.claude/</title>
</head>
<body>
<h1>Directory listing for /Printing/Key caps/.claude/</h1>
<hr>
<ul>
<li><a href="commands">commands</a></li>
<li><a href="settings.local.json">settings.local.json</a></li>
</ul>
<hr>
</body>
</html>

View File

@@ -0,0 +1,23 @@
{
"permissions": {
"allow": [
"Bash(where:*)",
"Bash(if exist \"C:\\Program Files\\OpenSCAD\\openscad.exe\" echo Found at C:Program FilesOpenSCADopenscad.exe)",
"Bash(\"/c/Program Files/OpenSCAD/openscad.exe\" -o \"Body5_scaled_98percent.stl\" \"Body5_scaled.scad\")",
"Bash(\"/c/Program Files/OpenSCAD/openscad.exe\" --clear-cache -o \"Body5_scaled_98percent.stl\" \"Body5_scaled.scad\")",
"Bash(\"/c/Program Files/OpenSCAD/openscad.exe\" -o \"Body5_TEST_circle.stl\" \"Body5_test_circle.scad\")",
"Bash(\"/c/Program Files/OpenSCAD/openscad.exe\" -o \"test_cylinder.stl\" \"test_cylinder.scad\")",
"Bash(\"/c/Program Files/OpenSCAD/openscad.exe\" -o \"Body5_scaled_FIXED_98percent.stl\" \"Body5_scaled_FIXED.scad\")",
"Bash(\"/c/Program Files/OpenSCAD/openscad.exe\" --clear-cache -o \"Body5(1)_scaled_98percent.stl\" \"Body5(1)_scaled.scad\")",
"Bash(\"/c/Program Files/OpenSCAD/openscad.exe\" -o \"Body5(1)_scaled_98percent.stl\" \"Body5(1)_scaled.scad\")",
"Bash(ls:*)",
"Bash(\"/c/Program Files/OpenSCAD/openscad.exe\" -D number_to_generate=1 -o \"Body5_num1.stl\" \"generate_numbered_keycaps.scad\")",
"Bash(cmd /c generate_all_numbered_keycaps.bat)",
"Bash(for i in {2..20})",
"Bash(do \"/c/Program Files/OpenSCAD/openscad.exe\" -D number_to_generate=$i -o \"Body5_num$i.stl\" \"generate_numbered_keycaps.scad\")",
"Bash(done)"
],
"deny": [],
"ask": []
}
}

View File

@@ -0,0 +1,55 @@
// =====================================================
// Keycap Body Scaling Script - FIXED VERSION
// =====================================================
// This script scales down the keycap body while preserving
// the Cherry MX stem at its original 4mm x 4mm dimensions
//
// FIX: Centers the model first so cylinder operations work correctly
// =====================================================
// ----- ADJUSTABLE PARAMETERS -----
body_scale_xy = 0.98; // 98% of original (2% smaller)
body_scale_z = 1.00; // Keep height at 100%
stem_diameter = 4.8; // Diameter of circular stem preservation region (4.8mm to preserve 4mm x 4mm cross)
stem_height = 10.0; // Height of stem region to preserve (tall enough to capture entire stem)
stem_z_offset = -1.0; // Z offset to position cylinder on the stem
$fn = 128; // Circle smoothness
stl_filename = "Body5(1).stl";
// =====================================================
// MAIN MODEL - CENTERED APPROACH
// =====================================================
// Center the imported model at origin
// The stem cross should be centered at X=209, Y=223.5 in the original STL
module centered_import() {
translate([-209, -223.5, 0]) // Center X and Y on the stem
import(stl_filename);
}
union() {
// Part 1: Scaled body WITHOUT the circular stem region
difference() {
scale([body_scale_xy, body_scale_xy, body_scale_z]) {
centered_import();
}
// Cut out a cylinder where the stem is
translate([0, 0, stem_z_offset]) {
cylinder(h = stem_height, d = stem_diameter + 0.5, center = true);
}
}
// Part 2: Original UNSCALED stem region (circular)
intersection() {
centered_import(); // NOT scaled - preserves original 4mm x 4mm cross
// Cylinder defines the 4.8mm circular stem region to keep at 100% scale
translate([0, 0, stem_z_offset]) {
cylinder(h = stem_height, d = stem_diameter, center = true);
}
}
}

View File

@@ -0,0 +1,98 @@
// =====================================================
// Keycap Body Scaling Script
// =====================================================
// This script scales down the keycap body while preserving
// the Cherry MX stem at its original 4mm x 4mm dimensions
// within a CIRCULAR region (for off-brand switches)
//
// Usage:
// 1. Adjust body_scale_xy below (start with 0.98 = 98%)
// 2. Adjust stem_x and stem_y to position circle on the cross
// 3. Press F5 to preview, F6 to render
// 4. Export STL: File > Export > Export as STL
// =====================================================
// ----- ADJUSTABLE PARAMETERS -----
// Body scaling factor for X and Y axes
body_scale_xy = 0.98; // 98% of original (2% smaller)
body_scale_z = 1.00; // Keep height at 100%
// ----- STEM DIMENSIONS -----
// Position the circular preservation region on the stem cross
stem_x = 209; // X position of stem center (adjust to align with cross)
stem_y = 223.5; // Y position of stem center (adjust to align with cross)
stem_z = 3.85; // Z position of stem center (adjust if needed)
stem_diameter = 5.5; // Diameter of circular region around the stem
stem_height = 8.0; // Height of stem region to preserve
$fn = 128; // Circle smoothness (128 = very smooth, 64 = balanced)
stl_filename = "Body5.stl";
// =====================================================
// MAIN MODEL
// =====================================================
union() {
// Part 1: Scaled body WITHOUT the circular stem region
difference() {
// The scaled keycap body (scaled around origin, then positioned)
translate([stem_x * (1 - body_scale_xy), stem_y * (1 - body_scale_xy), 0]) {
scale([body_scale_xy, body_scale_xy, body_scale_z]) {
import(stl_filename);
}
}
// Cut out a cylinder where the stem is
translate([stem_x, stem_y, stem_z]) {
cylinder(
h = stem_height,
d = stem_diameter + 0.5,
center = true
);
}
}
// Part 2: Original stem at 100% scale in CIRCULAR region
intersection() {
// The original unscaled model
import(stl_filename);
// A cylinder that defines the CIRCULAR stem region to keep
translate([stem_x, stem_y, stem_z]) {
cylinder(
h = stem_height,
d = stem_diameter,
center = true
);
}
}
}
// =====================================================
// NOTES
// =====================================================
//
// ADJUSTING THE CIRCULAR REGION POSITION:
// If the circle is not centered on the cross stem:
// 1. Open in OpenSCAD and press F5 to preview
// 2. Rotate the view to see the stem from below
// 3. Adjust stem_x and stem_y values until circle is centered on cross
// 4. Typical adjustment: try values between 208-210 for X, 222-225 for Y
//
// ITERATIVE TESTING WORKFLOW:
// 1. Start with body_scale_xy = 0.98 (98%)
// 2. Export STL and test print
// 3. If keycap is still too tight, decrease to 0.97
// 4. If keycap is too loose, increase to 0.99
// 5. Fine-tune in 0.005 increments (e.g., 0.975, 0.985)
//
// STEM DIAMETER ADJUSTMENT:
// - Increase stem_diameter if you need more clearance around the cross
// - Decrease stem_diameter for tighter tolerance
// - Typical range: 4.5mm to 6.0mm
//
// =====================================================

View File

@@ -0,0 +1,53 @@
// =====================================================
// Keycap Body Scaling Script - FIXED VERSION
// =====================================================
// This script scales down the keycap body while preserving
// the Cherry MX stem at its original 4mm x 4mm dimensions
//
// FIX: Centers the model first so cylinder operations work correctly
// =====================================================
// ----- ADJUSTABLE PARAMETERS -----
body_scale_xy = 0.98; // 98% of original (2% smaller)
body_scale_z = 1.00; // Keep height at 100%
stem_diameter = 5.5; // Diameter of circular stem preservation region
stem_height = 8.0; // Height of stem region to preserve
$fn = 128; // Circle smoothness
stl_filename = "Body5.stl";
// =====================================================
// MAIN MODEL - CENTERED APPROACH
// =====================================================
// First, we need to center the imported model
// The original STL is positioned around x≈209, y≈220
// We'll measure the bounding box and center it
module centered_import() {
// Import and center the model at origin
translate([-209, -223.5, -4.9]) // Approximate center based on STL coordinates
import(stl_filename);
}
union() {
// Part 1: Scaled body WITHOUT the circular stem region
difference() {
scale([body_scale_xy, body_scale_xy, body_scale_z]) {
centered_import();
}
// Cut out a cylinder where the stem is (now centered at origin)
cylinder(h = stem_height, d = stem_diameter + 0.5, center = true);
}
// Part 2: Original unscaled stem region (circular)
intersection() {
centered_import();
// Cylinder defines the circular stem region to keep
cylinder(h = stem_height, d = stem_diameter, center = true);
}
}

View File

@@ -0,0 +1,31 @@
// TEST VERSION - Exaggerated circular cutout to verify it's working
body_scale_xy = 0.98;
body_scale_z = 1.00;
stem_diameter = 8.0; // MUCH larger to make the circle obvious
stem_height = 10.0; // Taller to ensure we capture everything
stem_z_position = 0;
$fn = 128;
stl_filename = "Body5.stl";
union() {
difference() {
scale([body_scale_xy, body_scale_xy, body_scale_z]) {
import(stl_filename);
}
// Large obvious cylinder
translate([0, 0, stem_z_position]) {
cylinder(h = 20, d = stem_diameter + 0.5, center = true);
}
}
intersection() {
import(stl_filename);
// Large obvious cylinder
translate([0, 0, stem_z_position]) {
cylinder(h = 20, d = stem_diameter, center = true);
}
}
}

View File

@@ -0,0 +1,80 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is an OpenSCAD project for scaling 3D-printed mechanical keyboard keycaps while preserving the Cherry MX stem dimensions. The project contains scripts to reduce keycap body dimensions for better socket fit while maintaining the exact 4mm × 4mm stem cross dimensions required for Cherry MX switches.
## Key Commands
### Rendering STL Files
On Windows, OpenSCAD is typically installed at `C:\Program Files\OpenSCAD\openscad.exe`. Use Git Bash path format:
```bash
# Render a SCAD file to STL
"/c/Program Files/OpenSCAD/openscad.exe" -o "output.stl" "input.scad"
# Clear cache and render (recommended when changing imports)
"/c/Program Files/OpenSCAD/openscad.exe" --clear-cache -o "output.stl" "input.scad"
```
### Verify OpenSCAD Installation
```bash
# Check if OpenSCAD is installed
if exist "C:\Program Files\OpenSCAD\openscad.exe" echo Found at C:\Program Files\OpenSCAD\openscad.exe
```
## Project Architecture
### Main Scripts
- **`Body5_scaled_FIXED.scad`**: The primary working script that uses a centered-import approach. This is the recommended version for scaling keycaps.
- **`Body5_scaled.scad`**: Earlier version that manually positions the stem preservation cylinder using explicit X/Y coordinates.
- **`Body5_test_circle.scad`**: Test script with exaggerated parameters (larger cylinder) to verify the circular stem preservation is working.
- **`test_cylinder.scad`**: Simple cylinder rendering test for OpenSCAD verification.
### STL Files
- **`Body5.stl`**: The original keycap model that gets imported and processed by the SCAD scripts.
- **`Body2.stl`**: Alternative keycap model.
- **Generated STL files**: Output from rendering the SCAD scripts (e.g., `Body5_scaled_98percent.stl`).
### Scaling Algorithm
The scripts use a two-part union approach:
1. **Scaled Body with Cutout**: The entire keycap is scaled down by `body_scale_xy`, then a cylinder is subtracted where the stem should be
2. **Unscaled Stem Region**: A cylinder intersection preserves the original stem region at 100% scale
3. **Union**: Both parts are merged, creating a keycap with scaled body but original stem dimensions
The `_FIXED` version centers the model at the origin before operations, making cylinder placement simpler. The non-FIXED version requires manual positioning of the preservation cylinder using `stem_x` and `stem_y` coordinates.
### Critical Parameters
- **`body_scale_xy`**: X/Y scaling factor (typically 0.97-0.99 for 1-3% reduction)
- **`body_scale_z`**: Z scaling factor (keep at 1.00 to maintain keycap height)
- **`stem_diameter`**: Diameter of circular preservation region (default: 5.5mm)
- **`stem_height`**: Height of stem region (default: 8.0mm)
- **`$fn`**: OpenSCAD circle resolution (64-128, higher = smoother but slower)
### Centering Approach (FIXED version)
The `centered_import()` module translates the imported STL by `[-209, -223.5, -4.9]` to center it at the origin. These values were determined by analyzing the original STL's bounding box. This allows stem operations to use simple `cylinder()` calls at the origin rather than calculating offsets.
## Development Workflow
1. Modify SCAD script parameters (typically just `body_scale_xy`)
2. Render to STL using OpenSCAD command line or GUI (F5 preview, F6 full render in GUI)
3. Import STL into slicer and print test keycap
4. Measure fit and iterate on scaling factor
5. Typical iteration: start at 0.98, adjust by 0.01 increments based on fit testing
## Important Notes
- The Cherry MX stem cross is exactly 4mm × 4mm by specification and must not be scaled
- The circular preservation region accommodates off-brand switches that may have slightly different tolerances
- All SCAD scripts expect `Body5.stl` (or specified STL file) to be in the same directory
- Rendering is computationally intensive due to importing the STL twice (once for scaling, once for stem preservation)

View File

@@ -0,0 +1,109 @@
# Keycap Scaling Guide
## Quick Start
1. **Open the script**: Open `Body5_scaled.scad` in OpenSCAD
2. **Adjust scaling**: Edit the `body_scale_xy` parameter at the top (default: 0.98 = 98%)
3. **Preview**: Press `F5` to see a quick preview
4. **Render**: Press `F6` to fully render (required before export)
5. **Export**: Go to `File > Export > Export as STL`
6. **Test print**: Print and check the fit
7. **Iterate**: Adjust the scale parameter and repeat if needed
## Key Parameters
### `body_scale_xy`
- **Default**: `0.98` (98% of original size)
- **Purpose**: Scales the X and Y dimensions of the keycap body
- **Recommendations**:
- Start with `0.98` for a 2% reduction
- If too tight: try `0.97` (3% reduction)
- If too loose: try `0.99` (1% reduction)
- Fine-tune: use `0.975`, `0.985`, etc.
### `body_scale_z`
- **Default**: `1.00` (no scaling)
- **Purpose**: Scales the Z (height) dimension
- **Recommendation**: Keep at `1.00` to maintain keycap height
### Stem Parameters (Advanced)
Only adjust these if the stem preservation isn't working correctly:
- `stem_diameter`: Diameter of the circular preservation region (default: 5.5mm)
- `stem_height`: Height of the stem (default: 4.0mm)
- `stem_z_position`: Vertical position offset (default: -2.0mm)
- `$fn`: Circle smoothness (default: 64, range: 32-128)
## How It Works
The script uses a two-part approach:
1. **Scaled Body**: The entire keycap is scaled down by your chosen percentage, BUT a circular region around the stem is cut out
2. **Original Stem**: The circular stem region is preserved at 100% scale (keeping the 4mm × 4mm Cherry MX cross dimensions)
3. **Combined**: Both parts are merged together
This ensures:
- ✅ The outer keycap body fits better in sockets (scaled down)
- ✅ The Cherry MX stem remains exactly 4mm × 4mm (unscaled)
- ✅ The circular region around the stem is preserved (for off-brand switches)
- ✅ The stem stays centered and properly positioned
## Workflow for Iterative Testing
1. Export STL with `body_scale_xy = 0.98`
2. Slice and print the keycap
3. Test fit on your keyboard
4. **If too tight**: Decrease scale (try 0.97)
5. **If too loose**: Increase scale (try 0.99)
6. **If just right**: You're done!
## Troubleshooting
### The stem looks wrong or is scaled
- The circular stem region might not be positioned correctly
- Measure your stem in the original STL
- Adjust `stem_z_position`, `stem_diameter`, or `stem_height`
- If the circle is too jagged, increase `$fn` to 128
### The keycap won't render
- Make sure `Body5.stl` is in the same folder as the `.scad` file
- Check for errors in the OpenSCAD console (bottom of window)
### Rendering is slow
- This is normal! The script imports the STL twice
- `F5` preview is fast but lower quality
- `F6` render is slow but required for STL export
- Consider reducing the preview quality in OpenSCAD preferences
### The seam between body and stem is visible
- This is usually not noticeable in the printed part
- If needed, increase `stem_diameter` by 0.5mm increments (try 6.0mm or 6.5mm)
## File Structure
```
Key caps/
├── Body5.stl # Original keycap STL
├── Body5_scaled.scad # OpenSCAD scaling script
└── README_scaling.md # This guide
```
## Export Settings
When exporting STL from OpenSCAD:
- File format: STL (binary is smaller, ASCII is more compatible)
- Units: millimeters (mm)
## Next Steps
After exporting your scaled STL:
1. Import into your slicer (Cura, PrusaSlicer, etc.)
2. Use typical keycap print settings:
- Layer height: 0.1-0.2mm
- Infill: 15-20%
- Supports: Probably not needed for most keycaps
3. Print and test!
---
**Note**: The Cherry MX stem dimensions (4mm × 4mm) are standardized. The script is designed to preserve these exactly, so you should only need to adjust the `body_scale_xy` parameter for tolerance testing.

View File

@@ -0,0 +1,34 @@
@echo off
REM =====================================================
REM Batch Script to Generate All 20 Numbered Keycaps
REM =====================================================
REM This script uses OpenSCAD command line to generate
REM all 20 numbered keycap STL files automatically
REM =====================================================
echo Starting generation of 20 numbered keycap STL files...
echo.
set OPENSCAD="C:\Program Files\OpenSCAD\openscad.exe"
set SCRIPT=generate_numbered_keycaps.scad
REM Loop through numbers 1-20
for /L %%i in (1,1,20) do (
echo Generating Body5_num%%i.stl...
%OPENSCAD% -D number_to_generate=%%i -o Body5_num%%i.stl %SCRIPT%
if errorlevel 1 (
echo ERROR: Failed to generate Body5_num%%i.stl
pause
exit /b 1
)
echo Completed Body5_num%%i.stl
echo.
)
echo.
echo =====================================================
echo All 20 numbered keycap STL files generated successfully!
echo Files created: Body5_num1.stl through Body5_num20.stl
echo =====================================================
echo.
pause

View File

@@ -0,0 +1,102 @@
// =====================================================
// Numbered Keycap Generator - Multi-Color Printing
// =====================================================
// This script generates 20 numbered keycap STL files
// with recessed numbers for multi-material printing
//
// Numbers are 14mm tall, recessed 0.6mm deep
// Positioned on triangular top face, centered on apex-to-midpoint line
// Apex oriented as "up" (aligned with Cherry MX cross arm)
// =====================================================
// ----- ADJUSTABLE PARAMETERS -----
number_to_generate = 1; // Change this value (1-20) to generate different numbers
number_height = 14; // Height of numbers in mm
number_depth = 0.6; // Recess depth for color saturation
text_font = "Liberation Sans:style=Bold"; // Font (use bold for visibility)
text_thickness = number_depth; // Same as recess depth
// Triangular face positioning
// The top face is opposite the stem pocket
// One apex aligns with a Cherry MX cross arm
face_z_position = 8.0; // Approximate Z height of top triangular face
face_rotation = 0; // Rotation to align apex with cross arm
text_y_offset = 0; // Offset along apex-to-midpoint centerline
$fn = 128; // Circle smoothness
base_stl_filename = "Body5(1)_scaled_98percent.stl";
// =====================================================
// CENTERED IMPORT MODULE
// =====================================================
module centered_base() {
// Import the scaled keycap base
// Already centered from the previous scaling script
import(base_stl_filename);
}
// =====================================================
// NUMBERED TEXT MODULE
// =====================================================
module recessed_number(num) {
// Create text for the number
rotate([0, 0, face_rotation]) {
translate([0, text_y_offset, face_z_position]) {
rotate([0, 0, 0]) { // Text faces up
linear_extrude(height = text_thickness + 1) { // +1 to ensure clean cut
text(
str(num),
size = number_height,
halign = "center",
valign = "center",
font = text_font
);
}
}
}
}
}
// =====================================================
// MAIN MODEL
// =====================================================
difference() {
// The base keycap
centered_base();
// Subtract the recessed number
recessed_number(number_to_generate);
}
// =====================================================
// USAGE INSTRUCTIONS
// =====================================================
//
// MANUAL GENERATION (one at a time):
// 1. Change number_to_generate value (1-20)
// 2. Render with F6
// 3. Export as STL with desired filename
//
// COMMAND LINE GENERATION (all 20 files):
// Use the companion batch script or run:
// openscad -D number_to_generate=N -o Body5_numN.stl generate_numbered_keycaps.scad
//
// ADJUSTING POSITION:
// - face_z_position: Move numbers up/down on the keycap
// - face_rotation: Rotate around Z axis to align with cross arm
// - text_y_offset: Move along the apex-to-midpoint centerline
//
// MULTI-MATERIAL PRINTING:
// 1. Slice the numbered STL normally
// 2. At layer with recess (approx 0.6mm from top), insert material change
// 3. Printer will pause, swap filament for number color
// 4. Resume printing - number will be filled with new color
// 5. At end of recess depth, change back to original material
//
// =====================================================

View File

@@ -0,0 +1,24 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Directory listing for /Printing/Key caps/</title>
</head>
<body>
<h1>Directory listing for /Printing/Key caps/</h1>
<hr>
<ul>
<li><a href=".claude/">.claude/</a></li>
<li><a href="Body5%281%29_scaled.scad">Body5(1)_scaled.scad</a></li>
<li><a href="Body5_scaled.scad">Body5_scaled.scad</a></li>
<li><a href="Body5_scaled_FIXED.scad">Body5_scaled_FIXED.scad</a></li>
<li><a href="Body5_test_circle.scad">Body5_test_circle.scad</a></li>
<li><a href="CLAUDE.md">CLAUDE.md</a></li>
<li><a href="generate_all_numbered_keycaps.bat">generate_all_numbered_keycaps.bat</a></li>
<li><a href="generate_numbered_keycaps.scad">generate_numbered_keycaps.scad</a></li>
<li><a href="README_scaling.md">README_scaling.md</a></li>
<li><a href="test_cylinder.scad">test_cylinder.scad</a></li>
</ul>
<hr>
</body>
</html>

View File

@@ -0,0 +1,3 @@
// Simple cylinder test
$fn = 128;
cylinder(h = 10, d = 5.0, center = true);

View File

@@ -0,0 +1 @@
C:/Users/Fred/claude-shared/commands

View File

@@ -0,0 +1,16 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Directory listing for /Printing/Nameplates/.claude/</title>
</head>
<body>
<h1>Directory listing for /Printing/Nameplates/.claude/</h1>
<hr>
<ul>
<li><a href="commands">commands</a></li>
<li><a href="settings.local.json">settings.local.json</a></li>
</ul>
<hr>
</body>
</html>

View File

@@ -0,0 +1,36 @@
{
"permissions": {
"allow": [
"Bash(python:*)",
"Bash(dir:*)",
"Bash(\"C:\\Program Files\\OpenSCAD\\openscad.exe\" -D \"name=\"\"Test\"\"\" -o test_output.stl nameplate_template.scad)",
"Bash(\"C:\\Program Files\\OpenSCAD\\openscad.exe\" -o checkmark_test.stl checkmark_template.scad)",
"Bash(\"C:\\Program Files\\OpenSCAD\\openscad.exe\" -D \"name=\"\"Test\"\"\" -o test_hole_position.stl zipper_pull_template.scad)",
"Bash(\"C:\\Program Files\\OpenSCAD\\openscad.exe\" -D \"name=\"\"Christopher\"\"\" -o test_christopher.stl zipper_pull_template.scad)",
"Bash(\"C:\\Program Files\\OpenSCAD\\openscad.exe\" -D \"name=\"\"Christopher\"\"\" -o test_christopher_13pt.stl zipper_pull_template.scad)",
"Bash(\"C:\\Program Files\\OpenSCAD\\openscad.exe\" -D \"name=\"\"Fred\"\"\" -o test_fred_bold.stl zipper_pull_template.scad)",
"Bash(\"C:\\Program Files\\OpenSCAD\\openscad.exe\" -D \"name=\"\"Zoe\"\"\" -o test_zoe.stl zipper_pull_template.scad)",
"Bash(\"C:\\Program Files\\OpenSCAD\\openscad.exe\" -D \"name=\"\"Fred\"\"\" -o test_fred_dynamic.stl zipper_pull_template.scad)",
"Bash(\"C:\\Program Files\\OpenSCAD\\openscad.exe\" -D \"name=\"\"Christopher\"\"\" -o test_christopher_dynamic.stl zipper_pull_template.scad)",
"Bash(timeout:*)",
"Bash(\"C:\\Program Files\\OpenSCAD\\openscad.exe\" -D \"name=\"\"Zoe\"\"\" -o test_zoe_raised.stl zipper_pull_raised_text_template.scad)",
"Bash(\"C:\\Program Files\\OpenSCAD\\openscad.exe\" -D \"name=\"\"Fred\"\"\" -o test_fred_raised.stl zipper_pull_raised_text_template.scad)",
"Bash(\"C:\\Program Files\\OpenSCAD\\openscad.exe\" -D \"name=\"\"Christopher\"\"\" -o test_christopher_raised.stl zipper_pull_raised_text_template.scad)",
"Bash(\"C:\\Program Files\\OpenSCAD\\openscad.exe\" -D \"name=\"\"Fred\"\"\" -D \"base_text_size=13\" -o test_fred_debug.stl zipper_pull_raised_text_template.scad)",
"Bash(\"C:\\Program Files\\OpenSCAD\\openscad.exe\" -o test_simple_fred.stl test_simple_fred.scad)",
"Bash(\"C:\\Program Files\\OpenSCAD\\openscad.exe\" -D \"name=\"\"Fred\"\"\" -o test_fred_reduced_bold.stl zipper_pull_raised_text_template_REDUCED_BOLD.scad)",
"Bash(\"C:\\Program Files\\OpenSCAD\\openscad.exe\" -D \"name=\"\"Bennet\"\"\" -o test_bennet_facedown.stl zipper_pull_raised_text_template.scad)",
"Bash(\"C:\\Program Files\\OpenSCAD\\openscad.exe\" -D \"name=\"\"Bennett\"\"\" -o test_bennett_facedown.stl zipper_pull_raised_text_template.scad)",
"Bash(\"C:\\Program Files\\OpenSCAD\\openscad.exe\" -D \"name=\"\"Bennett\"\"\" -o test_bennett_facedown.3mf zipper_pull_raised_text_template.scad)",
"Bash(\"C:\\Program Files\\OpenSCAD\\openscad.exe\" -o test_font.stl test_font.scad)",
"Bash(\"C:\\Program Files\\OpenSCAD\\openscad.exe\" -o test_debug.3mf test_debug.scad)",
"Bash(\"C:\\Program Files\\OpenSCAD\\openscad.exe\" -o test_blue_only.3mf test_blue_only.scad)"
],
"deny": [],
"ask": []
},
"enableAllProjectMcpServers": true,
"enabledMcpjsonServers": [
"n8n-mcp"
]
}

View File

@@ -0,0 +1,278 @@
# AI Coding Instructions for Nameplates Generator
## Project Overview
A batch 3D nameplate generator using **Python + OpenSCAD**. Reads names from text file, generates parametric two-layer oval nameplates with engraved text, outputs STL files for 3D printing.
**Design**: White base (1.5mm) + blue top layer (1mm) with text engraved through blue layer to expose white underneath.
## Critical Dependencies
### OpenSCAD Installation
- **Path**: `C:\Program Files\OpenSCAD\openscad.exe` (hardcoded in line 36 of `generate_nameplates.py`)
- Download: https://openscad.org/downloads.html
- **Must update path** if installed elsewhere
### Font Requirements (CRITICAL)
- **Font**: Fordscript cursive font (`Fordscript.ttf` in project root)
- **Installation**: Must be installed in Windows (right-click → Install for all users)
- **Why**: OpenSCAD references fonts by name, not file path
- **Reference in SCAD**: `font="Fordscript"` (line 44 of `nameplate_template.scad`)
- **Path hardcoded**: `font_file = "C:/Users/Fred/claude/Fordscript.ttf"` in SCAD (line 10) - informational only, not used by OpenSCAD
## Architecture & Data Flow
```
names.txt → generate_nameplates.py → OpenSCAD CLI → output_stl/*.stl
nameplate_template.scad
```
### Pipeline
1. **Input**: `names.txt` (one name per line, UTF-8 encoded)
2. **Processing**: Python script iterates, calling OpenSCAD for each name
3. **OpenSCAD**: Renders 3D model from parametric template
4. **Output**: Individual STL files in `output_stl/` directory
### Key Files
- `generate_nameplates.py` - Batch processor, subprocess orchestration
- `nameplate_template.scad` - Parametric OpenSCAD template
- `names.txt` - Input data (plain text, one name per line)
- `output_stl/` - Generated STL files (created automatically)
- `Fordscript.ttf` - Custom font (must be installed)
## Critical Developer Workflows
### Generate All Nameplates
```bash
python generate_nameplates.py
```
### Test Single Nameplate (Manual)
```powershell
# PowerShell syntax
& "C:\Program Files\OpenSCAD\openscad.exe" -D 'name="TestName"' -o test_output.stl nameplate_template.scad
# Or from CMD
"C:\Program Files\OpenSCAD\openscad.exe" -D "name=\"TestName\"" -o test_output.stl nameplate_template.scad
```
### Preview in OpenSCAD GUI
```powershell
# Open template in GUI for visual editing
& "C:\Program Files\OpenSCAD\openscad.exe" nameplate_template.scad
```
Then manually edit `name` parameter at top of file to preview different text.
### Validate Font Installation
```powershell
# Check if font is installed
Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" | Select-String "Fordscript"
# Or check fonts directory
ls $env:WINDIR\Fonts\Fordscript*
```
## Project-Specific Conventions
### Name Sanitization
Python script converts names to safe filenames:
- Removes special characters (keeps alphanumeric, spaces, hyphens, underscores)
- Replaces spaces with underscores
- Example: `"John Doe"``John_Doe.stl`
### OpenSCAD Command Pattern
```python
cmd = [
r'C:\Program Files\OpenSCAD\openscad.exe',
'-D', f'name="{escaped_name}"', # Set variable
'-o', output_file, # Output file
template_file # Input SCAD file
]
```
**Key flags**:
- `-D name="value"` - Override parameter in SCAD file
- `-o file.stl` - Output file path
- No flag for input file (positional argument)
### SCAD Template Structure
```scad
// Parameters (overridable via -D flag)
name = "NAME";
oval_width = 100;
oval_height = 38;
// ... more parameters
// Modules (functions)
module oval(width, height, depth) { ... }
module base_layer() { ... }
module top_layer() { ... }
module nameplate() { ... }
// Execution (builds the model)
nameplate();
```
## Common Pitfalls
### Font Not Found Error
**Symptom**: OpenSCAD renders but text is missing or uses default font.
**Causes**:
1. Font not installed in Windows (right-click TTF → Install)
2. Font name mismatch (must be exactly "Fordscript" in SCAD)
3. OpenSCAD needs restart after font installation
**Fix**: Install font, restart OpenSCAD, verify with manual test.
### Special Characters in Names
**Symptom**: Subprocess error or malformed STL.
**Cause**: Shell escaping issues with quotes, backslashes, etc.
**Current handling**: `escaped_name = name.replace('"', '\\"')` (line 33)
**Extension**: Add more sanitization if needed:
```python
# Escape more shell-sensitive characters
escaped_name = name.replace('"', '\\"').replace('\\', '\\\\')
```
### OpenSCAD Path Issues
**Symptom**: `FileNotFoundError: OpenSCAD not found`.
**Fix**: Update hardcoded path in `generate_nameplates.py:36`:
```python
cmd = [
r'C:\Program Files\OpenSCAD\openscad.exe', # ← Update this
# ...
]
```
**Better approach**: Use environment variable or config file:
```python
OPENSCAD_PATH = os.getenv('OPENSCAD_PATH', r'C:\Program Files\OpenSCAD\openscad.exe')
```
## Modifying Dimensions
All dimensions in `nameplate_template.scad` (lines 6-12):
| Parameter | Current Value | Purpose |
|-----------|---------------|---------|
| `oval_width` | 100mm | Overall width |
| `oval_height` | 38mm | Overall height (proportional) |
| `base_thickness` | 1.5mm | White layer thickness |
| `top_thickness` | 1mm | Blue layer thickness |
| `base_offset` | 2mm | White extension beyond blue |
| `text_size` | 15mm | Font size |
| `text_depth` | 1mm | Engraving depth (full blue layer) |
### Maintaining Proportions
Height typically 38% of width for aesthetic oval shape:
```scad
oval_height = oval_width * 0.38;
```
### Text Depth Critical
`text_depth` must equal `top_thickness` to engrave through entire blue layer:
```scad
text_depth = 1; // Must match top_thickness for full engraving
```
If `text_depth < top_thickness`, text won't reach white base. If `text_depth > top_thickness`, text cuts into white base (undesirable).
## Extending the Template
### Add New Shape Option
```scad
// Add rectangle module
module rectangle(width, height, depth) {
cube([width, height, depth], center=true);
}
// Add shape parameter at top
shape = "oval"; // or "rectangle"
// Use conditional in base_layer
module base_layer() {
if (shape == "rectangle")
rectangle(oval_width + base_offset*2, oval_height + base_offset*2, base_thickness);
else
oval(oval_width + base_offset*2, oval_height + base_offset*2, base_thickness);
}
```
### Add Logo/Icon
```scad
// Import SVG or PNG
module logo() {
translate([0, -oval_height/3, base_thickness + top_thickness - text_depth])
linear_extrude(height=text_depth)
import("logo.svg", center=true);
}
// Add to top_layer difference
difference() {
// ... existing blue oval
logo(); // Cut logo
}
```
### Batch Different Sizes
Modify Python script to pass multiple parameters:
```python
cmd = [
r'C:\Program Files\OpenSCAD\openscad.exe',
'-D', f'name="{escaped_name}"',
'-D', f'oval_width={width}', # Variable width
'-D', f'text_size={text_size}', # Variable text size
'-o', output_file,
template_file
]
```
## Troubleshooting Commands
```powershell
# Check OpenSCAD version
& "C:\Program Files\OpenSCAD\openscad.exe" --version
# Test SCAD syntax (no output, just validation)
& "C:\Program Files\OpenSCAD\openscad.exe" --check nameplate_template.scad
# Render with verbose output
& "C:\Program Files\OpenSCAD\openscad.exe" -D 'name="Test"' -o test.stl nameplate_template.scad --verbose
# List available fonts in OpenSCAD (run in OpenSCAD console)
# Help → Font List
```
## 3D Printing Notes
- **Material**: Two colors required (white + blue recommended)
- **Layer Height**: 0.1-0.2mm for clean text
- **Infill**: 20-30% sufficient for thin nameplates
- **Supports**: Not needed (flat design)
- **Orientation**: Print flat (as modeled)
- **Bed Adhesion**: Brim or raft recommended for thin parts
## Quick Reference
### Add New Name
1. Edit `names.txt`, add line with new name
2. Run `python generate_nameplates.py`
3. Find STL in `output_stl/` directory
### Change All Dimensions
1. Edit parameters at top of `nameplate_template.scad`
2. Run `python generate_nameplates.py` to regenerate all
3. Or manually test with single name first
### Install Font on New Machine
1. Copy `Fordscript.ttf` to new machine
2. Right-click → "Install for all users"
3. Restart OpenSCAD if running
4. Test with manual OpenSCAD command

15
Printing/Nameplates/.github/index.html vendored Normal file
View File

@@ -0,0 +1,15 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Directory listing for /Printing/Nameplates/.github/</title>
</head>
<body>
<h1>Directory listing for /Printing/Nameplates/.github/</h1>
<hr>
<ul>
<li><a href="copilot-instructions.md">copilot-instructions.md</a></li>
</ul>
<hr>
</body>
</html>

Binary file not shown.

View File

@@ -0,0 +1,68 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a 3D nameplate generator that creates two-layer oval nameplates with engraved text using OpenSCAD. The system batch-processes names from a text file and outputs STL files ready for 3D printing.
## Architecture
**Pipeline:**
1. Python script (`generate_nameplates.py`) reads names from `names.txt`
2. For each name, it invokes OpenSCAD CLI with the parametric template
3. OpenSCAD renders the 3D model using `nameplate_template.scad`
4. STL files are output to `output_stl/` directory
**Design Structure:**
- Two-layer design: white base (1.5mm) + blue top layer (1mm)
- Text is engraved completely through the blue layer to expose white underneath
- Oval shape created by scaling a cylinder
- All dimensions parametric for easy customization
## Key Commands
**Generate nameplates:**
```bash
python generate_nameplates.py
```
**Test single nameplate (manual):**
```bash
"C:\Program Files\OpenSCAD\openscad.exe" -D "name=\"TestName\"" -o output.stl nameplate_template.scad
```
## Critical Configuration
**OpenSCAD Path:**
The script uses hardcoded path: `C:\Program Files\OpenSCAD\openscad.exe`
Update in `generate_nameplates.py:36` if OpenSCAD is installed elsewhere.
**Font Requirements:**
- Uses Fordscript font (Fordscript.ttf in project root)
- Font MUST be installed in Windows (right-click → Install) for OpenSCAD to access it
- OpenSCAD references fonts by name, not file path
- Referenced in template as `font="Fordscript"` (line 44)
**Dimensions:**
Default nameplate size: 3" tall × 8" wide (76mm × 200mm)
All dimensions in `nameplate_template.scad:6-12`
## File Purposes
- `nameplate_template.scad` - OpenSCAD parametric template defining the 3D geometry
- `generate_nameplates.py` - Batch processor that calls OpenSCAD for each name
- `names.txt` - Input file with one name per line
- `Fordscript.ttf` - Custom cursive font (must be installed in Windows)
- `output_stl/` - Generated STL files (one per name)
## Modifying Dimensions
Key parameters in `nameplate_template.scad`:
- `oval_height` - Overall height (currently 76mm = 3 inches)
- `oval_width` - Overall width (currently 200mm ≈ 8 inches)
- `base_thickness` - White layer thickness (1.5mm)
- `top_thickness` - Blue layer thickness (1mm)
- `text_depth` - Engraving depth (1mm - cuts through entire blue layer)
- `text_size` - Font size (30mm)
- `base_offset` - How much white extends beyond blue (2mm)

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 720 570.9" style="enable-background:new 0 0 720 570.9;" xml:space="preserve">
<style type="text/css">
.st0{fill:#7E4E8C;}
.st1{fill:#9C6BAD;}
</style>
<g id="XMLID_3_">
<path id="XMLID_188_" d="M717.3,120.1l-1.3-2.8l-0.3-0.8l-0.3-0.5l-0.5-1l-0.5-1l-0.3-0.5v-0.3c-0.5-1-0.3-0.5-0.5-0.8l-1.6-2.3
c-1-1.6-2.3-2.8-3.4-4.4c-1.3-1.3-2.6-2.6-3.9-3.9c-11.1-9.8-27.4-13.2-41.4-8.8c-1.8,0.5-3.9,1.3-5.7,2.1l-1.8,1l-1,0.5l-0.8,0.5
l-0.5,0.3l-7.2,4.4l-14.8,8.5l-29.2,17.1L582.3,139c0-11.9,0-24.1,0.3-36.2c0-7.2,0-14.2,0-21.5c0-3.6,0-7.2,0-10.9
c0-2.1-0.3-4.1-0.3-6.2s-0.5-4.1-0.8-6.2c-2.8-16.8-12.2-32.6-26.1-43.2c-7-5.4-14.8-9.3-23.3-11.9c-2.1-0.5-4.1-1.3-6.5-1.6
c-2.3-0.3-4.9-0.8-7-1L512.9,0h-6.2c-15.3,0-30.5,0.3-46.1,0.3c-62.1,0.3-126.3,1.3-191.5,1.8c-65.2,0.8-131.5,1-197.7,1
c-18.6-0.3-37,8-49.4,21.5c-6.2,6.7-11.1,14.8-14.2,23.5c-1.6,4.4-2.6,8.8-3.4,13.7c0,0.5-0.3,1.3-0.3,1.8v1.6l-0.3,3.1v0.8v1v4.7
c0,33.1-0.5,65.7-0.8,98.6C2.3,238.6,1.8,302.8,1,364.9c-0.3,31.1-0.5,61.6-0.8,91.6C0,471.5,0,486.2,0,501
c0,8.5,1.8,17.3,4.9,25.4c3.1,8,7.8,15.3,13.5,21.5c5.7,6.2,12.4,11.4,19.7,15.3l5.7,2.6c2.1,0.8,3.9,1.3,6,2.1
c3.9,1,7.8,2.1,12.4,2.6c1,0.3,2.3,0.3,3.1,0.5h2.6h2.6h0.8c1.3,0,0.8,0,1.3,0h1.3c3.4,0,6.7,0,10.4,0c6.7,0,13.7,0,20.2-0.3
c26.9-0.3,52.8-0.5,77.6-0.8c49.7-0.5,95.2-1,135.6-1.6c40.4-0.5,75.6-0.8,104.5-0.8c58-0.3,91.1-0.5,91.1-0.5s2.3,0,6.7-0.3
c4.4-0.8,11.1-1.6,19.1-4.9s17.3-9.3,25.4-18.9c3.9-4.9,7.8-10.6,10.6-17.1c0.5-1.8,1.3-3.4,1.8-5.2c0.5-1.8,1-3.6,1.6-5.4
c0.5-1.8,0.8-3.9,1-6c0.3-1,0.3-2.1,0.5-3.1c0-0.8,0-1.8,0-2.6s0-1.8,0.3-2.8v-1v-2.1c0-1.8,0-3.6,0-5.7c0-3.9,0-7.8,0-11.6
c0-8,0-16.3,0-25.1c0-17.6,0.3-36.5,0.3-56.7c0.3-40.4,0.8-85.9,1-135.6c0-7.5,0.3-15.3,0.3-23l24.6-14.2l58.7-34.2l29.2-17.1
l3.6-2.1c1.3-0.8,3.1-2.1,4.7-3.1c2.8-2.3,5.7-4.9,8-7.8c4.7-5.7,7.8-12.9,9.1-20.2C720.4,134.6,719.9,127.1,717.3,120.1z
M368.2,265.5l-111.5,66c-8.5-11.1-17.1-22.3-25.6-33.1l-29.5-38.3l-7.5-9.3c-0.8-0.8-1-1.6-2.1-2.6c-0.8-1-1.6-2.1-2.6-2.8
l-2.8-2.6c-1-0.8-2.1-1.6-3.1-2.3c-8.5-6-19.1-8.8-29.2-7.5c-5.2,0.5-10.4,2.1-15,4.4c-1.3,0.8-2.6,1.3-3.9,2.1
c-0.8,0.5-1,0.8-1.3,1l-1.3,0.8l-0.5,0.5l-0.5,0.5l-0.5,0.5l-1.6,1.3c-1,0.8-1.8,1.8-2.8,2.8c-7.2,7.8-11.4,17.9-11.6,28.5
c0,5.2,0.8,10.6,2.6,15.5c0.3,1.3,1,2.6,1.6,3.6l0.8,1.8l1,2.1l1,1.8c0.5,0.8,0.5,0.8,1,1.3l0.8,1.3l0.3,0.3v0.3l0.5,0.5l0.5,0.5
l15,18.9l30,37.5l30.3,37.5l7.5,9.3l3.9,4.7c1.6,2.1,3.4,3.9,5.4,5.7c4.1,3.6,8.8,6.5,13.7,8.3c2.6,1,5.2,1.6,7.8,2.1
c1.3,0.3,2.6,0.5,4.1,0.5h1c0.3,0,0.8,0,1,0h1.6h0.3c7.5,0,15.3-2.1,21.7-5.7l14.8-8.5l29.2-17.1l58.7-34.2L488.6,295l7.5-4.4
L494,482.9c-5.2,0-10.4,0-15.5,0l-26.7,0.3l-53.6,0.5l-106.9,1L84.6,487c0.5-32.3,0.8-64.7,1.3-97l0.8-104.3l0.5-52v-52.3l0.3-94.2
h43.5l53.6-0.3l106.9-0.3l106.9-1l53.6-0.8c15.5-0.3,30.8-0.5,46.3-0.8l-1,105.1l-12.4,7.2L368.2,265.5z"/>
<path id="XMLID_28_" class="st0" d="M565.4,498.4c0-1.8,0-3.6,0-5.7c0-3.9,0-7.8,0-11.6c0-8,0-16.3,0-25.1
c0-17.6-0.3-36.5-0.5-56.7c-0.3-40.4-0.8-85.9-1-135.6c0-4.1,0-8.3,0-12.4l-48.7,28.5l2.3,214.3c0,6.2-4.9,11.4-11.1,11.4l0,0h-0.3
c-8.8,0-17.9,0-26.7,0l-26.7-0.3l-53.6-0.5L78.4,501c-3.9,0-7-3.1-7-7l0,0c-0.5-34.7-1-69.6-1.3-104.3l-0.8-104.3l-0.3-52v-52
L68.8,77.1c0-5.4,4.4-9.8,9.8-10.1l0,0h53.6l53.6,0.3l106.9,0.3l106.9,1l53.6,0.8c17.9,0.3,35.7,0.5,53.6,0.8l0,0c3.9,0,7,3.1,7,7
l1,102.5l49.2-28.7c0-15.5,0-31.6-0.3-47.6c0-7.2,0-14.2,0-21.5c0-3.6,0-7.2,0-10.9c0-1.8-0.3-3.1-0.3-4.7c0-1.6-0.5-3.1-0.5-4.4
c-2.1-11.9-8.8-23.3-18.9-30.8c-4.9-3.9-10.6-6.7-16.6-8.5c-1.6-0.3-3.1-0.8-4.4-1c-1.6-0.3-2.6-0.5-4.4-0.8l-5.7-0.3h-5.2
c-15.3,0-30.5-0.3-46.1-0.3c-62.1-0.3-126.3-1.3-191.5-1.8c-65.2-0.8-131.5-1-197.7-1c-14.8,0-29,6.5-38.8,17.3
c-4.9,5.4-8.8,11.6-11.1,18.6c-1.3,3.4-2.1,7-2.3,10.4c0,0.5-0.3,0.8-0.3,1.3v1.6l-0.3,3.9c0,0.3,0,0.3,0,0.3l0,0v1.6v3.1
c0,33.1,0.3,66,0.8,98.6c0.8,65.2,1.3,129.4,2.1,191.5c0.3,31.1,0.5,61.6,0.8,91.6c0.3,15,0.3,29.8,0.5,44.3
c0,11.9,4.7,23.3,12.4,32.1c3.9,4.4,8.3,8,13.5,10.9l3.9,1.8l3.9,1.6c2.6,0.8,5.4,1.6,8,1.8c0.5,0,1.3,0.3,1.8,0.3l2.6,0.3l2.6,0.3
H73h0.3h1.3c3.4,0,6.7,0,10.4,0c6.7,0,13.5,0.3,20.2,0.3c26.9,0.3,52.8,0.5,77.6,0.8c49.7,0.5,95.2,1,135.6,1.6
c40.4,0.5,75.6,0.8,104.5,0.8c58,0.3,91.1,0.3,91.1,0.3s1.8,0,5.4-0.3c3.4-0.5,8.8-1,15-3.9c6.2-2.6,13.7-7.2,20.2-14.8
c3.1-3.9,6-8.3,8.3-13.7c0.5-1.3,1-2.6,1.6-4.1c0.5-1.3,0.8-2.8,1.3-4.4c0.5-1.6,0.5-2.8,0.8-4.4c0-0.8,0.3-1.3,0.3-2.1
s0-1.8,0-2.6s0-1.8,0-2.8v-0.3l0,0v-0.8L565.4,498.4z"/>
<path id="XMLID_29_" class="st1" d="M691.5,117.5c-5.7-5.2-14.5-7.2-22-4.9c-0.5,0-0.8,0.3-1.3,0.5c-0.3,0.3-0.8,0.3-1,0.3l-1.8,1
l-1,0.5l0,0l0,0l-0.5,0.3l-7.2,4.1l-14.8,8.5l-29.5,16.8l-117.7,67.5l-118,67.3l-118,67.3c-3.1,1.8-7,1-9.1-1.6l0,0l0,0
c-10.4-12.2-20.7-24.3-31.1-36.7l-31.1-36.7l-7.8-9.3c-0.5-0.8-1.3-1.6-1.8-2.1l-1.6-1.6l-1.6-1.3c-0.5-0.5-1.3-0.8-1.8-1.3
c-4.7-3.1-10.6-4.4-16.3-3.9c-2.8,0.3-5.4,1.3-8,2.6c-0.5,0.3-1,0.5-1.6,0.8c-0.3,0-0.8,0.5-1,0.8l-1.3,0.8l-0.3,0.3l0,0l0,0
l-0.3,0.3l-0.8,0.8c-0.5,0.5-1,1-1.6,1.6c-3.6,4.1-5.7,9.6-5.7,15.3c0,2.8,0.5,5.4,1.6,8c0.3,0.8,0.5,1.3,0.8,1.8l0.5,1l0.5,0.8
l0.3,0.8c0,0.3,0.5,0.8,0.8,1l0.8,1.3l0.3,0.3l0.5,0.5l15.3,18.6l30.5,37.3l30.3,37.3l7.5,9.3l3.9,4.7c1,1,1.8,2.1,2.8,2.8
c2.1,1.8,4.4,3.1,6.7,3.9c1.3,0.5,2.6,0.8,3.9,1c0.5,0,1.3,0.3,1.8,0.3h0.3c0,0,0.3,0,0.5,0h1.3c3.6,0,7.5-1,10.6-2.8l14.5-8.3
l29.5-17.1l58.7-33.9l117.5-68.1l117.5-68.1l58.7-33.9l29.5-16.8l3.6-2.1c1-0.8,1.8-1,2.6-1.6c1.6-1.3,3.1-2.6,4.1-3.9
c2.6-2.8,4.1-6.7,4.9-10.6c0.5-3.9,0.3-8-1-11.4C696.6,123.4,694.3,120.1,691.5,117.5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 720 570.7" style="enable-background:new 0 0 720 570.7;" xml:space="preserve">
<path id="XMLID_42_" d="M717.1,120l-1.3-2.8l-0.3-0.8l-0.3-0.5l-0.5-1l-0.5-1l-0.3-0.5V113c-0.5-1-0.3-0.5-0.5-0.8l-1.6-2.3
c-1-1.6-2.3-2.8-3.4-4.4c-1.3-1.3-2.6-2.6-3.9-3.9c-11.1-9.8-27.4-13.2-41.4-8.8c-1.8,0.5-3.9,1.3-5.7,2.1l-1.8,1l-1,0.5l-1,0.5
l-0.5,0.3l-7.2,4.4l-14.7,8.5L602,127.3l-20.2,11.6c0-11.9,0-24.1,0.3-36.2c0-7.2,0-14.2,0-21.5c0-3.6,0-7.2,0-10.9
c0-1.8-0.3-4.1-0.3-6.2c0-2.1-0.5-4.1-0.8-6.2c-2.8-16.8-12.2-32.6-26.1-43.2c-7-5.4-14.7-9.3-23.3-11.9c-2.1-0.5-4.1-1-6.5-1.6
c-2.3-0.3-4.9-0.8-7-1L512.5,0h-6.2c-15.3,0-30.5,0.3-46,0.3C398.1,0.5,334,1.6,268.8,2.1c-65.2,0.8-131.4,1-197.6,1
c-18.6-0.3-37,8-49.4,21.5c-6.2,6.7-11.1,14.7-14.2,23.5c-1.6,4.4-2.6,8.8-3.1,13.7c0,0.5-0.3,1.3-0.3,1.8v1.6l-0.3,3.1v0.8v1v1.6
v3.1c0,33.1-0.3,66-0.8,98.6C2.3,238.5,1.8,302.7,1,364.8c-0.3,31-0.5,61.6-0.8,91.6c0,15-0.3,29.7-0.3,44.5
c0,8.8,1.8,17.3,4.9,25.4c3.1,8,7.8,15.3,13.5,21.5c5.7,6.2,12.4,11.4,19.7,15.3l5.7,2.6c1.8,0.8,3.9,1.3,5.9,2.1
c3.9,1,7.8,2.1,12.4,2.6c1,0.3,2.3,0.3,3.1,0.5h2.6h2.6h0.5c1.3,0,0.8,0,1.3,0h1.3c3.4,0,6.7,0,10.3,0c6.7,0,13.5-0.3,20.2-0.3
c26.9-0.3,52.8-0.5,77.6-0.8c49.7-0.5,95.2-1,135.6-1.6c40.4-0.5,75.5-0.8,104.5-0.8c57.9-0.3,91.1-0.3,91.1-0.3s2.3,0,6.7-0.3
c4.4-0.5,11.1-1.6,19.1-4.9s17.3-9.3,25.6-18.6c3.9-4.9,7.8-10.6,10.6-17.1c0.5-1.8,1.3-3.4,1.8-5.2c0.5-1.8,1-3.6,1.6-5.4
c0.5-1.8,0.8-3.9,1-5.9c0.3-1,0.3-2.1,0.5-3.1c0-0.8,0-1.8,0.3-2.6c0-0.8,0-1.8,0.3-2.8v-1v-0.8V498c0-1.8,0-3.6,0-5.7
c0-3.9,0-7.8,0-11.6c0-8,0-16.3,0-25.1c0-17.6,0.3-36.5,0.5-56.7c0.3-40.4,0.8-85.9,1-135.6c0-7.5,0-15.3,0.3-23l24.6-14.2
l58.7-34.1l29.2-17.1l3.6-2.1c1.3-0.8,3.1-1.8,4.7-3.1c2.8-2.3,5.7-4.9,8-7.8c4.7-5.7,7.8-12.9,9.1-20.2
C720.2,134.5,719.7,127,717.1,120z M563.7,263.6c0.5,49.7,0.8,95.2,1,135.6c0.3,20.2,0.3,39.1,0.5,56.7c0,8.8,0,17.1,0,25.1
c0,3.9,0,8,0,11.6c0,1.8,0,3.9,0,5.7v1.3v0.8l0,0v0.3c0,1,0,1.8,0,2.8c0,0.8,0,1.8,0,2.6c0,0.8-0.3,1.6-0.3,2.1
c-0.3,1.6-0.3,2.8-0.8,4.4c-0.5,1.6-0.8,2.8-1.3,4.4c-0.5,1.3-1,2.8-1.6,4.1c-2.3,5.2-5.2,9.8-8.3,13.7
c-6.5,7.5-13.7,12.2-20.2,14.7c-6.2,2.6-11.6,3.4-15,3.9c-3.6,0.3-5.4,0.3-5.4,0.3s-33.1-0.3-91.1-0.3c-29,0-64.2-0.3-104.5-0.8
c-40.4-0.5-85.9-1-135.6-1.6c-24.8-0.3-50.7-0.5-77.6-0.8c-6.7,0-13.5-0.3-20.2-0.3c-3.4,0-6.7,0-10.3,0h-1.3c0,0,0,0-0.3,0h-0.5
l-2.6-0.3l-2.6-0.3c-0.8,0-1.3-0.3-1.8-0.3c-2.3-0.3-5.2-1-8-1.8l-3.9-1.6l-3.9-1.8c-4.9-2.8-9.6-6.5-13.5-10.9
c-7.5-8.8-12.2-20.2-12.4-32.1c-0.3-14.5-0.3-29.2-0.5-44.2c-0.3-30-0.5-60.5-0.8-91.6c-0.8-62.1-1.3-126.2-2.1-191.4
c-0.3-32.6-0.5-65.4-0.8-98.6v-3.1v-1.6l0,0c0,0,0,0,0-0.3l0.3-3.9v-1.6c0-0.5,0.3-0.8,0.3-1.3c0.5-3.1,1.3-6.7,2.3-10.3
c2.3-6.7,6.2-13.2,11.1-18.6c9.8-10.9,24.1-17.3,38.8-17.3c66.2,0,132.5,0.3,197.6,1c65.2,0.5,129.3,1.3,191.4,1.8
c15.5,0,31,0.3,46,0.3h5.2l5.7,0.3c1.6,0.3,2.8,0.5,4.4,0.8c1.6,0.3,3.1,0.8,4.4,1c5.9,1.8,11.6,4.7,16.6,8.5
c9.8,7.5,16.6,18.9,18.9,30.8c0.3,1.6,0.5,3.1,0.5,4.4c0,1.6,0.3,2.8,0.3,4.7c0,3.6,0,7.2,0,10.9c0,7.2,0,14.5,0,21.5
c0,16,0,32.1,0.3,47.6l-49.2,28.7l-1-102.4c0-3.9-3.1-7-7-7l0,0c-17.8-0.3-35.7-0.5-53.5-0.8l-53.5-0.8l-106.8-1l-106.8-0.3
L130.9,67H77.3l0,0c-5.4,0-9.8,4.4-9.8,10.1l0.3,104.3v52l0.3,52l0.8,104.3c0.5,34.7,0.8,69.6,1.3,104.3l0,0c0,3.9,3.1,7,7,7
l320.8,3.6l53.5,0.5l26.6,0.3c8.8,0,17.8,0,26.6,0h0.3l0,0c6.2,0,11.1-5.2,11.1-11.4l-2.3-214.2l48.6-28.5
C563.4,255.3,563.4,259.5,563.7,263.6z M256.6,331.1c-8.5-11.1-17.1-22.2-25.6-33.1l-29.5-38l-7.5-9.3c-0.8-0.8-1-1.6-2.1-2.3
c-0.8-1-1.6-1.8-2.6-2.8l-2.8-2.6c-1-0.8-2.1-1.6-3.1-2.3c-8.5-5.9-19.1-8.8-29.2-7.5c-5.2,0.5-10.3,2.1-15,4.4
c-1.3,0.8-2.6,1.3-3.9,2.1c-0.8,0.5-1,0.5-1.3,1l-1.3,0.8l-0.5,0.5l-0.5,0.5l-0.3,0.3l-0.3,0.3l-1.6,1.3c-1,0.8-1.8,1.8-2.8,2.8
c-7.2,7.8-11.4,17.8-11.6,28.5c0,5.2,0.8,10.6,2.3,15.5c0.3,1.3,1,2.6,1.6,3.6l0.8,1.8l1,1.8l1,1.8c0.5,0.8,0.5,0.8,1,1.3l0.8,1.3
l0.3,0.3v0.3l0.5,0.5l0.5,0.5l15,18.9l30,37.5l30,37.5l7.5,9.3l3.9,4.7c1.6,1.8,3.4,3.9,5.4,5.7c4.1,3.6,8.8,6.5,13.7,8.3
c2.6,1,5.2,1.6,7.8,2.1c1.3,0.3,2.6,0.5,4.1,0.5h1c0.5,0,0.8,0,1,0h1.6h0.3c7.5,0,15.3-2.1,21.7-5.7l14.7-8.5l29.2-17.1l58.7-34.1
l117.4-68.3l7.5-4.4l-2.1,192.2c-5.2,0-10.3,0-15.5,0l-26.6,0.3l-53.5,0.5L291,484.8l-206.7,2.3c0.5-32.3,0.8-64.7,1.3-97l0.8-104.3
l0.3-52v-52l0.3-94.2h43.5l53.5-0.3l106.8-0.3l106.8-1l53.5-0.8c15.5-0.3,30.8-0.5,46.3-0.8l-1,105l-12.4,7.2l-116.9,69.1
L256.6,331.1z M698.7,138.7c-0.8,3.9-2.3,7.5-4.9,10.6c-1.3,1.6-2.6,2.8-4.1,3.9c-0.8,0.5-1.6,1-2.6,1.6l-3.6,2.1L654,173.6
l-58.7,33.9l-117.4,68.3l-117.4,68l-58.7,33.9l-29.5,17.1l-14.5,8.3c-3.4,1.8-7,2.8-10.6,2.8h-1.3c-0.3,0-0.5,0-0.5,0H245
c-0.5,0-1.3-0.3-1.8-0.3c-1.3-0.3-2.6-0.5-3.9-1c-2.6-0.8-4.7-2.3-6.7-3.9c-1-1-1.8-1.8-2.8-2.8l-3.9-4.7l-7.5-9.3l-30.3-37.3
l-30.8-37.5L142,290.5l-0.5-0.5l-0.3-0.3l-0.8-1.3c-0.3-0.3-0.8-0.8-0.8-1l-0.3-0.8l-0.5-0.8l-0.5-1c-0.3-0.5-0.5-1.3-0.8-1.8
c-1-2.6-1.6-5.2-1.6-8c0-5.4,2.1-11.1,5.7-15.3c0.5-0.5,1-1,1.6-1.6l0.8-0.8l0.3-0.3l0,0l0,0l0.3-0.3l1.3-0.8c0.3-0.3,0.8-0.8,1-0.8
c0.5-0.3,1-0.5,1.6-0.8c2.6-1.3,5.2-2.1,8-2.6c5.7-0.8,11.6,0.5,16.3,3.9c0.5,0.5,1.3,0.8,1.8,1.3l1.6,1.3l1.6,1.6
c0.5,0.5,1.3,1.6,1.8,2.1l7.8,9.3l31,36.7c10.3,12.2,20.7,24.6,31,36.7l0,0l0,0c2.1,2.6,5.9,3.4,9.1,1.6l118-67.3l118-67.3
l117.7-67.5l29.5-16.8l14.7-8.5l7.2-4.1l0.5-0.3l0,0l0,0l1-0.5l1.8-1c0.3,0,0.8-0.3,1-0.3c0.3-0.3,0.8-0.3,1.3-0.5
c7.5-2.3,16.3-0.5,22,4.9c2.8,2.6,5.2,5.9,6.5,9.8C699,130.6,699.2,134.8,698.7,138.7z"/>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Directory listing for /Printing/Nameplates/Check-Mark/</title>
</head>
<body>
<h1>Directory listing for /Printing/Nameplates/Check-Mark/</h1>
<hr>
<ul>
<li><a href="CF00089-08%20Check%20Mark%2001.dxf">CF00089-08 Check Mark 01.dxf</a></li>
<li><a href="CF00089-08%20Check%20Mark%2001.eps">CF00089-08 Check Mark 01.eps</a></li>
<li><a href="CF00089-08%20Check%20Mark%2001.png">CF00089-08 Check Mark 01.png</a></li>
<li><a href="CF00089-08%20Check%20Mark%2001.svg">CF00089-08 Check Mark 01.svg</a></li>
<li><a href="CF00089-08%20Check%20Mark%2002.dxf">CF00089-08 Check Mark 02.dxf</a></li>
<li><a href="CF00089-08%20Check%20Mark%2002.eps">CF00089-08 Check Mark 02.eps</a></li>
<li><a href="CF00089-08%20Check%20Mark%2002.png">CF00089-08 Check Mark 02.png</a></li>
<li><a href="CF00089-08%20Check%20Mark%2002.svg">CF00089-08 Check Mark 02.svg</a></li>
<li><a href="CF00089-08%20Check%20Mark%2003.dxf">CF00089-08 Check Mark 03.dxf</a></li>
<li><a href="CF00089-08%20Check%20Mark%2003.eps">CF00089-08 Check Mark 03.eps</a></li>
<li><a href="CF00089-08%20Check%20Mark%2003.png">CF00089-08 Check Mark 03.png</a></li>
<li><a href="CF00089-08%20Check%20Mark%2003.svg">CF00089-08 Check Mark 03.svg</a></li>
<li><a href="Note.txt">Note.txt</a></li>
</ul>
<hr>
</body>
</html>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

@@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""
Add M600 filament change command to all objects in an Orca Slicer 3MF file.
This script modifies a 3MF file (which is a ZIP archive containing XML) to add
a height range modifier at 1.5mm (end of blue base layer) with M600 command
for all zipper pull objects.
Usage:
python add_filament_change_to_3mf.py input.3mf [output.3mf]
If output.3mf is not specified, creates input_modified.3mf
"""
import sys
import zipfile
import xml.etree.ElementTree as ET
from pathlib import Path
import shutil
import tempfile
import os
def add_height_range_to_3mf(input_file, output_file=None, height_mm=1.5):
"""
Add height range modifier with M600 at specified height to all objects in 3MF.
Args:
input_file: Path to input 3MF file
output_file: Path to output 3MF file (optional)
height_mm: Height in mm where filament change occurs (default: 1.5)
"""
input_path = Path(input_file)
if not input_path.exists():
print(f"Error: Input file '{input_file}' not found!")
return False
# Determine output filename
if output_file is None:
output_path = input_path.parent / f"{input_path.stem}_modified.3mf"
else:
output_path = Path(output_file)
print(f"Processing: {input_path}")
print(f"Output will be: {output_path}")
print(f"Filament change height: {height_mm}mm")
print("-" * 50)
# Create a temporary directory to work in
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Extract the 3MF (it's a ZIP file)
print("Extracting 3MF file...")
with zipfile.ZipFile(input_path, 'r') as zip_ref:
zip_ref.extractall(temp_path)
# Find and modify the 3D model file (usually 3D/3dmodel.model)
model_file = temp_path / "3D" / "3dmodel.model"
if not model_file.exists():
print("Error: Could not find 3dmodel.model in 3MF file!")
return False
print(f"Modifying {model_file}...")
# Parse the XML
tree = ET.parse(model_file)
root = tree.getroot()
# Define namespaces (3MF uses namespaces)
namespaces = {
'': 'http://schemas.microsoft.com/3dmanufacturing/core/2015/02',
'p': 'http://schemas.microsoft.com/3dmanufacturing/production/2015/06',
's': 'http://schemas.orca-3d.com/3mf/2023/06'
}
# Register namespaces for output
for prefix, uri in namespaces.items():
if prefix:
ET.register_namespace(prefix, uri)
else:
ET.register_namespace('', uri)
# Find all objects (build items)
# In Orca Slicer 3MF, objects are in <build><item> tags
build_elem = root.find('.//build', namespaces)
if build_elem is None:
print("Warning: No <build> element found. Looking for items directly...")
items = root.findall('.//item', namespaces)
else:
items = build_elem.findall('.//item', namespaces)
if not items:
print("Error: No items found in 3MF file!")
return False
print(f"Found {len(items)} object(s) in the file")
# Count modified items
modified_count = 0
# Add height range modifier to each item
for idx, item in enumerate(items, 1):
object_id = item.get('objectid', f'unknown_{idx}')
print(f" Processing object {idx}/{len(items)} (ID: {object_id})...")
# Check if this item already has metadata for height range
# In Orca Slicer, this is typically stored as metadata
# We need to add the height range modifier metadata
# Note: The exact XML structure for height range modifiers in Orca Slicer
# may vary. This is a generic approach that adds metadata.
# You may need to adjust based on actual Orca Slicer 3MF structure.
# Create or find metadata container
metadata_group = item.find('metadatagroup', namespaces)
if metadata_group is None:
metadata_group = ET.SubElement(item, 'metadatagroup')
# Add height range modifier metadata
# Format: height range from 0 to height_mm = blue (original)
# height range from height_mm to top = white (with M600)
height_modifier = ET.SubElement(metadata_group, 'metadata')
height_modifier.set('name', 'height_range_modifier')
height_modifier.text = f'{{"ranges":[{{"min":0,"max":{height_mm},"color":"RoyalBlue"}},{{"min":{height_mm},"max":999,"color":"white","gcode":"M600"}}]}}'
modified_count += 1
print(f"\nModified {modified_count} object(s)")
# Save the modified XML back to the file
print("Saving modified model file...")
tree.write(model_file, encoding='utf-8', xml_declaration=True)
# Re-create the 3MF (ZIP) file with all contents
print("Creating new 3MF file...")
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zip_out:
# Walk through the temp directory and add all files
for root_dir, dirs, files in os.walk(temp_path):
for file in files:
file_path = Path(root_dir) / file
arcname = file_path.relative_to(temp_path)
zip_out.write(file_path, arcname)
print(f"\nSuccess! Modified 3MF saved to: {output_path}")
print(f"\nNext steps:")
print(f"1. Open {output_path.name} in Orca Slicer")
print(f"2. Verify the height range modifiers are present")
print(f"3. Slice and check for M600 commands in the G-code")
return True
def main():
if len(sys.argv) < 2:
print("Usage: python add_filament_change_to_3mf.py input.3mf [output.3mf]")
print("\nAdds M600 filament change at 1.5mm to all objects in a 3MF file.")
print("If output.3mf is not specified, creates input_modified.3mf")
sys.exit(1)
input_file = sys.argv[1]
output_file = sys.argv[2] if len(sys.argv) > 2 else None
success = add_height_range_to_3mf(input_file, output_file)
if not success:
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,179 @@
#!/usr/bin/env python3
"""
Add M600 filament change to Orca Slicer 3MF file.
Based on actual Orca Slicer format analysis.
Adds global height_range_modifier metadata that applies to all objects.
Usage:
python add_height_modifier_orca.py input.3mf [output.3mf]
"""
import sys
import zipfile
import xml.etree.ElementTree as ET
from pathlib import Path
import tempfile
import os
def add_height_modifier(input_file, output_file=None, height_mm=1.5):
"""
Add height range modifier metadata to 3MF file.
Args:
input_file: Path to input 3MF file
output_file: Path to output 3MF file (optional)
height_mm: Height in mm where filament change occurs
"""
input_path = Path(input_file)
if not input_path.exists():
print(f"Error: Input file '{input_file}' not found!")
return False
if output_file is None:
output_path = input_path.parent / f"{input_path.stem}_modified.3mf"
else:
output_path = Path(output_file)
print(f"Processing: {input_path}")
print(f"Output: {output_path}")
print(f"Filament change at: {height_mm}mm")
print("-" * 60)
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Extract 3MF
print("Extracting 3MF...")
with zipfile.ZipFile(input_path, 'r') as zip_ref:
zip_ref.extractall(temp_path)
# Find model file
model_file = temp_path / "3D" / "3dmodel.model"
if not model_file.exists():
print("Error: Could not find 3dmodel.model!")
return False
print("Modifying model file...")
# Parse XML
tree = ET.parse(model_file)
root = tree.getroot()
# Register namespaces
namespaces = {
'': 'http://schemas.microsoft.com/3dmanufacturing/core/2015/02',
'BambuStudio': 'http://schemas.bambulab.com/package/2021',
'p': 'http://schemas.microsoft.com/3dmanufacturing/production/2015/06'
}
for prefix, uri in namespaces.items():
if prefix:
ET.register_namespace(prefix, uri)
else:
ET.register_namespace('', uri)
# Find or create metadata section (should be at root level, before <resources>)
# Check if height_range_modifier already exists
existing_modifier = None
for metadata in root.findall('.//metadata[@name="height_range_modifier"]', namespaces):
existing_modifier = metadata
break
if existing_modifier is not None:
print(" Found existing height_range_modifier, replacing...")
root.remove(existing_modifier)
# Create the height range modifier metadata
# Format: JSON string - we'll manually insert it to avoid auto-escaping
metadata_json = (
'{"ranges":['
f'{{"min":0,"max":{height_mm},"color":"RoyalBlue"}},'
f'{{"min":{height_mm},"max":999,"color":"white","gcode":"M600"}}'
']}'
)
# Create metadata element - we'll manually set the text with proper escaping
metadata_elem = ET.Element('metadata')
metadata_elem.set('name', 'height_range_modifier')
# Don't use .text = ... as it will auto-escape
# We'll replace this after writing
# Insert after the last existing metadata element (before <resources>)
resources = root.find('.//resources', namespaces)
if resources is not None:
resources_index = list(root).index(resources)
root.insert(resources_index, metadata_elem)
else:
# No resources found, just append
root.append(metadata_elem)
print(f" Added height_range_modifier metadata")
print(f" Blue layer: 0mm to {height_mm}mm")
print(f" White layer: {height_mm}mm to top (with M600)")
# Save modified XML
tree.write(model_file, encoding='utf-8', xml_declaration=True)
# Post-process the file to fix the escaping
# Python's ET auto-escapes, but we need &amp;quot; not &amp;amp;quot;
with open(model_file, 'r', encoding='utf-8') as f:
content = f.read()
# Replace the empty metadata element with our properly escaped version
escaped_json = metadata_json.replace('"', '&amp;quot;')
content = content.replace(
'<metadata name="height_range_modifier" />',
f'<metadata name="height_range_modifier">{escaped_json}</metadata>'
)
content = content.replace(
'<metadata name="height_range_modifier"></metadata>',
f'<metadata name="height_range_modifier">{escaped_json}</metadata>'
)
with open(model_file, 'w', encoding='utf-8') as f:
f.write(content)
# Re-create 3MF file
print("Creating modified 3MF...")
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zip_out:
for root_dir, dirs, files in os.walk(temp_path):
for file in files:
file_path = Path(root_dir) / file
arcname = file_path.relative_to(temp_path)
zip_out.write(file_path, arcname)
print()
print("Success! Modified 3MF saved.")
print()
print("Next steps:")
print("1. Open the modified file in Orca Slicer")
print("2. Check if height range modifier appears")
print("3. Slice and verify M600 in G-code")
print()
print("Note: This adds a GLOBAL height modifier that applies to")
print(" all objects in the build plate.")
return True
def main():
if len(sys.argv) < 2:
print("Usage: python add_height_modifier_orca.py input.3mf [output.3mf]")
print()
print("Adds M600 filament change at 1.5mm (global, applies to all objects)")
sys.exit(1)
input_file = sys.argv[1]
output_file = sys.argv[2] if len(sys.argv) > 2 else None
success = add_height_modifier(input_file, output_file)
if not success:
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,56 @@
// Two-Layer Check Mark Design
// Black base (box + check mark) + White fill layer on top
// Based on CF00089-08 Check Mark 01.svg
// Parameters - adjust these to customize
black_base_thickness = 0.5; // Black base layer (thin, bottom)
white_fill_thickness = 1.5; // White fill layer (middle)
black_top_thickness = 0.5; // Black top (check mark shows through)
desired_size = 14; // Desired box size (mm) - makes design square
// SVG file path
svg_file = "Check-Mark/CF00089-08 Check Mark 01.svg";
// Calculate dimensions from SVG viewBox (720 x 570.9)
svg_width = 720;
svg_height = 570.9;
// Scale factors to make the design exactly square
scale_x = desired_size / svg_width; // Scale for width
scale_y = desired_size / svg_height; // Scale for height
design_width = desired_size;
design_height = desired_size;
// White fill inset (how much smaller than the outer box)
white_inset = 1.5; // mm inset from edges (adjusted for 14mm box)
white_width = design_width - (white_inset * 2);
white_height = design_height - (white_inset * 2);
// Total thickness
total_thickness = black_base_thickness + white_fill_thickness + black_top_thickness;
echo(str("Design size: ", design_width, "mm x ", design_height, "mm"));
echo(str("White fill size: ", white_width, "mm x ", white_height, "mm"));
echo(str("Total thickness: ", total_thickness, "mm"));
// Build the two-layer design
module checkmark_design() {
// Layer 1: Black base - full SVG design (box + check mark)
color("black")
translate([0, 0, 0])
linear_extrude(height = total_thickness)
// Center the SVG design
translate([-design_width/2, -design_height/2, 0])
scale([scale_x, scale_y, 1])
import(svg_file, center = false);
// Layer 2: White fill - sits on top of black base, inside the box
color("white")
translate([0, 0, black_base_thickness])
linear_extrude(height = white_fill_thickness)
offset(r = 1) // Slightly round the corners
square([white_width, white_height], center = true);
}
// Render the complete design
checkmark_design();

Binary file not shown.

View File

@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""
Generate 3D nameplate STL files from a list of names using OpenSCAD.
This script reads names from a text file and generates individual STL files
for each name using an OpenSCAD template.
"""
import subprocess
import os
import sys
from pathlib import Path
def generate_stl(name, template_file, output_dir):
"""
Generate an STL file for a given name using OpenSCAD.
Args:
name: The name to put on the nameplate
template_file: Path to the OpenSCAD template file
output_dir: Directory where STL files will be saved
"""
# Create a safe filename from the name (remove special characters)
safe_name = "".join(c for c in name if c.isalnum() or c in (' ', '-', '_')).strip()
safe_name = safe_name.replace(' ', '_')
output_file = os.path.join(output_dir, f"{safe_name}.stl")
# Escape quotes in the name for the command line
escaped_name = name.replace('"', '\\"')
# Build the OpenSCAD command
# -D sets a variable, -o specifies output file
cmd = [
r'C:\Program Files\OpenSCAD\openscad.exe',
'-D', f'name="{escaped_name}"',
'-o', output_file,
template_file
]
print(f"Generating: {safe_name}.stl for '{name}'...")
try:
# Run OpenSCAD
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=True
)
print(f" [OK] Successfully created {safe_name}.stl")
return True
except subprocess.CalledProcessError as e:
print(f" [ERROR] Error generating {safe_name}.stl")
print(f" {e.stderr}")
return False
except FileNotFoundError:
print("Error: OpenSCAD not found. Please install OpenSCAD and ensure it's in your PATH.")
print("Download from: https://openscad.org/downloads.html")
sys.exit(1)
def main():
# Configuration
template_file = "nameplate_template.scad"
names_file = "names.txt"
output_dir = "output_stl"
# Check if template exists
if not os.path.exists(template_file):
print(f"Error: Template file '{template_file}' not found!")
sys.exit(1)
# Check if names file exists
if not os.path.exists(names_file):
print(f"Error: Names file '{names_file}' not found!")
sys.exit(1)
# Create output directory if it doesn't exist
Path(output_dir).mkdir(exist_ok=True)
# Read names from file
with open(names_file, 'r', encoding='utf-8') as f:
names = [line.strip() for line in f if line.strip()]
if not names:
print(f"Error: No names found in '{names_file}'")
sys.exit(1)
print(f"Found {len(names)} name(s) to process")
print(f"Output directory: {output_dir}")
print("-" * 50)
# Generate STL for each name
success_count = 0
for name in names:
if generate_stl(name, template_file, output_dir):
success_count += 1
print("-" * 50)
print(f"Complete! Generated {success_count}/{len(names)} STL files")
print(f"Files saved in: {output_dir}/")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""
Generate 3D zipper pull STL files from a list of names using OpenSCAD.
This script reads names from a text file and generates individual STL files
for each name using the zipper pull OpenSCAD template.
"""
import subprocess
import os
import sys
from pathlib import Path
def generate_stl(name, template_file, output_dir):
"""
Generate an STL file for a given name using OpenSCAD.
Args:
name: The name to put on the zipper pull
template_file: Path to the OpenSCAD template file
output_dir: Directory where STL files will be saved
"""
# Create a safe filename from the name (remove special characters)
safe_name = "".join(c for c in name if c.isalnum() or c in (' ', '-', '_')).strip()
safe_name = safe_name.replace(' ', '_')
output_file = os.path.join(output_dir, f"{safe_name}.stl")
# Escape quotes in the name for the command line
escaped_name = name.replace('"', '\\"')
# Build the OpenSCAD command
# -D sets a variable, -o specifies output file
cmd = [
r'C:\Program Files\OpenSCAD\openscad.exe',
'-D', f'name="{escaped_name}"',
'-o', output_file,
template_file
]
print(f"Generating: {safe_name}.stl for '{name}'...")
try:
# Run OpenSCAD
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=True
)
print(f" [OK] Successfully created {safe_name}.stl")
return True
except subprocess.CalledProcessError as e:
print(f" [ERROR] Error generating {safe_name}.stl")
print(f" {e.stderr}")
return False
except FileNotFoundError:
print("Error: OpenSCAD not found. Please install OpenSCAD and ensure it's in your PATH.")
print("Download from: https://openscad.org/downloads.html")
sys.exit(1)
def main():
# Configuration
template_file = "zipper_pull_template.scad"
names_file = "names.txt"
output_dir = "zipper-pulls"
# Check if template exists
if not os.path.exists(template_file):
print(f"Error: Template file '{template_file}' not found!")
sys.exit(1)
# Check if names file exists
if not os.path.exists(names_file):
print(f"Error: Names file '{names_file}' not found!")
sys.exit(1)
# Create output directory if it doesn't exist
Path(output_dir).mkdir(exist_ok=True)
# Read names from file
with open(names_file, 'r', encoding='utf-8') as f:
names = [line.strip() for line in f if line.strip()]
if not names:
print(f"Error: No names found in '{names_file}'")
sys.exit(1)
print(f"Found {len(names)} name(s) to process")
print(f"Output directory: {output_dir}")
print("-" * 50)
# Generate STL for each name
success_count = 0
for name in names:
if generate_stl(name, template_file, output_dir):
success_count += 1
print("-" * 50)
print(f"Complete! Generated {success_count}/{len(names)} STL files")
print(f"Files saved in: {output_dir}/")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""
Generate 3D zipper pull STL files with raised text from a list of names using OpenSCAD.
This script reads names from a text file and generates individual STL files
for each name using the raised text zipper pull OpenSCAD template.
"""
import subprocess
import os
import sys
from pathlib import Path
def generate_stl(name, template_file, output_dir):
"""
Generate an STL file for a given name using OpenSCAD.
Args:
name: The name to put on the zipper pull
template_file: Path to the OpenSCAD template file
output_dir: Directory where STL files will be saved
"""
# Create a safe filename from the name (remove special characters)
safe_name = "".join(c for c in name if c.isalnum() or c in (' ', '-', '_')).strip()
safe_name = safe_name.replace(' ', '_')
output_file = os.path.join(output_dir, f"{safe_name}.stl")
# Escape quotes in the name for the command line
escaped_name = name.replace('"', '\\"')
# Build the OpenSCAD command
# -D sets a variable, -o specifies output file
cmd = [
r'C:\Program Files\OpenSCAD\openscad.exe',
'-D', f'name="{escaped_name}"',
'-o', output_file,
template_file
]
print(f"Generating: {safe_name}.stl for '{name}'...")
try:
# Run OpenSCAD
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=True
)
print(f" [OK] Successfully created {safe_name}.stl")
return True
except subprocess.CalledProcessError as e:
print(f" [ERROR] Error generating {safe_name}.stl")
print(f" {e.stderr}")
return False
except FileNotFoundError:
print("Error: OpenSCAD not found. Please install OpenSCAD and ensure it's in your PATH.")
print("Download from: https://openscad.org/downloads.html")
sys.exit(1)
def main():
# Configuration
template_file = "zipper_pull_raised_text_template.scad"
names_file = "names.txt"
output_dir = "zipper-pulls-raised-text"
# Check if template exists
if not os.path.exists(template_file):
print(f"Error: Template file '{template_file}' not found!")
sys.exit(1)
# Check if names file exists
if not os.path.exists(names_file):
print(f"Error: Names file '{names_file}' not found!")
sys.exit(1)
# Create output directory if it doesn't exist
Path(output_dir).mkdir(exist_ok=True)
# Read names from file
with open(names_file, 'r', encoding='utf-8') as f:
names = [line.strip() for line in f if line.strip()]
if not names:
print(f"Error: No names found in '{names_file}'")
sys.exit(1)
print(f"Found {len(names)} name(s) to process")
print(f"Output directory: {output_dir}")
print("-" * 50)
# Generate STL for each name
success_count = 0
for name in names:
if generate_stl(name, template_file, output_dir):
success_count += 1
print("-" * 50)
print(f"Complete! Generated {success_count}/{len(names)} STL files")
print(f"Files saved in: {output_dir}/")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""
Generate 3D zipper pull STL files with raised text from a list of names using OpenSCAD.
This script reads names from a text file and generates individual STL files
for each name using the raised text zipper pull OpenSCAD template.
Version 2: Improved apostrophe handling - preserves apostrophes in both filenames and text rendering.
"""
import subprocess
import os
import sys
from pathlib import Path
def generate_stl(name, template_file, output_dir):
"""
Generate an STL file for a given name using OpenSCAD.
Args:
name: The name to put on the zipper pull
template_file: Path to the OpenSCAD template file
output_dir: Directory where STL files will be saved
"""
# Create a safe filename from the name
# Keep apostrophes in filename for accurate representation
safe_name = "".join(c for c in name if c.isalnum() or c in (' ', '-', '_', "\'")).strip()
safe_name = safe_name.replace(' ', '_')
output_file = os.path.join(output_dir, f"{safe_name}.stl")
# Escape the name for OpenSCAD command line
# Need to escape both quotes and apostrophes properly
escaped_name = name.replace('\\', '\\\\') # Escape backslashes first
escaped_name = escaped_name.replace('"', '\\"') # Escape double quotes
escaped_name = escaped_name.replace("'", "\\'") # Escape single quotes/apostrophes
# Build the OpenSCAD command
# -D sets a variable, -o specifies output file
cmd = [
r'C:\Program Files\OpenSCAD\openscad.exe',
'-D', f'name="{escaped_name}"',
'-o', output_file,
template_file
]
print(f"Generating: {safe_name}.stl for '{name}'...")
try:
# Run OpenSCAD
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=True
)
print(f" [OK] Successfully created {safe_name}.stl")
return True
except subprocess.CalledProcessError as e:
print(f" [ERROR] Error generating {safe_name}.stl")
print(f" {e.stderr}")
return False
except FileNotFoundError:
print("Error: OpenSCAD not found. Please install OpenSCAD and ensure it's in your PATH.")
print("Download from: https://openscad.org/downloads.html")
sys.exit(1)
def main():
# Configuration
template_file = "zipper_pull_raised_text_template.scad"
names_file = "names.txt"
output_dir = "zipper-pulls-raised-text-2"
# Check if template exists
if not os.path.exists(template_file):
print(f"Error: Template file '{template_file}' not found!")
sys.exit(1)
# Check if names file exists
if not os.path.exists(names_file):
print(f"Error: Names file '{names_file}' not found!")
sys.exit(1)
# Create output directory if it doesn't exist
Path(output_dir).mkdir(exist_ok=True)
# Read names from file
with open(names_file, 'r', encoding='utf-8') as f:
names = [line.strip() for line in f if line.strip()]
if not names:
print(f"Error: No names found in '{names_file}'")
sys.exit(1)
print(f"Found {len(names)} name(s) to process")
print(f"Output directory: {output_dir}")
print("-" * 50)
# Generate STL for each name
success_count = 0
for name in names:
if generate_stl(name, template_file, output_dir):
success_count += 1
print("-" * 50)
print(f"Complete! Generated {success_count}/{len(names)} STL files")
print(f"Files saved in: {output_dir}/")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,61 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Directory listing for /Printing/Nameplates/</title>
</head>
<body>
<h1>Directory listing for /Printing/Nameplates/</h1>
<hr>
<ul>
<li><a href=".claude/">.claude/</a></li>
<li><a href=".github/">.github/</a></li>
<li><a href="300.103.pdf">300.103.pdf</a></li>
<li><a href="add_filament_change_to_3mf.py">add_filament_change_to_3mf.py</a></li>
<li><a href="add_height_modifier_orca.py">add_height_modifier_orca.py</a></li>
<li><a href="Check-Mark/">Check-Mark/</a></li>
<li><a href="checkmark_template.scad">checkmark_template.scad</a></li>
<li><a href="CLAUDE.md">CLAUDE.md</a></li>
<li><a href="Fordscript.ttf">Fordscript.ttf</a></li>
<li><a href="fred_bennett_facedown.3mf">fred_bennett_facedown.3mf</a></li>
<li><a href="generate_nameplates.py">generate_nameplates.py</a></li>
<li><a href="generate_zipper_pulls.py">generate_zipper_pulls.py</a></li>
<li><a href="generate_zipper_pulls_raised_text.py">generate_zipper_pulls_raised_text.py</a></li>
<li><a href="generate_zipper_pulls_raised_text_2.py">generate_zipper_pulls_raised_text_2.py</a></li>
<li><a href="nameplate_template.scad">nameplate_template.scad</a></li>
<li><a href="names.txt">names.txt</a></li>
<li><a href="nul">nul</a></li>
<li><a href="output_stl/">output_stl/</a></li>
<li><a href="Screenshot%202025-11-07%20082805.png">Screenshot 2025-11-07 082805.png</a></li>
<li><a href="test_bennet_facedown.stl">test_bennet_facedown.stl</a></li>
<li><a href="test_bennett_facedown.3mf">test_bennett_facedown.3mf</a></li>
<li><a href="test_blue_only.3mf">test_blue_only.3mf</a></li>
<li><a href="test_blue_only.scad">test_blue_only.scad</a></li>
<li><a href="test_christopher.stl">test_christopher.stl</a></li>
<li><a href="test_christopher_13pt.stl">test_christopher_13pt.stl</a></li>
<li><a href="test_christopher_dynamic.stl">test_christopher_dynamic.stl</a></li>
<li><a href="test_christopher_raised.stl">test_christopher_raised.stl</a></li>
<li><a href="test_debug.3mf">test_debug.3mf</a></li>
<li><a href="test_debug.scad">test_debug.scad</a></li>
<li><a href="test_font.scad">test_font.scad</a></li>
<li><a href="test_font.stl">test_font.stl</a></li>
<li><a href="test_fred_bold.stl">test_fred_bold.stl</a></li>
<li><a href="test_fred_debug.stl">test_fred_debug.stl</a></li>
<li><a href="test_fred_dynamic.stl">test_fred_dynamic.stl</a></li>
<li><a href="test_fred_raised.stl">test_fred_raised.stl</a></li>
<li><a href="TEST_Fred_raised_text.stl">TEST_Fred_raised_text.stl</a></li>
<li><a href="test_fred_reduced_bold.stl">test_fred_reduced_bold.stl</a></li>
<li><a href="test_hole_position.stl">test_hole_position.stl</a></li>
<li><a href="test_simple_fred.scad">test_simple_fred.scad</a></li>
<li><a href="test_simple_fred.stl">test_simple_fred.stl</a></li>
<li><a href="test_zoe.stl">test_zoe.stl</a></li>
<li><a href="test_zoe_raised.stl">test_zoe_raised.stl</a></li>
<li><a href="zipper-pulls-raised-text/">zipper-pulls-raised-text/</a></li>
<li><a href="zipper-pulls-raised-text-2/">zipper-pulls-raised-text-2/</a></li>
<li><a href="zipper_pull_raised_text_template.scad">zipper_pull_raised_text_template.scad</a></li>
<li><a href="zipper_pull_raised_text_template_REDUCED_BOLD.scad">zipper_pull_raised_text_template_REDUCED_BOLD.scad</a></li>
<li><a href="zipper_pull_template.scad">zipper_pull_template.scad</a></li>
</ul>
<hr>
</body>
</html>

View File

@@ -0,0 +1,57 @@
// Two-Layer Oval Nameplate Template with Engraved Text
// Creates a white base oval with a blue top oval and engraved text
// Parameters - these can be overridden from command line
name = "NAME"; // Text to display on nameplate
oval_width = 100; // Width of the oval (mm) - max 100mm
oval_height = 38; // Height of the oval (mm) - proportional to width
base_thickness = 1.5; // Thickness of white base layer (mm)
top_thickness = 1; // Thickness of blue top layer (mm)
base_offset = 2; // How much larger the white base is (mm)
text_size = 15; // Font size for the text
text_depth = 1; // How deep the text is engraved (mm) - cuts through blue to show white
font_file = "C:/Users/Fred/claude/Fordscript.ttf"; // Path to the custom font file
// Module to create an oval (ellipse)
module oval(width, height, depth) {
scale([width/2, height/2, 1])
cylinder(h=depth, r=1, $fn=100);
}
// White base layer (larger oval)
module base_layer() {
color("white")
oval(
oval_width + base_offset*2,
oval_height + base_offset*2,
base_thickness
);
}
// Blue top layer with engraved text
module top_layer() {
color("RoyalBlue")
difference() {
// Blue oval
translate([0, 0, base_thickness])
oval(oval_width, oval_height, top_thickness);
// Engraved text (cuts into the blue layer)
translate([0, 0, base_thickness + top_thickness - text_depth + 0.01])
linear_extrude(height=text_depth)
text(name,
size=text_size,
font="Fordscript",
halign="center",
valign="center");
}
}
// Main nameplate assembly
module nameplate() {
base_layer();
top_layer();
}
// Generate the nameplate
nameplate();

View File

@@ -0,0 +1,58 @@
Skylar
Aaron
Kathrynn
Alice
Jaxxin
Sydney
Blake
Ayden
Bentley
Landon
Silas
Aniah
Silas
Donald
Hunter
Harper
Keith
Karleigh
Michael
Taylor
Adalynn
Harley
Danny
Carson
Aur'heir
Kinleigh
Bennett
Lucy
Grayson
Leon
Charlotte
Landon
Aiden
Nevaeh
Ayla
Leah
William
Lane
Liberty
Karter
Wyatt
Clare
Bryant
Cole
Jordan
Max
Zoey
Eli
Oliver
Jack
Tyler
Annabella
Amaya
Shane
Nicholas
Clay
Jayden
Alexis

View File

@@ -0,0 +1,15 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Directory listing for /Printing/Nameplates/output_stl/</title>
</head>
<body>
<h1>Directory listing for /Printing/Nameplates/output_stl/</h1>
<hr>
<ul>
<li><a href="2names%27.3mf">2names'.3mf</a></li>
</ul>
<hr>
</body>
</html>

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,61 @@
// Test - only blue layer from main template
name = "Bennett";
oval_width = 96;
oval_height = 30;
blue_thickness = 1;
white_thickness = 1.5;
base_text_size = 13;
name_length = len(name);
text_size = name_length <= 3 ? 18 :
name_length <= 5 ? 13 :
name_length <= 8 ? 11 :
11;
border_width = 2;
hole_diameter = 4;
hole_clearance = 4;
total_width = oval_width + border_width*2;
total_height = oval_height + border_width*2;
hole_x = -(total_width/2) + (hole_diameter/2) + hole_clearance;
module oval(width, height, depth) {
scale([width/2, height/2, 1])
cylinder(h=depth, r=1, $fn=100);
}
module bold_text() {
for (x = [-0.3, 0, 0.3]) {
for (y = [-0.3, 0, 0.3]) {
translate([x, y, 0])
linear_extrude(height=blue_thickness + 0.1)
text(name,
size=text_size,
font="Fordscript",
halign="center",
valign="center");
}
}
}
// Blue layer only
color("RoyalBlue")
difference() {
oval(total_width, total_height, blue_thickness);
// Cut out the border ring channel
translate([0, 0, -0.05])
difference() {
oval(total_width, total_height, blue_thickness + 0.1);
oval(oval_width, oval_height, blue_thickness + 0.2);
}
// Cut out text channels
translate([0, 0, -0.05])
bold_text();
// Cut out hole
translate([hole_x, 0, -0.05])
cylinder(h=blue_thickness + 0.1, d=hole_diameter, $fn=50);
}

Binary file not shown.

View File

@@ -0,0 +1,43 @@
// Debug - just show the blue layer with text cut out
name = "Bennett";
oval_width = 96;
oval_height = 30;
blue_thickness = 1;
border_width = 2;
name_length = len(name);
text_size = name_length <= 3 ? 18 :
name_length <= 5 ? 13 :
name_length <= 8 ? 11 :
11;
total_width = oval_width + border_width*2;
total_height = oval_height + border_width*2;
module oval(width, height, depth) {
scale([width/2, height/2, 1])
cylinder(h=depth, r=1, $fn=100);
}
module bold_text() {
for (x = [-0.3, 0, 0.3]) {
for (y = [-0.3, 0, 0.3]) {
translate([x, y, 0])
linear_extrude(height=blue_thickness + 0.1)
text(name,
size=text_size,
font="Fordscript",
halign="center",
valign="center");
}
}
}
// Just the blue with text cut - no border cut
color("RoyalBlue")
difference() {
oval(total_width, total_height, blue_thickness);
translate([0, 0, -0.05])
bold_text();
}

View File

@@ -0,0 +1,3 @@
// Test if Fordscript font renders
linear_extrude(height=2)
text("Bennett", size=11, font="Fordscript", halign="center", valign="center");

View File

@@ -0,0 +1,12 @@
// Simple test for Fred text rendering
name = "Fred";
text_size = 13;
color("white")
translate([0, 0, 1.5])
linear_extrude(height=1)
text(name,
size=text_size,
font="Fordscript",
halign="center",
valign="center");

View File

@@ -0,0 +1,70 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Directory listing for /Printing/Nameplates/zipper-pulls-raised-text-2/</title>
</head>
<body>
<h1>Directory listing for /Printing/Nameplates/zipper-pulls-raised-text-2/</h1>
<hr>
<ul>
<li><a href="Aaron.stl">Aaron.stl</a></li>
<li><a href="Adalynn.stl">Adalynn.stl</a></li>
<li><a href="Aiden.stl">Aiden.stl</a></li>
<li><a href="Alexis.stl">Alexis.stl</a></li>
<li><a href="Alice.stl">Alice.stl</a></li>
<li><a href="Amaya.stl">Amaya.stl</a></li>
<li><a href="Aniah.stl">Aniah.stl</a></li>
<li><a href="Annabella.stl">Annabella.stl</a></li>
<li><a href="Aur%27heir.stl">Aur'heir.stl</a></li>
<li><a href="Ayden.stl">Ayden.stl</a></li>
<li><a href="Ayla.stl">Ayla.stl</a></li>
<li><a href="Bennett.stl">Bennett.stl</a></li>
<li><a href="Bentley.stl">Bentley.stl</a></li>
<li><a href="Blake.stl">Blake.stl</a></li>
<li><a href="Bryant.stl">Bryant.stl</a></li>
<li><a href="Carson.stl">Carson.stl</a></li>
<li><a href="Charlotte.stl">Charlotte.stl</a></li>
<li><a href="Clare.stl">Clare.stl</a></li>
<li><a href="Clay.stl">Clay.stl</a></li>
<li><a href="Cole.stl">Cole.stl</a></li>
<li><a href="Danny.stl">Danny.stl</a></li>
<li><a href="Donald.stl">Donald.stl</a></li>
<li><a href="Eli.stl">Eli.stl</a></li>
<li><a href="Grayson.stl">Grayson.stl</a></li>
<li><a href="Harley.stl">Harley.stl</a></li>
<li><a href="Harper.stl">Harper.stl</a></li>
<li><a href="Hunter.stl">Hunter.stl</a></li>
<li><a href="Jack.stl">Jack.stl</a></li>
<li><a href="Jaxxin.stl">Jaxxin.stl</a></li>
<li><a href="Jayden.stl">Jayden.stl</a></li>
<li><a href="Jordan.stl">Jordan.stl</a></li>
<li><a href="Karleigh.stl">Karleigh.stl</a></li>
<li><a href="Karter.stl">Karter.stl</a></li>
<li><a href="Kathrynn.stl">Kathrynn.stl</a></li>
<li><a href="Keith.stl">Keith.stl</a></li>
<li><a href="Kinleigh.stl">Kinleigh.stl</a></li>
<li><a href="Landon.stl">Landon.stl</a></li>
<li><a href="Lane.stl">Lane.stl</a></li>
<li><a href="Leah.stl">Leah.stl</a></li>
<li><a href="Leon.stl">Leon.stl</a></li>
<li><a href="Liberty.stl">Liberty.stl</a></li>
<li><a href="Lucy.stl">Lucy.stl</a></li>
<li><a href="Max.stl">Max.stl</a></li>
<li><a href="Michael.stl">Michael.stl</a></li>
<li><a href="Nevaeh.stl">Nevaeh.stl</a></li>
<li><a href="Nicholas.stl">Nicholas.stl</a></li>
<li><a href="Oliver.stl">Oliver.stl</a></li>
<li><a href="Shane.stl">Shane.stl</a></li>
<li><a href="Silas.stl">Silas.stl</a></li>
<li><a href="Skylar.stl">Skylar.stl</a></li>
<li><a href="Sydney.stl">Sydney.stl</a></li>
<li><a href="Taylor.stl">Taylor.stl</a></li>
<li><a href="Tyler.stl">Tyler.stl</a></li>
<li><a href="William.stl">William.stl</a></li>
<li><a href="Wyatt.stl">Wyatt.stl</a></li>
<li><a href="Zoey.stl">Zoey.stl</a></li>
</ul>
<hr>
</body>
</html>

View File

@@ -0,0 +1,82 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Directory listing for /Printing/Nameplates/zipper-pulls-raised-text/</title>
</head>
<body>
<h1>Directory listing for /Printing/Nameplates/zipper-pulls-raised-text/</h1>
<hr>
<ul>
<li><a href="111.stl">111.stl</a></li>
<li><a href="aaron.3mf">aaron.3mf</a></li>
<li><a href="Aaron.stl">Aaron.stl</a></li>
<li><a href="Adalynn.stl">Adalynn.stl</a></li>
<li><a href="Aiden.stl">Aiden.stl</a></li>
<li><a href="Alexis.stl">Alexis.stl</a></li>
<li><a href="Alice.stl">Alice.stl</a></li>
<li><a href="Amaya.stl">Amaya.stl</a></li>
<li><a href="Aniah.stl">Aniah.stl</a></li>
<li><a href="Annabella.stl">Annabella.stl</a></li>
<li><a href="Aurheir.stl">Aurheir.stl</a></li>
<li><a href="Ayden.stl">Ayden.stl</a></li>
<li><a href="Ayla.stl">Ayla.stl</a></li>
<li><a href="Bennett.stl">Bennett.stl</a></li>
<li><a href="Bentley.stl">Bentley.stl</a></li>
<li><a href="Blake.stl">Blake.stl</a></li>
<li><a href="Bryant.stl">Bryant.stl</a></li>
<li><a href="Carson.stl">Carson.stl</a></li>
<li><a href="Charlotte.stl">Charlotte.stl</a></li>
<li><a href="Clare.stl">Clare.stl</a></li>
<li><a href="Clay.stl">Clay.stl</a></li>
<li><a href="Cole.stl">Cole.stl</a></li>
<li><a href="Daniel.stl">Daniel.stl</a></li>
<li><a href="Danny.stl">Danny.stl</a></li>
<li><a href="Donald.stl">Donald.stl</a></li>
<li><a href="Eli.stl">Eli.stl</a></li>
<li><a href="first%20plate.gcode">first plate.gcode</a></li>
<li><a href="Ford-Fred%20%282%29.stl">Ford-Fred (2).stl</a></li>
<li><a href="Grayson.stl">Grayson.stl</a></li>
<li><a href="Harley.stl">Harley.stl</a></li>
<li><a href="Harper.stl">Harper.stl</a></li>
<li><a href="Hunter.stl">Hunter.stl</a></li>
<li><a href="Jack.stl">Jack.stl</a></li>
<li><a href="Jaxxin.stl">Jaxxin.stl</a></li>
<li><a href="Jayden.stl">Jayden.stl</a></li>
<li><a href="Jordan.stl">Jordan.stl</a></li>
<li><a href="Karleigh.stl">Karleigh.stl</a></li>
<li><a href="Karter.stl">Karter.stl</a></li>
<li><a href="Kathrynn.stl">Kathrynn.stl</a></li>
<li><a href="Keith.stl">Keith.stl</a></li>
<li><a href="Kinleigh.stl">Kinleigh.stl</a></li>
<li><a href="Landon.stl">Landon.stl</a></li>
<li><a href="Lane.stl">Lane.stl</a></li>
<li><a href="Leah.stl">Leah.stl</a></li>
<li><a href="Leon.stl">Leon.stl</a></li>
<li><a href="Liberty.stl">Liberty.stl</a></li>
<li><a href="Lucy.stl">Lucy.stl</a></li>
<li><a href="Max.stl">Max.stl</a></li>
<li><a href="Michael.stl">Michael.stl</a></li>
<li><a href="Nevaeh.stl">Nevaeh.stl</a></li>
<li><a href="New%20Names.3mf">New Names.3mf</a></li>
<li><a href="Nicholas.stl">Nicholas.stl</a></li>
<li><a href="Oliver.stl">Oliver.stl</a></li>
<li><a href="pLATE%201.3mf">pLATE 1.3mf</a></li>
<li><a href="Shane.stl">Shane.stl</a></li>
<li><a href="Silas.stl">Silas.stl</a></li>
<li><a href="Skylar.stl">Skylar.stl</a></li>
<li><a href="Sydney.stl">Sydney.stl</a></li>
<li><a href="Taylor.stl">Taylor.stl</a></li>
<li><a href="top4.3mf">top4.3mf</a></li>
<li><a href="Tyler.stl">Tyler.stl</a></li>
<li><a href="William.stl">William.stl</a></li>
<li><a href="Wyatt.stl">Wyatt.stl</a></li>
<li><a href="zipper%20pulls.3mf">zipper pulls.3mf</a></li>
<li><a href="zipper%20pulls_FINAL.3mf">zipper pulls_FINAL.3mf</a></li>
<li><a href="zipper%20pulls_modified.3mf">zipper pulls_modified.3mf</a></li>
<li><a href="zipper%20pulls_with_M600.3mf">zipper pulls_with_M600.3mf</a></li>
<li><a href="Zoey.stl">Zoey.stl</a></li>
</ul>
<hr>
</body>
</html>

View File

@@ -0,0 +1,117 @@
// Face-Down Oval Zipper Pull Template
// Designed for face-down printing: text and border are channels in blue layer (printed first),
// then white backing fills the channels and covers the back.
// When flipped over, shows white text/border on blue background.
// Parameters - these can be overridden from command line
name = "NAME"; // Text to display on zipper pull
oval_width = 96; // Width of blue oval (mm) - total with border will be 100mm (96 + 2*2)
oval_height = 30; // Height of blue oval (mm) - total with border will be 34mm (30 + 2*2)
blue_thickness = 1; // Thickness of blue layer (mm) - this is the "show" side
white_thickness = 1.5; // Thickness of white backing layer (mm)
base_text_size = 13; // Base font size for medium names
// Dynamic font sizing based on name length (mimics Ford oval proportions)
// "Fred" (4 chars) at 13mm gives ~20% coverage - our target
name_length = len(name);
text_size = name_length <= 3 ? 18 : // Short names (Zoe, Sam, Al) - larger (~21% coverage)
name_length <= 5 ? 13 : // Medium names (Fred, John, Mary) - standard (~20% coverage)
name_length <= 8 ? 11 : // Longer names (Michael, Jessica) - smaller (~25% coverage)
11; // Very long names (Christopher) - increased to 11mm (~35% coverage)
// Border parameters
border_width = 2; // Width of the white border around the oval (mm)
// Hole parameters for zipper pull
hole_diameter = 4; // Diameter of the hole (mm)
hole_clearance = 4; // Minimum clearance from edge of outer border (mm)
// Total dimensions
total_width = oval_width + border_width*2; // 100mm
total_height = oval_height + border_width*2; // 34mm
// Hole position - on LEFT side when viewed from show side
// Since we print face-down, hole appears on RIGHT during printing
// When flipped, it will be on the left as expected
hole_x = -(total_width/2) + (hole_diameter/2) + hole_clearance;
// Module to create an oval (ellipse)
module oval(width, height, depth) {
scale([width/2, height/2, 1])
cylinder(h=depth, r=1, $fn=100);
}
// Module for text with fake bold effect
module bold_text() {
for (x = [-0.3, 0, 0.3]) {
for (y = [-0.3, 0, 0.3]) {
translate([x, y, 0])
linear_extrude(height=blue_thickness + 0.1)
text(name,
size=text_size,
font="Fordscript",
halign="center",
valign="center");
}
}
}
// Blue layer with channels for text and border (prints first, face-down)
// The channels will be filled with white when the white layer is printed
module blue_layer() {
color("RoyalBlue")
difference() {
// Full blue oval
oval(total_width, total_height, blue_thickness);
// Cut out the border ring channel (will be filled with white)
translate([0, 0, -0.05])
difference() {
oval(total_width, total_height, blue_thickness + 0.1);
oval(oval_width, oval_height, blue_thickness + 0.2);
}
// Cut out text channels (will be filled with white)
translate([0, 0, -0.05])
bold_text();
// Cut out hole
translate([hole_x, 0, -0.05])
cylinder(h=blue_thickness + 0.1, d=hole_diameter, $fn=50);
}
}
// White backing layer (prints second, on top of blue)
// Also fills the text and border channels
module white_layer() {
color("white")
difference() {
union() {
// Main backing layer on top of blue
translate([0, 0, blue_thickness])
oval(total_width, total_height, white_thickness);
// Fill the border channel
difference() {
oval(total_width, total_height, blue_thickness);
oval(oval_width, oval_height, blue_thickness + 0.1);
}
// Fill the text channels
bold_text();
}
// Cut out hole through entire white layer
translate([hole_x, 0, -0.05])
cylinder(h=blue_thickness + white_thickness + 0.2, d=hole_diameter, $fn=50);
}
}
// Main zipper pull assembly
module zipper_pull() {
blue_layer();
white_layer();
}
// Generate the zipper pull
zipper_pull();

View File

@@ -0,0 +1,88 @@
// Single-Layer Oval Zipper Pull Template with Raised Text
// Creates a blue base oval with raised white text and a hole for zipper attachment
// Parameters - these can be overridden from command line
name = "NAME"; // Text to display on zipper pull
oval_width = 96; // Width of blue oval (mm) - total with border will be 100mm (96 + 2*2)
oval_height = 30; // Height of blue oval (mm) - total with border will be 34mm (30 + 2*2)
base_thickness = 1.5; // Thickness of blue base layer (mm)
text_thickness = 1; // Thickness of raised white text (mm)
base_text_size = 13; // Base font size for medium names
font_file = "C:/Users/Fred/claude/Fordscript.ttf"; // Path to the custom font file
// Dynamic font sizing based on name length (mimics Ford oval proportions)
// "Fred" (4 chars) at 13mm gives ~20% coverage - our target
name_length = len(name);
text_size = name_length <= 3 ? 18 : // Short names (Zoe, Sam, Al) - larger
name_length <= 5 ? 13 : // Medium names (Fred, John, Mary) - standard
name_length <= 8 ? 11 : // Longer names (Michael, Jessica) - smaller
9; // Very long names (Christopher) - smallest
// Border parameters
border_width = 2; // Width of the white border around the oval (mm)
border_thickness = 1; // Thickness of the white border (mm)
// Hole parameters for zipper pull
hole_diameter = 4; // Diameter of the hole (mm)
hole_clearance = 3; // Minimum clearance from edge of outer border (mm) - increased to fully clear white border
// Module to create an oval (ellipse)
module oval(width, height, depth) {
scale([width/2, height/2, 1])
cylinder(h=depth, r=1, $fn=100);
}
// Blue base layer with hole
module base_layer() {
color("RoyalBlue")
difference() {
// Blue base matches the outer size of the white border
oval(oval_width + border_width*2, oval_height + border_width*2, base_thickness);
// Hole for zipper pull - positioned relative to outer edge
// Total width = oval_width + border_width*2
// Position: left edge + hole_radius + clearance
total_width = oval_width + border_width*2;
hole_x = -(total_width/2) + (hole_diameter/2) + hole_clearance;
translate([hole_x, 0, -0.05])
cylinder(h=base_thickness + 0.1, d=hole_diameter, $fn=50);
}
}
// White border around the oval
module white_border() {
color("white")
translate([0, 0, base_thickness])
difference() {
// Outer oval (larger)
oval(oval_width + border_width*2, oval_height + border_width*2, border_thickness);
// Inner oval (cut out the center)
translate([0, 0, -0.05])
oval(oval_width, oval_height, border_thickness + 0.1);
}
}
// Raised white text on top of base - REDUCED bold effect (3 renders instead of 9)
module raised_text() {
color("white")
// Reduced bold effect - only 3 offsets
for (x = [-0.4, 0, 0.4]) {
translate([x, 0, base_thickness])
linear_extrude(height=text_thickness)
text(name,
size=text_size,
font="Fordscript",
halign="center",
valign="center");
}
}
// Main zipper pull assembly
module zipper_pull() {
base_layer();
white_border();
raised_text();
}
// Generate the zipper pull
zipper_pull();

View File

@@ -0,0 +1,90 @@
// Two-Layer Oval Zipper Pull Template with Engraved Text
// Creates a white base oval with a blue top oval, engraved text, and a hole for zipper attachment
// Parameters - these can be overridden from command line
name = "NAME"; // Text to display on zipper pull
oval_width = 96; // Width of blue oval (mm) - white base will be 100mm total (96 + 2*2)
oval_height = 30; // Height of blue oval (mm) - white base will be 34mm total (30 + 2*2)
base_thickness = 1.5; // Thickness of white base layer (mm)
top_thickness = 1; // Thickness of blue top layer (mm)
base_offset = 2; // How much larger the white base is (mm)
base_text_size = 13; // Base font size for medium names (reduced to prevent interference with hole)
text_depth = 1; // How deep the text is engraved (mm) - cuts through blue to show white
font_file = "C:/Users/Fred/claude/Fordscript.ttf"; // Path to the custom font file
// Dynamic font sizing based on name length (mimics Ford oval proportions)
// "Fred" (4 chars) at 13mm gives ~20% coverage - our target
name_length = len(name);
text_size = name_length <= 3 ? 18 : // Short names (Zoe, Sam, Al) - larger
name_length <= 5 ? 13 : // Medium names (Fred, John, Mary) - standard
name_length <= 8 ? 11 : // Longer names (Michael, Jessica) - smaller
9; // Very long names (Christopher) - smallest
// Hole parameters for zipper pull
hole_diameter = 4; // Diameter of the hole (mm)
hole_clearance = 1; // Minimum clearance from edge of white base layer (mm)
// Module to create an oval (ellipse)
module oval(width, height, depth) {
scale([width/2, height/2, 1])
cylinder(h=depth, r=1, $fn=100);
}
// White base layer (larger oval) with hole
module base_layer() {
color("white")
difference() {
oval(
oval_width + base_offset*2,
oval_height + base_offset*2,
base_thickness
);
// Hole for zipper pull - positioned relative to white base edge
// Calculate from white base width: (oval_width + base_offset*2)
white_width = oval_width + base_offset*2;
hole_x = -(white_width/2) + (hole_diameter/2) + hole_clearance;
translate([hole_x, 0, -0.05])
cylinder(h=base_thickness + 0.1, d=hole_diameter, $fn=50);
}
}
// Blue top layer with engraved text and hole
module top_layer() {
color("RoyalBlue")
difference() {
// Blue oval
translate([0, 0, base_thickness])
oval(oval_width, oval_height, top_thickness);
// Engraved text (cuts into the blue layer)
// Multiple offset renders create a "fake bold" effect
for (x = [-0.3, 0, 0.3]) {
for (y = [-0.3, 0, 0.3]) {
translate([x, y, base_thickness + top_thickness - text_depth + 0.01])
linear_extrude(height=text_depth)
text(name,
size=text_size,
font="Fordscript",
halign="center",
valign="center");
}
}
// Hole for zipper pull (positioned on left side)
// Position: matches white base layer hole position
white_width = oval_width + base_offset*2;
hole_x = -(white_width/2) + (hole_diameter/2) + hole_clearance;
translate([hole_x, 0, base_thickness])
cylinder(h=top_thickness + 0.1, d=hole_diameter, $fn=50);
}
}
// Main zipper pull assembly
module zipper_pull() {
base_layer();
top_layer();
}
// Generate the zipper pull
zipper_pull();

20
Printing/index.html Normal file
View File

@@ -0,0 +1,20 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Directory listing for /Printing/</title>
</head>
<body>
<h1>Directory listing for /Printing/</h1>
<hr>
<ul>
<li><a href=".claude/">.claude/</a></li>
<li><a href=".git">.git</a></li>
<li><a href=".gitignore">.gitignore</a></li>
<li><a href="CLAUDE.md">CLAUDE.md</a></li>
<li><a href="Key%20caps/">Key caps/</a></li>
<li><a href="Nameplates/">Nameplates/</a></li>
</ul>
<hr>
</body>
</html>

265
README.md Normal file
View File

@@ -0,0 +1,265 @@
# Fred's Projects - Source of Truth
This is the master directory for all active projects. It serves as the "source of truth" for Claude Code sessions.
---
## Quick Start with VS Code Insiders
**Open the workspace**:
```
Double-click: fred-workspace.code-workspace
```
Or from VS Code: `File → Open Workspace → fred-workspace.code-workspace`
**Start Claude Code**:
```bash
cd C:\Users\Fred\projects
claude
```
**Tell Claude to load context**:
```
"Read .claude-context.md to understand my project structure"
```
---
## Projects Overview
### 🎯 [claude-workflows](claude-workflows/)
**ADHD-friendly productivity tools for Claude Code**
- Slash commands (`/push`, `/eod`)
- ADHD assistant with sidequest detection
- Auto-discovery for cross-project setup
**Start here when**: Working on Claude Code tooling, productivity features
---
### 🏥 [VA-Strategy](VA-Strategy/)
**VA disability claims management system**
- Goal: 100% VA rating via TDIU
- Current: 60% combined (30% highest single)
- Tracking, evidence, statements, forms
**Start here when**: Working on VA claims, medical documentation
**Quick commands**:
```bash
cd VA-Strategy
git status # Check what's changed
cat tracking/master-tracking.md # See current status
```
---
### 🏠 [infrastructure](infrastructure/)
**Home network, Home Assistant, smart home**
- Home Assistant configuration
- ESPHome devices (garage controller, furnace)
- Voice assistant system (GPU-accelerated, local)
- Network infrastructure (MQTT, DNS-over-TLS)
**Start here when**: Working on home automation, voice assistant, ESPHome
**Active subprojects**:
- Voice Assistant: Gaming PC + Surface Go
- Furnace Control: ESP32 planning phase
- Home Assistant: Main config
---
### ⚙️ [config](config/)
**Shared configuration files**
Minimal/placeholder for cross-project configs.
---
### 📚 [claude-code-history](claude-code-history/)
**Background: Claude Code session history**
Session transcripts, state files, stats. Mostly hidden from searches.
---
## Key Files
| File | Purpose |
|------|---------|
| `.claude-context.md` | Master context file - tells Claude about all projects |
| `fred-workspace.code-workspace` | VS Code multi-root workspace |
| `VSCODE-SETUP.md` | Detailed setup guide for VS Code + Claude |
| `README.md` | This file - quick reference |
---
## ADHD-Friendly Workflow
### How Sidequest Detection Works
1. You're working in one project (e.g., VA-Strategy)
2. You start exploring something related to another project (e.g., ESP32 for infrastructure)
3. Claude detects the context shift
4. Claude offers to:
- Track it as a side quest
- Switch projects formally
- Create a new project
- Return to original work
### Example
```
You: [Working in VA-Strategy on headache log]
You: "I wonder if I could automate headache tracking with Home Assistant"
Claude: 🤔 Side quest detected!
Current: VA-Strategy (headache log)
New idea: HA automation (infrastructure)
Options:
1. Continue exploring (I'll track it)
2. Switch to infrastructure project
3. Create new "health-automation" project
4. Return to headache log
```
---
## Common Workflows
### Start Working on a Project
```bash
cd C:\Users\Fred\projects\VA-Strategy
claude
# Tell Claude what you want to work on
```
### Switch Projects Mid-Session
Just tell Claude:
```
"I want to switch to working on infrastructure now"
```
Claude will track the context switch.
### Explore a Side Quest
```
"This is a side quest - I want to explore X for 20 minutes"
```
Claude will set a timer and check in.
### End of Day
```
/eod
```
Claude will:
- Commit your changes
- Show what you accomplished
- Prepare for tomorrow
---
## Setup Checklist
- [x] `.claude-context.md` created
- [x] Workspace file created
- [ ] Open workspace in VS Code Insiders
- [ ] Create ADHD assistant state directory:
```powershell
New-Item -ItemType Directory -Path "$env:USERPROFILE\.claude-assistant" -Force
Copy-Item "claude-workflows\.assistant\state.json.template" "$env:USERPROFILE\.claude-assistant\state.json"
```
- [ ] Start Claude Code session
- [ ] Test sidequest detection
---
## Files You Should Know About
### Global Context
- **`.claude-context.md`** - Tells Claude about all your projects
- **`fred-workspace.code-workspace`** - Multi-root workspace for VS Code
- **`VSCODE-SETUP.md`** - Detailed setup instructions
### ADHD Assistant
- **`claude-workflows/.assistant/personality.md`** - How Claude should behave
- **`claude-workflows/.assistant/state.json.template`** - Session state template
- **`~/.claude-assistant/state.json`** - Your active state file (to be created)
### Project-Specific
- **`VA-Strategy/CLAUDE.md`** - VA project context
- **`VA-Strategy/README.md`** - VA project overview
- **`infrastructure/README.md`** - Infrastructure overview
- **`claude-workflows/README.md`** - Workflows overview
---
## Customization
### Adjust ADHD Assistant Behavior
Edit: `~/.claude-assistant/state.json`
```json
{
"user": {
"preferences": {
"intervention_style": "gentle", // gentle | assertive | minimal
"stuck_threshold": 3, // How many times before intervention
"sidequest_time_limit_minutes": 30, // Check-in time
"celebrates_completions": true // Celebrate wins
}
}
}
```
### Add More Projects
Edit: `fred-workspace.code-workspace`
Add new folder:
```json
{
"path": "new-project",
"name": "📦 New Project"
}
```
---
## Getting Help
### Claude Code
- `/help` - Claude Code help
- Ask Claude: "How does sidequest detection work?"
### Project-Specific
- Each project has a README.md
- VA-Strategy and infrastructure have CLAUDE.md files
### Issues
Report at: https://github.com/anthropics/claude-code/issues
---
## Philosophy
This setup is designed to work **with** ADHD, not against it:
✓ Side quests are valid exploration
✓ Context switching is supported
✓ Progress is celebrated
✓ No judgment on workflow
✓ Gentle nudging, not rigid control
Claude is here to help you stay aware of what you're working on, not to police your focus.
---
**Ready?** Open `fred-workspace.code-workspace` and start a Claude session!

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

26
add-acme-provisioner.py Normal file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/env python3
import json
# Read the config
with open('C:/Users/Fred/AppData/Local/Temp/ca.json', 'r') as f:
config = json.load(f)
# Add ACME provisioner
acme_provisioner = {
"type": "ACME",
"name": "acme",
"forceCN": True,
"claims": {
"maxTLSCertDuration": "8760h",
"defaultTLSCertDuration": "8760h"
}
}
# Add to provisioners list
config['authority']['provisioners'].append(acme_provisioner)
# Write updated config
with open('C:/Users/Fred/AppData/Local/Temp/ca.json', 'w') as f:
json.dump(config, f, indent=4)
print("ACME provisioner added successfully")

29
add-cocktails-to-caddy.sh Normal file
View File

@@ -0,0 +1,29 @@
#!/bin/bash
# Add Bar Assistant (cocktails.nianticbooks.com) to Caddy
echo "Adding cocktails.nianticbooks.com to Caddy..."
sudo tee -a /etc/caddy/Caddyfile > /dev/null << 'EOF'
# Bar Assistant - Cocktail Recipe Manager
cocktails.nianticbooks.com {
reverse_proxy http://10.0.10.40:80 {
header_up X-Forwarded-For {remote_host}
header_up X-Real-IP {remote_host}
}
}
EOF
echo "Configuration added!"
echo ""
echo "Reloading Caddy..."
sudo systemctl reload caddy
echo ""
echo "Testing configuration..."
sleep 3
curl -I https://cocktails.nianticbooks.com 2>&1 | head -5
echo ""
echo "✅ Done! Bar Assistant should now be available at:"
echo " https://cocktails.nianticbooks.com"

71
aider-helpers.ps1 Normal file
View File

@@ -0,0 +1,71 @@
# Aider Helper Functions for Fred's Workflow
# Add to PowerShell profile: . C:\Users\Fred\projects\aider-helpers.ps1
# Quick launch Aider with 7b model (fast, everyday coding)
function aider-fast {
aider --model ollama/qwen2.5-coder:7b-instruct @args
}
# Launch Aider with 14b model (complex tasks, better reasoning)
function aider-smart {
aider --model ollama/qwen2.5-coder:14b-instruct-q4_K_M @args
}
# Launch Aider with architect mode (planning, design)
function aider-plan {
aider --model ollama/qwen2.5-coder:14b-instruct-q4_K_M --architect @args
}
# Quick commit with Aider (use for git commit messages)
function aider-commit {
aider --model ollama/qwen2.5-coder:7b-instruct --commit
}
# Launch Aider in watch mode (auto-reload on file changes)
function aider-watch {
aider --model ollama/qwen2.5-coder:7b-instruct --watch-files @args
}
# Show Aider status and current models
function aider-status {
Write-Host "=== Aider Status ===" -ForegroundColor Cyan
Write-Host ""
Write-Host "Available models:" -ForegroundColor Yellow
ollama list | Select-String "qwen2.5-coder"
Write-Host ""
Write-Host "Quick commands:" -ForegroundColor Yellow
Write-Host " aider-fast - Fast 7b model (everyday coding)"
Write-Host " aider-smart - Powerful 14b model (complex tasks)"
Write-Host " aider-plan - Architect mode (planning)"
Write-Host " aider-commit - Generate commit messages"
Write-Host " aider-watch - Watch mode (auto-reload)"
Write-Host ""
Write-Host "Example usage:" -ForegroundColor Yellow
Write-Host " cd C:\Users\Fred\projects\VA-Strategy"
Write-Host " aider-fast"
Write-Host " > Add a function to parse headache log entries"
}
# Token usage estimator
function aider-estimate {
param(
[Parameter(Mandatory=$false)]
[string]$model = "7b"
)
Write-Host "=== Token Cost Comparison ===" -ForegroundColor Cyan
Write-Host ""
Write-Host "Local Ollama (your setup):" -ForegroundColor Green
Write-Host " Cost: `$0.00 (free!)"
Write-Host " Speed: Fast (8GB RTX 5060)"
Write-Host " Privacy: 100% local"
Write-Host ""
Write-Host "Claude API (Claude Code):" -ForegroundColor Yellow
Write-Host " Cost: ~`$3-15 per million tokens"
Write-Host " Speed: Depends on network"
Write-Host " Limits: Monthly cap"
Write-Host ""
Write-Host "Recommendation: Use Aider for routine coding, Claude for complex architecture" -ForegroundColor Cyan
}
Write-Host "Aider helpers loaded! Type 'aider-status' for quick start." -ForegroundColor Green

20
aider-test.py Normal file
View File

@@ -0,0 +1,20 @@
# Simple calculator functions
from typing import Union
def add(a: int, b: int) -> int:
"""Add two integers."""
return a + b
def subtract(a: int, b: int) -> int:
"""Subtract two integers."""
return a - b
def multiply(a: int, b: int) -> int:
"""Multiply two integers."""
return a * b
def divide(a: Union[int, float], b: Union[int, float]) -> Union[int, float]:
"""Divide two numbers. Raises ValueError if divisor is zero."""
if b == 0:
raise ValueError("Cannot divide by zero.")
return a / b

View File

@@ -0,0 +1,157 @@
# Prometheus Alert Investigation - Feb 3, 2026
## 🔍 Investigation Summary
**Time:** 1:58 PM CST
**Investigator:** OpenClaw (Funky)
**Scope:** 4 hosts showing as DOWN in Prometheus
---
## 📊 Findings
### 1. pve-router (10.0.10.2) - Proxmox Host
**Status:** ⚠️ Host UP, Monitoring DOWN
**Issue:** node_exporter not responding on port 9100
```
✅ ICMP ping: Responding (0.4ms latency)
❌ node_exporter (port 9100): Timeout after 2 seconds
```
**Diagnosis:**
- Host is online and reachable
- node_exporter service likely stopped or not installed
- This is your office Proxmox host (i5)
**Action Required:**
```bash
ssh root@10.0.10.2
systemctl status prometheus-node-exporter
systemctl start prometheus-node-exporter
systemctl enable prometheus-node-exporter
```
---
### 2. vps-gaming (51.222.12.162) - OVH Gaming VPS
**Status:** ⚠️ Host UP, Monitoring DOWN
**Issue:** node_exporter not responding on port 9100
```
✅ ICMP ping: Responding (23.8ms latency - normal for OVH Canada)
❌ node_exporter (port 9100): Timeout after 2 seconds
```
**Diagnosis:**
- Host is online (WireGuard VPN likely working)
- node_exporter either not installed or firewall blocking port 9100
- Provider: OVH (deadeyeg4ming.vip)
**Action Required:**
```bash
ssh root@51.222.12.162
# Check if installed
systemctl status prometheus-node-exporter
# If not installed
apt update && apt install prometheus-node-exporter -y
# Check firewall
ufw status
ufw allow 9100/tcp # If using UFW
```
---
### 3. OpenClaw Gateway (10.0.10.41) - CT 130
**Status:** 🔴 Host DOWN / Missing node_exporter
**Issue:** Container reachable but node_exporter not installed
**Note:** This is the OpenClaw container (me!) - node_exporter should be installed for self-monitoring.
**Action Required:**
```bash
ssh root@10.0.10.41
apt update && apt install prometheus-node-exporter -y
systemctl enable --now prometheus-node-exporter
```
---
### 4. Available Container (10.0.10.42) - CT 131
**Status:** 🟢 Available for use
**Issue:** Container available but not yet deployed
**Note:** This container is available for future use.
---
## 🎯 Priority Action Items
### Critical (Affects Real Monitoring)
1. **Fix pve-router node_exporter** - This is a production Proxmox host
2. **Fix vps-gaming node_exporter** - This is your WireGuard VPN endpoint
### Low Priority (Game Servers)
3. **Decide on minecraft-forge** - Start if needed, or remove from Prometheus config
4. **Decide on minecraft-stoneblock** - Start if needed, or remove from Prometheus config
---
## 🔧 Quick Fix Commands
### For pve-router (10.0.10.2)
```bash
ssh root@10.0.10.2 "apt update && apt install prometheus-node-exporter -y && systemctl enable --now prometheus-node-exporter"
```
### For vps-gaming (51.222.12.162)
```bash
ssh root@51.222.12.162 "apt update && apt install prometheus-node-exporter -y && systemctl enable --now prometheus-node-exporter && ufw allow 9100/tcp"
```
### Clean Up Prometheus Config (Remove Game Servers)
If you don't want to monitor stopped game servers:
```bash
ssh root@10.0.10.25
nano /etc/prometheus/prometheus.yml
# Comment out or remove the minecraft targets (10.0.10.41, 10.0.10.42)
systemctl reload prometheus
```
---
## 📈 Expected Outcome
**After fixes:**
- ✅ 2/4 hosts back online (pve-router, vps-gaming)
- ✅ Only real infrastructure monitored
- ✅ No false positive alerts
- ✅ Inbox stays clean
**Time to fix:** ~5 minutes total
---
## 🚨 Current Alert Status
These hosts are **NOT firing critical Discord alerts** yet because:
- They're in "pending" state (less than 2 minutes down)
- Our threshold is **2+ minutes** before triggering
If you don't fix them, you'll get Discord alerts in:
- **~1-2 minutes** from now (they've been down for a while already)
---
## Notes
- pve-router and vps-gaming are **real issues** - these should be monitored
- Minecraft servers are probably **intentional** - you don't run them 24/7
- Consider removing game servers from Prometheus if you don't want to track them
Let me know if you want me to:
1. Fix the node_exporters remotely (if I have SSH access)
2. Remove game servers from Prometheus config
3. Both!

View File

@@ -0,0 +1,147 @@
# Alertmanager Configuration for Fred's Homelab - UPDATED
# Location: /etc/prometheus/alertmanager.yml
# Updated: 2026-02-03 (Reduced alert noise)
#
# Changes:
# - Only CRITICAL alerts trigger Discord notifications
# - WARNING alerts are logged but NOT sent to notification channels
# - Removed email notifications entirely
global:
resolve_timeout: 5m
# Root route - all alerts enter here
route:
# Group alerts by these labels to reduce noise
group_by: ['alertname', 'severity', 'instance']
# Wait 30s before sending first notification (allows grouping)
group_wait: 30s
# Wait 5min before sending additional alerts for same group
group_interval: 5m
# Resend alert every 12 hours if still firing
repeat_interval: 12h
# Default receiver - drops everything (warnings go here)
receiver: 'null'
# Child routes for specific alert types
routes:
# CRITICAL alerts - send to Discord webhook
- matchers:
- severity="critical"
receiver: 'discord-critical'
group_wait: 10s
repeat_interval: 1h
# WARNING alerts - explicitly drop (logged by Prometheus, not sent)
- matchers:
- severity="warning"
receiver: 'null'
repeat_interval: 24h
# Inhibition rules - prevent alert spam
inhibit_rules:
# If critical alert is firing, suppress warnings for same alert
- source_matchers:
- severity="critical"
target_matchers:
- severity="warning"
equal: ['alertname', 'instance']
# If host is down, suppress all other alerts from that host
- source_matchers:
- alertname="HostDown"
target_matchers:
- alertname!="HostDown"
equal: ['instance']
# Receivers - define where alerts go
receivers:
# Null receiver - drops alerts (used for warnings)
- name: 'null'
# Discord webhook for CRITICAL alerts
- name: 'discord-critical'
webhook_configs:
- url: 'https://discord.com/api/webhooks/1462667503301038285/ZVJDuek6VADA-RdI09xJDvqjveOWXgxQnMBcsQzoKwVPnNOACMCL5v-HN55-KVe4IZY0'
send_resolved: true
http_config:
follow_redirects: true
max_alerts: 0 # Send all alerts (no limit)
# ====================================
# Deployment Instructions
# ====================================
#
# 1. Backup existing config:
# ssh root@10.0.10.25 'cp /etc/prometheus/alertmanager.yml /etc/prometheus/alertmanager.yml.backup'
#
# 2. Upload this file:
# scp alertmanager-config-updated.yml root@10.0.10.25:/etc/prometheus/alertmanager.yml
#
# 3. Upload updated alert rules:
# scp prometheus-alert-rules-updated.yml root@10.0.10.25:/etc/prometheus/rules/homelab-alerts.yml
#
# 4. Reload Alertmanager:
# ssh root@10.0.10.25 'systemctl reload prometheus-alertmanager'
#
# 5. Reload Prometheus:
# ssh root@10.0.10.25 'systemctl reload prometheus'
#
# 6. Verify configuration:
# curl http://10.0.10.25:9093/api/v1/status
# curl http://10.0.10.25:9090/api/v1/rules
#
# 7. Test Discord webhook:
# curl -X POST http://10.0.10.25:9093/api/v1/alerts -d '[
# {
# "labels": {
# "alertname": "TestCriticalAlert",
# "severity": "critical",
# "instance": "test:9100"
# },
# "annotations": {
# "summary": "Test alert - please ignore"
# }
# }
# ]'
#
# ====================================
# Alert Flow Summary
# ====================================
#
# CRITICAL alerts:
# Prometheus → Alertmanager → Discord Webhook → Your Discord Server
#
# WARNING alerts:
# Prometheus → Alertmanager → null receiver (logged, not sent)
#
# You can view WARNING alerts in:
# - Prometheus UI: http://10.0.10.25:9090/alerts
# - Alertmanager UI: http://10.0.10.25:9093/#/alerts
#
# ====================================
# Expected Behavior After Update
# ====================================
#
# Your Discord will ONLY receive:
# ✅ Host completely down (HostDown)
# ✅ CPU >95% for 5 minutes (CriticalCPUUsage)
# ✅ Memory >95% for 5 minutes (CriticalMemoryUsage)
# ✅ Disk <5% free (DiskSpaceCritical)
# ✅ Proxmox node down (ProxmoxNodeDown)
# ✅ PostgreSQL down (PostgreSQLDown)
# ✅ VPS unreachable (VPSDown)
# ✅ Prometheus config reload failed
#
# Your inbox will receive:
# 🚫 NOTHING - all email notifications disabled
#
# Warnings (CPU 80-95%, memory 85-95%, etc.):
# 📊 Logged in Prometheus/Alertmanager UI only
#
# This should dramatically reduce notification noise while still
# catching critical issues that need immediate attention.

View File

@@ -0,0 +1,42 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Directory listing for /bible-reading-plan/</title>
</head>
<body>
<h1>Directory listing for /bible-reading-plan/</h1>
<hr>
<ul>
<li><a href=".claude/">.claude/</a></li>
<li><a href=".git/">.git/</a></li>
<li><a href=".gitignore">.gitignore</a></li>
<li><a href="ARCHITECTURE.md">ARCHITECTURE.md</a></li>
<li><a href="backend/">backend/</a></li>
<li><a href="CHANGES.md">CHANGES.md</a></li>
<li><a href="CLAUDE.md">CLAUDE.md</a></li>
<li><a href="data/">data/</a></li>
<li><a href="deploy-to-vps.sh">deploy-to-vps.sh</a></li>
<li><a href="DEPLOY_QUICKSTART.md">DEPLOY_QUICKSTART.md</a></li>
<li><a href="DEPLOY_TO_VPS.md">DEPLOY_TO_VPS.md</a></li>
<li><a href="deployment/">deployment/</a></li>
<li><a href="docker-compose.yml">docker-compose.yml</a></li>
<li><a href="Dockerfile.backend">Dockerfile.backend</a></li>
<li><a href="Dockerfile.frontend">Dockerfile.frontend</a></li>
<li><a href="frontend/">frontend/</a></li>
<li><a href="nginx.conf">nginx.conf</a></li>
<li><a href="PROJECT_SUMMARY.md">PROJECT_SUMMARY.md</a></li>
<li><a href="QUICKSTART.md">QUICKSTART.md</a></li>
<li><a href="README.md">README.md</a></li>
<li><a href="READY_TO_DEPLOY.md">READY_TO_DEPLOY.md</a></li>
<li><a href="Scaled%20M%27Cheyne%20Bible%20Reading%20Plan.xlsx">Scaled M'Cheyne Bible Reading Plan.xlsx</a></li>
<li><a href="Screenshot%202025-12-28%20140941.png">Screenshot 2025-12-28 140941.png</a></li>
<li><a href="setup.bat">setup.bat</a></li>
<li><a href="setup.sh">setup.sh</a></li>
<li><a href="SETUP_COMPLETE.md">SETUP_COMPLETE.md</a></li>
<li><a href="start-dev.bat">start-dev.bat</a></li>
<li><a href="voice-assistants/">voice-assistants/</a></li>
</ul>
<hr>
</body>
</html>

19
blackmagic/index.html Normal file
View File

@@ -0,0 +1,19 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Directory listing for /blackmagic/</title>
</head>
<body>
<h1>Directory listing for /blackmagic/</h1>
<hr>
<ul>
<li><a href=".claude/">.claude/</a></li>
<li><a href=".git/">.git/</a></li>
<li><a href="atem-config/">atem-config/</a></li>
<li><a href="church-video-production-docs/">church-video-production-docs/</a></li>
<li><a href="Gemini/">Gemini/</a></li>
</ul>
<hr>
</body>
</html>

View File

@@ -0,0 +1,13 @@
# Home Assistant - Version 2 (without Host header override)
bob.nianticbooks.com {
reverse_proxy https://10.0.10.24:8123 {
header_up X-Forwarded-Host {host}
header_up X-Forwarded-Proto {scheme}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
transport http {
tls_insecure_skip_verify
}
}
}

View File

@@ -0,0 +1,14 @@
# Home Assistant
bob.nianticbooks.com {
reverse_proxy https://10.0.10.24:8123 {
header_up Host {http.reverse_proxy.upstream.hostport}
header_up X-Forwarded-Host {host}
header_up X-Forwarded-Proto {scheme}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
transport http {
tls_insecure_skip_verify
}
}
}

23
claude-shared/index.html Normal file
View File

@@ -0,0 +1,23 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Directory listing for /claude-shared/</title>
</head>
<body>
<h1>Directory listing for /claude-shared/</h1>
<hr>
<ul>
<li><a href=".assistant/">.assistant/</a></li>
<li><a href="commands/">commands/</a></li>
<li><a href="GAMING-RIG-SETUP.md">GAMING-RIG-SETUP.md</a></li>
<li><a href="homelab.code-workspace">homelab.code-workspace</a></li>
<li><a href="QUICK-START.md">QUICK-START.md</a></li>
<li><a href="README.md">README.md</a></li>
<li><a href="SETUP-GUIDE.md">SETUP-GUIDE.md</a></li>
<li><a href="setup-symlinks.ps1">setup-symlinks.ps1</a></li>
<li><a href="setup-symlinks.sh">setup-symlinks.sh</a></li>
</ul>
<hr>
</body>
</html>

32
copy-to-omv.sh Normal file
View File

@@ -0,0 +1,32 @@
#!/bin/bash
# Copy deployment scripts to OMV share
# Run this from your PC/terminal where you have SSH access to Proxmox
PROXMOX_HOST="10.0.10.3"
OPENCLAW_HOST="10.0.10.28"
OMV_SCRIPTS="/mnt/omv-backups/scripts"
echo "📋 Copying deployment scripts to OMV share..."
echo ""
# Create scripts directory on Proxmox/OMV
echo "Creating scripts directory..."
ssh root@${PROXMOX_HOST} "mkdir -p ${OMV_SCRIPTS}"
# Copy deployment script from OpenClaw to OMV
echo "Copying deploy-inline.sh..."
ssh root@${OPENCLAW_HOST} "cat /root/.openclaw/workspace/fred-infrastructure/deploy-inline.sh" | \
ssh root@${PROXMOX_HOST} "cat > ${OMV_SCRIPTS}/deploy-prometheus-alerts.sh"
# Make executable
ssh root@${PROXMOX_HOST} "chmod +x ${OMV_SCRIPTS}/deploy-prometheus-alerts.sh"
echo ""
echo "✅ Script copied to OMV share!"
echo ""
echo "Location: ${PROXMOX_HOST}:${OMV_SCRIPTS}/deploy-prometheus-alerts.sh"
echo ""
echo "To deploy the alert fix, SSH to Proxmox and run:"
echo " ssh root@${PROXMOX_HOST}"
echo " bash ${OMV_SCRIPTS}/deploy-prometheus-alerts.sh"
echo ""

203
deploy-inline.sh Normal file
View File

@@ -0,0 +1,203 @@
#!/bin/bash
# Deploy reduced alerts - Run this from your PC/terminal
# Usage: bash deploy-inline.sh
echo "🚀 Deploying alert spam fix to Prometheus..."
echo ""
# Execute inside Prometheus container via Proxmox
ssh root@10.0.10.3 "pct exec 125 -- bash -s" << 'SCRIPT_END'
set -e
echo "📦 Backing up configs..."
mkdir -p /etc/prometheus/backups
cp /etc/prometheus/alertmanager.yml /etc/prometheus/backups/alertmanager.yml.$(date +%Y%m%d-%H%M%S) 2>/dev/null || true
cp /etc/prometheus/rules/homelab-alerts.yml /etc/prometheus/backups/homelab-alerts.yml.$(date +%Y%m%d-%H%M%S) 2>/dev/null || true
echo "✅ Backups saved"
echo "📝 Installing new Alertmanager config..."
cat > /etc/prometheus/alertmanager.yml << 'EOF'
global:
resolve_timeout: 5m
route:
group_by: ['alertname', 'severity', 'instance']
group_wait: 30s
group_interval: 5m
repeat_interval: 12h
receiver: 'null'
routes:
- matchers:
- severity="critical"
receiver: 'discord-critical'
group_wait: 10s
repeat_interval: 1h
- matchers:
- severity="warning"
receiver: 'null'
repeat_interval: 24h
inhibit_rules:
- source_matchers:
- severity="critical"
target_matchers:
- severity="warning"
equal: ['alertname', 'instance']
- source_matchers:
- alertname="HostDown"
target_matchers:
- alertname!="HostDown"
equal: ['instance']
receivers:
- name: 'null'
- name: 'discord-critical'
webhook_configs:
- url: 'https://discord.com/api/webhooks/1462667503301038285/ZVJDuek6VADA-RdI09xJDvqjveOWXgxQnMBcsQzoKwVPnNOACMCL5v-HN55-KVe4IZY0'
send_resolved: true
http_config:
follow_redirects: true
max_alerts: 0
EOF
echo "✅ Alertmanager config updated"
echo "📝 Installing new alert rules..."
cat > /etc/prometheus/rules/homelab-alerts.yml << 'EOF'
groups:
- name: infrastructure
interval: 30s
rules:
- alert: HostDown
expr: up == 0
for: 2m
labels:
severity: critical
category: infrastructure
annotations:
summary: "Host {{ $labels.instance }} is down"
description: "{{ $labels.instance }} has been unreachable for 2+ minutes"
- alert: HighCPUUsage
expr: 100 - (avg by(instance, hostname) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
for: 5m
labels:
severity: warning
category: performance
annotations:
summary: "High CPU on {{ $labels.hostname }}"
description: "CPU {{ $value | humanize }}%"
- alert: CriticalCPUUsage
expr: 100 - (avg by(instance, hostname) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 95
for: 5m
labels:
severity: critical
category: performance
annotations:
summary: "CRITICAL CPU on {{ $labels.hostname }}"
description: "CPU {{ $value | humanize }}%"
- alert: HighMemoryUsage
expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 85
for: 10m
labels:
severity: warning
category: performance
annotations:
summary: "High memory on {{ $labels.hostname }}"
description: "Memory {{ $value | humanize }}%"
- alert: CriticalMemoryUsage
expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 95
for: 5m
labels:
severity: critical
category: performance
annotations:
summary: "CRITICAL memory on {{ $labels.hostname }}"
description: "Memory {{ $value | humanize }}%"
- name: storage
interval: 1m
rules:
- alert: DiskSpaceLow
expr: (node_filesystem_avail_bytes{fstype!~"tmpfs|fuse.lxcfs|squashfs|overlay"} / node_filesystem_size_bytes) * 100 < 15
for: 5m
labels:
severity: warning
category: storage
annotations:
summary: "Low disk on {{ $labels.hostname }}"
description: "{{ $labels.mountpoint }} has {{ $value | humanize }}% free"
- alert: DiskSpaceCritical
expr: (node_filesystem_avail_bytes{fstype!~"tmpfs|fuse.lxcfs|squashfs|overlay"} / node_filesystem_size_bytes) * 100 < 5
for: 2m
labels:
severity: critical
category: storage
annotations:
summary: "CRITICAL disk on {{ $labels.hostname }}"
description: "{{ $labels.mountpoint }} only {{ $value | humanize }}% free!"
- name: services
interval: 1m
rules:
- alert: PostgreSQLDown
expr: up{app="postgres"} == 0
for: 2m
labels:
severity: critical
category: database
annotations:
summary: "PostgreSQL is down"
description: "PostgreSQL down for 2+ minutes"
- alert: VPSDown
expr: up{role="vps"} == 0
for: 2m
labels:
severity: critical
category: infrastructure
annotations:
summary: "VPS unreachable"
description: "VPS down for 2+ minutes"
- alert: ProxmoxNodeDown
expr: up{role="proxmox-host"} == 0
for: 2m
labels:
severity: critical
category: infrastructure
annotations:
summary: "Proxmox node down"
description: "Proxmox host unreachable for 2+ minutes"
EOF
echo "✅ Alert rules updated"
echo "🔄 Reloading services..."
systemctl reload prometheus
systemctl reload prometheus-alertmanager
echo ""
echo "✅ DEPLOYMENT COMPLETE!"
echo ""
echo "📊 Changes applied:"
echo " • CPU warning: 80%+ over 5min (logged only)"
echo " • CPU critical: 95%+ over 5min (Discord)"
echo " • Only CRITICAL alerts → Discord"
echo " • WARNING alerts → Logged, not sent"
echo " • Email notifications → DISABLED"
echo ""
echo "Your inbox spam should STOP immediately!"
echo ""
echo "🧪 Test Discord webhook:"
echo " curl -X POST http://localhost:9093/api/v1/alerts -d '[{\"labels\":{\"alertname\":\"Test\",\"severity\":\"critical\"},\"annotations\":{\"summary\":\"Test alert\"}}]'"
SCRIPT_END
echo ""
echo "🎉 Done! Check your Discord for the test alert."

82
deploy-reduced-alerts.sh Executable file
View File

@@ -0,0 +1,82 @@
#!/bin/bash
# Deploy Reduced Alert Configuration
# Updates Prometheus alert rules and Alertmanager config to reduce notification noise
# Only CRITICAL alerts trigger Discord notifications
set -e
PROMETHEUS_HOST="10.0.10.25"
PROMETHEUS_USER="root"
echo "🚀 Deploying reduced alert configuration to Prometheus..."
echo ""
# Check if files exist
if [ ! -f "prometheus-alert-rules-updated.yml" ]; then
echo "❌ Error: prometheus-alert-rules-updated.yml not found"
exit 1
fi
if [ ! -f "alertmanager-config-updated.yml" ]; then
echo "❌ Error: alertmanager-config-updated.yml not found"
exit 1
fi
# Backup existing configs
echo "📦 Backing up existing configurations..."
ssh ${PROMETHEUS_USER}@${PROMETHEUS_HOST} 'mkdir -p /etc/prometheus/backups'
ssh ${PROMETHEUS_USER}@${PROMETHEUS_HOST} "cp /etc/prometheus/alertmanager.yml /etc/prometheus/backups/alertmanager.yml.$(date +%Y%m%d-%H%M%S)" 2>/dev/null || true
ssh ${PROMETHEUS_USER}@${PROMETHEUS_HOST} "cp /etc/prometheus/rules/homelab-alerts.yml /etc/prometheus/backups/homelab-alerts.yml.$(date +%Y%m%d-%H%M%S)" 2>/dev/null || true
echo "✅ Backups created in /etc/prometheus/backups/"
echo ""
# Upload new configs
echo "📤 Uploading new configurations..."
scp alertmanager-config-updated.yml ${PROMETHEUS_USER}@${PROMETHEUS_HOST}:/etc/prometheus/alertmanager.yml
scp prometheus-alert-rules-updated.yml ${PROMETHEUS_USER}@${PROMETHEUS_HOST}:/etc/prometheus/rules/homelab-alerts.yml
echo "✅ Files uploaded"
echo ""
# Reload services
echo "🔄 Reloading Prometheus and Alertmanager..."
ssh ${PROMETHEUS_USER}@${PROMETHEUS_HOST} 'systemctl reload prometheus'
ssh ${PROMETHEUS_USER}@${PROMETHEUS_HOST} 'systemctl reload prometheus-alertmanager'
echo "✅ Services reloaded"
echo ""
# Verify configuration
echo "🔍 Verifying configuration..."
echo ""
echo "Prometheus status:"
ssh ${PROMETHEUS_USER}@${PROMETHEUS_HOST} 'systemctl status prometheus --no-pager -l | head -10'
echo ""
echo "Alertmanager status:"
ssh ${PROMETHEUS_USER}@${PROMETHEUS_HOST} 'systemctl status prometheus-alertmanager --no-pager -l | head -10'
echo ""
# Test API endpoints
echo "Testing API endpoints..."
echo ""
echo "Prometheus rules API:"
curl -s http://${PROMETHEUS_HOST}:9090/api/v1/rules | head -200
echo ""
echo "Alertmanager status API:"
curl -s http://${PROMETHEUS_HOST}:9093/api/v1/status | head -100
echo ""
echo "✅ Deployment complete!"
echo ""
echo "📊 Summary of changes:"
echo " • CPU alert threshold: 80%+ over 5 minutes (warning)"
echo " • CPU critical threshold: 95%+ over 5 minutes (notification)"
echo " • Only CRITICAL alerts sent to Discord"
echo " • WARNING alerts logged but NOT sent"
echo " • Email notifications completely disabled"
echo ""
echo "🔗 Check alert status:"
echo " Prometheus: http://${PROMETHEUS_HOST}:9090/alerts"
echo " Alertmanager: http://${PROMETHEUS_HOST}:9093/#/alerts"
echo ""
echo "🧪 Test critical alert (sends to Discord):"
echo " curl -X POST http://${PROMETHEUS_HOST}:9093/api/v1/alerts -d '[{\"labels\":{\"alertname\":\"TestCriticalAlert\",\"severity\":\"critical\",\"instance\":\"test:9100\"},\"annotations\":{\"summary\":\"Test - please ignore\"}}]'"
echo ""

134
deploy-uptime-kuma.sh Normal file
View File

@@ -0,0 +1,134 @@
#!/bin/bash
# Deploy Uptime Kuma on main-pve
# LXC Container 128 at 10.0.10.26
set -e
echo "=== Uptime Kuma Deployment Script ==="
echo ""
# Configuration
VMID=128
HOSTNAME="uptime-kuma"
IP="10.0.10.26"
GATEWAY="10.0.10.1"
CORES=2
MEMORY=2048
SWAP=512
DISK="8"
TEMPLATE="local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst"
echo "Configuration:"
echo " VMID: $VMID"
echo " Hostname: $HOSTNAME"
echo " IP: $IP/24"
echo " Resources: ${CORES} cores, ${MEMORY}MB RAM, ${DISK}GB disk"
echo ""
# Check if container already exists
if ssh root@10.0.10.3 "pct status $VMID 2>/dev/null"; then
echo "⚠️ Container $VMID already exists!"
echo ""
read -p "Delete and recreate? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "Stopping and removing container $VMID..."
ssh root@10.0.10.3 "pct stop $VMID 2>/dev/null || true"
sleep 2
ssh root@10.0.10.3 "pct destroy $VMID"
echo "✅ Container removed"
else
echo "❌ Aborted"
exit 1
fi
fi
echo "Creating LXC container..."
ssh root@10.0.10.3 << EOF
pct create $VMID $TEMPLATE \
--hostname $HOSTNAME \
--cores $CORES \
--memory $MEMORY \
--swap $SWAP \
--net0 name=eth0,bridge=vmbr0,ip=$IP/24,gw=$GATEWAY \
--storage local-lvm \
--rootfs 8 \
--unprivileged 1 \
--features nesting=1
echo "✅ Container created"
echo ""
echo "Starting container..."
pct start $VMID
sleep 5
echo "✅ Container started"
echo ""
echo "Installing Docker..."
pct exec $VMID -- bash -c '
apt update
apt install -y docker.io curl
systemctl enable docker
systemctl start docker
'
echo "✅ Docker installed"
echo ""
echo "Deploying Uptime Kuma..."
pct exec $VMID -- bash -c "
docker run -d \
--name uptime-kuma \
--restart=always \
-p 3001:3001 \
-v uptime-kuma:/app/data \
louislam/uptime-kuma:2
echo 'Waiting for Uptime Kuma to start...'
sleep 10
# Verify container is running
if docker ps | grep -q uptime-kuma; then
echo '✅ Uptime Kuma container running'
else
echo '❌ Uptime Kuma failed to start'
docker logs uptime-kuma
exit 1
fi
"
echo ""
echo "Testing Uptime Kuma endpoint..."
sleep 5
EOF
# Test from host
if curl -s -o /dev/null -w "%{http_code}" http://$IP:3001 | grep -q 200; then
echo "✅ Uptime Kuma is responding!"
else
echo "⚠️ Uptime Kuma not responding yet (may need more time to start)"
fi
echo ""
echo "=== Deployment Complete! ==="
echo ""
echo "🎉 Uptime Kuma deployed successfully!"
echo ""
echo "Access Uptime Kuma:"
echo " URL: http://$IP:3001"
echo " Initial setup required on first visit"
echo ""
echo "Next steps:"
echo "1. Go to http://$IP:3001"
echo "2. Create admin account"
echo "3. Add monitors for your services"
echo "4. Create status page"
echo ""
echo "Container management:"
echo " Start: ssh root@10.0.10.3 'pct start $VMID'"
echo " Stop: ssh root@10.0.10.3 'pct stop $VMID'"
echo " Shell: ssh root@10.0.10.3 'pct enter $VMID'"
echo " Logs: ssh root@10.0.10.3 'pct exec $VMID -- docker logs uptime-kuma -f'"
echo ""

View File

@@ -0,0 +1,36 @@
{
"folders": [
{
"name": "🎯 Claude Workflows",
"path": "claude-workflows"
},
{
"name": "🏥 VA Strategy",
"path": "VA-Strategy"
},
{
"name": "🏠 Infrastructure",
"path": "infrastructure"
},
{
"name": "⚙️ Config",
"path": "config"
}
],
"settings": {
"files.exclude": {
"**/node_modules": true,
"**/.git": false,
"**/claude-code-history": true
},
"search.exclude": {
"**/node_modules": true,
"**/claude-code-history": true
},
"files.watcherExclude": {
"**/node_modules/**": true,
"**/claude-code-history/**": true
},
"powershell.cwd": "🏠 Infrastructure"
}
}

View File

@@ -0,0 +1,36 @@
{
"folders": [
{
"path": "claude-workflows",
"name": "🎯 Claude Workflows"
},
{
"path": "VA-Strategy",
"name": "🏥 VA Strategy"
},
{
"path": "infrastructure",
"name": "🏠 Infrastructure"
},
{
"path": "config",
"name": "⚙️ Config"
}
],
"settings": {
"files.exclude": {
"**/node_modules": true,
"**/.git": false,
"**/claude-code-history": true
},
"search.exclude": {
"**/node_modules": true,
"**/claude-code-history": true
},
"files.watcherExclude": {
"**/node_modules/**": true,
"**/claude-code-history/**": true
},
"powershell.cwd": "🏠 Infrastructure"
}
}

Some files were not shown because too many files have changed in this diff Show More