Initial Filamenteka setup - Bambu Lab filament tracker

- React + TypeScript frontend with automatic color coding
- Confluence API integration for data sync
- AWS Amplify deployment with Terraform
- Support for all Bambu Lab filament colors including gradients
This commit is contained in:
DaX
2025-06-17 22:39:35 +02:00
parent 8cc137864b
commit c394d94bb0
23 changed files with 1090 additions and 0 deletions

163
src/pages/api/filaments.ts Normal file
View File

@@ -0,0 +1,163 @@
import axios from 'axios';
import type { Filament } from '../../types/filament';
// Mock data for development - replace with actual Confluence API integration
const mockFilaments: Filament[] = [
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Mistletoe Green", refill: "", vakum: "vakuum x1", otvoreno: "otvorena x1", kolicina: "2", cena: "" },
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Indigo Purple", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Black", refill: "", vakum: "", otvoreno: "2x otvorena", kolicina: "2", cena: "" },
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Black", refill: "Da", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" },
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Jade White", refill: "", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" },
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Gray", refill: "", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" },
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Red", refill: "", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" },
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Hot Pink", refill: "", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" },
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Cocoa Brown", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "White", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Cotton Candy Cloud", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Sunflower Yellow", refill: "", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" },
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Yellow", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Magenta", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Beige", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
{ brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Cyan", refill: "", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" },
{ brand: "BambuLab", tip: "PLA", finish: "Matte", boja: "Scarlet Red", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
{ brand: "BambuLab", tip: "PLA", finish: "Matte", boja: "Mandarin Orange", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
{ brand: "BambuLab", tip: "PLA", finish: "Matte", boja: "Marine Blue", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
{ brand: "BambuLab", tip: "PLA", finish: "Matte", boja: "Charcoal", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" },
{ brand: "BambuLab", tip: "PLA", finish: "Matte", boja: "Ivory White", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" }
];
interface ConfluencePageContent {
body: {
storage?: {
value: string;
};
view?: {
value: string;
};
};
}
async function fetchFromConfluence(): Promise<Filament[]> {
const confluenceUrl = process.env.CONFLUENCE_API_URL;
const confluenceToken = process.env.CONFLUENCE_TOKEN;
const pageId = process.env.CONFLUENCE_PAGE_ID;
if (!confluenceUrl || !confluenceToken || !pageId) {
console.warn('Confluence configuration missing, using mock data');
return mockFilaments;
}
try {
const response = await axios.get<ConfluencePageContent>(
`${confluenceUrl}/rest/api/content/${pageId}?expand=body.storage`,
{
headers: {
'Authorization': `Bearer ${confluenceToken}`,
'Accept': 'application/json'
}
}
);
const htmlContent = response.data.body.storage?.value || '';
return parseConfluenceTable(htmlContent);
} catch (error) {
console.error('Failed to fetch from Confluence:', error);
return mockFilaments;
}
}
function parseConfluenceTable(html: string): Filament[] {
// Simple HTML table parser - in production, use a proper HTML parser like cheerio
const filaments: Filament[] = [];
// Extract table rows using regex (simplified for example)
const tableMatch = html.match(/<table[^>]*>([\s\S]*?)<\/table>/);
if (!tableMatch) return mockFilaments;
const rowMatches = tableMatch[1].matchAll(/<tr[^>]*>([\s\S]*?)<\/tr>/g);
let isHeaderRow = true;
for (const rowMatch of rowMatches) {
if (isHeaderRow) {
isHeaderRow = false;
continue;
}
const cellMatches = [...rowMatch[1].matchAll(/<td[^>]*>([\s\S]*?)<\/td>/g)];
if (cellMatches.length >= 9) {
filaments.push({
brand: stripHtml(cellMatches[0][1]),
tip: stripHtml(cellMatches[1][1]),
finish: stripHtml(cellMatches[2][1]),
boja: stripHtml(cellMatches[3][1]),
refill: stripHtml(cellMatches[4][1]),
vakum: stripHtml(cellMatches[5][1]),
otvoreno: stripHtml(cellMatches[6][1]),
kolicina: stripHtml(cellMatches[7][1]),
cena: stripHtml(cellMatches[8][1])
});
}
}
return filaments.length > 0 ? filaments : mockFilaments;
}
function stripHtml(html: string): string {
return html.replace(/<[^>]*>/g, '').trim();
}
export async function handler(event: any) {
// For AWS Amplify
if (event.httpMethod !== 'GET') {
return {
statusCode: 405,
body: JSON.stringify({ error: 'Method not allowed' })
};
}
try {
const filaments = await fetchFromConfluence();
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'max-age=300' // 5 minutes cache
},
body: JSON.stringify(filaments)
};
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({
error: 'Failed to fetch filaments',
message: error instanceof Error ? error.message : 'Unknown error'
})
};
}
}
// For local development with Vite
export default async function(req: Request): Promise<Response> {
if (req.method !== 'GET') {
return new Response('Method not allowed', { status: 405 });
}
try {
const filaments = await fetchFromConfluence();
return new Response(JSON.stringify(filaments), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'max-age=300'
}
});
} catch (error) {
return new Response(JSON.stringify({
error: 'Failed to fetch filaments',
message: error instanceof Error ? error.message : 'Unknown error'
}), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
}