7d40cf95bf
- FastAPI Identify API (port 8081) - Neo4j propertygraf (64 noder, 80 kanter) - Ingest-pipeline (Python) - Byggscript + integrationstester - Docker Compose setup
329 lines
22 KiB
JavaScript
329 lines
22 KiB
JavaScript
const pptxgen = require("pptxgenjs");
|
||
const React = require("react");
|
||
const ReactDOMServer = require("react-dom/server");
|
||
const sharp = require("sharp");
|
||
const {
|
||
FaSearch, FaClipboardList, FaExclamationTriangle, FaLayerGroup, FaTree, FaIndustry,
|
||
FaTint, FaQuestionCircle, FaBullseye, FaCamera, FaCheckCircle, FaSyncAlt, FaShieldAlt,
|
||
FaExchangeAlt, FaHistory, FaRocket, FaChartLine, FaBuilding, FaLink, FaDatabase
|
||
} = require("react-icons/fa");
|
||
|
||
// Palett: asfalt + galvstål + signalgul (vägmarkering)
|
||
const DARK = "23272B", CARD_D = "2E343B", INK = "2A3138", BODY = "3A4149";
|
||
const MUTED = "6B7480", MUTED_L = "AEB6BF", LIGHT = "F3F5F7", WHITE = "FFFFFF";
|
||
const YEL = "F5B301", YEL_TINT = "FFF6DC", LINE = "D9DEE4";
|
||
const H = "Arial", B = "Calibri", MONO = "Courier New";
|
||
|
||
const mkShadow = () => ({ type: "outer", color: "000000", blur: 7, offset: 2, angle: 45, opacity: 0.14 });
|
||
|
||
async function icon(Comp, color, size = 256) {
|
||
const svg = ReactDOMServer.renderToStaticMarkup(React.createElement(Comp, { color, size: String(size) }));
|
||
const png = await sharp(Buffer.from(svg)).png().toBuffer();
|
||
return "image/png;base64," + png.toString("base64");
|
||
}
|
||
|
||
(async () => {
|
||
const P = {};
|
||
P.search = await icon(FaSearch, "#" + INK);
|
||
P.clip = await icon(FaClipboardList, "#" + INK);
|
||
P.warn = await icon(FaExclamationTriangle, "#" + INK);
|
||
P.layer = await icon(FaLayerGroup, "#" + INK);
|
||
P.tree = await icon(FaTree, "#" + INK);
|
||
P.ind = await icon(FaIndustry, "#" + INK);
|
||
P.tint = await icon(FaTint, "#" + INK);
|
||
P.q = await icon(FaQuestionCircle, "#" + WHITE);
|
||
P.bull = await icon(FaBullseye, "#" + WHITE);
|
||
P.cam = await icon(FaCamera, "#" + WHITE);
|
||
P.check = await icon(FaCheckCircle, "#" + WHITE);
|
||
P.sync = await icon(FaSyncAlt, "#" + INK);
|
||
P.shield = await icon(FaShieldAlt, "#" + INK);
|
||
P.exch = await icon(FaExchangeAlt, "#" + INK);
|
||
P.hist = await icon(FaHistory, "#" + INK);
|
||
P.rocket = await icon(FaRocket, "#" + INK);
|
||
P.chart = await icon(FaChartLine, "#" + INK);
|
||
P.build = await icon(FaBuilding, "#" + INK);
|
||
P.link = await icon(FaLink, "#" + INK);
|
||
P.db = await icon(FaDatabase, "#" + INK);
|
||
P.clipY = await icon(FaClipboardList, "#" + INK);
|
||
P.warnY = await icon(FaExclamationTriangle, "#" + INK);
|
||
P.checkY = await icon(FaCheckCircle, "#" + INK);
|
||
|
||
const pres = new pptxgen();
|
||
pres.layout = "LAYOUT_16x9";
|
||
pres.author = "Landvex";
|
||
pres.title = "Landvex — Infrastructure Object Library, pilotpitch";
|
||
|
||
const title = (s, t, dark = false) =>
|
||
s.addText(t, { x: 0.6, y: 0.42, w: 8.8, h: 0.7, fontFace: H, fontSize: 36, bold: true, color: dark ? WHITE : INK, margin: 0 });
|
||
|
||
const circleIcon = (s, data, cx, cy, d, fill) => {
|
||
s.addShape(pres.shapes.OVAL, { x: cx, y: cy, w: d, h: d, fill: { color: fill } });
|
||
const pad = d * 0.26;
|
||
s.addImage({ data, x: cx + pad, y: cy + pad, w: d - 2 * pad, h: d - 2 * pad });
|
||
};
|
||
|
||
// ---------- S1 · Titel (mörk) ----------
|
||
let s = pres.addSlide();
|
||
s.background = { color: DARK };
|
||
s.addText("LANDVEX × QUIXZOOM", { x: 0.6, y: 0.55, w: 5.8, h: 0.35, fontFace: H, fontSize: 13, bold: true, color: YEL, charSpacing: 4, margin: 0 });
|
||
s.addText([
|
||
{ text: "Infrastructure", options: { breakLine: true } },
|
||
{ text: "Object Library" }
|
||
], { x: 0.6, y: 1.05, w: 5.9, h: 1.8, fontFace: H, fontSize: 42, bold: true, color: WHITE, margin: 0, lineSpacing: 46 });
|
||
s.addText("Vet vad som står där ute — varje objekt i gaturummet, med källa på varje faktum.", { x: 0.6, y: 2.95, w: 5.5, h: 0.75, fontFace: B, fontSize: 15, color: MUTED_L, margin: 0 });
|
||
s.addText("37 klasser · 26 modeller · 167 grafkanter · 0 valideringsfel", { x: 0.6, y: 3.85, w: 5.7, h: 0.35, fontFace: H, fontSize: 13, bold: true, color: YEL, margin: 0 });
|
||
s.addText("Pilotpitch · juli 2026", { x: 0.6, y: 5.05, w: 4, h: 0.3, fontFace: B, fontSize: 11, color: MUTED, margin: 0 });
|
||
const chips = [
|
||
["LVX-TRP-0102", "Vägbelysningsarmatur"],
|
||
["LVX-P-0000026", "Tesla Supercharger V4"],
|
||
["LVX-VAT-0103", "Brandpost / hydrant"],
|
||
["LVX-ELN-0101", "Kabelskåp lågspänning"]
|
||
];
|
||
chips.forEach((c, i) => {
|
||
const y = 1.05 + i * 0.82;
|
||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 6.35, y, w: 3.05, h: 0.62, rectRadius: 0.06, fill: { color: CARD_D } });
|
||
s.addText([
|
||
{ text: c[0], options: { fontFace: MONO, fontSize: 11.5, bold: true, color: YEL, breakLine: true } },
|
||
{ text: c[1], options: { fontFace: B, fontSize: 10.5, color: WHITE } }
|
||
], { x: 6.55, y: y + 0.05, w: 2.75, h: 0.52, margin: 0, lineSpacing: 13 });
|
||
});
|
||
s.addNotes("Landvex bygger referensbiblioteket för fysisk infrastruktur. QUIXZOOM är fältarmen. Siffrorna är dagens bibliotek — validerat, inte vision.");
|
||
|
||
// ---------- S2 · Problemet ----------
|
||
s = pres.addSlide();
|
||
s.background = { color: WHITE };
|
||
title(s, "Gatan saknar identitet");
|
||
const rows2 = [
|
||
[P.clip, "Inventering är manuell och dyr", "Beståndsdata byggs om från noll vid varje upphandling och ägarbyte."],
|
||
[P.warn, "Utgångna modeller upptäcks för sent", "Reservdelsrisken syns först när armaturen slocknat eller locket spruckit."],
|
||
[P.search, "\u201dVad är detta?\u201d saknar svar i fält", "Tekniker, system och AI gissar — det finns ingen gemensam referens."]
|
||
];
|
||
rows2.forEach((r, i) => {
|
||
const y = 1.4 + i * 1.15;
|
||
circleIcon(s, r[0], 0.6, y, 0.52, YEL);
|
||
s.addText(r[1], { x: 1.35, y: y - 0.06, w: 4.15, h: 0.35, fontFace: H, fontSize: 15, bold: true, color: INK, margin: 0 });
|
||
s.addText(r[2], { x: 1.35, y: y + 0.3, w: 4.15, h: 0.62, fontFace: B, fontSize: 12, color: MUTED, margin: 0 });
|
||
});
|
||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 5.85, y: 1.35, w: 3.55, h: 3.4, rectRadius: 0.09, fill: { color: WHITE }, line: { color: LINE, width: 1 }, shadow: mkShadow() });
|
||
s.addText("60M+", { x: 6.1, y: 1.6, w: 3.05, h: 0.95, fontFace: H, fontSize: 58, bold: true, color: INK, margin: 0 });
|
||
s.addText("elstolpar enbart i USA — de flesta bär identiteten inbränd i godset.", { x: 6.1, y: 2.65, w: 3.05, h: 0.85, fontFace: B, fontSize: 12.5, color: MUTED, margin: 0 });
|
||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 6.1, y: 3.7, w: 3.05, h: 0.62, rectRadius: 0.06, fill: { color: YEL_TINT } });
|
||
s.addText("birthmark: år · klass · impregnering", { x: 6.28, y: 3.78, w: 2.75, h: 0.46, fontFace: MONO, fontSize: 10.5, bold: true, color: INK, margin: 0, valign: "middle" });
|
||
s.addNotes("Problemet är inte brist på data — det är brist på identitet. Ingen gemensam nyckel mellan fält, system och leverantörer.");
|
||
|
||
// ---------- S3 · Lösningen: fem nivåer ----------
|
||
s = pres.addSlide();
|
||
s.background = { color: WHITE };
|
||
title(s, "Ett bibliotek — fem nivåer");
|
||
const steps = [
|
||
["Domän", "TRP · VAT · ELN…"],
|
||
["Objektklass", "LVX-TRP-0102"],
|
||
["Typ", "Cobrahead-LED"],
|
||
["Produktmodell", "LVX-P-0000003"],
|
||
["Individ", "LVX-I-… (pilot)"]
|
||
];
|
||
steps.forEach((st, i) => {
|
||
const x = 0.6 + i * 1.9;
|
||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x, y: 1.35, w: 1.55, h: 0.95, rectRadius: 0.08, fill: { color: LIGHT } });
|
||
s.addText([
|
||
{ text: st[0], options: { fontFace: H, fontSize: 12.5, bold: true, color: INK, breakLine: true } },
|
||
{ text: st[1], options: { fontFace: MONO, fontSize: 9.5, color: MUTED } }
|
||
], { x: x + 0.12, y: 1.47, w: 1.31, h: 0.75, margin: 0, lineSpacing: 14 });
|
||
if (i < 4) s.addText("\u203A", { x: x + 1.55, y: 1.55, w: 0.35, h: 0.5, fontFace: H, fontSize: 20, color: MUTED, align: "center", margin: 0 });
|
||
});
|
||
const cards3 = [
|
||
[P.link, "Permanenta LVX-ID", "Identiteten överlever ägarbyten, versioner och systembyten — referensen alla kan peka på."],
|
||
[P.db, "Källa på varje faktum", "Problem utan källa publiceras aldrig. PLATSHÅLLARE-regeln är hård — juridik by design."],
|
||
[P.hist, "Bitemporal fakta", "Vad som gällde där ute — och när vi visste det. Ersättningskedjor blir spårbara."]
|
||
];
|
||
cards3.forEach((c, i) => {
|
||
const x = 0.6 + i * 3.07;
|
||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x, y: 2.85, w: 2.87, h: 2.15, rectRadius: 0.09, fill: { color: LIGHT } });
|
||
circleIcon(s, c[0], x + 0.22, y3(i), 0.46, YEL);
|
||
s.addText(c[1], { x: x + 0.85, y: 3.05, w: 1.95, h: 0.6, fontFace: H, fontSize: 13.5, bold: true, color: INK, margin: 0 });
|
||
s.addText(c[2], { x: x + 0.22, y: 3.75, w: 2.45, h: 1.1, fontFace: B, fontSize: 11.5, color: BODY, margin: 0 });
|
||
});
|
||
function y3() { return 3.05; }
|
||
s.addNotes("Fem nivåer: domän ned till individ. Piloten fyller individlagret för kundens geografi. ID:na är permanenta — det är vallgraven.");
|
||
|
||
// ---------- S4 · Bevis ----------
|
||
s = pres.addSlide();
|
||
s.background = { color: WHITE };
|
||
title(s, "Byggt — och validerat");
|
||
const stats = [
|
||
["37", "objektklasser — 31 källbelagda"],
|
||
["26", "produktmodeller — alla källbelagda"],
|
||
["167", "grafkanter i konsolidering v2"],
|
||
["0", "valideringsfel mot schemat"]
|
||
];
|
||
stats.forEach((st, i) => {
|
||
const x = 0.6 + i * 2.25;
|
||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x, y: 1.3, w: 2.05, h: 1.45, rectRadius: 0.09, fill: { color: WHITE }, line: { color: LINE, width: 1 }, shadow: mkShadow() });
|
||
s.addText(st[0], { x: x + 0.18, y: 1.42, w: 1.7, h: 0.7, fontFace: H, fontSize: 40, bold: true, color: i === 3 ? YEL : INK, margin: 0 });
|
||
s.addText(st[1], { x: x + 0.18, y: 2.14, w: 1.72, h: 0.55, fontFace: B, fontSize: 10.5, color: MUTED, margin: 0 });
|
||
});
|
||
s.addText("Verifieringstrappan — varje fältfoto lyfter posten ett steg", { x: 0.6, y: 3.12, w: 8.8, h: 0.3, fontFace: B, fontSize: 12.5, italic: true, color: MUTED, margin: 0 });
|
||
const stair = ["Obekräftad", "Källbelagd", "Fältverifierad", "Tillverkarbekräftad"];
|
||
stair.forEach((t, i) => {
|
||
const h = 0.55 + i * 0.28, x = 0.6 + i * 2.25;
|
||
s.addShape(pres.shapes.RECTANGLE, { x, y: 5.05 - h, w: 2.05, h, fill: { color: i === 3 ? YEL : ["E7EAEE", "D5DAE0", "BFC7CF"][i] } });
|
||
s.addText(t, { x: x + 0.1, y: 5.05 - h + 0.08, w: 1.85, h: 0.35, fontFace: H, fontSize: 11.5, bold: true, color: INK, margin: 0 });
|
||
});
|
||
s.addNotes("Detta är inte en idé — biblioteket finns, är källbelagt och maskinvaliderat. Trappan är kvalitetsmodellen: piloten flyttar poster till fältverifierad.");
|
||
|
||
// ---------- S5 · OCR-guldet ----------
|
||
s = pres.addSlide();
|
||
s.background = { color: WHITE };
|
||
title(s, "Identiteten sitter i godset");
|
||
const cards5 = [
|
||
[P.tree, "Stolpens birthmark", "Inbränd märkning i ögonhöjd: år, längd, klass och impregneringsmedel.", "år · klass · CCA/penta"],
|
||
[P.ind, "Gjutmärket", "Tillverkare, funktion och lastklass gjuts in i lock och ram.", "ULEFOS · EN124-2 · D400"],
|
||
[P.tint, "Hydrantens kropp", "AWWA kräver gjuten märkning — tillverkningsår och öppningsriktning.", "AMERICAN-DARLING · 1998"]
|
||
];
|
||
cards5.forEach((c, i) => {
|
||
const x = 0.6 + i * 3.07;
|
||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x, y: 1.3, w: 2.87, h: 3.15, rectRadius: 0.09, fill: { color: LIGHT } });
|
||
circleIcon(s, c[0], x + 0.22, 1.55, 0.5, YEL);
|
||
s.addText(c[1], { x: x + 0.88, y: 1.6, w: 1.95, h: 0.6, fontFace: H, fontSize: 14, bold: true, color: INK, margin: 0 });
|
||
s.addText(c[2], { x: x + 0.22, y: 2.35, w: 2.45, h: 1.05, fontFace: B, fontSize: 11.5, color: BODY, margin: 0 });
|
||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: x + 0.22, y: 3.6, w: 2.43, h: 0.55, rectRadius: 0.06, fill: { color: WHITE }, line: { color: LINE, width: 1 } });
|
||
s.addText(c[3], { x: x + 0.36, y: 3.66, w: 2.2, h: 0.43, fontFace: MONO, fontSize: 10, bold: true, color: INK, margin: 0, valign: "middle" });
|
||
});
|
||
s.addText("Ingen databas krävs på plats — objektet bär sin egen identitet. OCR-först är protokollets kärna.", { x: 0.6, y: 4.75, w: 8.8, h: 0.35, fontFace: B, fontSize: 13, italic: true, color: MUTED, margin: 0 });
|
||
s.addNotes("Läsbarheten är affärsidén: birthmarks, gjutmärken och typskyltar gör varje foto till en databaspost.");
|
||
|
||
// ---------- S6 · Flyhjulet ----------
|
||
s = pres.addSlide();
|
||
s.background = { color: WHITE };
|
||
title(s, "Flyhjulet: QUIXZOOM");
|
||
const nodes = [
|
||
[2.3, 1.12, P.q, "Kundfråga", "identify → okänd"],
|
||
[3.85, 2.38, P.bull, "Bounty", "skapas automatiskt"],
|
||
[2.3, 3.6, P.cam, "Zoomer-foto", "OCR + position"],
|
||
[0.75, 2.38, P.check, "Fältverifierad", "post + individ"]
|
||
];
|
||
nodes.forEach(n => {
|
||
circleIcon(s, n[2], n[0], n[1], 0.95, DARK);
|
||
s.addText(n[3], { x: n[0] - 0.45, y: n[1] + 0.92, w: 1.85, h: 0.28, fontFace: H, fontSize: 11.5, bold: true, color: INK, align: "center", margin: 0 });
|
||
s.addText(n[4], { x: n[0] - 0.45, y: n[1] + 1.16, w: 1.85, h: 0.24, fontFace: B, fontSize: 9.5, color: MUTED, align: "center", margin: 0 });
|
||
});
|
||
circleIcon(s, P.sync, 2.5, 2.6, 0.55, YEL);
|
||
s.addText("\u2198", { x: 3.5, y: 2.0, w: 0.4, h: 0.4, fontFace: H, fontSize: 16, color: MUTED, margin: 0 });
|
||
s.addText("\u2199", { x: 3.5, y: 3.3, w: 0.4, h: 0.4, fontFace: H, fontSize: 16, color: MUTED, margin: 0 });
|
||
s.addText("\u2196", { x: 1.85, y: 3.3, w: 0.4, h: 0.4, fontFace: H, fontSize: 16, color: MUTED, margin: 0 });
|
||
s.addText("\u2197", { x: 1.85, y: 2.0, w: 0.4, h: 0.4, fontFace: H, fontSize: 16, color: MUTED, margin: 0 });
|
||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 5.85, y: 1.3, w: 3.55, h: 3.2, rectRadius: 0.09, fill: { color: LIGHT } });
|
||
s.addText("Bounty-motorn", { x: 6.1, y: 1.5, w: 3.05, h: 0.35, fontFace: H, fontSize: 14, bold: true, color: INK, margin: 0 });
|
||
s.addText([
|
||
{ text: "16 aktiva bounties ur posternas OCR-fält", options: { bullet: true, breakLine: true } },
|
||
{ text: "Poäng: bas · först-på-individ ×2 · full OCR +50 %", options: { bullet: true, breakLine: true } },
|
||
{ text: "Hårda säkerhetsregler — allt fotas från gångyta", options: { bullet: true, breakLine: true } },
|
||
{ text: "Varje \u201dobekräftad\u201d i API:t skapar en bounty", options: { bullet: true } }
|
||
], { x: 6.1, y: 1.95, w: 3.1, h: 2.4, fontFace: B, fontSize: 11.5, color: BODY, paraSpaceAfter: 8, margin: 0 });
|
||
s.addText("Kundens fråga finansierar datan som gör nästa svar bättre.", { x: 0.6, y: 5.12, w: 8.8, h: 0.35, fontFace: B, fontSize: 13, italic: true, color: MUTED, margin: 0 });
|
||
s.addNotes("Flyhjulet: obesvarade kundfrågor blir bounties, Zoomers fotograferar, posten fältverifieras, svaret blir bättre. Andelen kunddrivna bounties är hälsotalet.");
|
||
|
||
// ---------- S7 · API:t ----------
|
||
s = pres.addSlide();
|
||
s.background = { color: WHITE };
|
||
title(s, "Produkten: Object API v0");
|
||
const eps = [
|
||
["POST", "/v0/identify", "bild in → modell ut"],
|
||
["GET", "/v0/objects/{lvx_id}", "hela posten + källor"],
|
||
["GET", "/v0/objects/{id}/succession", "ersättningskedjan"],
|
||
["GET", "/v0/search", "fritext + filter"],
|
||
["GET", "/v0/standards/{beteckning}", "objekt per standard"],
|
||
["POST", "/v0/feedback", "fel/okänt → bounty"]
|
||
];
|
||
eps.forEach((e, i) => {
|
||
const y = 1.3 + i * 0.58;
|
||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 0.6, y, w: 0.62, h: 0.32, rectRadius: 0.05, fill: { color: e[0] === "POST" ? YEL : CARD_D } });
|
||
s.addText(e[0], { x: 0.6, y, w: 0.62, h: 0.32, fontFace: H, fontSize: 9, bold: true, color: e[0] === "POST" ? INK : WHITE, align: "center", valign: "middle", margin: 0 });
|
||
s.addText(e[1], { x: 1.35, y: y + 0.01, w: 2.62, h: 0.32, fontFace: MONO, fontSize: 10.5, bold: true, color: INK, margin: 0, valign: "middle" });
|
||
s.addText(e[2], { x: 4.02, y: y + 0.01, w: 1.72, h: 0.32, fontFace: B, fontSize: 9.5, color: MUTED, margin: 0, valign: "middle" });
|
||
});
|
||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 5.85, y: 1.3, w: 3.55, h: 3.45, rectRadius: 0.09, fill: { color: WHITE }, line: { color: LINE, width: 1 }, shadow: mkShadow() });
|
||
circleIcon(s, P.shield, 6.1, 1.52, 0.46, YEL);
|
||
s.addText("Svarskontraktet", { x: 6.72, y: 1.58, w: 2.6, h: 0.35, fontFace: H, fontSize: 14, bold: true, color: INK, margin: 0 });
|
||
s.addText([
|
||
{ text: "verifieringsnivå — alltid med", options: { bullet: true, breakLine: true } },
|
||
{ text: "konfidens 0–1 — aldrig dold", options: { bullet: true, breakLine: true } },
|
||
{ text: "källor[] på varje påstående", options: { bullet: true, breakLine: true } },
|
||
{ text: "PLATSHÅLLARE filtreras bort vid serialisering", options: { bullet: true, breakLine: true } },
|
||
{ text: "svagt svar ⇒ bounty-ID följer med", options: { bullet: true } }
|
||
], { x: 6.1, y: 2.15, w: 3.1, h: 2.4, fontFace: B, fontSize: 11.5, color: BODY, paraSpaceAfter: 8, margin: 0 });
|
||
s.addText("Juridiken sitter i kontraktet — referenser och beteckningar, aldrig standardtext.", { x: 0.6, y: 5.05, w: 8.8, h: 0.35, fontFace: B, fontSize: 13, italic: true, color: MUTED, margin: 0 });
|
||
s.addNotes("Sex endpoints räcker för piloten. Kontraktet är differentieringen: ärlighet och proveniens är obligatoriska fält, inte fotnoter.");
|
||
|
||
// ---------- S8 · Golden cases ----------
|
||
s = pres.addSlide();
|
||
s.background = { color: WHITE };
|
||
title(s, "Tre svar som säljer");
|
||
const g = [
|
||
[P.exch, "Ersättningskedjan", "GE ERS identifieras → status: ersatt → ersättare: ERL-serien. Reservdelsrisken syns innan lagret tar slut.", "identify → succession"],
|
||
[P.hist, "Årsmodell ur godset", "American-Darling B-84-B, gjutet 1998, öppnar vänster — individdata utan register.", "konfidens 0,97 · källbelagd"],
|
||
[P.bull2 || P.search, "Ärlig när den inte vet", "Okänt UK-skåp → bästa klass, konfidens 0,71 — och en bounty skapas automatiskt.", "svar + BNTY-A0001"]
|
||
];
|
||
g.forEach((c, i) => {
|
||
const x = 0.6 + i * 3.07;
|
||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x, y: 1.3, w: 2.87, h: 3.15, rectRadius: 0.09, fill: { color: LIGHT } });
|
||
circleIcon(s, c[0], x + 0.22, 1.55, 0.5, YEL);
|
||
s.addText(c[1], { x: x + 0.88, y: 1.6, w: 1.95, h: 0.6, fontFace: H, fontSize: 14, bold: true, color: INK, margin: 0 });
|
||
s.addText(c[2], { x: x + 0.22, y: 2.35, w: 2.45, h: 1.15, fontFace: B, fontSize: 11.5, color: BODY, margin: 0 });
|
||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: x + 0.22, y: 3.6, w: 2.43, h: 0.55, rectRadius: 0.06, fill: { color: WHITE }, line: { color: LINE, width: 1 } });
|
||
s.addText(c[3], { x: x + 0.36, y: 3.66, w: 2.2, h: 0.43, fontFace: MONO, fontSize: 10, bold: true, color: INK, margin: 0, valign: "middle" });
|
||
});
|
||
s.addText("Alla tre körs på riktiga poster — demodatat ingår i pilotpaketet.", { x: 0.6, y: 4.75, w: 8.8, h: 0.35, fontFace: B, fontSize: 13, italic: true, color: MUTED, margin: 0 });
|
||
s.addNotes("Tre golden cases som demoas live: succession, individdata ur gjutgods, och ärlighet + bounty vid okänt objekt.");
|
||
|
||
// ---------- S9 · Paketering ----------
|
||
s = pres.addSlide();
|
||
s.background = { color: WHITE };
|
||
title(s, "Paketering");
|
||
const pk = [
|
||
[P.rocket, "Pilot", "1–3 designpartners", ["Fast månadsavgift, generös kvot", "Gemensam backlog styr utvecklingen", "Betalar även med fältaccess"], LIGHT],
|
||
[P.chart, "Pro", "entreprenad · besiktning · försäkring", ["Usage-based per identify", "Billiga uppslag driver LVX-ID-inlåsning", "Volymtrappa"], YEL_TINT],
|
||
[P.build, "Enterprise", "nätbolag · förvaltare · tillverkare", ["SLA + eget individlager (LVX-I)", "Bulkexport och integrationer", "Riktade bounties i egen geografi"], LIGHT]
|
||
];
|
||
pk.forEach((c, i) => {
|
||
const x = 0.6 + i * 3.07;
|
||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x, y: 1.3, w: 2.87, h: 3.1, rectRadius: 0.09, fill: { color: c[4] }, shadow: i === 1 ? mkShadow() : undefined });
|
||
circleIcon(s, c[0], x + 0.22, 1.52, 0.46, WHITE === c[4] ? LIGHT : WHITE);
|
||
s.addText(c[1], { x: x + 0.82, y: 1.55, w: 1.95, h: 0.35, fontFace: H, fontSize: 16, bold: true, color: INK, margin: 0 });
|
||
s.addText(c[2], { x: x + 0.22, y: 2.12, w: 2.45, h: 0.3, fontFace: B, fontSize: 10.5, italic: true, color: MUTED, margin: 0 });
|
||
s.addText(c[3].map((t, j) => ({ text: t, options: { bullet: true, breakLine: j < 2 } })), { x: x + 0.22, y: 2.5, w: 2.5, h: 1.75, fontFace: B, fontSize: 11, color: BODY, paraSpaceAfter: 7, margin: 0 });
|
||
});
|
||
s.addText("Identifieringen är värdet · uppslaget är räckvidden · bounty-krediter köper fältverifiering där kunden behöver den.", { x: 0.6, y: 4.75, w: 8.8, h: 0.35, fontFace: B, fontSize: 12.5, italic: true, color: MUTED, margin: 0 });
|
||
s.addNotes("SaaS-logiken: identify prissätts per värdefullt svar, uppslag hålls billiga för att göra LVX-ID till branschreferens, Enterprise får eget individlager.");
|
||
|
||
// ---------- S10 · Piloten (mörk) ----------
|
||
s = pres.addSlide();
|
||
s.background = { color: DARK };
|
||
title(s, "Piloten: 90 dagar till bevis", true);
|
||
const uc = [
|
||
[P.clipY, "Inventering", "Fota beståndet → klass, modell och status per objekt."],
|
||
[P.warnY, "Utgånget-listan", "Allt med status ersatt/utgången — med ersättare och källa."],
|
||
[P.checkY, "Problemflagg", "Källbelagda klassproblem matchade mot ert bestånd."]
|
||
];
|
||
uc.forEach((r, i) => {
|
||
const y = 1.45 + i * 1.1;
|
||
circleIcon(s, r[0], 0.6, y, 0.52, YEL);
|
||
s.addText(r[1], { x: 1.35, y: y - 0.05, w: 4.1, h: 0.35, fontFace: H, fontSize: 15, bold: true, color: WHITE, margin: 0 });
|
||
s.addText(r[2], { x: 1.35, y: y + 0.31, w: 4.1, h: 0.6, fontFace: B, fontSize: 12, color: MUTED_L, margin: 0 });
|
||
});
|
||
s.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 5.85, y: 1.35, w: 3.55, h: 3.35, rectRadius: 0.09, fill: { color: CARD_D } });
|
||
s.addText("Erbjudandet", { x: 6.1, y: 1.55, w: 3.05, h: 0.35, fontFace: H, fontSize: 14, bold: true, color: WHITE, margin: 0 });
|
||
s.addText([
|
||
{ text: "Avgränsat område, 90 dagar", options: { bullet: true, breakLine: true } },
|
||
{ text: "Fast pilotavgift — kvittas mot årsavtal", options: { bullet: true, breakLine: true } },
|
||
{ text: "Gemensam backlog styr bounties", options: { bullet: true, breakLine: true } },
|
||
{ text: "Ni behåller export av ert individlager", options: { bullet: true } }
|
||
], { x: 6.1, y: 2.0, w: 3.1, h: 1.85, fontFace: B, fontSize: 11.5, color: MUTED_L, paraSpaceAfter: 8, margin: 0 });
|
||
s.addText("Boka pilotworkshop \u2192", { x: 6.1, y: 4.05, w: 3.05, h: 0.45, fontFace: H, fontSize: 17, bold: true, color: YEL, margin: 0 });
|
||
s.addText("LANDVEX × QUIXZOOM · juli 2026", { x: 0.6, y: 5.08, w: 5, h: 0.3, fontFace: B, fontSize: 11, color: MUTED, charSpacing: 2, margin: 0 });
|
||
s.addNotes("Stäng med erbjudandet: 90 dagar, avgränsat område, tre användningsfall, fast avgift som kvittas. Nästa steg är en workshop där området och backloggen väljs.");
|
||
|
||
await pres.writeFile({ fileName: "/home/claude/landvex-pilotpitch.pptx" });
|
||
console.log("deck skriven");
|
||
})().catch(e => { console.error(e); process.exit(1); });
|