diff --git a/api/index.php b/api/index.php new file mode 100644 index 0000000..5dfd6c5 --- /dev/null +++ b/api/index.php @@ -0,0 +1,20 @@ + 'Unknown action', + 'action' => $action, +], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + diff --git a/config/config.php b/config/config.php new file mode 100644 index 0000000..ff23c1e --- /dev/null +++ b/config/config.php @@ -0,0 +1,13 @@ + { + cacheDom(); + bindEvents(); + await loadBootstrap(); +}); + +function cacheDom() { + ui.templateSelect = document.getElementById("rack-template-select"); + ui.projectName = document.getElementById("project-name"); + ui.componentFilter = document.getElementById("component-filter"); + ui.pluginInput = document.getElementById("plugin-input"); + ui.componentLibrary = document.getElementById("component-library"); + ui.rackSummary = document.getElementById("rack-summary"); + ui.rackGrid = document.getElementById("rack-grid"); + ui.projectStats = document.getElementById("project-stats"); + ui.bomOutput = document.getElementById("bom-output"); + ui.cableFrom = document.getElementById("cable-from"); + ui.cableTo = document.getElementById("cable-to"); + ui.cableSlack = document.getElementById("cable-slack"); + ui.cableOutput = document.getElementById("cable-output"); + ui.validationOutput = document.getElementById("validation-output"); + ui.exportBom = document.getElementById("export-bom"); +} + +function bindEvents() { + ui.templateSelect.addEventListener("change", () => { + state.rackTemplateId = ui.templateSelect.value; + state.placedItems = []; + renderAll(); + }); + + ui.projectName.addEventListener("input", () => { + state.projectName = ui.projectName.value.trim() || "Neues Rack-Projekt"; + renderSummary(); + }); + + ui.componentFilter.addEventListener("input", renderLibrary); + ui.cableSlack.addEventListener("input", renderCableEstimate); + ui.cableFrom.addEventListener("change", renderCableEstimate); + ui.cableTo.addEventListener("change", renderCableEstimate); + ui.pluginInput.addEventListener("change", importPluginPack); + ui.exportBom.addEventListener("click", copyBomCsv); +} + +async function loadBootstrap() { + const response = await fetch(window.APP_CONFIG.apiBootstrapUrl, { + headers: { + Accept: "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`Bootstrap request failed: ${response.status}`); + } + + state.bootstrap = await response.json(); + state.components = [...state.bootstrap.components]; + state.rackTemplateId = state.bootstrap.rackTemplates[0]?.id ?? null; + + renderTemplateOptions(); + renderAll(); +} + +function renderAll() { + renderSummary(); + renderLibrary(); + renderRack(); + renderStats(); + renderBom(); + renderCableSelectors(); + renderCableEstimate(); + renderValidation(); +} + +function renderTemplateOptions() { + const options = state.bootstrap.rackTemplates + .map((template) => { + const selected = template.id === state.rackTemplateId ? " selected" : ""; + return ``; + }) + .join(""); + + ui.templateSelect.innerHTML = options; +} + +function renderSummary() { + const rack = getCurrentRackTemplate(); + if (!rack) { + ui.rackSummary.innerHTML = ""; + return; + } + + ui.rackSummary.innerHTML = ` +
+ Projekt + ${escapeHtml(state.projectName)} +
+
+ Rack-Standard + ${rack.rackStandard === "19_inch" ? "19 inch" : "10 inch"} +
+
+ Kapazitaet + ${rack.totalU}U / ${rack.usableDepthMm} mm +
+ `; +} + +function renderLibrary() { + const rack = getCurrentRackTemplate(); + if (!rack) { + return; + } + + const needle = ui.componentFilter.value.trim().toLowerCase(); + const filtered = state.components.filter((component) => { + const compatible = component.rackStandard === rack.rackStandard; + const haystack = `${component.name} ${component.category} ${component.partNumber}`.toLowerCase(); + return compatible && (!needle || haystack.includes(needle)); + }); + + if (filtered.length === 0) { + ui.componentLibrary.innerHTML = `
Keine passenden Komponenten gefunden.
`; + return; + } + + ui.componentLibrary.innerHTML = filtered + .map( + (component) => ` +
+
+
+ ${escapeHtml(component.name)} +
${escapeHtml(component.partNumber)}
+
+ ${component.heightU}U +
+
+ ${escapeHtml(component.category)} + ${component.depthMm} mm tief + ${formatCurrency(component.priceNet, component.currency)} +
+ +
+ ` + ) + .join(""); + + ui.componentLibrary.querySelectorAll("[data-action='add-component']").forEach((button) => { + button.addEventListener("click", () => addComponentToRack(button.dataset.componentId)); + }); +} + +function addComponentToRack(componentId) { + const component = state.components.find((entry) => entry.id === componentId); + const rack = getCurrentRackTemplate(); + if (!component || !rack) { + return; + } + + const position = findFirstFreePosition(component.heightU); + if (position === null) { + alert("Keine freie Position fuer diese Komponente im aktuellen Rack."); + return; + } + + state.placedItems.push({ + placementId: `p${state.nextPlacementId++}`, + componentId: component.id, + startU: position, + }); + + renderAll(); +} + +function renderRack() { + const rack = getCurrentRackTemplate(); + if (!rack) { + return; + } + + ui.rackGrid.style.setProperty("--rack-unit-height", "38px"); + + const slots = []; + for (let u = rack.totalU; u >= 1; u -= 1) { + slots.push(` +
+ ${u}U +
+ `); + } + + ui.rackGrid.innerHTML = `${slots.join("")}
`; + + ui.rackGrid.querySelectorAll(".rack-slot").forEach((slot) => { + slot.addEventListener("dragover", (event) => { + event.preventDefault(); + slot.classList.add("is-drop-target"); + }); + slot.addEventListener("dragleave", () => slot.classList.remove("is-drop-target")); + slot.addEventListener("drop", (event) => { + event.preventDefault(); + slot.classList.remove("is-drop-target"); + if (!state.dragPlacementId) { + return; + } + movePlacedItem(state.dragPlacementId, Number(slot.dataset.slotU)); + }); + }); + + const layer = document.getElementById("rack-items-layer"); + const rackHeight = rack.totalU * 38; + layer.style.height = `${rackHeight}px`; + + state.placedItems.forEach((item) => { + const component = getComponent(item.componentId); + if (!component) { + return; + } + + const top = (rack.totalU - (item.startU + component.heightU) + 1) * 38; + const height = component.heightU * 38 - 2; + const element = document.createElement("article"); + element.className = "rack-item"; + element.dataset.standard = component.rackStandard; + element.draggable = true; + element.style.top = `${top}px`; + element.style.height = `${height}px`; + element.innerHTML = ` +
+
+ ${escapeHtml(component.name)} +
${component.heightU}U · ${component.depthMm} mm · ${formatCurrency(component.priceNet, component.currency)}
+
+ ${item.startU}U +
+
+ + + +
+ `; + + element.addEventListener("dragstart", () => { + state.dragPlacementId = item.placementId; + element.classList.add("is-dragging"); + }); + element.addEventListener("dragend", () => { + state.dragPlacementId = null; + element.classList.remove("is-dragging"); + }); + element.querySelector("[data-action='move-up']").addEventListener("click", () => movePlacedItem(item.placementId, item.startU + 1)); + element.querySelector("[data-action='move-down']").addEventListener("click", () => movePlacedItem(item.placementId, item.startU - 1)); + element.querySelector("[data-action='remove']").addEventListener("click", () => { + state.placedItems = state.placedItems.filter((entry) => entry.placementId !== item.placementId); + renderAll(); + }); + + layer.appendChild(element); + }); +} + +function movePlacedItem(placementId, requestedStartU) { + const rack = getCurrentRackTemplate(); + const item = state.placedItems.find((entry) => entry.placementId === placementId); + const component = item ? getComponent(item.componentId) : null; + if (!rack || !item || !component) { + return; + } + + const normalizedStart = Math.max(1, Math.min(requestedStartU, rack.totalU - component.heightU + 1)); + const occupied = state.placedItems.some((entry) => { + if (entry.placementId === placementId) { + return false; + } + const other = getComponent(entry.componentId); + if (!other) { + return false; + } + return rangesOverlap(normalizedStart, component.heightU, entry.startU, other.heightU); + }); + + if (occupied) { + return; + } + + item.startU = normalizedStart; + renderAll(); +} + +function renderStats() { + const rack = getCurrentRackTemplate(); + if (!rack) { + return; + } + + const usedU = state.placedItems.reduce((sum, item) => { + const component = getComponent(item.componentId); + return sum + (component?.heightU ?? 0); + }, 0); + + const usedWeight = state.placedItems.reduce((sum, item) => { + const component = getComponent(item.componentId); + return sum + (component?.weightKg ?? 0); + }, 0); + + const usedPower = state.placedItems.reduce((sum, item) => { + const component = getComponent(item.componentId); + return sum + (component?.powerW ?? 0); + }, 0); + + ui.projectStats.innerHTML = ` +
+ Belegte U + ${usedU} / ${rack.totalU} +
+
+ Freie U + ${rack.totalU - usedU} +
+
+ Gewicht + ${usedWeight.toFixed(1)} / ${rack.maxWeightKg} kg +
+
+ Leistung + ${usedPower} W +
+ `; +} + +function renderBom() { + const aggregated = new Map(); + + state.placedItems.forEach((item) => { + const component = getComponent(item.componentId); + if (!component) { + return; + } + + const key = component.partNumber; + if (!aggregated.has(key)) { + aggregated.set(key, { + quantity: 0, + name: component.name, + manufacturer: component.manufacturer, + partNumber: component.partNumber, + currency: component.currency, + priceNet: component.priceNet, + }); + } + + aggregated.get(key).quantity += 1; + }); + + const lines = Array.from(aggregated.values()); + if (lines.length === 0) { + ui.bomOutput.innerHTML = `
Noch keine Komponenten platziert.
`; + return; + } + + ui.bomOutput.innerHTML = lines + .map((line) => { + const total = line.quantity * line.priceNet; + return ` +
+
+ ${escapeHtml(line.name)} +
${escapeHtml(line.partNumber)} · ${escapeHtml(line.manufacturer)}
+
+
${line.quantity} × ${formatCurrency(total, line.currency)}
+
+ `; + }) + .join(""); +} + +function renderCableSelectors() { + const options = [''] + .concat( + state.placedItems.map((item) => { + const component = getComponent(item.componentId); + if (!component) { + return ""; + } + return ``; + }) + ) + .join(""); + + ui.cableFrom.innerHTML = options; + ui.cableTo.innerHTML = options; +} + +function renderCableEstimate() { + const fromId = ui.cableFrom.value; + const toId = ui.cableTo.value; + const slackPercent = Number(ui.cableSlack.value || 0); + + if (!fromId || !toId || fromId === toId) { + ui.cableOutput.textContent = "Fuer eine Schaetzung zwei verschiedene Komponenten auswaehlen."; + return; + } + + const from = state.placedItems.find((item) => item.placementId === fromId); + const to = state.placedItems.find((item) => item.placementId === toId); + const rack = getCurrentRackTemplate(); + const fromComponent = from ? getComponent(from.componentId) : null; + const toComponent = to ? getComponent(to.componentId) : null; + + if (!from || !to || !rack || !fromComponent || !toComponent) { + ui.cableOutput.textContent = "Kabelschaetzung momentan nicht verfuegbar."; + return; + } + + const verticalMm = Math.abs(from.startU - to.startU) * 44.45; + const depthAllowance = Math.min(rack.usableDepthMm * 0.35, 280); + const sideAllowance = estimateSideAllowance(fromComponent, toComponent); + const rawLength = verticalMm + depthAllowance + sideAllowance; + const withSlack = rawLength * (1 + slackPercent / 100); + const recommended = recommendCableLength(withSlack); + + ui.cableOutput.innerHTML = ` + Geschaetzt: ${Math.round(rawLength)} mm
+ Mit Reserve: ${Math.round(withSlack)} mm
+ Bestelllaenge: ${recommended.toFixed(2)} m + `; +} + +function renderValidation() { + const rack = getCurrentRackTemplate(); + if (!rack) { + return; + } + + const notes = []; + const usedU = state.placedItems.reduce((sum, item) => { + const component = getComponent(item.componentId); + return sum + (component?.heightU ?? 0); + }, 0); + const usedWeight = state.placedItems.reduce((sum, item) => { + const component = getComponent(item.componentId); + return sum + (component?.weightKg ?? 0); + }, 0); + + if (usedU <= rack.totalU) { + notes.push({ className: "ok", text: `U-Belegung ist gueltig. ${rack.totalU - usedU}U frei.` }); + } else { + notes.push({ className: "warn", text: "Rack ist ueberbelegt." }); + } + + if (usedWeight <= rack.maxWeightKg) { + notes.push({ className: "ok", text: `Gewicht innerhalb der Rack-Grenze (${usedWeight.toFixed(1)} kg).` }); + } else { + notes.push({ className: "warn", text: `Gewicht ueberschreitet die Rack-Grenze von ${rack.maxWeightKg} kg.` }); + } + + const tooDeep = state.placedItems + .map((item) => ({ item, component: getComponent(item.componentId) })) + .filter(({ component }) => component && component.depthMm > rack.usableDepthMm); + + if (tooDeep.length === 0) { + notes.push({ className: "ok", text: "Keine Tiefenkonflikte erkannt." }); + } else { + tooDeep.forEach(({ component }) => { + notes.push({ className: "warn", text: `${component.name} ist tiefer als das Rack erlaubt.` }); + }); + } + + ui.validationOutput.innerHTML = notes.map((note) => `
  • ${escapeHtml(note.text)}
  • `).join(""); +} + +function importPluginPack(event) { + const file = event.target.files?.[0]; + if (!file) { + return; + } + + const reader = new FileReader(); + reader.onload = () => { + try { + const payload = JSON.parse(String(reader.result || "{}")); + validatePluginPack(payload); + const imported = payload.components.map((component, index) => ({ + ...component, + id: component.id || `plugin-${Date.now()}-${index}`, + })); + state.components = deduplicateComponents([...state.components, ...imported]); + renderAll(); + ui.pluginInput.value = ""; + alert(`Plugin-Pack "${payload.name}" importiert.`); + } catch (error) { + alert(`Plugin-Import fehlgeschlagen: ${error.message}`); + } + }; + reader.readAsText(file); +} + +function validatePluginPack(payload) { + if (!payload || typeof payload !== "object") { + throw new Error("Ungueltiges JSON."); + } + ["manifestVersion", "name", "version", "components"].forEach((key) => { + if (!(key in payload)) { + throw new Error(`Pflichtfeld fehlt: ${key}`); + } + }); + if (!Array.isArray(payload.components) || payload.components.length === 0) { + throw new Error("Plugin enthaelt keine Komponenten."); + } + payload.components.forEach((component) => { + ["name", "partNumber", "rackStandard", "heightU", "depthMm", "priceNet", "currency"].forEach((key) => { + if (!(key in component)) { + throw new Error(`Komponente unvollstaendig: ${component.name || "unbekannt"} (${key} fehlt)`); + } + }); + }); +} + +async function copyBomCsv() { + const rows = [["quantity", "manufacturer", "partNumber", "name", "unitPrice", "currency", "totalPrice"]]; + const aggregated = new Map(); + + state.placedItems.forEach((item) => { + const component = getComponent(item.componentId); + if (!component) { + return; + } + const key = component.partNumber; + if (!aggregated.has(key)) { + aggregated.set(key, { quantity: 0, component }); + } + aggregated.get(key).quantity += 1; + }); + + aggregated.forEach(({ quantity, component }) => { + rows.push([ + String(quantity), + component.manufacturer, + component.partNumber, + component.name, + String(component.priceNet), + component.currency, + String(quantity * component.priceNet), + ]); + }); + + const csv = rows.map((row) => row.map(csvEscape).join(",")).join("\n"); + + try { + await navigator.clipboard.writeText(csv); + ui.exportBom.textContent = "CSV kopiert"; + window.setTimeout(() => { + ui.exportBom.textContent = "CSV kopieren"; + }, 1600); + } catch (error) { + console.error(error); + alert("CSV konnte nicht in die Zwischenablage kopiert werden."); + } +} + +function findFirstFreePosition(heightU) { + const rack = getCurrentRackTemplate(); + if (!rack) { + return null; + } + + for (let start = 1; start <= rack.totalU - heightU + 1; start += 1) { + const blocked = state.placedItems.some((item) => { + const component = getComponent(item.componentId); + return component && rangesOverlap(start, heightU, item.startU, component.heightU); + }); + if (!blocked) { + return start; + } + } + + return null; +} + +function rangesOverlap(startA, heightA, startB, heightB) { + const endA = startA + heightA - 1; + const endB = startB + heightB - 1; + return startA <= endB && startB <= endA; +} + +function getCurrentRackTemplate() { + return state.bootstrap?.rackTemplates.find((template) => template.id === state.rackTemplateId) ?? null; +} + +function getComponent(componentId) { + return state.components.find((component) => component.id === componentId) ?? null; +} + +function deduplicateComponents(components) { + const byId = new Map(); + components.forEach((component) => { + byId.set(component.id, component); + }); + return Array.from(byId.values()); +} + +function estimateSideAllowance(fromComponent, toComponent) { + const fromSide = fromComponent.ports?.[0]?.side ?? "front"; + const toSide = toComponent.ports?.[0]?.side ?? "front"; + return fromSide === toSide ? 160 : 420; +} + +function recommendCableLength(lengthMm) { + const options = [0.25, 0.5, 1, 1.5, 2, 3, 5, 7.5, 10]; + const meters = lengthMm / 1000; + return options.find((option) => option >= meters) ?? Math.ceil(meters); +} + +function formatCurrency(value, currency) { + return new Intl.NumberFormat("de-DE", { + style: "currency", + currency: currency || "EUR", + maximumFractionDigits: 0, + }).format(value); +} + +function escapeHtml(value) { + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +function csvEscape(value) { + const stringValue = String(value ?? ""); + if (stringValue.includes(",") || stringValue.includes('"') || stringValue.includes("\n")) { + return `"${stringValue.replaceAll('"', '""')}"`; + } + return stringValue; +} diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..ec18e09 --- /dev/null +++ b/public/index.php @@ -0,0 +1,142 @@ + + + + + + + <?= htmlspecialchars($pageTitle, ENT_QUOTES) ?> + + + + +
    +
    +
    +

    code.it concept workspace

    +

    Rack Planner

    +

    + Browserbasierter Arbeitsstand fuer die Planung von 10" und 19" Racks. + Die aktuelle Domain ist nur Konzeptionsumgebung, nicht die spaetere Produkt-Domain. +

    +
    +
    +
    + Stand + MVP Prototype +
    +
    + Fokus + Editor, BOM, Kabel +
    +
    +
    + +
    +
    +
    +

    Projekt

    +

    Rack-Vorlage, Bibliothek und Plugin-Import.

    +
    + + + + + +
    + + +
    + +
    +
    + +
    +
    +

    Rack-Editor

    +

    Komponenten koennen per Drag-and-Drop verschoben werden. Jede Position referenziert eine U-Einheit.

    +
    + +
    +
    +
    +
    +
    + + +
    +
    + + + + + diff --git a/src/data/components.json b/src/data/components.json new file mode 100644 index 0000000..1f72314 --- /dev/null +++ b/src/data/components.json @@ -0,0 +1,151 @@ +[ + { + "id": "patchpanel-24-cat6a", + "name": "Patch Panel 24 Port Cat6A", + "manufacturer": "Generic", + "partNumber": "PP-24-C6A-1U", + "category": "patch_panel", + "rackStandard": "19_inch", + "heightU": 1, + "depthMm": 95, + "weightKg": 1.2, + "priceNet": 79, + "currency": "EUR", + "powerW": 0, + "ports": [ + { "name": "Front Ports", "side": "front", "offsetYmm": 10 } + ] + }, + { + "id": "switch-24-gbe", + "name": "24 Port Gigabit Switch", + "manufacturer": "Generic", + "partNumber": "SW-24-GBE-1U", + "category": "switch", + "rackStandard": "19_inch", + "heightU": 1, + "depthMm": 220, + "weightKg": 2.4, + "priceNet": 320, + "currency": "EUR", + "powerW": 35, + "ports": [ + { "name": "Uplink", "side": "front", "offsetYmm": 10 } + ] + }, + { + "id": "pdu-8-way", + "name": "PDU 8-way 1U", + "manufacturer": "Generic", + "partNumber": "PDU-8-1U", + "category": "pdu", + "rackStandard": "19_inch", + "heightU": 1, + "depthMm": 60, + "weightKg": 1.6, + "priceNet": 119, + "currency": "EUR", + "powerW": 0, + "ports": [ + { "name": "Power Out", "side": "rear", "offsetYmm": 10 } + ] + }, + { + "id": "ups-lineinteractive-2u", + "name": "UPS Line-Interactive 2U", + "manufacturer": "Generic", + "partNumber": "UPS-LI-2U", + "category": "ups", + "rackStandard": "19_inch", + "heightU": 2, + "depthMm": 420, + "weightKg": 18, + "priceNet": 499, + "currency": "EUR", + "powerW": 900, + "ports": [ + { "name": "Power In", "side": "rear", "offsetYmm": 45 } + ] + }, + { + "id": "shelf-1u", + "name": "Rack Shelf 1U", + "manufacturer": "Generic", + "partNumber": "SHELF-1U", + "category": "shelf", + "rackStandard": "19_inch", + "heightU": 1, + "depthMm": 350, + "weightKg": 3.5, + "priceNet": 49, + "currency": "EUR", + "powerW": 0, + "ports": [] + }, + { + "id": "blank-panel-1u", + "name": "Blank Panel 1U", + "manufacturer": "Generic", + "partNumber": "BLANK-1U", + "category": "blank_panel", + "rackStandard": "10_inch", + "heightU": 1, + "depthMm": 20, + "weightKg": 0.2, + "priceNet": 9, + "currency": "EUR", + "powerW": 0, + "ports": [] + }, + { + "id": "switch-8-10inch", + "name": "8 Port Switch 10\"", + "manufacturer": "Generic", + "partNumber": "SW-8-10-1U", + "category": "switch", + "rackStandard": "10_inch", + "heightU": 1, + "depthMm": 140, + "weightKg": 1.1, + "priceNet": 139, + "currency": "EUR", + "powerW": 18, + "ports": [ + { "name": "Front Ports", "side": "front", "offsetYmm": 10 } + ] + }, + { + "id": "patchpanel-12-10inch", + "name": "Patch Panel 12 Port 10\"", + "manufacturer": "Generic", + "partNumber": "PP-12-10-1U", + "category": "patch_panel", + "rackStandard": "10_inch", + "heightU": 1, + "depthMm": 85, + "weightKg": 0.9, + "priceNet": 55, + "currency": "EUR", + "powerW": 0, + "ports": [ + { "name": "Front Ports", "side": "front", "offsetYmm": 10 } + ] + }, + { + "id": "mini-ups-10inch", + "name": "Mini UPS 10\" 2U", + "manufacturer": "Generic", + "partNumber": "UPS-10-2U", + "category": "ups", + "rackStandard": "10_inch", + "heightU": 2, + "depthMm": 240, + "weightKg": 9.8, + "priceNet": 269, + "currency": "EUR", + "powerW": 400, + "ports": [ + { "name": "Power In", "side": "rear", "offsetYmm": 45 } + ] + } +] diff --git a/src/data/rack-templates.json b/src/data/rack-templates.json new file mode 100644 index 0000000..2f7cbd3 --- /dev/null +++ b/src/data/rack-templates.json @@ -0,0 +1,34 @@ +[ + { + "id": "rack-10-wall-9u", + "name": "10\" Wall Rack 9U", + "rackStandard": "10_inch", + "totalU": 9, + "usableDepthMm": 260, + "maxWeightKg": 35 + }, + { + "id": "rack-10-wall-12u", + "name": "10\" Wall Rack 12U", + "rackStandard": "10_inch", + "totalU": 12, + "usableDepthMm": 300, + "maxWeightKg": 45 + }, + { + "id": "rack-19-floor-24u", + "name": "19\" Floor Rack 24U", + "rackStandard": "19_inch", + "totalU": 24, + "usableDepthMm": 800, + "maxWeightKg": 600 + }, + { + "id": "rack-19-floor-42u", + "name": "19\" Floor Rack 42U", + "rackStandard": "19_inch", + "totalU": 42, + "usableDepthMm": 1000, + "maxWeightKg": 1000 + } +] diff --git a/src/functions.php b/src/functions.php new file mode 100644 index 0000000..afa7ac9 --- /dev/null +++ b/src/functions.php @@ -0,0 +1,63 @@ + [ + 'name' => 'code.it Rack Planner', + 'environment' => defined('APP_ENV') ? APP_ENV : 'prod', + 'baseUrl' => $GLOBALS['app_base_url'] ?? '', + 'apiBaseUrl' => $GLOBALS['app_api_base'] ?? '', + 'projectDomainNote' => 'Current domain is a concept workspace only, not the final product domain.', + ], + 'rackTemplates' => app_get_rack_templates(), + 'components' => app_get_catalog_components(), + 'pluginFormat' => [ + 'version' => 1, + 'acceptedExtensions' => ['json'], + 'requiredKeys' => ['manifestVersion', 'name', 'version', 'components'], + ], + ]; +} +