Compare commits
1 Commits
v0.0.7
...
94dbbc7743
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94dbbc7743 |
@@ -1,13 +1,13 @@
|
|||||||
name: Build & Push
|
name: Build & Push
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CHART_PATH: web/charts/factorio-signal-exporter
|
REGISTRY: git.sebse.de
|
||||||
|
IMAGE: git.sebse.de/sebse/factorio-signal-exporter
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -17,24 +17,17 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Bump version
|
- name: Install Helm
|
||||||
id: version
|
uses: azure/setup-helm@v4
|
||||||
uses: https://git.sebse.de/sebse/actions/version-bump@v1
|
|
||||||
|
- name: Log in to Gitea Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ github.token }}
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ secrets.GITEA_USERNAME }}
|
||||||
|
password: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
|
||||||
- name: Set env vars
|
- name: Extract metadata
|
||||||
run: |
|
|
||||||
echo "IMAGE=git.sebse.de/${{ github.repository }}" >> $GITHUB_ENV
|
|
||||||
echo "IMAGE_MIGRATE=git.sebse.de/${{ github.repository }}-migrate" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Docker login
|
|
||||||
uses: https://git.sebse.de/sebse/actions/docker-login@v1
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
|
||||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
|
||||||
|
|
||||||
- name: Extract metadata (app)
|
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
@@ -42,57 +35,29 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
type=sha,prefix=,format=short
|
type=sha,prefix=,format=short
|
||||||
type=raw,value=latest
|
type=raw,value=latest
|
||||||
type=raw,value=${{ steps.version.outputs.version }}
|
|
||||||
|
|
||||||
- name: Build and push app
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: ./web
|
context: ./web
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
cache-from: type=registry,ref=${{ env.IMAGE }}:latest
|
cache-from: type=registry,ref=${{ env.IMAGE }}:latest
|
||||||
cache-to: type=inline
|
cache-to: type=inline
|
||||||
|
|
||||||
- name: Extract metadata (migrate)
|
- name: Package Helm chart
|
||||||
id: meta-migrate
|
run: |
|
||||||
uses: docker/metadata-action@v5
|
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
|
||||||
with:
|
helm package web/charts/factorio-signal-exporter \
|
||||||
images: ${{ env.IMAGE_MIGRATE }}
|
--version "0.0.0-${SHORT_SHA}" \
|
||||||
tags: |
|
--app-version "${SHORT_SHA}"
|
||||||
type=sha,prefix=,format=short
|
|
||||||
type=raw,value=latest
|
|
||||||
type=raw,value=${{ steps.version.outputs.version }}
|
|
||||||
|
|
||||||
- name: Build and push migrate
|
- name: Push Helm chart to Gitea
|
||||||
uses: docker/build-push-action@v5
|
run: |
|
||||||
with:
|
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
|
||||||
context: ./web
|
curl -u ${{ secrets.GITEA_USERNAME }}:${{ secrets.GITEA_TOKEN }} \
|
||||||
file: ./web/Dockerfile.migrate
|
-X POST \
|
||||||
push: true
|
https://${{ env.REGISTRY }}/api/packages/sebse/helm/api/charts \
|
||||||
tags: ${{ steps.meta-migrate.outputs.tags }}
|
-F "chart=@factorio-signal-exporter-0.0.0-${SHORT_SHA}.tgz" \
|
||||||
cache-from: type=registry,ref=${{ env.IMAGE_MIGRATE }}:latest
|
--fail-with-body
|
||||||
cache-to: type=inline
|
|
||||||
|
|
||||||
- name: Package and push helm chart
|
|
||||||
uses: https://git.sebse.de/sebse/actions/helm-package-push@v1
|
|
||||||
with:
|
|
||||||
chart-path: ${{ env.CHART_PATH }}
|
|
||||||
version: ${{ steps.version.outputs.version }}
|
|
||||||
app-version: ${{ steps.version.outputs.version }}
|
|
||||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
|
||||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
|
||||||
|
|
||||||
- name: Package and push Factorio mod
|
|
||||||
uses: https://git.sebse.de/sebse/actions/factorio-mod-push@v1
|
|
||||||
with:
|
|
||||||
mod-folder: ./plugin
|
|
||||||
version: ${{ steps.version.outputs.version }}
|
|
||||||
factorio-token: ${{ secrets.FACTORIO_TOKEN }}
|
|
||||||
|
|
||||||
- name: Deploy to prod
|
|
||||||
uses: https://git.sebse.de/sebse/actions/helm-deploy@v1
|
|
||||||
with:
|
|
||||||
app: factorio-signal-exporter
|
|
||||||
profile: prod
|
|
||||||
version: ${{ steps.version.outputs.version }}
|
|
||||||
ssh-key: ${{ secrets.HELMBOT_SSH_KEY }}
|
|
||||||
1126
n8n/Factorio Item Import.json
Normal file
1126
n8n/Factorio Item Import.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,3 +0,0 @@
|
|||||||
# Signal Exporter
|
|
||||||
|
|
||||||
Provides a custom combinator that reads both connected circuit and logistic networks, writing the data to a JSON file.
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
FROM node:24-alpine
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY package.json package-lock.json* ./
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
COPY migrations/ ./migrations/
|
|
||||||
|
|
||||||
CMD ["npm", "run", "migrate"]
|
|
||||||
@@ -16,7 +16,7 @@ export const POST = withAuth(async (req: NextRequest) => {
|
|||||||
filter_combinators = null, filter_items = null,
|
filter_combinators = null, filter_items = null,
|
||||||
filter_items_exclude = null, filter_items_regex = false,
|
filter_items_exclude = null, filter_items_regex = false,
|
||||||
y_min = null, y_max = null, y_scale = 'linear',
|
y_min = null, y_max = null, y_scale = 'linear',
|
||||||
series_limit = 20, order_by = 'value_asc',
|
series_limit = 20, order_by = 'time',
|
||||||
} = body;
|
} = body;
|
||||||
|
|
||||||
if (!title) return NextResponse.json({ error: 'title required' }, { status: 400 });
|
if (!title) return NextResponse.json({ error: 'title required' }, { status: 400 });
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const GET = withAuth(async (req: NextRequest) => {
|
|||||||
const from = p.get('from');
|
const from = p.get('from');
|
||||||
const to = p.get('to');
|
const to = p.get('to');
|
||||||
const useRegex = p.get('regex') === 'true';
|
const useRegex = p.get('regex') === 'true';
|
||||||
const orderBy = p.get('order_by') ?? 'value_asc';
|
const orderBy = p.get('order_by') ?? 'time';
|
||||||
const limit = p.get('limit') ? parseInt(p.get('limit')!, 10) : null;
|
const limit = p.get('limit') ? parseInt(p.get('limit')!, 10) : null;
|
||||||
|
|
||||||
const conditions: string[] = [];
|
const conditions: string[] = [];
|
||||||
@@ -28,15 +28,12 @@ export const GET = withAuth(async (req: NextRequest) => {
|
|||||||
if (itemsWhitelist.length > 0) {
|
if (itemsWhitelist.length > 0) {
|
||||||
if (useRegex) {
|
if (useRegex) {
|
||||||
const localeMap = getServerLocaleMap();
|
const localeMap = getServerLocaleMap();
|
||||||
const localeKeys = [...new Set(itemsWhitelist.flatMap(p => matchKeys(p, localeMap)))];
|
// Each pattern is expanded to matching keys (tested against key AND localized name).
|
||||||
const sqlPattern = itemsWhitelist.map(p => `(${p})`).join('|');
|
// Union all patterns — if a pattern matches nothing, it contributes no keys.
|
||||||
const orConds = [`item_key ~* $${i++}`];
|
const keys = [...new Set(itemsWhitelist.flatMap(p => matchKeys(p, localeMap)))];
|
||||||
values.push(sqlPattern);
|
if (keys.length === 0) return NextResponse.json([]);
|
||||||
if (localeKeys.length > 0) {
|
conditions.push(`item_key = ANY($${i++})`);
|
||||||
orConds.push(`item_key = ANY($${i++})`);
|
values.push(keys);
|
||||||
values.push(localeKeys);
|
|
||||||
}
|
|
||||||
conditions.push(`(${orConds.join(' OR ')})`);
|
|
||||||
} else {
|
} else {
|
||||||
conditions.push(`item_key = ANY($${i++})`);
|
conditions.push(`item_key = ANY($${i++})`);
|
||||||
values.push(itemsWhitelist);
|
values.push(itemsWhitelist);
|
||||||
@@ -46,15 +43,12 @@ export const GET = withAuth(async (req: NextRequest) => {
|
|||||||
if (itemsBlacklist.length > 0) {
|
if (itemsBlacklist.length > 0) {
|
||||||
if (useRegex) {
|
if (useRegex) {
|
||||||
const localeMap = getServerLocaleMap();
|
const localeMap = getServerLocaleMap();
|
||||||
const localeKeys = [...new Set(itemsBlacklist.flatMap(p => matchKeys(p, localeMap)))];
|
const keys = [...new Set(itemsBlacklist.flatMap(p => matchKeys(p, localeMap)))];
|
||||||
const sqlPattern = itemsBlacklist.map(p => `(${p})`).join('|');
|
// If blacklist pattern matches nothing, nothing to exclude — skip condition
|
||||||
const andConds = [`item_key !~* $${i++}`];
|
if (keys.length > 0) {
|
||||||
values.push(sqlPattern);
|
conditions.push(`item_key != ALL($${i++})`);
|
||||||
if (localeKeys.length > 0) {
|
values.push(keys);
|
||||||
andConds.push(`item_key != ALL($${i++})`);
|
|
||||||
values.push(localeKeys);
|
|
||||||
}
|
}
|
||||||
conditions.push(`(${andConds.join(' AND ')})`);
|
|
||||||
} else {
|
} else {
|
||||||
conditions.push(`item_key != ALL($${i++})`);
|
conditions.push(`item_key != ALL($${i++})`);
|
||||||
values.push(itemsBlacklist);
|
values.push(itemsBlacklist);
|
||||||
@@ -84,15 +78,10 @@ export const GET = withAuth(async (req: NextRequest) => {
|
|||||||
if (itemsWhitelist.length > 0) {
|
if (itemsWhitelist.length > 0) {
|
||||||
if (useRegex) {
|
if (useRegex) {
|
||||||
const localeMap = getServerLocaleMap();
|
const localeMap = getServerLocaleMap();
|
||||||
const localeKeys = [...new Set(itemsWhitelist.flatMap(p => matchKeys(p, localeMap)))];
|
const keys = [...new Set(itemsWhitelist.flatMap(p => matchKeys(p, localeMap)))];
|
||||||
const sqlPattern = itemsWhitelist.map(p => `(${p})`).join('|');
|
if (keys.length === 0) return NextResponse.json([]);
|
||||||
const orConds = [`item_key ~* $${j++}`];
|
baseConditions.push(`item_key = ANY($${j++})`);
|
||||||
baseValues.push(sqlPattern);
|
baseValues.push(keys);
|
||||||
if (localeKeys.length > 0) {
|
|
||||||
orConds.push(`item_key = ANY($${j++})`);
|
|
||||||
baseValues.push(localeKeys);
|
|
||||||
}
|
|
||||||
baseConditions.push(`(${orConds.join(' OR ')})`);
|
|
||||||
} else {
|
} else {
|
||||||
baseConditions.push(`item_key = ANY($${j++})`);
|
baseConditions.push(`item_key = ANY($${j++})`);
|
||||||
baseValues.push(itemsWhitelist);
|
baseValues.push(itemsWhitelist);
|
||||||
@@ -101,15 +90,11 @@ export const GET = withAuth(async (req: NextRequest) => {
|
|||||||
if (itemsBlacklist.length > 0) {
|
if (itemsBlacklist.length > 0) {
|
||||||
if (useRegex) {
|
if (useRegex) {
|
||||||
const localeMap = getServerLocaleMap();
|
const localeMap = getServerLocaleMap();
|
||||||
const localeKeys = [...new Set(itemsBlacklist.flatMap(p => matchKeys(p, localeMap)))];
|
const keys = [...new Set(itemsBlacklist.flatMap(p => matchKeys(p, localeMap)))];
|
||||||
const sqlPattern = itemsBlacklist.map(p => `(${p})`).join('|');
|
if (keys.length > 0) {
|
||||||
const andConds = [`item_key !~* $${j++}`];
|
baseConditions.push(`item_key != ALL($${j++})`);
|
||||||
baseValues.push(sqlPattern);
|
baseValues.push(keys);
|
||||||
if (localeKeys.length > 0) {
|
|
||||||
andConds.push(`item_key != ALL($${j++})`);
|
|
||||||
baseValues.push(localeKeys);
|
|
||||||
}
|
}
|
||||||
baseConditions.push(`(${andConds.join(' AND ')})`);
|
|
||||||
} else {
|
} else {
|
||||||
baseConditions.push(`item_key != ALL($${j++})`);
|
baseConditions.push(`item_key != ALL($${j++})`);
|
||||||
baseValues.push(itemsBlacklist);
|
baseValues.push(itemsBlacklist);
|
||||||
@@ -162,11 +147,8 @@ export const GET = withAuth(async (req: NextRequest) => {
|
|||||||
`(combinator = $${i + idx * 2} AND item_key = $${i + idx * 2 + 1})`,
|
`(combinator = $${i + idx * 2} AND item_key = $${i + idx * 2 + 1})`,
|
||||||
);
|
);
|
||||||
const fullWhere = `${where ? where + ' AND' : 'WHERE'} (${seriesConditions.join(' OR ')})`;
|
const fullWhere = `${where ? where + ' AND' : 'WHERE'} (${seriesConditions.join(' OR ')})`;
|
||||||
const orderCase = top.map((_, idx) =>
|
|
||||||
`WHEN combinator = $${i + idx * 2} AND item_key = $${i + idx * 2 + 1} THEN ${idx}`,
|
|
||||||
).join(' ');
|
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
`SELECT ${selectCols} FROM signals ${fullWhere} ORDER BY CASE ${orderCase} ELSE ${top.length} END, real_time ASC`,
|
`SELECT ${selectCols} FROM signals ${fullWhere} ORDER BY real_time ASC`,
|
||||||
[...values, ...top.flatMap(r => [r.combinator, r.item_key])],
|
[...values, ...top.flatMap(r => [r.combinator, r.item_key])],
|
||||||
);
|
);
|
||||||
return NextResponse.json(result.rows);
|
return NextResponse.json(result.rows);
|
||||||
@@ -193,11 +175,8 @@ export const GET = withAuth(async (req: NextRequest) => {
|
|||||||
`(combinator = $${i + idx * 2} AND item_key = $${i + idx * 2 + 1})`,
|
`(combinator = $${i + idx * 2} AND item_key = $${i + idx * 2 + 1})`,
|
||||||
);
|
);
|
||||||
const fullWhere = `${where ? where + ' AND' : 'WHERE'} (${seriesConditions.join(' OR ')})`;
|
const fullWhere = `${where ? where + ' AND' : 'WHERE'} (${seriesConditions.join(' OR ')})`;
|
||||||
const orderCase = top.map((_, idx) =>
|
|
||||||
`WHEN combinator = $${i + idx * 2} AND item_key = $${i + idx * 2 + 1} THEN ${idx}`,
|
|
||||||
).join(' ');
|
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
`SELECT ${selectCols} FROM signals ${fullWhere} ORDER BY CASE ${orderCase} ELSE ${top.length} END, real_time ASC`,
|
`SELECT ${selectCols} FROM signals ${fullWhere} ORDER BY real_time ASC`,
|
||||||
[...values, ...top.flatMap(r => [r.combinator, r.item_key])],
|
[...values, ...top.flatMap(r => [r.combinator, r.item_key])],
|
||||||
);
|
);
|
||||||
return NextResponse.json(result.rows);
|
return NextResponse.json(result.rows);
|
||||||
|
|||||||
@@ -14,5 +14,4 @@ body {
|
|||||||
background: var(--background);
|
background: var(--background);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
font-family: ui-sans-serif, system-ui, sans-serif;
|
font-family: ui-sans-serif, system-ui, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import { readFileSync, writeFileSync } from 'fs';
|
|
||||||
const CSV = 'public/factorio_item_colors.csv';
|
|
||||||
|
|
||||||
function hexToHsl(h: string): [number, number, number] {
|
|
||||||
let r = parseInt(h.slice(1, 3), 16) / 255, g = parseInt(h.slice(3, 5), 16) / 255, b = parseInt(h.slice(5, 7), 16) / 255;
|
|
||||||
const mx = Math.max(r, g, b), mn = Math.min(r, g, b), l = (mx + mn) / 2;
|
|
||||||
if (mx === mn) return [0, 0, Math.round(l * 100)];
|
|
||||||
const d = mx - mn, s = l > 0.5 ? d / (2 - mx - mn) : d / (mx + mn);
|
|
||||||
let hue = 0;
|
|
||||||
if (mx === r) hue = ((g - b) / d + (g < b ? 6 : 0)) * 60;
|
|
||||||
else if (mx === g) hue = ((b - r) / d + 2) * 60;
|
|
||||||
else hue = ((r - g) / d + 4) * 60;
|
|
||||||
return [Math.round(hue), Math.round(s * 100), Math.round(l * 100)];
|
|
||||||
}
|
|
||||||
function hslToHex(h: number, s: number, l: number): string {
|
|
||||||
s /= 100; l /= 100;
|
|
||||||
const c = (1 - Math.abs(2 * l - 1)) * s, x = c * (1 - Math.abs(((h / 60) % 2) - 1)), m = l - c / 2;
|
|
||||||
let r = 0, g = 0, b = 0;
|
|
||||||
if (h < 60) { r = c; g = x; } else if (h < 120) { r = x; g = c; } else if (h < 180) { g = c; b = x; }
|
|
||||||
else if (h < 240) { g = x; b = c; } else if (h < 300) { r = x; b = c; } else { r = c; b = x; }
|
|
||||||
const to = (v: number) => Math.round((v + m) * 255).toString(16).padStart(2, '0');
|
|
||||||
return '#' + to(r) + to(g) + to(b);
|
|
||||||
}
|
|
||||||
function djb2(s: string): number {
|
|
||||||
let h = 5381;
|
|
||||||
for (let i = 0; i < s.length; i++) h = (h * 33) ^ s.charCodeAt(i);
|
|
||||||
return h >>> 0;
|
|
||||||
}
|
|
||||||
function hueDist(a: number, b: number): number {
|
|
||||||
const d = Math.abs(a - b);
|
|
||||||
return Math.min(d, 360 - d);
|
|
||||||
}
|
|
||||||
|
|
||||||
const lines = readFileSync(CSV, 'utf-8').trim().split('\n');
|
|
||||||
const header = lines[0];
|
|
||||||
const raw = lines.slice(1).filter(l => l.trim()).map(l => {
|
|
||||||
const [k, c] = l.split(',');
|
|
||||||
return { key: k.trim(), color: c.trim() };
|
|
||||||
});
|
|
||||||
|
|
||||||
// Dedup by key
|
|
||||||
const seen = new Set<string>();
|
|
||||||
const rows: typeof raw = [];
|
|
||||||
for (const r of raw) { if (seen.has(r.key)) continue; seen.add(r.key); rows.push(r); }
|
|
||||||
|
|
||||||
const n = rows.length;
|
|
||||||
const parent = Array.from({ length: n }, (_, i) => i);
|
|
||||||
function find(x: number): number {
|
|
||||||
while (parent[x] !== x) { parent[x] = parent[parent[x]]; x = parent[x]; }
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
function union(a: number, b: number) { parent[find(a)] = find(b); }
|
|
||||||
|
|
||||||
const hsl = rows.map((r, i) => ({ ...r, hsl: hexToHsl(r.color), idx: i }));
|
|
||||||
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
for (let j = i + 1; j < n; j++) {
|
|
||||||
if (hueDist(hsl[i].hsl[0], hsl[j].hsl[0]) <= 0.5 &&
|
|
||||||
Math.abs(hsl[i].hsl[2] - hsl[j].hsl[2]) <= 0.5)
|
|
||||||
union(i, j);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const groups = new Map<number, typeof hsl>();
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
const root = find(i);
|
|
||||||
if (!groups.has(root)) groups.set(root, []);
|
|
||||||
groups.get(root)!.push(hsl[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let fixed = 0;
|
|
||||||
for (const [, items] of groups) {
|
|
||||||
if (items.length < 2) continue;
|
|
||||||
items.sort((a, b) => a.idx - b.idx);
|
|
||||||
const [oh, os, ol] = items[0].hsl;
|
|
||||||
for (let i = 1; i < items.length; i++) {
|
|
||||||
const hash = djb2(items[i].key);
|
|
||||||
const h = (oh + (hash % 5 - 2) + i * 7 + 360) % 360;
|
|
||||||
const l = Math.max(0, Math.min(100, ol + ((hash >> 4) % 5 - 2)));
|
|
||||||
items[i].color = hslToHex(h, os, l);
|
|
||||||
fixed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hsl.sort((a, b) => a.idx - b.idx);
|
|
||||||
writeFileSync(CSV, header + '\n' + hsl.map(r => `${r.key},${r.color}`).join('\n') + '\n');
|
|
||||||
if (fixed) console.log(`Fixed ${fixed} close colors`);
|
|
||||||
@@ -9,12 +9,10 @@ spec:
|
|||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
{{- include "factorio-signal-exporter.selectorLabels" . | nindent 6 }}
|
{{- include "factorio-signal-exporter.selectorLabels" . | nindent 6 }}
|
||||||
app.kubernetes.io/component: app
|
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
{{- include "factorio-signal-exporter.selectorLabels" . | nindent 8 }}
|
{{- include "factorio-signal-exporter.selectorLabels" . | nindent 8 }}
|
||||||
app.kubernetes.io/component: app
|
|
||||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
spec:
|
spec:
|
||||||
{{- with .Values.imagePullSecrets }}
|
{{- with .Values.imagePullSecrets }}
|
||||||
@@ -23,8 +21,9 @@ spec:
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
initContainers:
|
initContainers:
|
||||||
- name: migrate
|
- name: migrate
|
||||||
image: {{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}
|
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
|
||||||
imagePullPolicy: {{ .Values.initImage.pullPolicy }}
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
command: ["npm", "run", "migrate"]
|
||||||
env:
|
env:
|
||||||
- name: DATABASE_URL
|
- name: DATABASE_URL
|
||||||
valueFrom:
|
valueFrom:
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ spec:
|
|||||||
type: {{ .Values.service.type }}
|
type: {{ .Values.service.type }}
|
||||||
selector:
|
selector:
|
||||||
{{- include "factorio-signal-exporter.selectorLabels" . | nindent 4 }}
|
{{- include "factorio-signal-exporter.selectorLabels" . | nindent 4 }}
|
||||||
app.kubernetes.io/component: app
|
|
||||||
ports:
|
ports:
|
||||||
- port: {{ .Values.service.port }}
|
- port: {{ .Values.service.port }}
|
||||||
targetPort: 3000
|
targetPort: 3000
|
||||||
@@ -1,12 +1,7 @@
|
|||||||
image:
|
image:
|
||||||
repository: git.sebse.de/sebse/factorio-signal-exporter
|
repository: git.sebse.de/sebse/factorio-signal-exporter
|
||||||
tag: latest
|
tag: latest
|
||||||
pullPolicy: Always
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
initImage:
|
|
||||||
repository: git.sebse.de/sebse/factorio-signal-exporter-migrate
|
|
||||||
tag: latest
|
|
||||||
pullPolicy: Always
|
|
||||||
|
|
||||||
imagePullSecrets: []
|
imagePullSecrets: []
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useRef, useState, useCallback } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -32,55 +32,22 @@ interface CardShellProps extends HeaderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CardShell({ title, onEdit, onDelete, empty, children, legendContainerRef }: CardShellProps) {
|
export function CardShell({ title, onEdit, onDelete, empty, children, legendContainerRef }: CardShellProps) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [legendHeight, setLegendHeight] = useState<number | null>(null);
|
|
||||||
const dragRef = useRef<{ startY: number; startH: number } | null>(null);
|
|
||||||
|
|
||||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
const el = legendContainerRef?.current;
|
|
||||||
if (!el) return;
|
|
||||||
dragRef.current = { startY: e.clientY, startH: el.offsetHeight };
|
|
||||||
|
|
||||||
function onMove(ev: MouseEvent) {
|
|
||||||
if (!dragRef.current) return;
|
|
||||||
const delta = dragRef.current.startY - ev.clientY;
|
|
||||||
const containerH = containerRef.current?.offsetHeight ?? 400;
|
|
||||||
const newH = Math.max(32, Math.min(containerH - 64, dragRef.current.startH + delta));
|
|
||||||
setLegendHeight(newH);
|
|
||||||
}
|
|
||||||
function onUp() {
|
|
||||||
dragRef.current = null;
|
|
||||||
document.removeEventListener('mousemove', onMove);
|
|
||||||
document.removeEventListener('mouseup', onUp);
|
|
||||||
}
|
|
||||||
document.addEventListener('mousemove', onMove);
|
|
||||||
document.addEventListener('mouseup', onUp);
|
|
||||||
}, [legendContainerRef]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="h-full flex flex-col bg-gray-900 rounded border border-gray-700 overflow-hidden">
|
<div className="h-full flex flex-col bg-gray-900 rounded border border-gray-700 overflow-hidden">
|
||||||
<Header title={title} onEdit={onEdit} onDelete={onDelete} />
|
<Header title={title} onEdit={onEdit} onDelete={onDelete} />
|
||||||
{empty ? (
|
{empty ? (
|
||||||
<EmptyState />
|
<EmptyState />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
{/* Plot fills remaining space */}
|
||||||
<div className="flex-1 min-h-0">
|
<div className="flex-1 min-h-0">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
{legendContainerRef && <>
|
{/* Legend scrolls independently, capped at 25% card height */}
|
||||||
<div
|
<div
|
||||||
onMouseDown={handleMouseDown}
|
ref={legendContainerRef}
|
||||||
className="shrink-0 h-1.5 cursor-row-resize bg-gray-800 hover:bg-gray-700 active:bg-gray-600"
|
className="shrink-0 max-h-[25%] overflow-y-auto border-t border-gray-800 px-2 py-1 text-[10px] text-gray-400 [&_.u-legend]:w-full [&_.u-series]:flex [&_.u-series]:items-center [&_.u-series_th]:flex [&_.u-series_th]:items-center [&_.u-series_th]:gap-1 [&_.u-marker]:w-3 [&_.u-marker]:h-0.5 [&_.u-marker]:shrink-0 [&_.u-label]:truncate [&_.u-value]:ml-auto [&_.u-value]:font-mono [&_.u-value]:shrink-0"
|
||||||
/>
|
/>
|
||||||
<div
|
|
||||||
ref={legendContainerRef}
|
|
||||||
className="shrink-0 overflow-y-auto px-2 py-1 text-[10px] text-gray-400 [&_.u-legend]:w-full [&_.u-series]:flex [&_.u-series]:items-center [&_.u-series_th]:flex [&_.u-series_th]:items-center [&_.u-series_th]:gap-1 [&_.u-marker]:w-3 [&_.u-marker]:h-0.5 [&_.u-marker]:shrink-0 [&_.u-label]:truncate [&_.u-value]:ml-auto [&_.u-value]:font-mono [&_.u-value]:shrink-0"
|
|
||||||
style={legendHeight != null ? { height: legendHeight, maxHeight: legendHeight } : { maxHeight: '25%' }}
|
|
||||||
/>
|
|
||||||
<style>{'.u-legend .u-series:first-child { display: none; }'}</style>
|
|
||||||
</>}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,11 +2,8 @@
|
|||||||
|
|
||||||
import 'uplot/dist/uPlot.min.css';
|
import 'uplot/dist/uPlot.min.css';
|
||||||
import uPlot from 'uplot';
|
import uPlot from 'uplot';
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { useApp } from '@/lib/context';
|
import { useApp } from '@/lib/context';
|
||||||
import { resolveName } from '@/lib/localization';
|
import { resolveName } from '@/lib/localization';
|
||||||
import { getColorMap } from '@/lib/colors';
|
|
||||||
import type { ColorMap } from '@/lib/colors';
|
|
||||||
import { CardShell } from './CardShell';
|
import { CardShell } from './CardShell';
|
||||||
import { makeYScale, makeAnnotationHooks, makeSignalsSeries, makeSignalsAxes, CURSOR_NO_DRAG } from './plotHelpers';
|
import { makeYScale, makeAnnotationHooks, makeSignalsSeries, makeSignalsAxes, CURSOR_NO_DRAG } from './plotHelpers';
|
||||||
import { buildSeriesData } from './seriesData';
|
import { buildSeriesData } from './seriesData';
|
||||||
@@ -26,9 +23,6 @@ interface Props {
|
|||||||
|
|
||||||
export default function SignalsChart({ config, rows, sessions, alerts, timeMode, onEdit, onDelete }: Props) {
|
export default function SignalsChart({ config, rows, sessions, alerts, timeMode, onEdit, onDelete }: Props) {
|
||||||
const { localeMap } = useApp();
|
const { localeMap } = useApp();
|
||||||
const locale = typeof navigator !== 'undefined' ? navigator.language : 'de-DE';
|
|
||||||
const [colorMap, setColorMap] = useState<ColorMap>(new Map());
|
|
||||||
useEffect(() => { getColorMap().then(setColorMap); }, []);
|
|
||||||
|
|
||||||
const { containerRef, legendRef } = usePlot(
|
const { containerRef, legendRef } = usePlot(
|
||||||
(el, w, h, lRef) => {
|
(el, w, h, lRef) => {
|
||||||
@@ -50,8 +44,8 @@ export default function SignalsChart({ config, rows, sessions, alerts, timeMode,
|
|||||||
if (lRef.current) lRef.current.appendChild(legendEl);
|
if (lRef.current) lRef.current.appendChild(legendEl);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
series: makeSignalsSeries(keys, timeMode, key => resolveName(key, localeMap), colorMap),
|
series: makeSignalsSeries(keys, timeMode, key => resolveName(key, localeMap)),
|
||||||
axes: makeSignalsAxes(timeMode, locale),
|
axes: makeSignalsAxes(timeMode),
|
||||||
scales: {
|
scales: {
|
||||||
x: { time: false },
|
x: { time: false },
|
||||||
y: makeYScale(config.y_min ?? null, config.y_max ?? null, config.y_scale),
|
y: makeYScale(config.y_min ?? null, config.y_max ?? null, config.y_scale),
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { useApp } from '@/lib/context';
|
import { useApp } from '@/lib/context';
|
||||||
import { resolveName } from '@/lib/localization';
|
import { resolveName } from '@/lib/localization';
|
||||||
import { formatSI } from '@/lib/formatNumber';
|
|
||||||
import { getColorMap, getItemColor } from '@/lib/colors';
|
|
||||||
import type { ColorMap } from '@/lib/colors';
|
|
||||||
import { CardShell } from './CardShell';
|
import { CardShell } from './CardShell';
|
||||||
import type { ChartConfig, SignalRow } from '@/lib/types';
|
import type { ChartConfig, SignalRow } from '@/lib/types';
|
||||||
|
|
||||||
@@ -16,14 +12,12 @@ interface Props {
|
|||||||
|
|
||||||
export default function TableViz({ config, rows, onEdit, onDelete }: Props) {
|
export default function TableViz({ config, rows, onEdit, onDelete }: Props) {
|
||||||
const { localeMap } = useApp();
|
const { localeMap } = useApp();
|
||||||
const [colorMap, setColorMap] = useState<ColorMap>(new Map());
|
|
||||||
useEffect(() => { getColorMap().then(setColorMap); }, []);
|
|
||||||
|
|
||||||
const latest = new Map<string, { green?: number; red?: number }>();
|
const latest = new Map<string, { green?: number; red?: number }>();
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
latest.set(`${row.combinator}::${row.item_key}`, { green: row.green, red: row.red });
|
latest.set(`${row.combinator}::${row.item_key}`, { green: row.green, red: row.red });
|
||||||
}
|
}
|
||||||
const tableRows = [...latest.entries()].slice(0, config.series_limit);
|
const tableRows = [...latest.entries()].sort((a, b) => (a[1].green ?? 0) - (b[1].green ?? 0));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardShell title={config.title} onEdit={onEdit} onDelete={onDelete} empty={rows.length === 0}>
|
<CardShell title={config.title} onEdit={onEdit} onDelete={onDelete} empty={rows.length === 0}>
|
||||||
@@ -42,20 +36,16 @@ export default function TableViz({ config, rows, onEdit, onDelete }: Props) {
|
|||||||
const [combinator, item_key] = key.split('::');
|
const [combinator, item_key] = key.split('::');
|
||||||
return (
|
return (
|
||||||
<tr key={key} className="border-t border-gray-800 hover:bg-gray-800/50">
|
<tr key={key} className="border-t border-gray-800 hover:bg-gray-800/50">
|
||||||
<td className="px-2 py-0.5 flex items-center gap-1.5">
|
<td className="px-2 py-0.5">{resolveName(item_key, localeMap)}</td>
|
||||||
<span className="inline-block w-2 h-2 rounded-full shrink-0"
|
|
||||||
style={{ backgroundColor: getItemColor(item_key, colorMap) }} />
|
|
||||||
{resolveName(item_key, localeMap)}
|
|
||||||
</td>
|
|
||||||
<td className="px-2 py-0.5 text-gray-500">{combinator}</td>
|
<td className="px-2 py-0.5 text-gray-500">{combinator}</td>
|
||||||
{config.signal_type !== 'red' && (
|
{config.signal_type !== 'red' && (
|
||||||
<td className={`px-2 py-0.5 text-right font-mono ${(vals.green ?? 0) < 0 ? 'text-red-400' : 'text-green-400'}`}>
|
<td className={`px-2 py-0.5 text-right font-mono ${(vals.green ?? 0) < 0 ? 'text-red-400' : 'text-green-400'}`}>
|
||||||
{vals.green != null ? formatSI(vals.green, undefined, 0) : '--'}
|
{vals.green?.toLocaleString() ?? '--'}
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
{config.signal_type !== 'green' && (
|
{config.signal_type !== 'green' && (
|
||||||
<td className="px-2 py-0.5 text-right font-mono text-orange-400">
|
<td className="px-2 py-0.5 text-right font-mono text-orange-400">
|
||||||
{vals.red != null ? formatSI(vals.red, undefined, 0) : '--'}
|
{vals.red?.toLocaleString() ?? '--'}
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import 'uplot/dist/uPlot.min.css';
|
|||||||
import uPlot from 'uplot';
|
import uPlot from 'uplot';
|
||||||
import { CardShell } from './CardShell';
|
import { CardShell } from './CardShell';
|
||||||
import { AXIS_BASE, CURSOR_NO_DRAG, makeYScale } from './plotHelpers';
|
import { AXIS_BASE, CURSOR_NO_DRAG, makeYScale } from './plotHelpers';
|
||||||
import { formatSI } from '@/lib/formatNumber';
|
|
||||||
import { usePlot } from './usePlot';
|
import { usePlot } from './usePlot';
|
||||||
import type { ChartConfig, UpsRow } from '@/lib/types';
|
import type { ChartConfig, UpsRow } from '@/lib/types';
|
||||||
import type { TimeMode } from '@/lib/types';
|
import type { TimeMode } from '@/lib/types';
|
||||||
@@ -32,9 +31,9 @@ export default function UpsChart({ config, upsRows, timeMode, onEdit, onDelete }
|
|||||||
|
|
||||||
const xAxis: uPlot.Axis = {
|
const xAxis: uPlot.Axis = {
|
||||||
...AXIS_BASE,
|
...AXIS_BASE,
|
||||||
values: timeMode === 'real'
|
...(timeMode === 'real' && {
|
||||||
? (_u, vals) => vals.map(v => v == null ? '' : new Date(v * 1000).toLocaleTimeString())
|
values: (_u, vals) => vals.map(v => v == null ? '' : new Date(v * 1000).toLocaleTimeString()),
|
||||||
: (_u, vals) => vals.map(v => v == null ? '' : formatSI(v)),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
return new uPlot({
|
return new uPlot({
|
||||||
@@ -50,7 +49,7 @@ export default function UpsChart({ config, upsRows, timeMode, onEdit, onDelete }
|
|||||||
{ label: timeMode === 'tick' ? 'Tick' : 'Time' },
|
{ label: timeMode === 'tick' ? 'Tick' : 'Time' },
|
||||||
{ label: 'UPS', stroke: '#4ade80', width: 1.5 },
|
{ label: 'UPS', stroke: '#4ade80', width: 1.5 },
|
||||||
],
|
],
|
||||||
axes: [xAxis, { ...AXIS_BASE, values: (_u, vals) => vals.map(v => v == null ? '' : formatSI(v)) }],
|
axes: [xAxis, { ...AXIS_BASE }],
|
||||||
scales: {
|
scales: {
|
||||||
x: { time: false },
|
x: { time: false },
|
||||||
y: makeYScale(config.y_min ?? null, config.y_max ?? null, config.y_scale),
|
y: makeYScale(config.y_min ?? null, config.y_max ?? null, config.y_scale),
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
import uPlot from 'uplot';
|
import uPlot from 'uplot';
|
||||||
import type { ChartConfig } from '@/lib/types';
|
import type { ChartConfig } from '@/lib/types';
|
||||||
import { formatSI } from '@/lib/formatNumber';
|
|
||||||
import type { ColorMap } from '@/lib/colors';
|
// --- Color helpers ---
|
||||||
import { getItemColor } from '@/lib/colors';
|
|
||||||
|
function djb2(s: string): number {
|
||||||
|
let h = 5381;
|
||||||
|
for (let i = 0; i < s.length; i++) h = (h * 33) ^ s.charCodeAt(i);
|
||||||
|
return h >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hslColor(key: string): string {
|
||||||
|
const hue = djb2(key) % 360;
|
||||||
|
return `hsl(${hue},70%,60%)`;
|
||||||
|
}
|
||||||
|
|
||||||
const SEMANTIC_GREEN = '#4ade80';
|
const SEMANTIC_GREEN = '#4ade80';
|
||||||
const SEMANTIC_RED = '#f87171';
|
const SEMANTIC_RED = '#f87171';
|
||||||
@@ -13,11 +23,10 @@ export interface SeriesStyle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getSeriesStyle(
|
export function getSeriesStyle(
|
||||||
key: string,
|
key: string,
|
||||||
uCombs: number,
|
uCombs: number,
|
||||||
uItems: number,
|
uItems: number,
|
||||||
uSigs: number,
|
uSigs: number,
|
||||||
colorMap: ColorMap = new Map(),
|
|
||||||
): SeriesStyle {
|
): SeriesStyle {
|
||||||
const [combinator, item_key, sig] = key.split('::');
|
const [combinator, item_key, sig] = key.split('::');
|
||||||
|
|
||||||
@@ -25,9 +34,9 @@ export function getSeriesStyle(
|
|||||||
return { color: sig === 'green' ? SEMANTIC_GREEN : SEMANTIC_RED, dash: undefined };
|
return { color: sig === 'green' ? SEMANTIC_GREEN : SEMANTIC_RED, dash: undefined };
|
||||||
}
|
}
|
||||||
if (uItems > 1) {
|
if (uItems > 1) {
|
||||||
return { color: getItemColor(item_key, colorMap), dash: uSigs > 1 && sig === 'red' ? [4, 2] : undefined };
|
return { color: hslColor(item_key), dash: uSigs > 1 && sig === 'red' ? [4, 2] : undefined };
|
||||||
}
|
}
|
||||||
return { color: getItemColor(combinator, colorMap), dash: uSigs > 1 && sig === 'red' ? [4, 2] : undefined };
|
return { color: hslColor(combinator), dash: uSigs > 1 && sig === 'red' ? [4, 2] : undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -127,7 +136,6 @@ export function makeSignalsSeries(
|
|||||||
keys: string[],
|
keys: string[],
|
||||||
timeMode: 'real' | 'tick',
|
timeMode: 'real' | 'tick',
|
||||||
resolveName: (key: string) => string,
|
resolveName: (key: string) => string,
|
||||||
colorMap: ColorMap = new Map(),
|
|
||||||
): uPlot.Series[] {
|
): uPlot.Series[] {
|
||||||
const uCombs = new Set(keys.map(k => k.split('::')[0])).size;
|
const uCombs = new Set(keys.map(k => k.split('::')[0])).size;
|
||||||
const uItems = new Set(keys.map(k => k.split('::')[1])).size;
|
const uItems = new Set(keys.map(k => k.split('::')[1])).size;
|
||||||
@@ -145,7 +153,7 @@ export function makeSignalsSeries(
|
|||||||
xSeries,
|
xSeries,
|
||||||
...keys.map(k => {
|
...keys.map(k => {
|
||||||
const [, item_key] = k.split('::');
|
const [, item_key] = k.split('::');
|
||||||
const { color, dash } = getSeriesStyle(k, uCombs, uItems, uSigs, colorMap);
|
const { color, dash } = getSeriesStyle(k, uCombs, uItems, uSigs);
|
||||||
return {
|
return {
|
||||||
label: getSeriesLabel(k, uCombs, uItems, uSigs, resolveName(item_key)),
|
label: getSeriesLabel(k, uCombs, uItems, uSigs, resolveName(item_key)),
|
||||||
stroke: color,
|
stroke: color,
|
||||||
@@ -156,20 +164,15 @@ export function makeSignalsSeries(
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeSignalsAxes(timeMode: 'real' | 'tick', locale?: string): uPlot.Axis[] {
|
export function makeSignalsAxes(timeMode: 'real' | 'tick'): uPlot.Axis[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
...AXIS_BASE,
|
...AXIS_BASE,
|
||||||
values: timeMode === 'real'
|
...(timeMode === 'real' && {
|
||||||
? (_u: uPlot, vals: (number | null)[]) =>
|
values: (_u: uPlot, vals: (number | null)[]) =>
|
||||||
vals.map(v => v == null ? '' : new Date(v * 1000).toLocaleTimeString())
|
vals.map(v => v == null ? '' : new Date(v * 1000).toLocaleTimeString()),
|
||||||
: (_u: uPlot, vals: (number | null)[]) =>
|
}),
|
||||||
vals.map(v => v == null ? '' : formatSI(v, locale)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...AXIS_BASE,
|
|
||||||
values: (_u: uPlot, vals: (number | null)[]) =>
|
|
||||||
vals.map(v => v == null ? '' : formatSI(v, locale)),
|
|
||||||
},
|
},
|
||||||
|
{ ...AXIS_BASE },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ export default function ChartEditor({ initial, onSave, onClose }: Props) {
|
|||||||
const [whitelist, setWhitelist] = useState((initial?.filter_items ?? []).join(', '));
|
const [whitelist, setWhitelist] = useState((initial?.filter_items ?? []).join(', '));
|
||||||
const [blacklist, setBlacklist] = useState((initial?.filter_items_exclude ?? []).join(', '));
|
const [blacklist, setBlacklist] = useState((initial?.filter_items_exclude ?? []).join(', '));
|
||||||
const [useRegex, setUseRegex] = useState(initial?.filter_items_regex ?? false);
|
const [useRegex, setUseRegex] = useState(initial?.filter_items_regex ?? false);
|
||||||
const [orderBy, setOrderBy] = useState<ChartConfig['order_by']>(initial?.order_by ?? 'value_asc');
|
const [orderBy, setOrderBy] = useState<ChartConfig['order_by']>(initial?.order_by ?? 'time');
|
||||||
const [seriesLimit, setSeriesLimit] = useState(initial?.series_limit ?? 20);
|
const [seriesLimit, setSeriesLimit] = useState(initial?.series_limit ?? 20);
|
||||||
const [yMin, setYMin] = useState(initial?.y_min?.toString() ?? '');
|
const [yMin, setYMin] = useState(initial?.y_min?.toString() ?? '');
|
||||||
const [yMax, setYMax] = useState(initial?.y_max?.toString() ?? '');
|
const [yMax, setYMax] = useState(initial?.y_max?.toString() ?? '');
|
||||||
@@ -123,6 +123,7 @@ export default function ChartEditor({ initial, onSave, onClose }: Props) {
|
|||||||
|
|
||||||
<label className="block text-sm text-gray-400 mb-1">Sort series by</label>
|
<label className="block text-sm text-gray-400 mb-1">Sort series by</label>
|
||||||
<select value={orderBy} onChange={e => setOrderBy(e.target.value as ChartConfig['order_by'])} className={`${inputCls} mb-3`}>
|
<select value={orderBy} onChange={e => setOrderBy(e.target.value as ChartConfig['order_by'])} className={`${inputCls} mb-3`}>
|
||||||
|
<option value="time">Time (no sort)</option>
|
||||||
<option value="value_asc">Latest lowest values</option>
|
<option value="value_asc">Latest lowest values</option>
|
||||||
<option value="value_desc">Latest highest values</option>
|
<option value="value_desc">Latest highest values</option>
|
||||||
<option value="delta_asc">Biggest decrease (last 10 min)</option>
|
<option value="delta_asc">Biggest decrease (last 10 min)</option>
|
||||||
|
|||||||
@@ -151,18 +151,6 @@ export default function Dashboard({ alerts }: Props) {
|
|||||||
onLayoutChange={handleLayoutChange}
|
onLayoutChange={handleLayoutChange}
|
||||||
gridConfig={{ cols: COLS, rowHeight: ROW_HEIGHT, margin: [8, 8] }}
|
gridConfig={{ cols: COLS, rowHeight: ROW_HEIGHT, margin: [8, 8] }}
|
||||||
dragConfig={{ handle: '.drag-handle' }}
|
dragConfig={{ handle: '.drag-handle' }}
|
||||||
resizeConfig={{
|
|
||||||
handleComponent: (axis, ref) => (
|
|
||||||
<span ref={ref}
|
|
||||||
className="react-resizable-handle react-resizable-handle-se"
|
|
||||||
style={{
|
|
||||||
borderRight: '3px solid #4b5563',
|
|
||||||
borderBottom: '3px solid #4b5563',
|
|
||||||
opacity: 0.6,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{charts.map(c => (
|
{charts.map(c => (
|
||||||
<div key={c.id} className="drag-handle cursor-grab active:cursor-grabbing">
|
<div key={c.id} className="drag-handle cursor-grab active:cursor-grabbing">
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
export type ColorMap = Map<string, string>;
|
|
||||||
|
|
||||||
declare global { var __colorMapCache: ColorMap | undefined; }
|
|
||||||
|
|
||||||
export function parseColorCsv(text: string): ColorMap {
|
|
||||||
const map: ColorMap = new Map();
|
|
||||||
const lines = text.split(/\r?\n/).slice(1);
|
|
||||||
for (const line of lines) {
|
|
||||||
if (!line.trim()) continue;
|
|
||||||
const [key, color] = line.split(',');
|
|
||||||
if (key && color) map.set(key.trim(), color.trim());
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getColorMap(): Promise<ColorMap> {
|
|
||||||
if (globalThis.__colorMapCache) return globalThis.__colorMapCache;
|
|
||||||
try {
|
|
||||||
const res = await fetch('/factorio_item_colors.csv');
|
|
||||||
globalThis.__colorMapCache = res.ok ? parseColorCsv(await res.text()) : new Map();
|
|
||||||
} catch {
|
|
||||||
globalThis.__colorMapCache = new Map();
|
|
||||||
}
|
|
||||||
return globalThis.__colorMapCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
function djb2(s: string): number {
|
|
||||||
let h = 5381;
|
|
||||||
for (let i = 0; i < s.length; i++) h = (h * 33) ^ s.charCodeAt(i);
|
|
||||||
return h >>> 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getItemColor(key: string, colorMap: ColorMap): string {
|
|
||||||
return colorMap.get(key) ?? `hsl(${djb2(key) % 360},70%,60%)`;
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
const SI_THRESHOLDS = [
|
|
||||||
{ limit: 1_000_000_000, divisor: 1_000_000_000, suffix: 'G' },
|
|
||||||
{ limit: 1_000_000, divisor: 1_000_000, suffix: 'M' },
|
|
||||||
{ limit: 1_000, divisor: 1_000, suffix: 'K' },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export function formatSI(v: number, locale?: string, fractionDigits?: number): string {
|
|
||||||
const abs = Math.abs(v);
|
|
||||||
const fd = fractionDigits ?? 3;
|
|
||||||
for (const { limit, divisor, suffix } of SI_THRESHOLDS) {
|
|
||||||
if (abs >= limit) {
|
|
||||||
const formatted = new Intl.NumberFormat(locale, {
|
|
||||||
maximumFractionDigits: fd,
|
|
||||||
minimumFractionDigits: 0,
|
|
||||||
}).format(v / divisor);
|
|
||||||
return `${formatted}${suffix}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new Intl.NumberFormat(locale, {
|
|
||||||
maximumFractionDigits: fractionDigits != null ? fractionDigits : 0,
|
|
||||||
minimumFractionDigits: 0,
|
|
||||||
}).format(v);
|
|
||||||
}
|
|
||||||
5
web/package-lock.json
generated
5
web/package-lock.json
generated
@@ -986,7 +986,6 @@
|
|||||||
"integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==",
|
"integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
"pg-protocol": "*",
|
"pg-protocol": "*",
|
||||||
@@ -999,7 +998,6 @@
|
|||||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
@@ -1797,7 +1795,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
|
||||||
"integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
|
"integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pg-connection-string": "^2.12.0",
|
"pg-connection-string": "^2.12.0",
|
||||||
"pg-pool": "^3.13.0",
|
"pg-pool": "^3.13.0",
|
||||||
@@ -1971,7 +1968,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
|
||||||
"integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
|
"integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -1981,7 +1977,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz",
|
||||||
"integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==",
|
"integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,379 +0,0 @@
|
|||||||
item_key,color
|
|
||||||
copper-ore,#bf8040
|
|
||||||
copper-plate,#b87333
|
|
||||||
copper-cable,#e65100
|
|
||||||
iron-ore,#8d6e63
|
|
||||||
iron-plate,#bdbdbd
|
|
||||||
iron-gear-wheel,#a0a0a0
|
|
||||||
iron-stick,#888888
|
|
||||||
steel-plate,#757575
|
|
||||||
steel-gear-wheel,#5a5a5a
|
|
||||||
steel-beam,#424242
|
|
||||||
stone,#a1887f
|
|
||||||
stone-brick,#b8956a
|
|
||||||
coal,#37474f
|
|
||||||
uranium-ore,#66bb6a
|
|
||||||
uranium-238,#7cb342
|
|
||||||
uranium-235,#64dd17
|
|
||||||
crude-oil,#1a1a1a
|
|
||||||
heavy-oil,#4e342e
|
|
||||||
light-oil,#ff8a65
|
|
||||||
petroleum-gas,#1a0030
|
|
||||||
lubricant,#1b5e20
|
|
||||||
sulfuric-acid,#b2dfdb
|
|
||||||
water,#1565c0
|
|
||||||
steam,#e1f5fe
|
|
||||||
wood,#6d4c41
|
|
||||||
raw-wood,#3e2723
|
|
||||||
plastic-bar,#b0bec5
|
|
||||||
sulfur,#fff176
|
|
||||||
explosives,#ef5350
|
|
||||||
battery,#ff9800
|
|
||||||
empty-barrel,#90a4ae
|
|
||||||
filled-barrel,#546e7a
|
|
||||||
electronic-circuit,#4caf50
|
|
||||||
advanced-circuit,#e53935
|
|
||||||
processing-unit,#1565d2
|
|
||||||
automation-science-pack,#d32f2f
|
|
||||||
logistic-science-pack,#43a047
|
|
||||||
military-science-pack,#616161
|
|
||||||
chemical-science-pack,#00acc1
|
|
||||||
production-science-pack,#ff8f00
|
|
||||||
utility-science-pack,#7b1fa2
|
|
||||||
space-science-pack,#f48fb1
|
|
||||||
pipe,#78909c
|
|
||||||
engine-unit,#795548
|
|
||||||
electric-engine-unit,#4fc3f7
|
|
||||||
flying-robot-frame,#5e35b1
|
|
||||||
rocket-fuel,#ff5722
|
|
||||||
rocket-control-unit,#2e7d32
|
|
||||||
low-density-structure,#bcaaa4
|
|
||||||
heat-pipe,#bf360c
|
|
||||||
heat-exchanger,#263238
|
|
||||||
steam-turbine,#455a64
|
|
||||||
concrete,#9e9e9e
|
|
||||||
refined-concrete,#6d6d6d
|
|
||||||
landfill,#388e3c
|
|
||||||
cliff-explosives,#d84315
|
|
||||||
nuclear-fuel,#00c853
|
|
||||||
solid-fuel,#0d47a1
|
|
||||||
grenade,#5d4037
|
|
||||||
cluster-grenade,#c62828
|
|
||||||
landmine,#4a148c
|
|
||||||
fish,#29b6f6
|
|
||||||
glass,#4dd0e1
|
|
||||||
rail,#9e9e9e
|
|
||||||
rail-signal,#449c53
|
|
||||||
rail-chain-signal,#e33d2b
|
|
||||||
train-stop,#323f48
|
|
||||||
locomotive,#ce3c2c
|
|
||||||
cargo-wagon,#937966
|
|
||||||
fluid-wagon,#1450b8
|
|
||||||
artillery-wagon,#3d3d3d
|
|
||||||
artillery-turret,#383838
|
|
||||||
flamethrower-turret,#cc4e14
|
|
||||||
gun-turret,#5c5c5c
|
|
||||||
laser-turret,#e45d2f
|
|
||||||
radar,#378b45
|
|
||||||
roboport,#2196f3
|
|
||||||
construction-robot,#449c5f
|
|
||||||
logistic-robot,#133baa
|
|
||||||
speed-module,#f44336
|
|
||||||
speed-module-2,#e91e63
|
|
||||||
speed-module-3,#9c27b0
|
|
||||||
speed-module-5,#4c1178
|
|
||||||
effectivity-module,#4cae70
|
|
||||||
effectivity-module-2,#3a9256
|
|
||||||
effectivity-module-3,#30823f
|
|
||||||
effectivity-module-4,#1d6327
|
|
||||||
productivity-module,#ffb005
|
|
||||||
productivity-module-2,#ef6c00
|
|
||||||
productivity-module-3,#eb6600
|
|
||||||
productivity-module-5,#c5500d
|
|
||||||
beacon,#2078f3
|
|
||||||
substation,#b427b4
|
|
||||||
medium-electric-pole,#687d8d
|
|
||||||
big-electric-pole,#566c7b
|
|
||||||
small-electric-pole,#8c9eab
|
|
||||||
small-iron-electric-pole,#8996a9
|
|
||||||
steel-chest,#787878
|
|
||||||
iron-chest,#907c64
|
|
||||||
wooden-chest,#634b3b
|
|
||||||
transport-belt,#b3b3b3
|
|
||||||
fast-transport-belt,#50b1f7
|
|
||||||
express-transport-belt,#e56f34
|
|
||||||
underground-belt,#bfbfbf
|
|
||||||
fast-underground-belt,#4193f6
|
|
||||||
express-underground-belt,#e48d2f
|
|
||||||
splitter,#bdbdbd
|
|
||||||
fast-splitter,#3277f5
|
|
||||||
express-splitter,#dd8a1d
|
|
||||||
long-handed-inserter,#80604d
|
|
||||||
fast-inserter,#80664d
|
|
||||||
burner-inserter,#7c694b
|
|
||||||
inserter,#7c704b
|
|
||||||
stack-inserter,#47322a
|
|
||||||
burner-mining-drill,#706943
|
|
||||||
electric-mining-drill,#737345
|
|
||||||
area-mining-drill,#7c804d
|
|
||||||
electric-furnace,#384751
|
|
||||||
industrial-furnace,#475666
|
|
||||||
assembling-machine-3,#666666
|
|
||||||
centrifuge,#546578
|
|
||||||
chemical-plant,#0092c7
|
|
||||||
oil-refinery,#0f0f0f
|
|
||||||
pumpjack,#433428
|
|
||||||
offshore-pump,#162eca
|
|
||||||
boiler,#9e9e9e
|
|
||||||
steam-engine,#6c7d93
|
|
||||||
solar-panel,#ffb74d
|
|
||||||
accumulator,#0c59e9
|
|
||||||
lamp,#ffeb3b
|
|
||||||
constant-combinator,#495369
|
|
||||||
decider-combinator,#4d556f
|
|
||||||
arithmetic-combinator,#585b7e
|
|
||||||
power-switch,#ffb405
|
|
||||||
programmable-speaker,#751a89
|
|
||||||
aai-signal-receiver,#323c48
|
|
||||||
aai-signal-transmitter,#2a2f3c
|
|
||||||
textplate-small-copper,#e68600
|
|
||||||
used-up-uranium-fuel-cell,#34384b
|
|
||||||
uranium-fuel-cell,#5db669
|
|
||||||
electric-motor,#5976f7
|
|
||||||
motor,#4652f6
|
|
||||||
automation-core,#1712a5
|
|
||||||
iron-beam,#878787
|
|
||||||
kr-advanced-solar-panel,#ffc23d
|
|
||||||
kr-advanced-transport-belt,#26c6da
|
|
||||||
kr-advanced-loader,#0070a3
|
|
||||||
kr-advanced-splitter,#00bcd4
|
|
||||||
kr-superior-inserter,#74804d
|
|
||||||
kr-superior-filter-inserter,#5d4437
|
|
||||||
kr-superior-long-inserter,#6f804d
|
|
||||||
kr-superior-long-filter-inserter,#5d4737
|
|
||||||
kr-superior-underground-belt,#0054a8
|
|
||||||
kr-superior-loader,#0048bd
|
|
||||||
kr-fuel-refinery,#bb580c
|
|
||||||
kr-quarry-drill,#68804d
|
|
||||||
kr-express-loader,#00838f
|
|
||||||
kr-electric-mining-drill-mk2,#47382a
|
|
||||||
kr-steel-pipe,#6e6e6e
|
|
||||||
kr-steel-pipe-to-ground,#787878
|
|
||||||
kr-fluid-storage-2,#2415c1
|
|
||||||
kr-se-loader,#0039b3
|
|
||||||
beryllium,#81c784
|
|
||||||
beryllium-ore,#558b2f
|
|
||||||
beryllium-sulfate,#00695c
|
|
||||||
holmium,#ba68c8
|
|
||||||
holmium-ore,#ab47bc
|
|
||||||
holmium-solution,#e1bee7
|
|
||||||
cryonite,#00a8e0
|
|
||||||
cryonite-rod,#29aedb
|
|
||||||
cryonite-solution,#80deea
|
|
||||||
iridium,#cfd8dc
|
|
||||||
iridium-ore,#689f38
|
|
||||||
iridium-ingot,#e0e0e0
|
|
||||||
vulcanite,#ff7043
|
|
||||||
vulcanite-block,#e64a19
|
|
||||||
vulcanite-powder,#ffab91
|
|
||||||
imersite,#8e24aa
|
|
||||||
imersite-crystal,#283593
|
|
||||||
imersite-powder,#ce93d8
|
|
||||||
vita,#8bc34a
|
|
||||||
vita-extract,#19572a
|
|
||||||
vita-germination,#006064
|
|
||||||
naquium,#ffd54f
|
|
||||||
naquium-ore,#ffca28
|
|
||||||
naquium-ingot,#fff9c4
|
|
||||||
methane-ice,#b2ebf2
|
|
||||||
core-fragment,#4db6ac
|
|
||||||
rare-metals,#b0b0b0
|
|
||||||
raw-rare-metals,#a8a8a8
|
|
||||||
raw-imersite,#821e8f
|
|
||||||
sand,#c2b280
|
|
||||||
silicon,#6c7893
|
|
||||||
quartz,#e6e6e6
|
|
||||||
coke,#474747
|
|
||||||
se-copper-ingot,#b08130
|
|
||||||
se-iron-ingot,#84755c
|
|
||||||
se-steel-ingot,#707070
|
|
||||||
se-beryllium-ingot,#87c995
|
|
||||||
se-holmium-ingot,#c056c2
|
|
||||||
se-iridium-ingot,#d6dce1
|
|
||||||
se-cryonite-rod,#2397d1
|
|
||||||
se-cryonite-slush,#7bccea
|
|
||||||
se-vulcanite-crushed,#ff7b29
|
|
||||||
se-vulcanite-enriched,#ff6d29
|
|
||||||
se-vulcanite-block,#dc5a18
|
|
||||||
imersium-plate,#8e1d9a
|
|
||||||
se-kr-imersium-sulfide,#9f1e96
|
|
||||||
se-kr-fine-imersite-powder,#d58bd5
|
|
||||||
enriched-iron,#7e7358
|
|
||||||
electronic-components,#4eb183
|
|
||||||
empty-data-card,#512da8
|
|
||||||
heat-shielding,#00bfa5
|
|
||||||
thermodynamic-boiler,#311b92
|
|
||||||
cryogenic-plant,#0097a7
|
|
||||||
core-drill,#3d5afe
|
|
||||||
core-miner,#1a237e
|
|
||||||
se-quantum-processor,#123db5
|
|
||||||
se-holmium-cable,#c256bb
|
|
||||||
se-holmium-solenoid,#c45aae
|
|
||||||
se-superconductive-cable,#1c176e
|
|
||||||
se-data-storage-substrate,#4527a0
|
|
||||||
se-machine-learning-data,#3949ab
|
|
||||||
se-empty-data,#5c6bc0
|
|
||||||
se-broken-data,#7986cb
|
|
||||||
se-junk-data,#9fa8da
|
|
||||||
se-scrap,#757575
|
|
||||||
se-contaminated-scrap,#3a3122
|
|
||||||
se-genetic-data,#64b979
|
|
||||||
se-significant-data,#3f9751
|
|
||||||
se-experimental-genetic-data,#308249
|
|
||||||
se-atomic-data,#008fd6
|
|
||||||
se-star-probe-data,#ffea4d
|
|
||||||
se-significant-specimen,#00bdb6
|
|
||||||
se-specimen,#26a69a
|
|
||||||
se-bio-sludge,#2c7749
|
|
||||||
se-nutrient-gel,#005c5a
|
|
||||||
se-nutrient-gel-barrel,#004d40
|
|
||||||
mineral-water,#3236f5
|
|
||||||
chlorine,#b0dbde
|
|
||||||
nitric-acid,#ffcc80
|
|
||||||
se-bioscrubber,#174f30
|
|
||||||
se-space-coolant-hot,#da6e16
|
|
||||||
se-space-water,#6c59f7
|
|
||||||
se-chemical-gel,#ad25aa
|
|
||||||
se-vitalic-acid,#7ec44f
|
|
||||||
se-vitalic-reagent,#6db342
|
|
||||||
se-vitalic-epoxy,#5d9f38
|
|
||||||
se-neural-gel,#b39ddb
|
|
||||||
se-neural-gel-2,#9575cd
|
|
||||||
se-plasma-stream,#d50000
|
|
||||||
se-proton-stream,#e040fb
|
|
||||||
se-ion-stream,#651fff
|
|
||||||
se-particle-stream,#304ffe
|
|
||||||
se-vitamelange-extract,#427a2a
|
|
||||||
se-vitamelange-spice,#498c31
|
|
||||||
se-water-ice,#e3f2fd
|
|
||||||
lithium,#9eadb7
|
|
||||||
lithium-chloride,#b0b8c4
|
|
||||||
lithium-sulfur-battery,#db9ad7
|
|
||||||
fertilizer,#4a432b
|
|
||||||
biomass,#33691e
|
|
||||||
biomethanol,#419b5d
|
|
||||||
se-surface-teleporter,#24166a
|
|
||||||
se-observation-frame,#5eab3f
|
|
||||||
se-observation-frame-blank,#5eb83d
|
|
||||||
se-core-fragment-se-beryllium,#6bbd80
|
|
||||||
se-core-fragment-se-cryonite,#0070e0
|
|
||||||
se-core-fragment-se-holmium,#ca6dad
|
|
||||||
se-core-fragment-se-imersite,#931f80
|
|
||||||
se-core-fragment-se-iridium,#c7ced6
|
|
||||||
se-core-fragment-se-naquium,#ffdf29
|
|
||||||
se-core-fragment-se-vita,#62c247
|
|
||||||
se-core-fragment-se-vulcanite,#ca6a16
|
|
||||||
se-core-fragment-omni,#46a5aa
|
|
||||||
se-core-fragment-se-iridium-ore,#409f38
|
|
||||||
se-core-fragment-se-vitamelange,#4ac144
|
|
||||||
se-aeroframe-bulkhead,#4ea9b7
|
|
||||||
se-aeroframe-scaffold,#26a2a6
|
|
||||||
se-aeroframe-pole,#00897b
|
|
||||||
se-heavy-girder,#686f8d
|
|
||||||
se-heavy-bearing,#607d8b
|
|
||||||
se-heavy-composite,#484766
|
|
||||||
se-space-pipe,#4e9bb7
|
|
||||||
se-space-transport-belt,#2280c9
|
|
||||||
se-space-underground-belt,#001cbd
|
|
||||||
se-space-splitter,#0057d1
|
|
||||||
se-space-accumulator,#734bf7
|
|
||||||
se-space-solar-panel,#ffe438
|
|
||||||
se-space-solar-panel-2,#ffa726
|
|
||||||
se-space-elevator-cable,#4b2494
|
|
||||||
se-space-pipe-to-ground,#5191b8
|
|
||||||
se-space-mirror,#d9d9d9
|
|
||||||
se-space-rail,#787a9b
|
|
||||||
se-space-platform-scaffold,#b8b8b8
|
|
||||||
se-space-probe-rocket,#ff913d
|
|
||||||
se-space-capsule,#42a5f5
|
|
||||||
se-meteor-defence-ammo,#ed533b
|
|
||||||
se-dynamic-emitter,#3913aa
|
|
||||||
se-gammaray-detector,#009bb3
|
|
||||||
se-pylon-substation,#ffff52
|
|
||||||
se-rocket-launch-pad,#707070
|
|
||||||
se-rocket-landing-pad,#5e5e5e
|
|
||||||
se-cargo-rocket-fuel-tank,#ff7a05
|
|
||||||
se-cargo-rocket-cargo-pod,#ff985c
|
|
||||||
se-cargo-rocket-section,#ffa570
|
|
||||||
se-cargo-rocket-section-packed,#ffdd75
|
|
||||||
se-lifesupport-canister,#7a32f5
|
|
||||||
se-used-lifesupport-canister,#929bb0
|
|
||||||
se-canister,#898fa9
|
|
||||||
se-magnetic-canister,#ca6da3
|
|
||||||
se-iridium-ore,#348c31
|
|
||||||
se-iridium-ore-crushed,#35973d
|
|
||||||
se-beryllium-ore,#3b7a2a
|
|
||||||
se-beryllium-sulfate,#005461
|
|
||||||
se-holmium-ore,#a53eac
|
|
||||||
se-holmium-ore-crushed,#a93da0
|
|
||||||
se-compact-beacon,#4612a5
|
|
||||||
se-recycling-facility,#49a780
|
|
||||||
se-rocket-science-pack,#e3e3e3
|
|
||||||
lubricant-barrel,#ffc107
|
|
||||||
heavy-oil-barrel,#434028
|
|
||||||
light-oil-barrel,#ffb56b
|
|
||||||
petroleum-gas-barrel,#240038
|
|
||||||
se-material-science-pack-1,#fffc42
|
|
||||||
se-material-science-pack-2,#ffbe1a
|
|
||||||
se-material-science-pack-3,#f5c800
|
|
||||||
se-material-science-pack-4,#e67e00
|
|
||||||
se-material-testing-pack,#f57c00
|
|
||||||
se-material-insight,#ffe0b2
|
|
||||||
se-material-catalogue-1,#efff42
|
|
||||||
se-material-catalogue-2,#ffd11a
|
|
||||||
se-material-catalogue-3,#f5e400
|
|
||||||
se-material-catalogue-4,#e6a100
|
|
||||||
se-astronomic-science-pack-1,#565abd
|
|
||||||
se-astronomic-science-pack-2,#373aa4
|
|
||||||
se-astronomic-insight,#c5cae9
|
|
||||||
se-astronomic-catalogue-1,#6c61c2
|
|
||||||
se-astronomic-catalogue-2,#423bb0
|
|
||||||
se-astronomic-catalogue-3,#2a2c98
|
|
||||||
se-astronomic-catalogue-4,#371b83
|
|
||||||
se-biological-science-pack-1,#69f0ae
|
|
||||||
se-biological-science-pack-2,#00e676
|
|
||||||
se-biological-science-pack-3,#00ad5f
|
|
||||||
se-biological-science-pack-4,#009624
|
|
||||||
se-biological-insight,#b9f6ca
|
|
||||||
se-biological-catalogue-1,#6af0c1
|
|
||||||
se-biological-catalogue-2,#00e68a
|
|
||||||
se-biological-catalogue-3,#00c77e
|
|
||||||
se-biological-catalogue-4,#009431
|
|
||||||
se-energy-science-pack-1,#f17493
|
|
||||||
se-energy-science-pack-2,#f06292
|
|
||||||
se-energy-science-pack-3,#ec407a
|
|
||||||
se-energy-science-pack-4,#d81b60
|
|
||||||
se-energy-insight,#fce4ec
|
|
||||||
se-energy-catalogue-1,#f17482
|
|
||||||
se-energy-catalogue-2,#ee446f
|
|
||||||
se-energy-catalogue-3,#e92549
|
|
||||||
se-energy-catalogue-4,#bf183f
|
|
||||||
se-deep-space-science-pack-1,#bfa6de
|
|
||||||
se-deep-space-science-pack-2,#a27cd0
|
|
||||||
se-deep-space-science-pack-3,#7e57c2
|
|
||||||
se-deep-space-science-pack-4,#6f37b9
|
|
||||||
se-kr-matter-science-pack-1,#ffef8a
|
|
||||||
se-kr-matter-science-pack-2,#e0ff57
|
|
||||||
se-kr-matter-liberation-data,#ffe494
|
|
||||||
blank-tech-card,#898ca9
|
|
||||||
singularity-tech-card,#351566
|
|
||||||
ltn-combinator,#4c1b83
|
|
||||||
ltn-stop,#383a51
|
|
||||||
ltn-delivery-address,#286c4b
|
|
||||||
ltn-provider-stack-threshold,#f34a1b
|
|
||||||
ltn-requester-stack-threshold,#1643f3
|
|
||||||
ltn-provider-threshold,#d45d35
|
|
||||||
ltn-requester-threshold,#5c12a5
|
|
||||||
ltn-max-trains,#621281
|
|
||||||
ltn-max-train-length,#66127d
|
|
||||||
ltn-locked-slots,#534d6f
|
|
||||||
|
Reference in New Issue
Block a user