mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-18 20:59:55 -05:00
feat: system notifications updates, Thingport grab in System health check, node 24+ (#52)
* feat: update system notifications * feat: add Thingport grab to system health check * ci: upgrade to Node.js 24 and centralize version management
This commit is contained in:
1 parent
0bc0f99482
commit
358dfc1a03
47 files changed
+2534
-2784
No files matched your search
@@ -52,7 +52,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
cache-dependency-path: backend/package-lock.json
|
||||
- run: npm ci --no-audit --no-fund
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
cache-dependency-path: ${{ inputs.project_dir }}/package-lock.json
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
cache-dependency-path: extension/package-lock.json
|
||||
- run: npm ci
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
cache-dependency-path: extension/package-lock.json
|
||||
- run: npm ci
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
if: steps.gate.outputs.run != 'false'
|
||||
with:
|
||||
node-version: 22 # semantic-release 25 needs Node 22.14+
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
cache-dependency-path: extension/package-lock.json
|
||||
- if: steps.gate.outputs.run != 'false'
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
if: steps.creds.outputs.ok != 'false'
|
||||
with:
|
||||
node-version: 20
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
cache-dependency-path: extension/package-lock.json
|
||||
- if: steps.creds.outputs.ok != 'false'
|
||||
@@ -139,7 +139,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
cache-dependency-path: extension/package-lock.json
|
||||
- run: npm ci
|
||||
@@ -168,7 +168,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
cache-dependency-path: extension/package-lock.json
|
||||
- run: npm ci
|
||||
@@ -183,7 +183,7 @@ jobs:
|
||||
VERSION: ${{ needs.release.outputs.version }}
|
||||
TAG: ${{ needs.release.outputs.tag }}
|
||||
run: |
|
||||
jq -n --arg notes "Automated update to version $VERSION. Changes: https://github.com/TautvydasDerzinskas/Thingport/releases/tag/$TAG -- the bundles aren't minified; to rebuild them from the attached source (Node 20+): npm ci && npm run build:firefox, output in dist/firefox. Documentation: https://thingport.net/docs/grab/" \
|
||||
jq -n --arg notes "Automated update to version $VERSION. Changes: https://github.com/TautvydasDerzinskas/Thingport/releases/tag/$TAG -- the bundles aren't minified; to rebuild them from the attached source (Node 24+): npm ci && npm run build:firefox, output in dist/firefox. Documentation: https://thingport.net/docs/grab/" \
|
||||
'{version: {approval_notes: $notes}}' > dist/amo-metadata.json
|
||||
# Mozilla may hold a listed version for manual review, so don't wait for the signed file.
|
||||
- name: Submit to addons.mozilla.org
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: web/.nvmrc
|
||||
node-version-file: .nvmrc
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: bridge/go.mod
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: web/.nvmrc
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
cache-dependency-path: regression/package-lock.json
|
||||
- run: npm ci --no-audit --no-fund
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22 # semantic-release 25 needs Node 22.14+
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: web/.nvmrc
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
cache-dependency-path: web/package-lock.json
|
||||
# Supplies the Pages URL and base path. Skipped for PR builds, which may come from forks.
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: web/.nvmrc
|
||||
node-version-file: .nvmrc
|
||||
- run: node .github/wiki/build.mjs "$RUNNER_TEMP/wiki-out"
|
||||
- name: Push to the wiki
|
||||
env:
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
},
|
||||
"rules": {
|
||||
"no-underscore-dangle": ["error", { "allow": ["_max", "_count", "_sum", "_avg", "_min"] }],
|
||||
"no-await-in-loop": "off"
|
||||
"no-await-in-loop": "off",
|
||||
"consistent-function-scoping": "off"
|
||||
},
|
||||
"env": {
|
||||
"builtin": true,
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
FROM node:20-alpine AS dependencies
|
||||
FROM node:24-alpine AS dependencies
|
||||
WORKDIR /app
|
||||
RUN apk add --no-cache openssl
|
||||
COPY package*.json ./
|
||||
@@ -15,7 +15,7 @@ FROM dependencies AS build
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20-alpine AS runtime
|
||||
FROM node:24-alpine AS runtime
|
||||
# Set by build-image.yml to the commit that triggered this image's build -- exposed via
|
||||
# GET /settings/version-check so the admin settings page can tell whether a newer commit
|
||||
# touching backend/ has landed on main since this image was published. Left "unknown" for a
|
||||
@@ -31,7 +31,7 @@ COPY --from=build /app/dist ./dist
|
||||
COPY package*.json ./
|
||||
COPY prisma ./prisma
|
||||
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
# node:20-alpine already ships a "node" user/group at uid/gid 1000; remove it first so our own
|
||||
# node:24-alpine already ships a "node" user/group at uid/gid 1000; remove it first so our own
|
||||
# "appuser"/"appgroup" (which docker-entrypoint.sh re-aligns to PUID/PGID at container start)
|
||||
# can claim that uid/gid without colliding.
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh \
|
||||
|
||||
Generated
+511
-474
File diff suppressed because it is too large.
Load diff
+36
-29
@@ -5,7 +5,7 @@
|
||||
"license": "AGPL-3.0-only",
|
||||
"type": "commonjs",
|
||||
"engines": {
|
||||
"node": ">=20.19"
|
||||
"node": ">=24"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/server.ts",
|
||||
@@ -18,47 +18,54 @@
|
||||
"lint": "oxlint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.20.0",
|
||||
"@prisma/client": "^5.22.0",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"cheerio": "^1.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^4.21.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"cheerio": "^1.2.0",
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^18.0.8",
|
||||
"express": "^4.22.3",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"jszip": "^3.10.2",
|
||||
"meshoptimizer": "^1.3.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"multer": "^2.0.1",
|
||||
"nodemailer": "^10.0.1",
|
||||
"mime-types": "^3.0.2",
|
||||
"multer": "^2.4.0",
|
||||
"nodemailer": "^10.1.2",
|
||||
"openid-client": "^6.8.8",
|
||||
"prisma": "^5.20.0",
|
||||
"sharp": "^0.35.4",
|
||||
"prisma": "^5.22.0",
|
||||
"sharp": "^0.35.5",
|
||||
"svg-captcha": "^1.4.0",
|
||||
"three": "^0.160.1",
|
||||
"three": "^0.186.1",
|
||||
"turndown": "^7.2.4",
|
||||
"turndown-plugin-gfm": "^1.0.2",
|
||||
"yauzl": "^3.2.0",
|
||||
"zod": "^3.23.8"
|
||||
"yauzl": "^3.4.0",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jsonwebtoken": "^9.0.7",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/multer": "^2.2.0",
|
||||
"@types/node": "^20.14.0",
|
||||
"@types/nodemailer": "^8.0.1",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^4.17.25",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
"@types/multer": "^2.3.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/nodemailer": "^8.0.2",
|
||||
"@types/supertest": "^7.2.1",
|
||||
"@types/three": "^0.160.0",
|
||||
"@types/three": "^0.186.0",
|
||||
"@types/turndown": "^5.0.6",
|
||||
"@types/yauzl": "^2.10.3",
|
||||
"oxlint": "^1.81.0",
|
||||
"supertest": "^7.2.2",
|
||||
"tsx": "^4.19.1",
|
||||
"typescript": "^5.6.3",
|
||||
"vitest": "^4.1.11"
|
||||
"@types/yauzl": "^3.4.0",
|
||||
"oxlint": "^1.88.0",
|
||||
"supertest": "^7.3.1",
|
||||
"tsx": "^4.23.15",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.1.12"
|
||||
},
|
||||
"overrides": {
|
||||
"qs": "^6.16.0"
|
||||
},
|
||||
"allowScripts": {
|
||||
"prisma": true,
|
||||
"@prisma/client": true,
|
||||
"@prisma/engines": true,
|
||||
"esbuild": true,
|
||||
"fsevents": false
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import express, { type Express, type NextFunction, type Request, type Response }
|
||||
import cors from "cors";
|
||||
import { resolveCorsOrigins } from "./cors";
|
||||
import { HttpError } from "./utils/fileUtils";
|
||||
import { trackClients } from "./services/clientActivityService";
|
||||
|
||||
import healthRoutes from "./routes/health";
|
||||
import captchaRoutes from "./routes/captcha";
|
||||
@@ -42,6 +43,7 @@ export function createApp(): Express {
|
||||
);
|
||||
|
||||
app.use(express.json());
|
||||
app.use("/api", trackClients);
|
||||
|
||||
// The frontend's nginx proxies /api/* here unmodified.
|
||||
app.use("/api", healthRoutes);
|
||||
|
||||
@@ -45,9 +45,16 @@ export const TAG_MOSAICS = path.join(STORAGE, "tag-mosaics");
|
||||
export const NORMALIZED_3MFS = path.join(STORAGE, "normalized-3mf");
|
||||
// Large multi-part projects take minutes on a slow CPU; the page waits for it, not the request.
|
||||
export const NORMALIZE_3MF_TIMEOUT_SECONDS = envInt("NORMALIZE_3MF_TIMEOUT_SECONDS", 600);
|
||||
|
||||
// A worker can't even start below a few MB, and Node 24 then takes the whole process down with it.
|
||||
const MIN_PREVIEW_MEMORY_MB = 8;
|
||||
|
||||
// Past either limit the render worker is killed and the plate gets no 3D preview. Memory is a
|
||||
// watchdog check, so a fast burst can overshoot it briefly.
|
||||
export const MODEL_PREVIEW_MAX_MEMORY_MB = envInt("MODEL_PREVIEW_MAX_MEMORY_MB", defaultPreviewMemoryMb());
|
||||
export const MODEL_PREVIEW_MAX_MEMORY_MB = Math.max(
|
||||
MIN_PREVIEW_MEMORY_MB,
|
||||
envInt("MODEL_PREVIEW_MAX_MEMORY_MB", defaultPreviewMemoryMb()),
|
||||
);
|
||||
export const MODEL_PREVIEW_TIMEOUT_SECONDS = envInt("MODEL_PREVIEW_TIMEOUT_SECONDS", 180);
|
||||
|
||||
// Only the first run's value: it's copied into the database, and managed there (Admin > Auth) from then on.
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { NextFunction, Request, Response } from "express";
|
||||
import { prisma } from "../db";
|
||||
import { isExtensionRequest } from "./captchaService";
|
||||
|
||||
// When Thingport Grab last reached this instance, for System Health. A web page can't ask whether an extension is
|
||||
// installed, but Grab makes requests of its own here.
|
||||
|
||||
export type Client = "grab";
|
||||
|
||||
const SETTING_KEY: Record<Client, string> = { grab: "client_seen_grab" };
|
||||
// Grab makes requests all day; the last-seen time only needs to be roughly right.
|
||||
const WRITE_EVERY_MS = 10 * 60 * 1000;
|
||||
const lastWritten = new Map<Client, number>();
|
||||
|
||||
function record(client: Client) {
|
||||
const now = Date.now();
|
||||
if (now - (lastWritten.get(client) ?? 0) < WRITE_EVERY_MS) return;
|
||||
lastWritten.set(client, now);
|
||||
const value = { seen_at: new Date(now).toISOString() };
|
||||
// Never holds up or breaks the request it was seen on.
|
||||
void prisma.setting
|
||||
.upsert({ where: { key: SETTING_KEY[client] }, create: { key: SETTING_KEY[client], value }, update: { value } })
|
||||
.catch(() => lastWritten.delete(client));
|
||||
}
|
||||
|
||||
export function trackClients(req: Request, _res: Response, next: NextFunction) {
|
||||
if (isExtensionRequest(req)) record("grab");
|
||||
next();
|
||||
}
|
||||
|
||||
export async function clientLastSeen(client: Client): Promise<Date | null> {
|
||||
const row = await prisma.setting.findUnique({ where: { key: SETTING_KEY[client] } });
|
||||
const seen = (row?.value as { seen_at?: string } | undefined)?.seen_at;
|
||||
return seen ? new Date(seen) : null;
|
||||
}
|
||||
|
||||
/** For tests: forget what was written, so the next request records again. */
|
||||
export function resetClientTracking() {
|
||||
lastWritten.clear();
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
SYSTEM_NOTIFICATIONS_URL,
|
||||
} from "../config";
|
||||
import { prisma } from "../db";
|
||||
import { clientLastSeen } from "./clientActivityService";
|
||||
import { listAiProviderModels } from "./aiCategorizationService";
|
||||
import { resolveAiConnection } from "./aiConnectionService";
|
||||
import { testFlaresolverr } from "./flaresolverr";
|
||||
@@ -29,6 +30,7 @@ export const HEALTH_COMPONENTS = [
|
||||
"internet",
|
||||
"flaresolverr",
|
||||
"thingiverse",
|
||||
"grab",
|
||||
"ai",
|
||||
"smtp",
|
||||
] as const;
|
||||
@@ -230,6 +232,18 @@ async function checkSmtp(): Promise<Found> {
|
||||
}
|
||||
}
|
||||
|
||||
// Seen, not tested: it can't be reached from here, so this is the last time it reached Thingport.
|
||||
async function checkGrab(): Promise<Found> {
|
||||
const [seen, connection] = await Promise.all([
|
||||
clientLastSeen("grab"),
|
||||
// Connections made before Grab was tracked still count.
|
||||
prisma.grabConnection.findFirst({ orderBy: { lastUsedAt: { sort: "desc", nulls: "last" } } }),
|
||||
]);
|
||||
const connected = connection ? (connection.lastUsedAt ?? connection.createdAt) : null;
|
||||
const last = seen && connected ? (seen > connected ? seen : connected) : (seen ?? connected);
|
||||
return last ? found("ok", "ok", { last_seen_at: last.toISOString() }) : found("not_configured", "not_seen");
|
||||
}
|
||||
|
||||
const CHECKS: Record<HealthComponent, () => Promise<Found>> = {
|
||||
database: checkDatabase,
|
||||
storage: checkStorage,
|
||||
@@ -237,6 +251,7 @@ const CHECKS: Record<HealthComponent, () => Promise<Found>> = {
|
||||
internet: checkInternet,
|
||||
flaresolverr: checkFlaresolverr,
|
||||
thingiverse: checkThingiverse,
|
||||
grab: checkGrab,
|
||||
ai: checkAi,
|
||||
smtp: checkSmtp,
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { prisma } from "../db";
|
||||
import { SYSTEM_NOTIFICATIONS_URL } from "../config";
|
||||
import { UNREAD_RETENTION_DAYS } from "./notificationService";
|
||||
import { createKeyedLock } from "../utils/concurrency";
|
||||
import { getBackendVersion } from "./versionService";
|
||||
|
||||
const FETCH_TIMEOUT_MS = 15_000;
|
||||
const SYNC_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
||||
@@ -19,16 +20,58 @@ const entrySchema = z.object({
|
||||
.url()
|
||||
.refine((value) => value.startsWith("https://"))
|
||||
.nullish(),
|
||||
// An in-app page opened on click, e.g. "/admin"; "//" would leave the app.
|
||||
path: z
|
||||
.string()
|
||||
.trim()
|
||||
.regex(/^\/(?!\/)\S*$/)
|
||||
.max(200)
|
||||
.nullish(),
|
||||
published_at: z.coerce.date(),
|
||||
// Admins by default: most announcements are about running the instance.
|
||||
audience: z.enum(["admins", "all"]).default("admins"),
|
||||
// Which app versions get it: "1.0.0", "<1.0.0", "<=1.0.0", ">1.0.0" or ">=1.0.0"; omitted for all.
|
||||
version: z
|
||||
.string()
|
||||
.trim()
|
||||
.regex(/^(<=|>=|<|>|=)?\s*\d+\.\d+\.\d+$/)
|
||||
.nullish(),
|
||||
});
|
||||
export type SystemNotificationEntry = z.infer<typeof entrySchema>;
|
||||
|
||||
/** One bad entry is skipped rather than hiding the rest. */
|
||||
function compareVersions(a: string, b: string): number {
|
||||
const pa = a.split(".").map(Number);
|
||||
const pb = b.split(".").map(Number);
|
||||
for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pa[i] - pb[i];
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** An entry aimed at certain versions is held back while this instance's version is unknown. */
|
||||
export function matchesVersion(range: string | null | undefined, version: string | null): boolean {
|
||||
if (!range) return true;
|
||||
const own = version?.match(/^\d+\.\d+\.\d+/)?.[0];
|
||||
if (!own) return false;
|
||||
const [, op = "=", target] = range.match(/^(<=|>=|<|>|=)?\s*(.+)$/)!;
|
||||
const diff = compareVersions(own, target);
|
||||
switch (op) {
|
||||
case "<":
|
||||
return diff < 0;
|
||||
case "<=":
|
||||
return diff <= 0;
|
||||
case ">":
|
||||
return diff > 0;
|
||||
case ">=":
|
||||
return diff >= 0;
|
||||
default:
|
||||
return diff === 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** One bad entry is skipped rather than hiding the rest. Versions before 1.0.0 read only `notifications`
|
||||
* and ignore `version`, so entries they mustn't get live in `entries`. */
|
||||
export function parseSystemNotifications(data: unknown): SystemNotificationEntry[] {
|
||||
const list = (data as { notifications?: unknown } | null)?.notifications;
|
||||
if (!Array.isArray(list)) return [];
|
||||
const file = data as { notifications?: unknown; entries?: unknown } | null;
|
||||
const list = [file?.notifications, file?.entries].flatMap((l) => (Array.isArray(l) ? l : []));
|
||||
return list.flatMap((raw) => {
|
||||
const parsed = entrySchema.safeParse(raw);
|
||||
return parsed.success ? [parsed.data] : [];
|
||||
@@ -51,10 +94,20 @@ const withDeliveryLock = createKeyedLock();
|
||||
|
||||
/** Sends each user every entry meant for them they haven't had yet, as an ordinary notification dated
|
||||
* when it was posted. Future entries wait for their date; ones past the unread retention aren't sent. */
|
||||
export function deliverSystemNotifications(entries: SystemNotificationEntry[], now = new Date()): Promise<number> {
|
||||
export function deliverSystemNotifications(
|
||||
entries: SystemNotificationEntry[],
|
||||
now = new Date(),
|
||||
version = getBackendVersion(),
|
||||
): Promise<number> {
|
||||
return withDeliveryLock("system", async () => {
|
||||
const oldest = now.getTime() - UNREAD_RETENTION_DAYS * DAY_MS;
|
||||
const due = entries.filter((e) => e.published_at.getTime() <= now.getTime() && e.published_at.getTime() >= oldest);
|
||||
// No receipt for a version mismatch, so an entry still reaches an instance that updates into its range.
|
||||
const due = entries.filter(
|
||||
(e) =>
|
||||
e.published_at.getTime() <= now.getTime() &&
|
||||
e.published_at.getTime() >= oldest &&
|
||||
matchesVersion(e.version, version),
|
||||
);
|
||||
if (!due.length) return 0;
|
||||
const [users, receipts] = await Promise.all([
|
||||
prisma.user.findMany({ select: { id: true, role: true } }),
|
||||
@@ -83,6 +136,7 @@ export function deliverSystemNotifications(entries: SystemNotificationEntry[], n
|
||||
title: entry.title,
|
||||
body: entry.body ?? null,
|
||||
externalUrl: entry.url ?? null,
|
||||
internalPath: entry.path ?? null,
|
||||
createdAt: entry.published_at,
|
||||
})),
|
||||
}),
|
||||
|
||||
@@ -4,8 +4,8 @@ import fsSync from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
// Separate file because limits are read when config.ts loads. 1 MB is below a worker's startup
|
||||
// cost, so every render hits a limit.
|
||||
// Separate file because limits are read when config.ts loads. 1 MB is raised to the 8 MB floor (any lower and
|
||||
// the worker can't start, which crashes the whole process on Node 24); 8 MB is still too little for a render.
|
||||
vi.hoisted(() => {
|
||||
process.env.MODEL_PREVIEW_MAX_MEMORY_MB = "1";
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import request from "supertest";
|
||||
import { createApp } from "../src/app";
|
||||
import { prisma } from "../src/db";
|
||||
import { resetClientTracking } from "../src/services/clientActivityService";
|
||||
|
||||
const app = createApp();
|
||||
let admin: string;
|
||||
@@ -36,6 +37,24 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("system health", () => {
|
||||
it("reports Grab as not seen until it makes a request of its own", async () => {
|
||||
await prisma.setting.deleteMany({ where: { key: "client_seen_grab" } });
|
||||
await prisma.grabConnection.deleteMany();
|
||||
resetClientTracking();
|
||||
const test = async () =>
|
||||
resultOf(
|
||||
(await request(app).post("/api/admin/health/test").set(as(admin)).send({ component: "grab" })).body,
|
||||
"grab",
|
||||
)!;
|
||||
expect(await test()).toMatchObject({ status: "not_configured", code: "not_seen" });
|
||||
|
||||
await request(app).get("/api/health").set("X-Thingport-Client", "grab");
|
||||
// Recorded without holding up the request.
|
||||
await vi.waitFor(async () => {
|
||||
expect(await test()).toMatchObject({ status: "ok", facts: { last_seen_at: expect.any(String) } });
|
||||
});
|
||||
});
|
||||
|
||||
it("lists every component by importance, the database first and SMTP, which is optional, last", async () => {
|
||||
const res = await request(app).get("/api/admin/health").set(as(admin));
|
||||
expect(res.status).toBe(200);
|
||||
@@ -46,6 +65,7 @@ describe("system health", () => {
|
||||
"internet",
|
||||
"flaresolverr",
|
||||
"thingiverse",
|
||||
"grab",
|
||||
"ai",
|
||||
"smtp",
|
||||
]);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createApp } from "../src/app";
|
||||
import { prisma } from "../src/db";
|
||||
import {
|
||||
deliverSystemNotifications,
|
||||
matchesVersion,
|
||||
parseSystemNotifications,
|
||||
type SystemNotificationEntry,
|
||||
} from "../src/services/systemNotificationService";
|
||||
@@ -51,11 +52,19 @@ describe("system notifications", () => {
|
||||
{ id: "bad-url", title: "x", published_at: "2026-10-01T00:00:00Z", url: "not a url" },
|
||||
{ id: "plain-http", title: "x", published_at: "2026-10-01T00:00:00Z", url: "http://example.com" },
|
||||
{ id: "bad-audience", title: "x", published_at: "2026-10-01T00:00:00Z", audience: "everyone" },
|
||||
{ id: "bad-version", title: "x", published_at: "2026-10-01T00:00:00Z", version: "~1.0" },
|
||||
{ id: "off-site-path", title: "x", published_at: "2026-10-01T00:00:00Z", path: "//evil.example" },
|
||||
{ id: "for-old", title: "x", published_at: "2026-10-01T00:00:00Z", version: "<1.0.0" },
|
||||
],
|
||||
});
|
||||
expect(parsed.map((e) => e.id)).toEqual(["ok"]);
|
||||
expect(parsed.map((e) => e.id)).toEqual(["ok", "for-old"]);
|
||||
expect(parsed[0].audience).toBe("admins");
|
||||
expect(parseSystemNotifications({ nope: true })).toEqual([]);
|
||||
const both = parseSystemNotifications({
|
||||
notifications: [{ id: "legacy", title: "x", published_at: "2026-10-01T00:00:00Z" }],
|
||||
entries: [{ id: "new", title: "x", published_at: "2026-10-01T00:00:00Z", version: "1.0.0" }],
|
||||
});
|
||||
expect(both.map((e) => e.id)).toEqual(["legacy", "new"]);
|
||||
});
|
||||
|
||||
it("sends due entries to admins only, dated when they were posted", async () => {
|
||||
@@ -80,6 +89,28 @@ describe("system notifications", () => {
|
||||
expect((await systemOf(admin.id)).some((n) => n.title === "Title everyone")).toBe(true);
|
||||
});
|
||||
|
||||
it("matches version ranges", () => {
|
||||
expect(matchesVersion(undefined, null)).toBe(true);
|
||||
expect(matchesVersion("<1.0.0", "0.9.12")).toBe(true);
|
||||
expect(matchesVersion("<1.0.0", "1.0.0")).toBe(false);
|
||||
expect(matchesVersion("1.0.0", "1.0.0")).toBe(true);
|
||||
expect(matchesVersion("=1.0.0", "1.0.1")).toBe(false);
|
||||
expect(matchesVersion(">1.0.0", "1.10.0")).toBe(true);
|
||||
expect(matchesVersion(">=1.0.0", "1.0.0")).toBe(true);
|
||||
expect(matchesVersion("<=1.0.0", "1.0.1")).toBe(false);
|
||||
expect(matchesVersion("1.0.0", null)).toBe(false);
|
||||
});
|
||||
|
||||
it("sends a version-targeted entry only once the instance is in range", async () => {
|
||||
const targeted = entry("v1", 1, { version: "1.0.0", path: "/admin" });
|
||||
await deliverSystemNotifications([targeted], new Date(), "0.9.0");
|
||||
expect((await systemOf(admin.id)).some((n) => n.title === "Title v1")).toBe(false);
|
||||
await deliverSystemNotifications([targeted], new Date(), "1.0.0");
|
||||
const sent = (await systemOf(admin.id)).filter((n) => n.title === "Title v1");
|
||||
expect(sent).toHaveLength(1);
|
||||
expect(sent[0].internalPath).toBe("/admin");
|
||||
});
|
||||
|
||||
it("never sends the same entry twice, even after it was deleted", async () => {
|
||||
const once = entry("once", 1);
|
||||
await deliverSystemNotifications([once]);
|
||||
|
||||
+10
-4
@@ -15,7 +15,7 @@ instance, follow the Docker Compose instructions in the [README](../README.md) i
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js 20 or newer.** `backend/package.json` sets `engines.node >= 20`, and CI builds on 20.
|
||||
- **Node.js 24 or newer.** The root `.nvmrc` says 24 (`nvm use` picks it up), and CI and the Docker images run on it.
|
||||
- **Docker**, to run Postgres. Everything else runs natively so you get hot reload.
|
||||
- **Go 1.21+** -- only if you're changing `bridge/`.
|
||||
|
||||
@@ -226,18 +226,19 @@ Windows and Linux binaries cross-compile with `CGO_ENABLED=0`.
|
||||
Announcements for every instance, such as a known bug or a new feature, live in
|
||||
[`system-notifications.json`](../system-notifications.json) at the repo root. Each backend fetches it
|
||||
from `main` at startup and every 6 hours, and sends each entry once to each user it's meant for, as an
|
||||
ordinary notification (the System tab). To post one, add an entry and merge it:
|
||||
ordinary notification (the System tab). To post one, add an entry to `entries` and merge it:
|
||||
|
||||
```json
|
||||
{
|
||||
"notifications": [
|
||||
"entries": [
|
||||
{
|
||||
"id": "2026-10-imports-fail-on-printables",
|
||||
"title": "Printables imports fail since Oct 10",
|
||||
"body": "Printables changed its API. A fix is on the way; update when it lands.",
|
||||
"url": "https://github.com/TautvydasDerzinskas/Thingport/issues/123",
|
||||
"published_at": "2026-10-11T12:00:00Z",
|
||||
"audience": "admins"
|
||||
"audience": "admins",
|
||||
"version": "<1.2.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -245,8 +246,13 @@ ordinary notification (the System tab). To post one, add an entry and merge it:
|
||||
|
||||
- `id` must be unique and never reused: it's what stops an entry being sent twice.
|
||||
- `body` and `url` are optional; `url` must be `https://`, and the notification shows its domain.
|
||||
- `path` is an optional in-app page opened on click, e.g. `/admin`.
|
||||
- `published_at` is the date shown; a future date waits until then.
|
||||
- `audience` is `admins` (the default) or `all` for every user.
|
||||
- `version` limits it to instances on certain versions: `1.2.0` exactly, or `<`, `<=`, `>`, `>=` a
|
||||
version. Leave it out for all versions. An instance that later updates into the range still gets it.
|
||||
- Versions before 1.0.0 read only the `notifications` list and ignore `version`, so add there only what
|
||||
every one of them should get; everything else goes in `entries`.
|
||||
- Entries older than 90 days aren't sent, so old ones can stay in the file or be removed.
|
||||
- `SYSTEM_NOTIFICATIONS_URL` points a backend at another file; an empty value turns fetching off.
|
||||
|
||||
|
||||
@@ -16,15 +16,16 @@ for.
|
||||
|
||||
## What's checked
|
||||
|
||||
| Part | Needed | What it's for, and what breaks without it |
|
||||
| ------------ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Database | Required | Holds every model, collection, user and setting. Without it nothing in Thingport works. |
|
||||
| File storage | Required | Where model files, pictures and 3D previews are kept. Uploads, imports and downloads fail if it isn't writable or runs out of space. |
|
||||
| Memory | Required | Building 3D previews of large models takes the most. With too little, previews fail or the server restarts. |
|
||||
| FlareSolverr | Recommended | Gets past Cloudflare's checks, mostly on Thingiverse and Printables. Without it, imports and syncs from those sites are blocked whenever Cloudflare challenges the server. |
|
||||
| Thingiverse | Recommended | The instance's Thingiverse Access Token, used for every Thingiverse import, synced collection and author lookup. Not checked while Thingiverse is [turned off](../PROVIDER_SETUP.md#turning-a-provider-off). |
|
||||
| AI provider | Optional | The default AI model behind [AI categorization and improved descriptions](ai.md). Everything else works without it. |
|
||||
| Email (SMTP) | Optional | Sends invitations, email confirmations and password resets. Without it, invitation links are shown to copy instead, and new accounts don't confirm their address. |
|
||||
| Part | Needed | What it's for, and what breaks without it |
|
||||
| -------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Database | Required | Holds every model, collection, user and setting. Without it nothing in Thingport works. |
|
||||
| File storage | Required | Where model files, pictures and 3D previews are kept. Uploads, imports and downloads fail if it isn't writable or runs out of space. |
|
||||
| Memory | Required | Building 3D previews of large models takes the most. With too little, previews fail or the server restarts. |
|
||||
| FlareSolverr | Recommended | Gets past Cloudflare's checks, mostly on Thingiverse and Printables. Without it, imports and syncs from those sites are blocked whenever Cloudflare challenges the server. |
|
||||
| Thingiverse | Recommended | The instance's Thingiverse Access Token, used for every Thingiverse import, synced collection and author lookup. Not checked while Thingiverse is [turned off](../PROVIDER_SETUP.md#turning-a-provider-off). |
|
||||
| Thingport Grab | Recommended | The [browser extension](../../extension/README.md) for importing while you browse MakerWorld, Printables and Thingiverse, whole collections included, and for bringing over Printables likes. Shown as seen once any browser uses it with this instance. |
|
||||
| AI provider | Optional | The default AI model behind [AI categorization and improved descriptions](ai.md). Everything else works without it. |
|
||||
| Email (SMTP) | Optional | Sends invitations, email confirmations and password resets. Without it, invitation links are shown to copy instead, and new accounts don't confirm their address. |
|
||||
|
||||
## What the colours mean
|
||||
|
||||
@@ -35,6 +36,8 @@ for.
|
||||
broken until it's fixed.
|
||||
- **Not configured** (grey): not set up. Fine for an optional part you don't use. For FlareSolverr or Thingiverse,
|
||||
imports from those sites will fail sooner or later.
|
||||
Thingport can't look into anyone's browser, so Grab stays grey until it's first used with this instance; get it
|
||||
from the **Downloads** page.
|
||||
- **Not tested**: hasn't been checked yet. Click **Test** or **Run full test**.
|
||||
|
||||
Every problem comes with what to do about it, for example: check that the database container is running, make sure
|
||||
|
||||
+16
-16
@@ -8,7 +8,7 @@ workflow shared by the whole repo is in the root **[CONTRIBUTING.md](../CONTRIBU
|
||||
## Getting started
|
||||
|
||||
The extension is TypeScript, bundled with [esbuild](https://esbuild.github.io/), with styles in
|
||||
SCSS. You need Node.js 20 or newer. Everything below runs from this `extension/` folder:
|
||||
SCSS. You need Node.js 24 or newer. Everything below runs from this `extension/` folder:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
@@ -73,20 +73,20 @@ uses the light one.
|
||||
|
||||
### Scripts
|
||||
|
||||
| Command | What it does |
|
||||
| ------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| `npm run dev` / `dev:firefox` | Watch build of `dist/chrome` / `dist/firefox` |
|
||||
| `npm run build` | Builds `dist/chrome`, `dist/firefox` and `dist/edge` |
|
||||
| `npm run build:chrome` / `build:firefox` / `build:edge` | Builds one browser |
|
||||
| `npm run zip` | Builds everything and packages the store zips (below) |
|
||||
| `npm run zip:chrome` / `zip:firefox` / `zip:edge` | Same for one store |
|
||||
| `npm run typecheck` | `tsc --noEmit` |
|
||||
| `npm run lint` | oxlint (also run on commit by the repo's pre-commit hook) |
|
||||
| `npm run lint:firefox` | Builds for Firefox and runs `web-ext lint` on the result |
|
||||
| `npm run screenshots` | Regenerates the README screenshots (see below) |
|
||||
| `npm run store-assets` | Renders the store logo, promotional tiles and PNG screenshots (see below) |
|
||||
| `npm run verify` | Typecheck + lint + build |
|
||||
| `npm run release:dry-run` | Shows the next version semantic-release would release, and why (Node 22.14+) |
|
||||
| Command | What it does |
|
||||
| ------------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| `npm run dev` / `dev:firefox` | Watch build of `dist/chrome` / `dist/firefox` |
|
||||
| `npm run build` | Builds `dist/chrome`, `dist/firefox` and `dist/edge` |
|
||||
| `npm run build:chrome` / `build:firefox` / `build:edge` | Builds one browser |
|
||||
| `npm run zip` | Builds everything and packages the store zips (below) |
|
||||
| `npm run zip:chrome` / `zip:firefox` / `zip:edge` | Same for one store |
|
||||
| `npm run typecheck` | `tsc --noEmit` |
|
||||
| `npm run lint` | oxlint (also run on commit by the repo's pre-commit hook) |
|
||||
| `npm run lint:firefox` | Builds for Firefox and runs `web-ext lint` on the result |
|
||||
| `npm run screenshots` | Regenerates the README screenshots (see below) |
|
||||
| `npm run store-assets` | Renders the store logo, promotional tiles and PNG screenshots (see below) |
|
||||
| `npm run verify` | Typecheck + lint + build |
|
||||
| `npm run release:dry-run` | Shows the next version semantic-release would release, and why |
|
||||
|
||||
The only difference between the browser builds is `manifest.json` (see `scripts/manifest.ts`):
|
||||
Chrome and Edge run the background as a service worker, Firefox as an event page
|
||||
@@ -225,7 +225,7 @@ follow from this:
|
||||
The commit messages also become the changelog and the GitHub release notes, so write them for a
|
||||
reader.
|
||||
|
||||
To see what the next release would be, without releasing anything (needs Node 22.14+):
|
||||
To see what the next release would be, without releasing anything:
|
||||
|
||||
```bash
|
||||
npm run release:dry-run
|
||||
|
||||
Generated
+806
-793
File diff suppressed because it is too large.
Load diff
+11
-6
@@ -28,18 +28,23 @@
|
||||
"@semantic-release/changelog": "^7.0.0",
|
||||
"@semantic-release/exec": "^7.1.0",
|
||||
"@semantic-release/git": "^11.0.1",
|
||||
"@types/chrome": "^0.3.0",
|
||||
"@types/node": "^20.19.0",
|
||||
"@types/chrome": "^0.3.5",
|
||||
"@types/node": "^24.0.0",
|
||||
"esbuild": "^0.28.2",
|
||||
"fflate": "^0.8.3",
|
||||
"jszip": "^3.10.2",
|
||||
"oxlint": "^1.85.0",
|
||||
"playwright": "^1.63.0",
|
||||
"sass": "^1.105.0",
|
||||
"oxlint": "^1.88.0",
|
||||
"playwright": "^1.64.1",
|
||||
"sass": "^1.105.2",
|
||||
"semantic-release": "^25.0.9",
|
||||
"semantic-release-monorepo": "^8.0.2",
|
||||
"tsx": "^4.23.15",
|
||||
"typescript": "^5.9.3",
|
||||
"web-ext": "^8.10.0"
|
||||
"web-ext": "^10.8.0"
|
||||
},
|
||||
"allowScripts": {
|
||||
"esbuild": true,
|
||||
"@parcel/watcher": true,
|
||||
"fsevents": false
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,9 @@
|
||||
"jsx-no-new-object-as-prop": "off",
|
||||
"jsx-no-jsx-as-prop": "off",
|
||||
"jsx-no-new-array-as-prop": "off",
|
||||
"set-state-in-effect": "off"
|
||||
"set-state-in-effect": "off",
|
||||
"incompatible-library": "off",
|
||||
"consistent-function-scoping": "off"
|
||||
},
|
||||
"env": {
|
||||
"builtin": true,
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# Installing and building run on the build machine's own platform: their output, the static dist, is the
|
||||
# same for every architecture, and running Node under QEMU for the arm64 image is slow and has crashed
|
||||
# npm ci mid-install. Only the nginx stage below is built per platform.
|
||||
FROM --platform=$BUILDPLATFORM node:20-alpine AS dependencies
|
||||
FROM --platform=$BUILDPLATFORM node:24-alpine AS dependencies
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci --no-audit --no-fund
|
||||
|
||||
Generated
+271
-377
File diff suppressed because it is too large.
Load diff
+22
-18
@@ -18,31 +18,35 @@
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@emotion/react": "^11.13.0",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@mui/icons-material": "^6.1.0",
|
||||
"@mui/material": "^6.1.0",
|
||||
"@tanstack/react-virtual": "3.14.13",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@mui/icons-material": "^6.5.0",
|
||||
"@mui/material": "^6.5.0",
|
||||
"@tanstack/react-virtual": "3.14.21",
|
||||
"fflate": "0.8.3",
|
||||
"i18next": "^23.15.0",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"i18next": "^23.16.8",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"occt-import-js": "0.0.23",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^15.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-i18next": "^15.7.4",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router-dom": "^6.30.6",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"three": "^0.160.0"
|
||||
"three": "^0.186.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/three": "^0.160.0",
|
||||
"oxlint": "^1.81.0",
|
||||
"typescript": "5.6.3",
|
||||
"vite": "^6.4.3",
|
||||
"vitest": "3.2.6"
|
||||
"@types/react": "^18.3.31",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@types/three": "^0.186.0",
|
||||
"oxlint": "^1.88.0",
|
||||
"typescript": "5.9.3",
|
||||
"vite": "^6.4.4",
|
||||
"vitest": "4.1.12"
|
||||
},
|
||||
"allowScripts": {
|
||||
"esbuild": true,
|
||||
"fsevents": false
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ export type AdminUser = {
|
||||
};
|
||||
|
||||
export type HealthComponent =
|
||||
"database" | "storage" | "memory" | "internet" | "flaresolverr" | "thingiverse" | "ai" | "smtp";
|
||||
"database" | "storage" | "memory" | "internet" | "flaresolverr" | "thingiverse" | "grab" | "ai" | "smtp";
|
||||
export type HealthStatus = "ok" | "warning" | "error" | "not_configured";
|
||||
|
||||
export type HealthResult = {
|
||||
|
||||
@@ -46,7 +46,9 @@ export default function LightBurnPreview({ url, assetId, filename, imgSx }: Ligh
|
||||
if (!payload) {
|
||||
throw new Error("Preview not found in LightBurn file");
|
||||
}
|
||||
const objectUrl = URL.createObjectURL(new Blob([payload.data], { type: payload.mime }));
|
||||
const objectUrl = URL.createObjectURL(
|
||||
new Blob([payload.data as Uint8Array<ArrayBuffer>], { type: payload.mime }),
|
||||
);
|
||||
previewCache.set(cacheKey, objectUrl);
|
||||
if (alive) {
|
||||
setPreview(objectUrl);
|
||||
|
||||
@@ -551,6 +551,7 @@
|
||||
"testAll": "Run full test",
|
||||
"test": "Test",
|
||||
"settings": "Settings",
|
||||
"download": "Download",
|
||||
"testedAgo": "Tested {{when}}",
|
||||
"neverTested": "Not tested yet",
|
||||
"lastFullTest": "Last full test: {{when}} ({{ago}})",
|
||||
@@ -572,7 +573,8 @@
|
||||
"memory": "{{available}} available of {{total}}",
|
||||
"memoryContainer": "{{available}} available of the container's {{total}}",
|
||||
"process": "Thingport uses {{used}}",
|
||||
"latency": "answered in {{ms}} ms"
|
||||
"latency": "answered in {{ms}} ms",
|
||||
"lastSeen": "last seen {{when}}"
|
||||
},
|
||||
"problems": {
|
||||
"unknown": "This component reported a problem. The details below come from it directly."
|
||||
@@ -632,6 +634,13 @@
|
||||
"timeout": "Thingiverse didn't answer in time. Try again later."
|
||||
}
|
||||
},
|
||||
"grab": {
|
||||
"name": "Thingport Grab",
|
||||
"description": "The browser extension for importing models from MakerWorld, Printables and Thingiverse while you browse them, including whole collections, with no links to copy. It also brings over Printables likes, which only you can see.",
|
||||
"problems": {
|
||||
"not_seen": "No browser has used Grab with this instance yet. Install it from Downloads and connect it to Thingport. It's the easiest way to import, and some sites only let a browser get their files."
|
||||
}
|
||||
},
|
||||
"ai": {
|
||||
"name": "AI provider",
|
||||
"description": "The default AI model, behind AI categorization and improved descriptions. Everything else works without it.",
|
||||
|
||||
@@ -552,6 +552,7 @@
|
||||
"testAll": "Patikrinti viską",
|
||||
"test": "Patikrinti",
|
||||
"settings": "Nustatymai",
|
||||
"download": "Atsisiųsti",
|
||||
"testedAgo": "Patikrinta {{when}}",
|
||||
"neverTested": "Dar netikrinta",
|
||||
"lastFullTest": "Paskutinis pilnas patikrinimas: {{when}} ({{ago}})",
|
||||
@@ -573,7 +574,8 @@
|
||||
"memory": "laisva {{available}} iš {{total}}",
|
||||
"memoryContainer": "laisva {{available}} iš konteineriui skirtų {{total}}",
|
||||
"process": "Thingport naudoja {{used}}",
|
||||
"latency": "atsakė per {{ms}} ms"
|
||||
"latency": "atsakė per {{ms}} ms",
|
||||
"lastSeen": "paskutinį kartą {{when}}"
|
||||
},
|
||||
"problems": {
|
||||
"unknown": "Šis komponentas pranešė apie problemą. Žemiau – jo paties pateikta informacija."
|
||||
@@ -633,6 +635,13 @@
|
||||
"timeout": "Thingiverse laiku neatsakė. Bandykite vėliau."
|
||||
}
|
||||
},
|
||||
"grab": {
|
||||
"name": "Thingport Grab",
|
||||
"description": "Naršyklės plėtinys, kuriuo modelius iš MakerWorld, Printables ir Thingiverse importuojate tiesiog naršydami, net ištisas kolekcijas, be nuorodų kopijavimo. Jis taip pat perkelia Printables patiktukus, kuriuos matote tik jūs.",
|
||||
"problems": {
|
||||
"not_seen": "Su šiuo serveriu Grab dar nenaudota nė vienoje naršyklėje. Įdiekite jį skiltyje „Atsisiuntimai“ ir prijunkite prie Thingport. Taip importuoti paprasčiausia, o kai kurios svetainės failus duoda tik naršyklei."
|
||||
}
|
||||
},
|
||||
"ai": {
|
||||
"name": "DI tiekėjas",
|
||||
"description": "Numatytasis DI modelis, naudojamas DI kategorizavimui ir aprašymų tobulinimui. Visa kita veikia ir be jo.",
|
||||
|
||||
@@ -12,6 +12,7 @@ import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import AutoAwesomeOutlinedIcon from "@mui/icons-material/AutoAwesomeOutlined";
|
||||
import CloudOutlinedIcon from "@mui/icons-material/CloudOutlined";
|
||||
import ExtensionOutlinedIcon from "@mui/icons-material/ExtensionOutlined";
|
||||
import FolderOutlinedIcon from "@mui/icons-material/FolderOutlined";
|
||||
import LanguageIcon from "@mui/icons-material/Language";
|
||||
import MailOutlineIcon from "@mui/icons-material/MailOutline";
|
||||
@@ -37,8 +38,11 @@ type Props = {
|
||||
|
||||
type Need = "required" | "recommended" | "optional";
|
||||
|
||||
/** How much Thingport needs each one, and where it's set up. Order comes from the server: by importance. */
|
||||
const COMPONENTS: Record<HealthComponent, { icon: React.ReactNode; need: Need; settings?: string }> = {
|
||||
type ComponentInfo = { icon: React.ReactNode; need: Need; settings?: string; download?: string };
|
||||
|
||||
/** How much Thingport needs each one, and where it's set up or, for Grab, downloaded. Order comes from the
|
||||
* server: by importance. */
|
||||
const COMPONENTS: Record<HealthComponent, ComponentInfo> = {
|
||||
database: { icon: <StorageIcon />, need: "required", settings: "/admin-connections?tab=database" },
|
||||
storage: { icon: <FolderOutlinedIcon />, need: "required", settings: "/admin-storage" },
|
||||
memory: { icon: <MemoryIcon />, need: "required", settings: "/admin-rendering" },
|
||||
@@ -46,6 +50,7 @@ const COMPONENTS: Record<HealthComponent, { icon: React.ReactNode; need: Need; s
|
||||
internet: { icon: <LanguageIcon />, need: "recommended" },
|
||||
flaresolverr: { icon: <ShieldOutlinedIcon />, need: "recommended", settings: "/admin-connections?tab=flaresolverr" },
|
||||
thingiverse: { icon: <CloudOutlinedIcon />, need: "recommended", settings: "/admin-providers" },
|
||||
grab: { icon: <ExtensionOutlinedIcon />, need: "recommended", download: "/downloads" },
|
||||
ai: { icon: <AutoAwesomeOutlinedIcon />, need: "optional", settings: "/admin-ai" },
|
||||
smtp: { icon: <MailOutlineIcon />, need: "optional", settings: "/admin-connections?tab=smtp" },
|
||||
};
|
||||
@@ -58,7 +63,11 @@ const STATUS_COLOR: Record<HealthStatus, "success" | "warning" | "error" | "defa
|
||||
};
|
||||
|
||||
/** The facts worth a glance, in words; the rest stay out of the way. */
|
||||
function factsLine(result: HealthResult, t: (key: string, options?: Record<string, unknown>) => string): string[] {
|
||||
function factsLine(
|
||||
result: HealthResult,
|
||||
t: (key: string, options?: Record<string, unknown>) => string,
|
||||
language: string,
|
||||
): string[] {
|
||||
const { facts } = result;
|
||||
const lines: string[] = [];
|
||||
const bytes = (key: string) => (typeof facts[key] === "number" ? formatFileSize(facts[key] as number) : null);
|
||||
@@ -78,6 +87,9 @@ function factsLine(result: HealthResult, t: (key: string, options?: Record<strin
|
||||
);
|
||||
lines.push(t("adminSettings.health.facts.process", { used: bytes("process_bytes") }));
|
||||
}
|
||||
if (typeof facts.last_seen_at === "string") {
|
||||
lines.push(t("adminSettings.health.facts.lastSeen", { when: relativeTime(facts.last_seen_at, language) }));
|
||||
}
|
||||
if (result.latency_ms !== null) lines.push(t("adminSettings.health.facts.latency", { ms: result.latency_ms }));
|
||||
return lines;
|
||||
}
|
||||
@@ -121,9 +133,9 @@ function ComponentRow({
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t(`${base}.description`)}
|
||||
</Typography>
|
||||
{result && result.status !== "not_configured" && factsLine(result, t).length > 0 && (
|
||||
{result && result.status !== "not_configured" && factsLine(result, t, i18n.language).length > 0 && (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{factsLine(result, t).join(" · ")}
|
||||
{factsLine(result, t, i18n.language).join(" · ")}
|
||||
</Typography>
|
||||
)}
|
||||
{problem && (
|
||||
@@ -151,6 +163,11 @@ function ComponentRow({
|
||||
sx={{ flexShrink: 0, pl: { xs: 4.75, sm: 0 } }}
|
||||
>
|
||||
<Stack direction="row" spacing={1}>
|
||||
{info.download && (
|
||||
<Button size="small" component={RouterLink} to={info.download}>
|
||||
{t("adminSettings.health.download")}
|
||||
</Button>
|
||||
)}
|
||||
{info.settings && (
|
||||
<Button size="small" component={RouterLink} to={info.settings}>
|
||||
{t("adminSettings.health.settings")}
|
||||
|
||||
@@ -67,7 +67,7 @@ export default function ActivitySection({ onUnauthorized }: Props) {
|
||||
}, [range, onUnauthorized, t]);
|
||||
|
||||
// The year list comes with the first answer; until then, this year.
|
||||
const years = loaded?.data.years ?? [new Date().getFullYear()];
|
||||
const years = loaded?.data.years ?? [thisYear];
|
||||
|
||||
return (
|
||||
<Box>
|
||||
|
||||
@@ -84,7 +84,7 @@ export function buildUploadEntriesFromZip(
|
||||
const parts = path.split("/");
|
||||
const filename = parts[parts.length - 1];
|
||||
const type = guessMimeType(filename);
|
||||
const file = new File([content], filename, type ? { type } : undefined);
|
||||
const file = new File([content as Uint8Array<ArrayBuffer>], filename, type ? { type } : undefined);
|
||||
const relativePath = normalizedBase ? `${normalizedBase}/${path}` : path;
|
||||
entries.push({ file, relativePath });
|
||||
}
|
||||
|
||||
Generated
+82
-85
@@ -11,8 +11,8 @@
|
||||
"@semantic-release/exec": "^7.1.0",
|
||||
"@semantic-release/git": "^11.0.1",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.2.10",
|
||||
"prettier": "^3.9.9",
|
||||
"lint-staged": "^16.4.0",
|
||||
"prettier": "^3.9.10",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"prettier-plugin-sh": "^0.20.2",
|
||||
"semantic-release": "^25.0.9",
|
||||
@@ -1657,17 +1657,34 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cli-truncate": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz",
|
||||
"integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==",
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz",
|
||||
"integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"slice-ansi": "^5.0.0",
|
||||
"string-width": "^7.0.0"
|
||||
"slice-ansi": "^8.0.0",
|
||||
"string-width": "^8.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-truncate/node_modules/string-width": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz",
|
||||
"integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-east-asian-width": "^1.5.0",
|
||||
"strip-ansi": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
@@ -1716,13 +1733,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "13.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
|
||||
"integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
|
||||
"version": "14.0.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
|
||||
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/compare-func": {
|
||||
@@ -2753,13 +2770,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
|
||||
"integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
|
||||
"integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-east-asian-width": "^1.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
@@ -2962,19 +2982,6 @@
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/lilconfig": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antonk52"
|
||||
}
|
||||
},
|
||||
"node_modules/lines-and-columns": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
@@ -2983,41 +2990,50 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lint-staged": {
|
||||
"version": "15.5.2",
|
||||
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.2.tgz",
|
||||
"integrity": "sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==",
|
||||
"version": "16.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz",
|
||||
"integrity": "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^5.4.1",
|
||||
"commander": "^13.1.0",
|
||||
"debug": "^4.4.0",
|
||||
"execa": "^8.0.1",
|
||||
"lilconfig": "^3.1.3",
|
||||
"listr2": "^8.2.5",
|
||||
"micromatch": "^4.0.8",
|
||||
"pidtree": "^0.6.0",
|
||||
"commander": "^14.0.3",
|
||||
"listr2": "^9.0.5",
|
||||
"picomatch": "^4.0.3",
|
||||
"string-argv": "^0.3.2",
|
||||
"yaml": "^2.7.0"
|
||||
"tinyexec": "^1.0.4",
|
||||
"yaml": "^2.8.2"
|
||||
},
|
||||
"bin": {
|
||||
"lint-staged": "bin/lint-staged.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.12.0"
|
||||
"node": ">=20.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/lint-staged"
|
||||
}
|
||||
},
|
||||
"node_modules/lint-staged/node_modules/picomatch": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
|
||||
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/listr2": {
|
||||
"version": "8.3.3",
|
||||
"resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz",
|
||||
"integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==",
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz",
|
||||
"integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cli-truncate": "^4.0.0",
|
||||
"cli-truncate": "^5.0.0",
|
||||
"colorette": "^2.0.20",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"log-update": "^6.1.0",
|
||||
@@ -3025,7 +3041,7 @@
|
||||
"wrap-ansi": "^9.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/load-json-file": {
|
||||
@@ -3134,22 +3150,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/log-update/node_modules/is-fullwidth-code-point": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
|
||||
"integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-east-asian-width": "^1.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/log-update/node_modules/slice-ansi": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
|
||||
@@ -5662,19 +5662,6 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pidtree": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.1.tgz",
|
||||
"integrity": "sha512-e0F9AOF1JMrCfBsyJOwU9lNvQ0WtXTq0j/4jk0BQ5JSI9VAybPXmDpPRw/2FQ3e5d3ZFN1mLh7jW99m/jjaptw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"pidtree": "bin/pidtree.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/pify": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
|
||||
@@ -5776,9 +5763,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.9.9",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz",
|
||||
"integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==",
|
||||
"version": "3.9.10",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz",
|
||||
"integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -6879,17 +6866,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/slice-ansi": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz",
|
||||
"integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz",
|
||||
"integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.0.0",
|
||||
"is-fullwidth-code-point": "^4.0.0"
|
||||
"ansi-styles": "^6.2.3",
|
||||
"is-fullwidth-code-point": "^5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
|
||||
@@ -7232,6 +7219,16 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz",
|
||||
"integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@
|
||||
"@semantic-release/exec": "^7.1.0",
|
||||
"@semantic-release/git": "^11.0.1",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.2.10",
|
||||
"prettier": "^3.9.9",
|
||||
"lint-staged": "^16.4.0",
|
||||
"prettier": "^3.9.10",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"prettier-plugin-sh": "^0.20.2",
|
||||
"semantic-release": "^25.0.9",
|
||||
|
||||
Generated
+20
-20
@@ -7,19 +7,19 @@
|
||||
"name": "thingport-regression",
|
||||
"license": "AGPL-3.0-only",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.63.0",
|
||||
"@types/node": "^20.14.0",
|
||||
"@playwright/test": "^1.64.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"fflate": "^0.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz",
|
||||
"integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==",
|
||||
"version": "1.64.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.64.1.tgz",
|
||||
"integrity": "sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.63.0"
|
||||
"playwright": "1.64.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -29,13 +29,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.43",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
|
||||
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
|
||||
"version": "24.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz",
|
||||
"integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
"undici-types": ">=7.24.0 <7.24.7"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
@@ -46,13 +46,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.63.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz",
|
||||
"integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==",
|
||||
"version": "1.64.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.1.tgz",
|
||||
"integrity": "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.63.0"
|
||||
"playwright-core": "1.64.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -62,9 +62,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.63.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz",
|
||||
"integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==",
|
||||
"version": "1.64.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz",
|
||||
"integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -75,9 +75,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"version": "7.24.6",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
|
||||
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
"screenshots": "node screenshots.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.63.0",
|
||||
"@types/node": "^20.14.0",
|
||||
"@playwright/test": "^1.64.1",
|
||||
"@types/node": "^24.0.0",
|
||||
"fflate": "^0.8.3"
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,26 @@
|
||||
"url": "https://github.com/TautvydasDerzinskas/Thingport",
|
||||
"published_at": "2026-10-11T12:00:00Z",
|
||||
"audience": "all"
|
||||
},
|
||||
{
|
||||
"id": "2026-10-update-to-1.0.0",
|
||||
"title": "Thingport 1.0.0 is out",
|
||||
"body": "The first stable release brings a remade administration panel, single sign-on providers, and many bug fixes and UI improvements. Update to get them.",
|
||||
"url": "https://github.com/TautvydasDerzinskas/Thingport/releases",
|
||||
"published_at": "2026-10-17T12:00:00Z",
|
||||
"audience": "admins",
|
||||
"version": "<1.0.0"
|
||||
}
|
||||
],
|
||||
"entries": [
|
||||
{
|
||||
"id": "2026-10-welcome-to-1.0.0",
|
||||
"title": "Welcome to Thingport 1.0.0",
|
||||
"body": "Thanks for updating to the first stable release! Run a system health check in Administration to make sure everything is set up right. From now on, releases will come less often but pack more changes.",
|
||||
"path": "/admin",
|
||||
"published_at": "2026-10-17T12:00:00Z",
|
||||
"audience": "admins",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
22
|
||||
+1
-1
@@ -13,7 +13,7 @@ npm run build # static output in dist/
|
||||
npm run check # type-check
|
||||
```
|
||||
|
||||
Requires Node 22.12+ (see `.nvmrc`).
|
||||
Requires Node 24+ (see the root `.nvmrc`).
|
||||
|
||||
## Where the content lives
|
||||
|
||||
|
||||
Generated
+455
-903
File diff suppressed because it is too large.
Load diff
+7
-4
@@ -5,7 +5,7 @@
|
||||
"license": "AGPL-3.0-only",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
"node": ">=24"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
@@ -14,15 +14,18 @@
|
||||
"check": "astro check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/markdown-remark": "^7.3.1",
|
||||
"@astrojs/markdown-remark": "^7.3.3",
|
||||
"@astrojs/rss": "^4.0.19",
|
||||
"@astrojs/sitemap": "^3.7.4",
|
||||
"astro": "^7.3.4",
|
||||
"astro": "^7.4.0",
|
||||
"unist-util-visit": "^5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "^0.9.10",
|
||||
"sharp": "^0.35.4",
|
||||
"sharp": "^0.35.5",
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"allowScripts": {
|
||||
"fsevents": false
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user