iobroker.cometvisu 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/admin/i18n/es.json +8 -0
- package/admin/i18n/fr.json +8 -0
- package/admin/i18n/it.json +8 -0
- package/admin/i18n/nl.json +8 -0
- package/admin/i18n/pl.json +8 -0
- package/admin/i18n/pt.json +8 -0
- package/admin/i18n/ru.json +8 -0
- package/admin/i18n/uk.json +8 -0
- package/admin/i18n/zh-cn.json +8 -0
- package/admin/jsonConfig.json +3 -0
- package/build/lib/releases.js +4 -4
- package/build/lib/releases.js.map +2 -2
- package/build/lib/web.js +2 -2
- package/build/lib/web.js.map +1 -1
- package/build/main.js +2 -2
- package/build/main.js.map +1 -1
- package/io-package.json +28 -3
- package/package.json +9 -9
- package/admin/i18n/{de/translations.json → de.json} +1 -1
- package/admin/i18n/{en/translations.json → en.json} +1 -1
package/README.md
CHANGED
|
@@ -57,6 +57,17 @@ replaces that entry.
|
|
|
57
57
|
Placeholder for the next version (at the beginning of the line):
|
|
58
58
|
### **WORK IN PROGRESS**
|
|
59
59
|
-->
|
|
60
|
+
### 0.0.4 (2026-09-06)
|
|
61
|
+
|
|
62
|
+
- (joltcoke) the adapter now requires node.js 22 and is tested on 22 and 24
|
|
63
|
+
- (joltcoke) the admin page is available in all eleven languages ioBroker ships
|
|
64
|
+
- (joltcoke) updated express to 5, TypeScript to 6 in both packages, axios and tar to their current releases
|
|
65
|
+
- (joltcoke) dependabot updates are scheduled by cron, wait seven days and use the ioBroker automerge action
|
|
66
|
+
|
|
67
|
+
### 0.0.3 (2026-09-05)
|
|
68
|
+
|
|
69
|
+
- (joltcoke) "ioBroker" is no longer listed in "common.keywords", where the adapter checker rejects it
|
|
70
|
+
|
|
60
71
|
### 0.0.2 (2026-09-05)
|
|
61
72
|
|
|
62
73
|
- (joltcoke) keywords now contain "ioBroker", as the adapter checker asks for
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"CometVisu": "CometVisu",
|
|
3
|
+
"CometVisu version": "Versión CometVisu",
|
|
4
|
+
"Delivery": "Entrega",
|
|
5
|
+
"Serve through this web instance": "Servir a través de esta instancia web",
|
|
6
|
+
"The selected version is unpacked and activated when you save.": "La versión seleccionada se descomprime y se activa al guardar.",
|
|
7
|
+
"This web instance serves the visualisation under /cometvisu and protects it with its own login.": "Esta instancia web sirve la visualización en /cometvisu y la protege con su propio inicio de sesión."
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"CometVisu": "CometVisu",
|
|
3
|
+
"CometVisu version": "Version CometVisu",
|
|
4
|
+
"Delivery": "Livraison",
|
|
5
|
+
"Serve through this web instance": "Servir via cette instance Web",
|
|
6
|
+
"The selected version is unpacked and activated when you save.": "La version sélectionnée est décompressée et activée lorsque vous enregistrez.",
|
|
7
|
+
"This web instance serves the visualisation under /cometvisu and protects it with its own login.": "Cette instance Web sert la visualisation sous /cometvisu et la protège avec son propre login."
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"CometVisu": "CometVisu",
|
|
3
|
+
"CometVisu version": "Versione CometVisu",
|
|
4
|
+
"Delivery": "Consegna",
|
|
5
|
+
"Serve through this web instance": "Servire tramite questa istanza Web",
|
|
6
|
+
"The selected version is unpacked and activated when you save.": "La versione selezionata viene decompressa e attivata quando si salva.",
|
|
7
|
+
"This web instance serves the visualisation under /cometvisu and protects it with its own login.": "Questa istanza web serve la visualizzazione sotto /cometvisu e la protegge con il proprio login."
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"CometVisu": "CometVisu",
|
|
3
|
+
"CometVisu version": "CometVisu-versie",
|
|
4
|
+
"Delivery": "Levering",
|
|
5
|
+
"Serve through this web instance": "Serveer via deze webinstantie",
|
|
6
|
+
"The selected version is unpacked and activated when you save.": "De geselecteerde versie wordt uitgepakt en geactiveerd wanneer u opslaat.",
|
|
7
|
+
"This web instance serves the visualisation under /cometvisu and protects it with its own login.": "Deze webinstantie verzorgt de visualisatie onder /cometvisu en beschermt deze met een eigen login."
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"CometVisu": "CometVisu",
|
|
3
|
+
"CometVisu version": "Wersja CometVisu",
|
|
4
|
+
"Delivery": "Dostawa",
|
|
5
|
+
"Serve through this web instance": "Obsługuj przez tę instancję internetową",
|
|
6
|
+
"The selected version is unpacked and activated when you save.": "Wybrana wersja zostanie rozpakowana i aktywowana po zapisaniu.",
|
|
7
|
+
"This web instance serves the visualisation under /cometvisu and protects it with its own login.": "Ta instancja internetowa służy do wizualizacji w /cometvisu i chroni ją własnym loginem."
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"CometVisu": "CometVisu",
|
|
3
|
+
"CometVisu version": "Versão CometVisu",
|
|
4
|
+
"Delivery": "Entrega",
|
|
5
|
+
"Serve through this web instance": "Servir por meio desta instância da web",
|
|
6
|
+
"The selected version is unpacked and activated when you save.": "A versão selecionada é descompactada e ativada quando você salva.",
|
|
7
|
+
"This web instance serves the visualisation under /cometvisu and protects it with its own login.": "Esta instância web serve a visualização em /cometvisu e a protege com seu próprio login."
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"CometVisu": "CometVisu",
|
|
3
|
+
"CometVisu version": "Версия CometVisu",
|
|
4
|
+
"Delivery": "Доставка",
|
|
5
|
+
"Serve through this web instance": "Обслуживание через этот веб-экземпляр",
|
|
6
|
+
"The selected version is unpacked and activated when you save.": "Выбранная версия распаковывается и активируется при сохранении.",
|
|
7
|
+
"This web instance serves the visualisation under /cometvisu and protects it with its own login.": "Этот веб-экземпляр обслуживает визуализацию в /cometvisu и защищает ее с помощью собственного входа в систему."
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"CometVisu": "CometVisu",
|
|
3
|
+
"CometVisu version": "Версія CometVisu",
|
|
4
|
+
"Delivery": "Доставка",
|
|
5
|
+
"Serve through this web instance": "Обслуговуйте через цей веб-екземпляр",
|
|
6
|
+
"The selected version is unpacked and activated when you save.": "Вибрана версія розпаковується та активується під час збереження.",
|
|
7
|
+
"This web instance serves the visualisation under /cometvisu and protects it with its own login.": "Цей веб-екземпляр обслуговує візуалізацію в /cometvisu та захищає її власним логіном."
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"CometVisu": "CometVisu",
|
|
3
|
+
"CometVisu version": "CometVisu版本",
|
|
4
|
+
"Delivery": "送货",
|
|
5
|
+
"Serve through this web instance": "通过此 Web 实例提供服务",
|
|
6
|
+
"The selected version is unpacked and activated when you save.": "保存时,所选版本将被解压并激活。",
|
|
7
|
+
"This web instance serves the visualisation under /cometvisu and protects it with its own login.": "该 Web 实例在 /cometvisu 下提供可视化服务,并使用自己的登录名对其进行保护。"
|
|
8
|
+
}
|
package/admin/jsonConfig.json
CHANGED
|
@@ -31,8 +31,11 @@
|
|
|
31
31
|
"allowDeactivate": false,
|
|
32
32
|
"label": "Serve through this web instance",
|
|
33
33
|
"help": "This web instance serves the visualisation under /cometvisu and protects it with its own login.",
|
|
34
|
+
"xs": 12,
|
|
34
35
|
"sm": 12,
|
|
35
36
|
"md": 8,
|
|
37
|
+
"lg": 8,
|
|
38
|
+
"xl": 8,
|
|
36
39
|
"newLine": true
|
|
37
40
|
}
|
|
38
41
|
}
|
package/build/lib/releases.js
CHANGED
|
@@ -45,9 +45,9 @@ __export(releases_exports, {
|
|
|
45
45
|
});
|
|
46
46
|
module.exports = __toCommonJS(releases_exports);
|
|
47
47
|
var import_axios = __toESM(require("axios"));
|
|
48
|
-
var
|
|
49
|
-
var fs = __toESM(require("fs"));
|
|
50
|
-
var path = __toESM(require("path"));
|
|
48
|
+
var import_node_crypto = require("node:crypto");
|
|
49
|
+
var fs = __toESM(require("node:fs"));
|
|
50
|
+
var path = __toESM(require("node:path"));
|
|
51
51
|
var tar = __toESM(require("tar"));
|
|
52
52
|
const REPO = "CometVisu/CometVisu";
|
|
53
53
|
const RELEASES_URL = `https://api.github.com/repos/${REPO}/releases?per_page=100`;
|
|
@@ -166,7 +166,7 @@ function customRootDir(dataDir) {
|
|
|
166
166
|
}
|
|
167
167
|
function customBuildDir(dataDir, file) {
|
|
168
168
|
const readable = file.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 64);
|
|
169
|
-
const hash = (0,
|
|
169
|
+
const hash = (0, import_node_crypto.createHash)("sha1").update(file).digest("hex").slice(0, 8);
|
|
170
170
|
return path.join(customRootDir(dataDir), `${readable}__${hash}`);
|
|
171
171
|
}
|
|
172
172
|
async function unpackUploadedTarball(tgzPath, dataDir, log, source) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/releases.ts"],
|
|
4
|
-
"sourcesContent": ["// Lists CometVisu GitHub releases and downloads/unpacks the selected one into the instance\n// data directory, so the webserver can serve it.\n\nimport axios from 'axios';\nimport { createHash } from 'crypto';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as tar from 'tar';\n\nconst REPO = 'CometVisu/CometVisu';\nconst RELEASES_URL = `https://api.github.com/repos/${REPO}/releases?per_page=100`;\n\n/** Directory (and legacy version value) for a build the user uploaded instead of a GitHub release. */\nexport const CUSTOM_VERSION = '__custom__';\n/** Legacy version values for uploaded builds were \"__custom__:<file name>\". */\nexport const CUSTOM_PREFIX = `${CUSTOM_VERSION}:`;\n\n// The admin shows one list: the archives the user uploaded (they are the only files in the\n// instance's \"files\" meta object) and the GitHub releases, which the admin component fetches\n// itself. The configured value carries the prefix of the entry that was picked.\n/** Value prefix for an archive the user uploaded; the rest is the file name as stored. */\nexport const CUSTOM_FILE_PREFIX = '[Custom] ';\n/** Value prefix for a GitHub release; the rest is the tag. */\nexport const OFFICIAL_FILE_PREFIX = '[Official] ';\n\n// values written by earlier adapter versions\nconst LEGACY_CUSTOM_PREFIX = 'Custom: ';\nconst LEGACY_OFFICIAL_PREFIX = 'Official: ';\nconst LEGACY_LATEST_LABEL = 'latest release';\n\n/** What a configured version value refers to. */\nexport type VersionSelection = { kind: 'custom'; file: string } | { kind: 'release'; tag: string };\n\n/**\n * Resolve the configured version to either an uploaded archive or a GitHub release tag. The values\n * written by earlier adapter versions are still accepted, so an existing instance keeps working.\n *\n * @param value the configured version value\n * @param legacyBuildUpload file name from the former separate upload field, used by the bare\n * \"__custom__\" legacy value\n */\nexport function resolveVersionSelection(value: string, legacyBuildUpload?: string): VersionSelection {\n if (value.startsWith(CUSTOM_FILE_PREFIX)) {\n return { kind: 'custom', file: value.slice(CUSTOM_FILE_PREFIX.length) };\n }\n if (value.startsWith(OFFICIAL_FILE_PREFIX)) {\n return { kind: 'release', tag: value.slice(OFFICIAL_FILE_PREFIX.length) };\n }\n // legacy values\n if (value.startsWith(LEGACY_CUSTOM_PREFIX)) {\n // those uploads were stored including the prefix\n return { kind: 'custom', file: value };\n }\n if (value.startsWith(LEGACY_OFFICIAL_PREFIX)) {\n const tag = value.slice(LEGACY_OFFICIAL_PREFIX.length);\n return { kind: 'release', tag: tag === LEGACY_LATEST_LABEL ? '' : tag };\n }\n if (value.startsWith(CUSTOM_PREFIX)) {\n return { kind: 'custom', file: value.slice(CUSTOM_PREFIX.length) };\n }\n if (value === CUSTOM_VERSION) {\n return { kind: 'custom', file: legacyBuildUpload || '' };\n }\n return { kind: 'release', tag: value };\n}\n\ninterface Asset {\n name: string;\n browser_download_url: string;\n size: number;\n}\ninterface Release {\n tag_name: string;\n name: string;\n prerelease: boolean;\n assets: Asset[];\n}\n\nfunction githubHeaders(): Record<string, string> {\n return {\n Accept: 'application/vnd.github+json',\n 'User-Agent': 'iobroker.cometvisu',\n };\n}\n\n/**\n * The CometVisu build archive of a release, e.g. \"CometVisu-v0.12.6.tar.gz\".\n *\n * @param release the GitHub release to inspect\n */\nfunction buildAsset(release: Release): Asset | undefined {\n const exact = `cometvisu-${release.tag_name.toLowerCase()}.tar.gz`;\n return (\n release.assets.find(a => a.name.toLowerCase() === exact) ||\n release.assets.find(a => /^CometVisu-.*\\.tar\\.gz$/i.test(a.name))\n );\n}\n\nasync function fetchReleases(): Promise<Release[]> {\n const res = await axios.get<Release[]>(RELEASES_URL, { headers: githubHeaders(), timeout: 20000 });\n // only releases that actually ship a built archive can be served\n return res.data.filter(r => !!buildAsset(r));\n}\n\nasync function resolveRelease(version: string): Promise<Release> {\n const releases = await fetchReleases();\n if (!releases.length) {\n throw new Error('no CometVisu release with a build archive was found');\n }\n if (!version) {\n // latest: newest stable, or newest overall if there is no stable release\n return releases.find(r => !r.prerelease) || releases[0];\n }\n const release = releases.find(r => r.tag_name === version);\n if (!release) {\n throw new Error(`CometVisu release \"${version}\" not found`);\n }\n return release;\n}\n\n/**\n * Finds the directory to serve inside the extracted archive. CometVisu ships its app under a\n * \"cometvisu/release/\" sub path, so this returns the shallowest directory that holds index.html.\n *\n * @param dir the directory the archive was extracted into\n */\nexport function findHtmlRoot(dir: string): string {\n // breadth-first search so the app root (\"release/\") wins over deeper index.html files\n let level = [dir];\n for (let depth = 0; depth < 5 && level.length; depth++) {\n const next: string[] = [];\n for (const current of level) {\n if (fs.existsSync(path.join(current, 'index.html'))) {\n return current;\n }\n for (const entry of fs.readdirSync(current, { withFileTypes: true })) {\n if (entry.isDirectory()) {\n next.push(path.join(current, entry.name));\n }\n }\n }\n level = next;\n }\n return dir;\n}\n\n/**\n * Make sure the requested version is available locally and return the directory to serve.\n * Downloads and unpacks the release once, then reuses the cached copy.\n *\n * @param dataDir the instance data directory to unpack into\n * @param version the release tag to serve, '' for the latest release\n * @param log the adapter logger\n */\nexport async function ensureRelease(\n dataDir: string,\n version: string,\n log: ioBroker.Logger,\n): Promise<{ tag: string; htmlRoot: string }> {\n const release = await resolveRelease(version);\n const targetDir = path.join(dataDir, 'cometvisu', release.tag_name);\n const marker = path.join(targetDir, '.complete');\n\n if (fs.existsSync(marker)) {\n log.debug(`CometVisu ${release.tag_name} is already present`);\n return { tag: release.tag_name, htmlRoot: findHtmlRoot(targetDir) };\n }\n\n const asset = buildAsset(release);\n if (!asset) {\n throw new Error(`release ${release.tag_name} has no CometVisu build archive`);\n }\n\n // a partial previous download must not be served\n fs.rmSync(targetDir, { recursive: true, force: true });\n fs.mkdirSync(targetDir, { recursive: true });\n\n log.info(`downloading CometVisu ${release.tag_name} (${asset.name}, ${Math.round(asset.size / 1048576)} MB)`);\n const res = await axios.get<NodeJS.ReadableStream>(asset.browser_download_url, {\n headers: githubHeaders(),\n responseType: 'stream',\n timeout: 120000,\n });\n\n await extractTarball(res.data, targetDir);\n\n fs.writeFileSync(marker, new Date().toISOString());\n log.info(`CometVisu ${release.tag_name} unpacked to ${targetDir}`);\n return { tag: release.tag_name, htmlRoot: findHtmlRoot(targetDir) };\n}\n\n/**\n * Extract a .tar.gz into targetDir. node-tar auto-detects the gzip, so both a download stream and a\n * path to a local .tgz can be handed in.\n *\n * @param source a readable .tar.gz stream or the path to a local .tar.gz file\n * @param targetDir the directory to extract into (must already exist)\n */\nfunction extractTarball(source: NodeJS.ReadableStream | string, targetDir: string): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const extract = tar.x({ cwd: targetDir });\n const input: NodeJS.ReadableStream = typeof source === 'string' ? fs.createReadStream(source) : source;\n input.on('error', reject);\n extract.on('error', reject);\n extract.on('finish', () => resolve());\n input.pipe(extract);\n });\n}\n\n/** What an uploaded archive was when it was unpacked, so a re-upload can be told apart. */\nexport interface CustomBuildSource {\n /** name of the uploaded archive in the instance's file storage */\n file: string;\n /** byte size of that archive, unknown for builds of an older adapter version */\n size?: number;\n /** upload time of that archive, unknown for builds of an older adapter version */\n modifiedAt?: number;\n}\n\n/**\n * Directory that holds one sub directory per uploaded build.\n *\n * @param dataDir the instance data directory\n */\nfunction customRootDir(dataDir: string): string {\n return path.join(dataDir, 'cometvisu', 'custom');\n}\n\n/**\n * The directory an uploaded archive is unpacked into. Every upload gets its own one, so several\n * uploaded builds can exist side by side and switching between them needs no unpacking. The name is\n * derived from the archive's file name, which makes an upload under the same name replace exactly\n * that build. The hash keeps names apart that only the sanitizing would make equal (\"a b.tgz\" and\n * \"a_b.tgz\").\n *\n * @param dataDir the instance data directory\n * @param file name of the uploaded archive\n */\nexport function customBuildDir(dataDir: string, file: string): string {\n const readable = file.replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 64);\n const hash = createHash('sha1').update(file).digest('hex').slice(0, 8);\n return path.join(customRootDir(dataDir), `${readable}__${hash}`);\n}\n\n/**\n * Unpack a CometVisu build the user uploaded through the admin into its own directory and return the\n * directory to serve. Behaves like {@link ensureRelease}, only the source is a local file.\n *\n * @param tgzPath path to the uploaded .tar.gz file\n * @param dataDir the instance data directory to unpack into\n * @param log the adapter logger\n * @param source describes the uploaded archive, stored so a later start can tell whether this build\n * still matches the archive in the file storage\n */\nexport async function unpackUploadedTarball(\n tgzPath: string,\n dataDir: string,\n log: ioBroker.Logger,\n source: CustomBuildSource,\n): Promise<{ tag: string; htmlRoot: string }> {\n if (!fs.existsSync(tgzPath)) {\n throw new Error(`uploaded build archive not found at ${tgzPath}`);\n }\n const targetDir = customBuildDir(dataDir, source.file);\n\n // a previous build of the same archive must be replaced completely\n fs.rmSync(targetDir, { recursive: true, force: true });\n fs.mkdirSync(targetDir, { recursive: true });\n\n log.info(`unpacking uploaded CometVisu build \"${source.file}\"`);\n await extractTarball(tgzPath, targetDir);\n\n const htmlRoot = findHtmlRoot(targetDir);\n if (htmlRoot === targetDir && !fs.existsSync(path.join(targetDir, 'index.html'))) {\n fs.rmSync(targetDir, { recursive: true, force: true });\n throw new Error('the uploaded archive does not contain a CometVisu build (no index.html found)');\n }\n\n fs.writeFileSync(path.join(targetDir, '.complete'), new Date().toISOString());\n fs.writeFileSync(path.join(targetDir, '.source'), JSON.stringify(source));\n log.info(`uploaded CometVisu build unpacked to ${targetDir}`);\n return { tag: source.file, htmlRoot };\n}\n\n/**\n * Read the \".source\" marker of an unpacked build. Older adapter versions stored the plain file name,\n * which is still understood - the missing size and time then simply mean \"unknown\".\n *\n * @param targetDir directory of the unpacked build\n */\nfunction readSource(targetDir: string): CustomBuildSource | null {\n const sourceFile = path.join(targetDir, '.source');\n if (!fs.existsSync(sourceFile)) {\n return null;\n }\n const content = fs.readFileSync(sourceFile, 'utf8').trim();\n try {\n const parsed = JSON.parse(content);\n return typeof parsed?.file === 'string' ? (parsed as CustomBuildSource) : null;\n } catch {\n return content ? { file: content } : null;\n }\n}\n\n/**\n * Directory + source info of the build unpacked from a given archive, or null if it is not unpacked.\n *\n * @param dataDir the instance data directory\n * @param file name of the uploaded archive\n */\nexport function readCustomBuild(\n dataDir: string,\n file: string,\n): { htmlRoot: string; source: CustomBuildSource | null } | null {\n const targetDir = customBuildDir(dataDir, file);\n if (!fs.existsSync(path.join(targetDir, '.complete'))) {\n return null;\n }\n return { htmlRoot: findHtmlRoot(targetDir), source: readSource(targetDir) };\n}\n\n/**\n * All unpacked builds with the archive each one was made from, used to drop those whose archive was\n * deleted in the admin.\n *\n * @param dataDir the instance data directory\n */\nexport function listCustomBuilds(dataDir: string): { dir: string; file: string | null }[] {\n const root = customRootDir(dataDir);\n if (!fs.existsSync(root)) {\n return [];\n }\n return fs\n .readdirSync(root, { withFileTypes: true })\n .filter(entry => entry.isDirectory())\n .map(entry => {\n const dir = path.join(root, entry.name);\n return { dir, file: readSource(dir)?.file ?? null };\n });\n}\n\n/**\n * Remove the build that was unpacked from a given archive.\n *\n * @param dataDir the instance data directory\n * @param file name of the uploaded archive\n */\nexport function removeCustomBuild(dataDir: string, file: string): boolean {\n const targetDir = customBuildDir(dataDir, file);\n if (!fs.existsSync(targetDir)) {\n return false;\n }\n fs.rmSync(targetDir, { recursive: true, force: true });\n return true;\n}\n\n/**\n * Remove the single directory all uploads shared before every upload got its own one.\n *\n * @param dataDir the instance data directory\n */\nexport function removeLegacyCustomBuild(dataDir: string): boolean {\n const legacyDir = path.join(dataDir, 'cometvisu', CUSTOM_VERSION);\n if (!fs.existsSync(legacyDir)) {\n return false;\n }\n fs.rmSync(legacyDir, { recursive: true, force: true });\n return true;\n}\n\n/**\n * Drop every unpacked release except the one that is served. A release is not kept as an archive but\n * as its unpacked directory, so trying out versions would pile them up without this.\n *\n * @param dataDir the instance data directory\n * @param keepTag tag of the release to keep, null when an uploaded build is served\n * @returns the tags whose directories were removed\n */\nexport function pruneReleaseBuilds(dataDir: string, keepTag: string | null): string[] {\n const root = path.join(dataDir, 'cometvisu');\n if (!fs.existsSync(root)) {\n return [];\n }\n // the uploaded builds live next to the releases and are kept\n const keep = new Set([CUSTOM_VERSION, 'custom', keepTag]);\n const removed: string[] = [];\n for (const entry of fs.readdirSync(root, { withFileTypes: true })) {\n if (!entry.isDirectory() || keep.has(entry.name)) {\n continue;\n }\n fs.rmSync(path.join(root, entry.name), { recursive: true, force: true });\n removed.push(entry.name);\n }\n return removed;\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,mBAAkB;AAClB,
|
|
4
|
+
"sourcesContent": ["// Lists CometVisu GitHub releases and downloads/unpacks the selected one into the instance\n// data directory, so the webserver can serve it.\n\nimport axios from 'axios';\nimport { createHash } from 'node:crypto';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport * as tar from 'tar';\n\nconst REPO = 'CometVisu/CometVisu';\nconst RELEASES_URL = `https://api.github.com/repos/${REPO}/releases?per_page=100`;\n\n/** Directory (and legacy version value) for a build the user uploaded instead of a GitHub release. */\nexport const CUSTOM_VERSION = '__custom__';\n/** Legacy version values for uploaded builds were \"__custom__:<file name>\". */\nexport const CUSTOM_PREFIX = `${CUSTOM_VERSION}:`;\n\n// The admin shows one list: the archives the user uploaded (they are the only files in the\n// instance's \"files\" meta object) and the GitHub releases, which the admin component fetches\n// itself. The configured value carries the prefix of the entry that was picked.\n/** Value prefix for an archive the user uploaded; the rest is the file name as stored. */\nexport const CUSTOM_FILE_PREFIX = '[Custom] ';\n/** Value prefix for a GitHub release; the rest is the tag. */\nexport const OFFICIAL_FILE_PREFIX = '[Official] ';\n\n// values written by earlier adapter versions\nconst LEGACY_CUSTOM_PREFIX = 'Custom: ';\nconst LEGACY_OFFICIAL_PREFIX = 'Official: ';\nconst LEGACY_LATEST_LABEL = 'latest release';\n\n/** What a configured version value refers to. */\nexport type VersionSelection = { kind: 'custom'; file: string } | { kind: 'release'; tag: string };\n\n/**\n * Resolve the configured version to either an uploaded archive or a GitHub release tag. The values\n * written by earlier adapter versions are still accepted, so an existing instance keeps working.\n *\n * @param value the configured version value\n * @param legacyBuildUpload file name from the former separate upload field, used by the bare\n * \"__custom__\" legacy value\n */\nexport function resolveVersionSelection(value: string, legacyBuildUpload?: string): VersionSelection {\n if (value.startsWith(CUSTOM_FILE_PREFIX)) {\n return { kind: 'custom', file: value.slice(CUSTOM_FILE_PREFIX.length) };\n }\n if (value.startsWith(OFFICIAL_FILE_PREFIX)) {\n return { kind: 'release', tag: value.slice(OFFICIAL_FILE_PREFIX.length) };\n }\n // legacy values\n if (value.startsWith(LEGACY_CUSTOM_PREFIX)) {\n // those uploads were stored including the prefix\n return { kind: 'custom', file: value };\n }\n if (value.startsWith(LEGACY_OFFICIAL_PREFIX)) {\n const tag = value.slice(LEGACY_OFFICIAL_PREFIX.length);\n return { kind: 'release', tag: tag === LEGACY_LATEST_LABEL ? '' : tag };\n }\n if (value.startsWith(CUSTOM_PREFIX)) {\n return { kind: 'custom', file: value.slice(CUSTOM_PREFIX.length) };\n }\n if (value === CUSTOM_VERSION) {\n return { kind: 'custom', file: legacyBuildUpload || '' };\n }\n return { kind: 'release', tag: value };\n}\n\ninterface Asset {\n name: string;\n browser_download_url: string;\n size: number;\n}\ninterface Release {\n tag_name: string;\n name: string;\n prerelease: boolean;\n assets: Asset[];\n}\n\nfunction githubHeaders(): Record<string, string> {\n return {\n Accept: 'application/vnd.github+json',\n 'User-Agent': 'iobroker.cometvisu',\n };\n}\n\n/**\n * The CometVisu build archive of a release, e.g. \"CometVisu-v0.12.6.tar.gz\".\n *\n * @param release the GitHub release to inspect\n */\nfunction buildAsset(release: Release): Asset | undefined {\n const exact = `cometvisu-${release.tag_name.toLowerCase()}.tar.gz`;\n return (\n release.assets.find(a => a.name.toLowerCase() === exact) ||\n release.assets.find(a => /^CometVisu-.*\\.tar\\.gz$/i.test(a.name))\n );\n}\n\nasync function fetchReleases(): Promise<Release[]> {\n const res = await axios.get<Release[]>(RELEASES_URL, { headers: githubHeaders(), timeout: 20000 });\n // only releases that actually ship a built archive can be served\n return res.data.filter(r => !!buildAsset(r));\n}\n\nasync function resolveRelease(version: string): Promise<Release> {\n const releases = await fetchReleases();\n if (!releases.length) {\n throw new Error('no CometVisu release with a build archive was found');\n }\n if (!version) {\n // latest: newest stable, or newest overall if there is no stable release\n return releases.find(r => !r.prerelease) || releases[0];\n }\n const release = releases.find(r => r.tag_name === version);\n if (!release) {\n throw new Error(`CometVisu release \"${version}\" not found`);\n }\n return release;\n}\n\n/**\n * Finds the directory to serve inside the extracted archive. CometVisu ships its app under a\n * \"cometvisu/release/\" sub path, so this returns the shallowest directory that holds index.html.\n *\n * @param dir the directory the archive was extracted into\n */\nexport function findHtmlRoot(dir: string): string {\n // breadth-first search so the app root (\"release/\") wins over deeper index.html files\n let level = [dir];\n for (let depth = 0; depth < 5 && level.length; depth++) {\n const next: string[] = [];\n for (const current of level) {\n if (fs.existsSync(path.join(current, 'index.html'))) {\n return current;\n }\n for (const entry of fs.readdirSync(current, { withFileTypes: true })) {\n if (entry.isDirectory()) {\n next.push(path.join(current, entry.name));\n }\n }\n }\n level = next;\n }\n return dir;\n}\n\n/**\n * Make sure the requested version is available locally and return the directory to serve.\n * Downloads and unpacks the release once, then reuses the cached copy.\n *\n * @param dataDir the instance data directory to unpack into\n * @param version the release tag to serve, '' for the latest release\n * @param log the adapter logger\n */\nexport async function ensureRelease(\n dataDir: string,\n version: string,\n log: ioBroker.Logger,\n): Promise<{ tag: string; htmlRoot: string }> {\n const release = await resolveRelease(version);\n const targetDir = path.join(dataDir, 'cometvisu', release.tag_name);\n const marker = path.join(targetDir, '.complete');\n\n if (fs.existsSync(marker)) {\n log.debug(`CometVisu ${release.tag_name} is already present`);\n return { tag: release.tag_name, htmlRoot: findHtmlRoot(targetDir) };\n }\n\n const asset = buildAsset(release);\n if (!asset) {\n throw new Error(`release ${release.tag_name} has no CometVisu build archive`);\n }\n\n // a partial previous download must not be served\n fs.rmSync(targetDir, { recursive: true, force: true });\n fs.mkdirSync(targetDir, { recursive: true });\n\n log.info(`downloading CometVisu ${release.tag_name} (${asset.name}, ${Math.round(asset.size / 1048576)} MB)`);\n const res = await axios.get<NodeJS.ReadableStream>(asset.browser_download_url, {\n headers: githubHeaders(),\n responseType: 'stream',\n timeout: 120000,\n });\n\n await extractTarball(res.data, targetDir);\n\n fs.writeFileSync(marker, new Date().toISOString());\n log.info(`CometVisu ${release.tag_name} unpacked to ${targetDir}`);\n return { tag: release.tag_name, htmlRoot: findHtmlRoot(targetDir) };\n}\n\n/**\n * Extract a .tar.gz into targetDir. node-tar auto-detects the gzip, so both a download stream and a\n * path to a local .tgz can be handed in.\n *\n * @param source a readable .tar.gz stream or the path to a local .tar.gz file\n * @param targetDir the directory to extract into (must already exist)\n */\nfunction extractTarball(source: NodeJS.ReadableStream | string, targetDir: string): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const extract = tar.x({ cwd: targetDir });\n const input: NodeJS.ReadableStream = typeof source === 'string' ? fs.createReadStream(source) : source;\n input.on('error', reject);\n extract.on('error', reject);\n extract.on('finish', () => resolve());\n input.pipe(extract);\n });\n}\n\n/** What an uploaded archive was when it was unpacked, so a re-upload can be told apart. */\nexport interface CustomBuildSource {\n /** name of the uploaded archive in the instance's file storage */\n file: string;\n /** byte size of that archive, unknown for builds of an older adapter version */\n size?: number;\n /** upload time of that archive, unknown for builds of an older adapter version */\n modifiedAt?: number;\n}\n\n/**\n * Directory that holds one sub directory per uploaded build.\n *\n * @param dataDir the instance data directory\n */\nfunction customRootDir(dataDir: string): string {\n return path.join(dataDir, 'cometvisu', 'custom');\n}\n\n/**\n * The directory an uploaded archive is unpacked into. Every upload gets its own one, so several\n * uploaded builds can exist side by side and switching between them needs no unpacking. The name is\n * derived from the archive's file name, which makes an upload under the same name replace exactly\n * that build. The hash keeps names apart that only the sanitizing would make equal (\"a b.tgz\" and\n * \"a_b.tgz\").\n *\n * @param dataDir the instance data directory\n * @param file name of the uploaded archive\n */\nexport function customBuildDir(dataDir: string, file: string): string {\n const readable = file.replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 64);\n const hash = createHash('sha1').update(file).digest('hex').slice(0, 8);\n return path.join(customRootDir(dataDir), `${readable}__${hash}`);\n}\n\n/**\n * Unpack a CometVisu build the user uploaded through the admin into its own directory and return the\n * directory to serve. Behaves like {@link ensureRelease}, only the source is a local file.\n *\n * @param tgzPath path to the uploaded .tar.gz file\n * @param dataDir the instance data directory to unpack into\n * @param log the adapter logger\n * @param source describes the uploaded archive, stored so a later start can tell whether this build\n * still matches the archive in the file storage\n */\nexport async function unpackUploadedTarball(\n tgzPath: string,\n dataDir: string,\n log: ioBroker.Logger,\n source: CustomBuildSource,\n): Promise<{ tag: string; htmlRoot: string }> {\n if (!fs.existsSync(tgzPath)) {\n throw new Error(`uploaded build archive not found at ${tgzPath}`);\n }\n const targetDir = customBuildDir(dataDir, source.file);\n\n // a previous build of the same archive must be replaced completely\n fs.rmSync(targetDir, { recursive: true, force: true });\n fs.mkdirSync(targetDir, { recursive: true });\n\n log.info(`unpacking uploaded CometVisu build \"${source.file}\"`);\n await extractTarball(tgzPath, targetDir);\n\n const htmlRoot = findHtmlRoot(targetDir);\n if (htmlRoot === targetDir && !fs.existsSync(path.join(targetDir, 'index.html'))) {\n fs.rmSync(targetDir, { recursive: true, force: true });\n throw new Error('the uploaded archive does not contain a CometVisu build (no index.html found)');\n }\n\n fs.writeFileSync(path.join(targetDir, '.complete'), new Date().toISOString());\n fs.writeFileSync(path.join(targetDir, '.source'), JSON.stringify(source));\n log.info(`uploaded CometVisu build unpacked to ${targetDir}`);\n return { tag: source.file, htmlRoot };\n}\n\n/**\n * Read the \".source\" marker of an unpacked build. Older adapter versions stored the plain file name,\n * which is still understood - the missing size and time then simply mean \"unknown\".\n *\n * @param targetDir directory of the unpacked build\n */\nfunction readSource(targetDir: string): CustomBuildSource | null {\n const sourceFile = path.join(targetDir, '.source');\n if (!fs.existsSync(sourceFile)) {\n return null;\n }\n const content = fs.readFileSync(sourceFile, 'utf8').trim();\n try {\n const parsed = JSON.parse(content);\n return typeof parsed?.file === 'string' ? (parsed as CustomBuildSource) : null;\n } catch {\n return content ? { file: content } : null;\n }\n}\n\n/**\n * Directory + source info of the build unpacked from a given archive, or null if it is not unpacked.\n *\n * @param dataDir the instance data directory\n * @param file name of the uploaded archive\n */\nexport function readCustomBuild(\n dataDir: string,\n file: string,\n): { htmlRoot: string; source: CustomBuildSource | null } | null {\n const targetDir = customBuildDir(dataDir, file);\n if (!fs.existsSync(path.join(targetDir, '.complete'))) {\n return null;\n }\n return { htmlRoot: findHtmlRoot(targetDir), source: readSource(targetDir) };\n}\n\n/**\n * All unpacked builds with the archive each one was made from, used to drop those whose archive was\n * deleted in the admin.\n *\n * @param dataDir the instance data directory\n */\nexport function listCustomBuilds(dataDir: string): { dir: string; file: string | null }[] {\n const root = customRootDir(dataDir);\n if (!fs.existsSync(root)) {\n return [];\n }\n return fs\n .readdirSync(root, { withFileTypes: true })\n .filter(entry => entry.isDirectory())\n .map(entry => {\n const dir = path.join(root, entry.name);\n return { dir, file: readSource(dir)?.file ?? null };\n });\n}\n\n/**\n * Remove the build that was unpacked from a given archive.\n *\n * @param dataDir the instance data directory\n * @param file name of the uploaded archive\n */\nexport function removeCustomBuild(dataDir: string, file: string): boolean {\n const targetDir = customBuildDir(dataDir, file);\n if (!fs.existsSync(targetDir)) {\n return false;\n }\n fs.rmSync(targetDir, { recursive: true, force: true });\n return true;\n}\n\n/**\n * Remove the single directory all uploads shared before every upload got its own one.\n *\n * @param dataDir the instance data directory\n */\nexport function removeLegacyCustomBuild(dataDir: string): boolean {\n const legacyDir = path.join(dataDir, 'cometvisu', CUSTOM_VERSION);\n if (!fs.existsSync(legacyDir)) {\n return false;\n }\n fs.rmSync(legacyDir, { recursive: true, force: true });\n return true;\n}\n\n/**\n * Drop every unpacked release except the one that is served. A release is not kept as an archive but\n * as its unpacked directory, so trying out versions would pile them up without this.\n *\n * @param dataDir the instance data directory\n * @param keepTag tag of the release to keep, null when an uploaded build is served\n * @returns the tags whose directories were removed\n */\nexport function pruneReleaseBuilds(dataDir: string, keepTag: string | null): string[] {\n const root = path.join(dataDir, 'cometvisu');\n if (!fs.existsSync(root)) {\n return [];\n }\n // the uploaded builds live next to the releases and are kept\n const keep = new Set([CUSTOM_VERSION, 'custom', keepTag]);\n const removed: string[] = [];\n for (const entry of fs.readdirSync(root, { withFileTypes: true })) {\n if (!entry.isDirectory() || keep.has(entry.name)) {\n continue;\n }\n fs.rmSync(path.join(root, entry.name), { recursive: true, force: true });\n removed.push(entry.name);\n }\n return removed;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["axios"]
|
|
7
7
|
}
|
package/build/lib/web.js
CHANGED
|
@@ -34,8 +34,8 @@ __export(web_exports, {
|
|
|
34
34
|
module.exports = __toCommonJS(web_exports);
|
|
35
35
|
var utils = __toESM(require("@iobroker/adapter-core"));
|
|
36
36
|
var import_express = __toESM(require("express"));
|
|
37
|
-
var fs = __toESM(require("fs"));
|
|
38
|
-
var path = __toESM(require("path"));
|
|
37
|
+
var fs = __toESM(require("node:fs"));
|
|
38
|
+
var path = __toESM(require("node:path"));
|
|
39
39
|
var import_releases = require("./releases");
|
|
40
40
|
var import_webSocket = require("./webSocket");
|
|
41
41
|
class web {
|
package/build/lib/web.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/web.ts"],
|
|
4
|
-
"sourcesContent": ["// Web extension for iobroker.web: serves the CometVisu build through the web adapter, so its login,\n// session and socket protect and supply the visualisation. This module is loaded by iobroker.web\n// (common.webExtension in io-package.json) and runs inside that adapter's process - the download and\n// unpacking of a build stays in our own adapter process, this one only serves what is on disk.\n\nimport * as utils from '@iobroker/adapter-core';\nimport express from 'express';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { findHtmlRoot, readCustomBuild, resolveVersionSelection } from './releases';\nimport { type WebNative, classifyWebSocket } from './webSocket';\n\n/** The part of our instance object this extension needs. */\ninterface InstanceSettings {\n _id: string;\n native: {\n version?: string;\n buildUpload?: string;\n };\n}\n\n/** Settings iobroker.web hands to every extension. */\ninterface WebSettings {\n secure?: boolean;\n port?: number;\n language?: string;\n defaultUser?: string;\n auth?: boolean;\n}\n\n/**\n * Exported twice on purpose, because iobroker.web changed how it picks the class up: up to 7.0.8 it\n * only unwraps a default export, from 7.0.9 on it also looks for `module[<file name>]`. The class\n * name therefore has to stay `web`, and the default export below keeps the older loader working.\n */\nexport class web {\n private readonly log: ioBroker.Logger;\n /** namespace of our own instance, e.g. \"cometvisu.0\" */\n private readonly namespace: string;\n private readonly native: InstanceSettings['native'];\n /** path the visualisation is mounted under */\n private readonly mountPath: string;\n /** serves the current build, null while there is none to serve */\n private handler: express.RequestHandler | null = null;\n /** directory the current handler serves, null while there is nothing to serve */\n private servedRoot: string | null = null;\n /** set once unload() ran, so a late request does not resurrect the handler */\n private unloaded = false;\n /** whether the web instance offers a socket at all */\n private hasSocket = false;\n /** port of an external socket adapter, null while the socket is on the port of the web adapter */\n private socketPort: number | null = null;\n /** whether an external socket is reached over TLS */\n private socketSecure = false;\n\n /**\n * Mounts the visualisation into the web adapter's express app.\n *\n * @param _server the web adapter's http(s) server, unused\n * @param _webSettings settings of the web instance, unused\n * @param adapter the web adapter, used for logging\n * @param instanceSettings our own instance object\n * @param app express app of the web adapter\n */\n public constructor(\n _server: unknown,\n _webSettings: WebSettings,\n adapter: ioBroker.Adapter,\n instanceSettings: InstanceSettings,\n app: express.Express,\n ) {\n this.log = adapter.log;\n this.namespace = instanceSettings._id.substring('system.adapter.'.length);\n this.native = instanceSettings.native || {};\n // several instances must not fight over the same path\n this.mountPath = this.namespace.endsWith('.0') ? '/cometvisu' : `/${this.namespace}`;\n\n this.resolveSocket(adapter);\n this.refresh();\n // one stable mount point; what is behind it follows the configuration and the disk\n app.use(this.mountPath, (req, res, next) => {\n if (this.unloaded) {\n next();\n return;\n }\n // Without the trailing slash the browser would resolve every relative asset against the\n // parent path, so send it to the directory form first.\n if (req.originalUrl === this.mountPath) {\n res.redirect(301, `${this.mountPath}/`);\n return;\n }\n // Tell CometVisu which backend to connect to. The request reaches us through the web\n // adapter, so its origin - and with it its socket, login and session - is the right\n // one, unless the socket lives in another adapter on its own port. Which protocol that\n // socket speaks does not have to be told: CometVisu loads the matching client library\n // from /socket.io.js of that very server. This is only a default, the visu config wins.\n res.setHeader('X-CometVisu-Backend-Name', 'iobroker');\n if (this.hasSocket) {\n res.setHeader('X-CometVisu-Backend-IoBroker-Url', this.socketUrl(req));\n }\n\n // The adapter unpacks the build in its own process, which may still be running - or may\n // have replaced the build under the same name. Re-resolving whenever nothing is served\n // (or what was served disappeared) picks that up without restarting the web adapter.\n if (!this.servedRoot || !fs.existsSync(this.servedRoot)) {\n this.refresh();\n }\n if (!this.handler) {\n res.status(503).send('No CometVisu build has been prepared yet - please check the adapter instance.');\n return;\n }\n this.handler(req, res, next);\n });\n this.log.info(`CometVisu is served by the web adapter under ${this.mountPath}/`);\n }\n\n // No welcomePage() on purpose: iobroker.web appends what the extensions return there *after* it\n // has de-duplicated the list it built from common.localLinks, so an entry here would simply show\n // up a second time. common.localLinks covers both the welcome screen and the link in the admin.\n\n /**\n * Pick up a version that was selected in the admin without restarting the web adapter.\n *\n * @param id id of the changed object\n * @param obj the changed object\n */\n public objectChange(id: string, obj: ioBroker.Object | null | undefined): void {\n if (id !== `system.adapter.${this.namespace}` || !obj) {\n return;\n }\n const native = (obj as unknown as InstanceSettings).native || {};\n if (native.version === this.native.version && native.buildUpload === this.native.buildUpload) {\n return;\n }\n this.native.version = native.version;\n this.native.buildUpload = native.buildUpload;\n this.log.info(`CometVisu version changed to \"${native.version || ''}\"`);\n this.refresh();\n }\n\n /** Called by the web adapter when our instance goes away. */\n public unload(): void {\n // the mount stays in place, but stops handling anything\n this.unloaded = true;\n this.handler = null;\n this.servedRoot = null;\n }\n\n /**\n * Directory of the build the instance is configured to serve, or null when it is not unpacked\n * (yet). Uses the same resolution as the adapter itself.\n */\n private resolveHtmlRoot(): string | null {\n const dataDir = utils.getAbsoluteInstanceDataDir(this.namespace);\n const selection = resolveVersionSelection(this.native.version || '', this.native.buildUpload);\n\n if (selection.kind === 'custom') {\n return readCustomBuild(dataDir, selection.file)?.htmlRoot ?? null;\n }\n\n // A release is unpacked into a directory named after its tag. An empty tag comes from older\n // configurations (\"latest\"), where only the adapter knows which tag that resolved to - it\n // keeps just the release being served, so the single remaining directory is the right one.\n const releaseDir = selection.tag\n ? path.join(dataDir, 'cometvisu', selection.tag)\n : this.findSingleReleaseDir(dataDir);\n\n return releaseDir && fs.existsSync(path.join(releaseDir, '.complete')) ? findHtmlRoot(releaseDir) : null;\n }\n\n /**\n * The one unpacked release directory, ignoring the uploaded builds.\n *\n * @param dataDir the instance data directory\n */\n private findSingleReleaseDir(dataDir: string): string | null {\n const root = path.join(dataDir, 'cometvisu');\n if (!fs.existsSync(root)) {\n return null;\n }\n const dirs = fs\n .readdirSync(root, { withFileTypes: true })\n .filter(entry => entry.isDirectory() && entry.name !== 'custom' && !entry.name.startsWith('__'))\n .map(entry => path.join(root, entry.name));\n return dirs.length === 1 ? dirs[0] : null;\n }\n\n /**\n * Work out which socket CometVisu has to connect to. An external socket adapter runs on its own\n * port, which is only known after reading its instance object - until that answer arrives, and\n * if it never does, the origin of the request is used.\n *\n * @param adapter the web adapter we run in\n */\n private resolveSocket(adapter: ioBroker.Adapter): void {\n const setup = classifyWebSocket(adapter.config as WebNative);\n if (setup.kind === 'none') {\n this.log.error(\n `${adapter.namespace} serves no socket (\"none\"), so CometVisu cannot reach ioBroker - ` +\n 'configure a socket in that web instance',\n );\n return;\n }\n this.hasSocket = true;\n if (setup.kind !== 'external') {\n return;\n }\n adapter.getForeignObjectAsync(setup.instance).then(\n obj => {\n const native = obj?.native as { port?: number; secure?: boolean } | undefined;\n if (!native?.port) {\n this.log.warn(`cannot read the port of ${setup.instance}, using the port of the web adapter`);\n return;\n }\n this.socketPort = native.port;\n this.socketSecure = !!native.secure;\n this.log.info(`CometVisu connects to ${setup.instance} on port ${native.port} (${setup.transport})`);\n },\n (e: Error) => this.log.warn(`cannot read ${setup.instance}: ${e.message}`),\n );\n }\n\n /**\n * The websocket URL to hand to CometVisu for this request.\n *\n * @param req the request being answered\n */\n private socketUrl(req: express.Request): string {\n if (this.socketPort === null) {\n return `${req.secure ? 'wss' : 'ws'}://${req.headers.host}/`;\n }\n // same host as the request, but the port of the adapter that serves the socket\n const host = (req.headers.host || '').replace(/:\\d+$/, '');\n return `${this.socketSecure ? 'wss' : 'ws'}://${host}:${this.socketPort}/`;\n }\n\n /** Points the handler at the configured build, or drops it when there is none. */\n private refresh(): void {\n const htmlRoot = this.resolveHtmlRoot();\n if (!htmlRoot) {\n if (this.servedRoot) {\n this.log.warn(`the CometVisu build served for ${this.namespace} disappeared`);\n }\n this.servedRoot = null;\n this.handler = null;\n return;\n }\n if (htmlRoot === this.servedRoot && this.handler) {\n return;\n }\n\n const router = express.Router();\n // same overlay as the adapter's own web server: a user provided \"resource\" folder wins over\n // the one shipped with the build\n const overlayDir = path.join(utils.getAbsoluteInstanceDataDir(this.namespace), 'resource');\n if (fs.existsSync(overlayDir)) {\n router.use('/resource', express.static(overlayDir));\n }\n router.use(express.static(htmlRoot));\n\n this.handler = router;\n this.servedRoot = htmlRoot;\n this.log.info(`CometVisu (${this.namespace}) serves ${htmlRoot}`);\n }\n}\n\nexport default web;\n"],
|
|
4
|
+
"sourcesContent": ["// Web extension for iobroker.web: serves the CometVisu build through the web adapter, so its login,\n// session and socket protect and supply the visualisation. This module is loaded by iobroker.web\n// (common.webExtension in io-package.json) and runs inside that adapter's process - the download and\n// unpacking of a build stays in our own adapter process, this one only serves what is on disk.\n\nimport * as utils from '@iobroker/adapter-core';\nimport express from 'express';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { findHtmlRoot, readCustomBuild, resolveVersionSelection } from './releases';\nimport { type WebNative, classifyWebSocket } from './webSocket';\n\n/** The part of our instance object this extension needs. */\ninterface InstanceSettings {\n _id: string;\n native: {\n version?: string;\n buildUpload?: string;\n };\n}\n\n/** Settings iobroker.web hands to every extension. */\ninterface WebSettings {\n secure?: boolean;\n port?: number;\n language?: string;\n defaultUser?: string;\n auth?: boolean;\n}\n\n/**\n * Exported twice on purpose, because iobroker.web changed how it picks the class up: up to 7.0.8 it\n * only unwraps a default export, from 7.0.9 on it also looks for `module[<file name>]`. The class\n * name therefore has to stay `web`, and the default export below keeps the older loader working.\n */\nexport class web {\n private readonly log: ioBroker.Logger;\n /** namespace of our own instance, e.g. \"cometvisu.0\" */\n private readonly namespace: string;\n private readonly native: InstanceSettings['native'];\n /** path the visualisation is mounted under */\n private readonly mountPath: string;\n /** serves the current build, null while there is none to serve */\n private handler: express.RequestHandler | null = null;\n /** directory the current handler serves, null while there is nothing to serve */\n private servedRoot: string | null = null;\n /** set once unload() ran, so a late request does not resurrect the handler */\n private unloaded = false;\n /** whether the web instance offers a socket at all */\n private hasSocket = false;\n /** port of an external socket adapter, null while the socket is on the port of the web adapter */\n private socketPort: number | null = null;\n /** whether an external socket is reached over TLS */\n private socketSecure = false;\n\n /**\n * Mounts the visualisation into the web adapter's express app.\n *\n * @param _server the web adapter's http(s) server, unused\n * @param _webSettings settings of the web instance, unused\n * @param adapter the web adapter, used for logging\n * @param instanceSettings our own instance object\n * @param app express app of the web adapter\n */\n public constructor(\n _server: unknown,\n _webSettings: WebSettings,\n adapter: ioBroker.Adapter,\n instanceSettings: InstanceSettings,\n app: express.Express,\n ) {\n this.log = adapter.log;\n this.namespace = instanceSettings._id.substring('system.adapter.'.length);\n this.native = instanceSettings.native || {};\n // several instances must not fight over the same path\n this.mountPath = this.namespace.endsWith('.0') ? '/cometvisu' : `/${this.namespace}`;\n\n this.resolveSocket(adapter);\n this.refresh();\n // one stable mount point; what is behind it follows the configuration and the disk\n app.use(this.mountPath, (req, res, next) => {\n if (this.unloaded) {\n next();\n return;\n }\n // Without the trailing slash the browser would resolve every relative asset against the\n // parent path, so send it to the directory form first.\n if (req.originalUrl === this.mountPath) {\n res.redirect(301, `${this.mountPath}/`);\n return;\n }\n // Tell CometVisu which backend to connect to. The request reaches us through the web\n // adapter, so its origin - and with it its socket, login and session - is the right\n // one, unless the socket lives in another adapter on its own port. Which protocol that\n // socket speaks does not have to be told: CometVisu loads the matching client library\n // from /socket.io.js of that very server. This is only a default, the visu config wins.\n res.setHeader('X-CometVisu-Backend-Name', 'iobroker');\n if (this.hasSocket) {\n res.setHeader('X-CometVisu-Backend-IoBroker-Url', this.socketUrl(req));\n }\n\n // The adapter unpacks the build in its own process, which may still be running - or may\n // have replaced the build under the same name. Re-resolving whenever nothing is served\n // (or what was served disappeared) picks that up without restarting the web adapter.\n if (!this.servedRoot || !fs.existsSync(this.servedRoot)) {\n this.refresh();\n }\n if (!this.handler) {\n res.status(503).send('No CometVisu build has been prepared yet - please check the adapter instance.');\n return;\n }\n this.handler(req, res, next);\n });\n this.log.info(`CometVisu is served by the web adapter under ${this.mountPath}/`);\n }\n\n // No welcomePage() on purpose: iobroker.web appends what the extensions return there *after* it\n // has de-duplicated the list it built from common.localLinks, so an entry here would simply show\n // up a second time. common.localLinks covers both the welcome screen and the link in the admin.\n\n /**\n * Pick up a version that was selected in the admin without restarting the web adapter.\n *\n * @param id id of the changed object\n * @param obj the changed object\n */\n public objectChange(id: string, obj: ioBroker.Object | null | undefined): void {\n if (id !== `system.adapter.${this.namespace}` || !obj) {\n return;\n }\n const native = (obj as unknown as InstanceSettings).native || {};\n if (native.version === this.native.version && native.buildUpload === this.native.buildUpload) {\n return;\n }\n this.native.version = native.version;\n this.native.buildUpload = native.buildUpload;\n this.log.info(`CometVisu version changed to \"${native.version || ''}\"`);\n this.refresh();\n }\n\n /** Called by the web adapter when our instance goes away. */\n public unload(): void {\n // the mount stays in place, but stops handling anything\n this.unloaded = true;\n this.handler = null;\n this.servedRoot = null;\n }\n\n /**\n * Directory of the build the instance is configured to serve, or null when it is not unpacked\n * (yet). Uses the same resolution as the adapter itself.\n */\n private resolveHtmlRoot(): string | null {\n const dataDir = utils.getAbsoluteInstanceDataDir(this.namespace);\n const selection = resolveVersionSelection(this.native.version || '', this.native.buildUpload);\n\n if (selection.kind === 'custom') {\n return readCustomBuild(dataDir, selection.file)?.htmlRoot ?? null;\n }\n\n // A release is unpacked into a directory named after its tag. An empty tag comes from older\n // configurations (\"latest\"), where only the adapter knows which tag that resolved to - it\n // keeps just the release being served, so the single remaining directory is the right one.\n const releaseDir = selection.tag\n ? path.join(dataDir, 'cometvisu', selection.tag)\n : this.findSingleReleaseDir(dataDir);\n\n return releaseDir && fs.existsSync(path.join(releaseDir, '.complete')) ? findHtmlRoot(releaseDir) : null;\n }\n\n /**\n * The one unpacked release directory, ignoring the uploaded builds.\n *\n * @param dataDir the instance data directory\n */\n private findSingleReleaseDir(dataDir: string): string | null {\n const root = path.join(dataDir, 'cometvisu');\n if (!fs.existsSync(root)) {\n return null;\n }\n const dirs = fs\n .readdirSync(root, { withFileTypes: true })\n .filter(entry => entry.isDirectory() && entry.name !== 'custom' && !entry.name.startsWith('__'))\n .map(entry => path.join(root, entry.name));\n return dirs.length === 1 ? dirs[0] : null;\n }\n\n /**\n * Work out which socket CometVisu has to connect to. An external socket adapter runs on its own\n * port, which is only known after reading its instance object - until that answer arrives, and\n * if it never does, the origin of the request is used.\n *\n * @param adapter the web adapter we run in\n */\n private resolveSocket(adapter: ioBroker.Adapter): void {\n const setup = classifyWebSocket(adapter.config as WebNative);\n if (setup.kind === 'none') {\n this.log.error(\n `${adapter.namespace} serves no socket (\"none\"), so CometVisu cannot reach ioBroker - ` +\n 'configure a socket in that web instance',\n );\n return;\n }\n this.hasSocket = true;\n if (setup.kind !== 'external') {\n return;\n }\n adapter.getForeignObjectAsync(setup.instance).then(\n obj => {\n const native = obj?.native as { port?: number; secure?: boolean } | undefined;\n if (!native?.port) {\n this.log.warn(`cannot read the port of ${setup.instance}, using the port of the web adapter`);\n return;\n }\n this.socketPort = native.port;\n this.socketSecure = !!native.secure;\n this.log.info(`CometVisu connects to ${setup.instance} on port ${native.port} (${setup.transport})`);\n },\n (e: Error) => this.log.warn(`cannot read ${setup.instance}: ${e.message}`),\n );\n }\n\n /**\n * The websocket URL to hand to CometVisu for this request.\n *\n * @param req the request being answered\n */\n private socketUrl(req: express.Request): string {\n if (this.socketPort === null) {\n return `${req.secure ? 'wss' : 'ws'}://${req.headers.host}/`;\n }\n // same host as the request, but the port of the adapter that serves the socket\n const host = (req.headers.host || '').replace(/:\\d+$/, '');\n return `${this.socketSecure ? 'wss' : 'ws'}://${host}:${this.socketPort}/`;\n }\n\n /** Points the handler at the configured build, or drops it when there is none. */\n private refresh(): void {\n const htmlRoot = this.resolveHtmlRoot();\n if (!htmlRoot) {\n if (this.servedRoot) {\n this.log.warn(`the CometVisu build served for ${this.namespace} disappeared`);\n }\n this.servedRoot = null;\n this.handler = null;\n return;\n }\n if (htmlRoot === this.servedRoot && this.handler) {\n return;\n }\n\n const router = express.Router();\n // same overlay as the adapter's own web server: a user provided \"resource\" folder wins over\n // the one shipped with the build\n const overlayDir = path.join(utils.getAbsoluteInstanceDataDir(this.namespace), 'resource');\n if (fs.existsSync(overlayDir)) {\n router.use('/resource', express.static(overlayDir));\n }\n router.use(express.static(htmlRoot));\n\n this.handler = router;\n this.servedRoot = htmlRoot;\n this.log.info(`CometVisu (${this.namespace}) serves ${htmlRoot}`);\n }\n}\n\nexport default web;\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": ["express"]
|
|
7
7
|
}
|
package/build/main.js
CHANGED
|
@@ -22,8 +22,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
22
|
mod
|
|
23
23
|
));
|
|
24
24
|
var utils = __toESM(require("@iobroker/adapter-core"));
|
|
25
|
-
var fs = __toESM(require("fs"));
|
|
26
|
-
var path = __toESM(require("path"));
|
|
25
|
+
var fs = __toESM(require("node:fs"));
|
|
26
|
+
var path = __toESM(require("node:path"));
|
|
27
27
|
var import_releases = require("./lib/releases");
|
|
28
28
|
var import_webSocket = require("./lib/webSocket");
|
|
29
29
|
const FORMER_OWN_SERVER_SETTINGS = [
|
package/build/main.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/main.ts"],
|
|
4
|
-
"sourcesContent": ["/*\n * Created with @iobroker/create-adapter v3.1.5\n */\n\nimport * as utils from '@iobroker/adapter-core';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n CUSTOM_FILE_PREFIX,\n type CustomBuildSource,\n OFFICIAL_FILE_PREFIX,\n type VersionSelection,\n ensureRelease,\n listCustomBuilds,\n pruneReleaseBuilds,\n readCustomBuild,\n removeCustomBuild,\n removeLegacyCustomBuild,\n resolveVersionSelection,\n unpackUploadedTarball,\n} from './lib/releases';\nimport { type WebNative, classifyWebSocket } from './lib/webSocket';\n\n// The visualisation is served by iobroker.web through the extension in lib/web.ts, which runs in\n// that adapter's process. This adapter only keeps the selected build ready on disk.\n\n// Settings of the web server this adapter used to run itself. They were dropped from io-package.json,\n// but an instance created before that still carries them: the upgrade only adds missing defaults\n// (extendNative() in @iobroker/js-controller-cli), it never removes what is gone.\nconst FORMER_OWN_SERVER_SETTINGS = [\n 'port',\n 'ip',\n 'findNextPort',\n 'secure',\n 'certPublic',\n 'certPrivate',\n 'certChained',\n 'auth',\n 'defaultUser',\n 'defaultUserPassword',\n 'ttl',\n 'ownServer',\n];\n\nclass Cometvisu extends utils.Adapter {\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({ ...options, name: 'cometvisu' });\n this.on('ready', this.onReady.bind(this));\n this.on('message', this.onMessage.bind(this));\n this.on('unload', this.onUnload.bind(this));\n }\n\n private async onReady(): Promise<void> {\n await this.setState('info.connection', { val: false, ack: true });\n await this.dropFormerServerSettings();\n\n // 1) make sure the selected CometVisu build is available locally\n const dataDir = utils.getAbsoluteInstanceDataDir(this);\n await this.pruneCustomBuilds(dataDir);\n let htmlRoot: string;\n // tag of the release being prepared, null while an uploaded build is used\n let servedTag: string | null = null;\n try {\n const selection = await this.resolveVersion();\n if (selection.kind === 'custom') {\n // the build the user uploaded through the admin\n htmlRoot = await this.prepareCustomBuild(dataDir, selection.file);\n this.log.info(`uploaded CometVisu build prepared in ${htmlRoot}`);\n } else {\n const result = await ensureRelease(dataDir, selection.tag, this.log);\n htmlRoot = result.htmlRoot;\n servedTag = result.tag;\n this.log.info(`CometVisu ${result.tag} prepared in ${htmlRoot}`);\n }\n } catch (e) {\n this.log.error(`could not prepare the CometVisu build: ${e instanceof Error ? e.message : String(e)}`);\n return;\n }\n\n // A release is cached as its unpacked directory, so only the one in use is kept - this\n // runs after the build was prepared, both because \"latest\" only resolves to a tag in there\n // and so a failed preparation leaves the existing cache alone.\n const dropped = pruneReleaseBuilds(dataDir, servedTag);\n if (dropped.length) {\n this.log.info(`removed unpacked release(s) no longer in use: ${dropped.join(', ')}`);\n }\n\n // A user provided \"resource\" folder next to the version folders acts as an overlay for\n // /resource/*, which the extension puts in front of the build's own resource folder. It is\n // created here so there is a place to drop files into even when it was never used.\n fs.mkdirSync(path.join(dataDir, 'resource'), { recursive: true });\n\n if (!this.config.webInstance) {\n this.log.error(\n 'the visualisation is served by the web adapter - please install it and select its ' +\n 'instance in the settings',\n );\n return;\n }\n if (!(await this.checkWebSocket())) {\n return;\n }\n await this.setState('info.connection', { val: true, ack: true });\n this.log.info(`the CometVisu build is ready, ${this.config.webInstance} serves it under /cometvisu`);\n }\n\n /**\n * Remove the settings of the web server this adapter used to run itself. They stay in an\n * instance that was created while they still existed, where they are misleading - the admin\n * keeps showing a port that nothing listens on, and an encrypted password no code reads.\n *\n * The object is written back as a whole instead of extending it: whether extendObject deletes a\n * single attribute is not something this could rely on. Changing our own configuration makes the\n * controller restart the instance once; there is nothing left to do on the next start, so this\n * just carries on afterwards.\n */\n private async dropFormerServerSettings(): Promise<void> {\n const id = `system.adapter.${this.namespace}`;\n let obj: ioBroker.Object | null | undefined;\n try {\n obj = await this.getForeignObjectAsync(id);\n } catch (e) {\n this.log.warn(`cannot read ${id}: ${e instanceof Error ? e.message : String(e)}`);\n return;\n }\n const native = obj?.native as Record<string, unknown> | undefined;\n if (!native) {\n return;\n }\n\n const found = FORMER_OWN_SERVER_SETTINGS.filter(key => key in native);\n if (!found.length) {\n return;\n }\n for (const key of found) {\n delete native[key];\n }\n\n try {\n await this.setForeignObjectAsync(id, obj!);\n this.log.info(`removed settings of the former own web server: ${found.join(', ')}`);\n } catch (e) {\n this.log.warn(`cannot clean up ${id}: ${e instanceof Error ? e.message : String(e)}`);\n }\n }\n\n /**\n * Report which socket CometVisu will use, and say so clearly when the web instance offers none.\n * The extension reads the same configuration in the process of the web adapter, but a CometVisu\n * problem is looked for in this log, so the message belongs here as well.\n */\n private async checkWebSocket(): Promise<boolean> {\n const instance = `system.adapter.${this.config.webInstance}`;\n let obj: ioBroker.Object | null | undefined;\n try {\n obj = await this.getForeignObjectAsync(instance);\n } catch (e) {\n this.log.warn(`cannot read ${instance}: ${e instanceof Error ? e.message : String(e)}`);\n return true;\n }\n if (!obj) {\n this.log.error(\n `${instance} does not exist - the visualisation is served by the web adapter, please ` +\n 'install it and select its instance in the settings',\n );\n return false;\n }\n\n const setup = classifyWebSocket(obj.native as WebNative);\n if (setup.kind === 'none') {\n this.log.error(\n `${this.config.webInstance} serves no socket (\"none\"), so CometVisu cannot reach ioBroker - ` +\n 'configure a socket in that web instance',\n );\n return false;\n }\n const where = setup.kind === 'external' ? setup.instance : this.config.webInstance;\n this.log.info(`CometVisu will talk ${setup.transport} to ${where}`);\n return true;\n }\n\n /**\n * What the configured version points at. The admin stores a prefixed entry of the version list,\n * but a bare value can still show up from an older configuration. Such a value is an uploaded\n * archive if a file by that name exists, and a release tag otherwise.\n */\n private async resolveVersion(): Promise<VersionSelection> {\n const value = (this.config.version || '').replace(/^\\/+/, '');\n const selection = resolveVersionSelection(value, this.config.buildUpload);\n if (selection.kind === 'custom' || !selection.tag) {\n return selection;\n }\n if (value.startsWith(OFFICIAL_FILE_PREFIX)) {\n return selection;\n }\n // a bare value: an upload if such a file exists, otherwise a release tag\n for (const candidate of [value, `${CUSTOM_FILE_PREFIX}${value}`]) {\n try {\n if (await this.fileExistsAsync(`${this.namespace}.files`, candidate)) {\n return { kind: 'custom', file: candidate };\n }\n } catch {\n // no file storage yet - treat the value as a release tag\n }\n }\n return selection;\n }\n\n /**\n * Return the directory to serve for an uploaded (\"custom\") build. Every upload has its own\n * directory, so switching back to a build that was unpacked before costs no unpacking at all.\n * Unpacking only happens when that directory is missing, or when the archive was uploaded again\n * under the same name - which the size and time of the stored file reveal.\n *\n * @param dataDir the instance data directory\n * @param file name of the uploaded archive to serve\n */\n private async prepareCustomBuild(dataDir: string, file: string): Promise<string> {\n if (!file) {\n throw new Error('no CometVisu build selected - please upload one in the adapter settings');\n }\n const source = await this.uploadSource(file);\n const current = readCustomBuild(dataDir, file);\n if (current && this.matchesUpload(current.source, source)) {\n this.log.debug(`uploaded CometVisu build \"${file}\" is already unpacked`);\n return current.htmlRoot;\n }\n\n const tmp = path.join(dataDir, 'upload.tar.gz');\n try {\n const stored = await this.readFileAsync(`${this.namespace}.files`, file);\n const buffer = Buffer.isBuffer(stored.file) ? stored.file : Buffer.from(stored.file, 'binary');\n\n fs.mkdirSync(dataDir, { recursive: true });\n fs.writeFileSync(tmp, buffer);\n const result = await unpackUploadedTarball(tmp, dataDir, this.log, source);\n return result.htmlRoot;\n } finally {\n fs.rmSync(tmp, { force: true });\n }\n }\n\n /**\n * Size and modification time of an uploaded archive, used to notice that it was replaced by a\n * new upload of the same name without reading the whole archive.\n *\n * @param file name of the uploaded archive\n */\n private async uploadSource(file: string): Promise<CustomBuildSource> {\n try {\n const entries = await this.readDirAsync(`${this.namespace}.files`, '/');\n const entry = entries.find(e => e.file === file);\n if (entry) {\n return { file, size: entry.stats?.size, modifiedAt: entry.modifiedAt };\n }\n } catch {\n // no file storage yet - the read of the archive itself will report the real problem\n }\n return { file };\n }\n\n /**\n * Whether an unpacked build was made from exactly the archive that is stored now. A marker\n * without size and time comes from an older adapter version, so that build is unpacked again.\n *\n * @param unpacked the source the build was unpacked from\n * @param stored the archive currently in the file storage\n */\n private matchesUpload(unpacked: CustomBuildSource | null, stored: CustomBuildSource): boolean {\n if (!unpacked || unpacked.file !== stored.file) {\n return false;\n }\n if (unpacked.size === undefined || unpacked.modifiedAt === undefined) {\n return false;\n }\n return unpacked.size === stored.size && unpacked.modifiedAt === stored.modifiedAt;\n }\n\n /**\n * Drop the builds whose archive is no longer in the file storage, plus the single directory all\n * uploads shared before every upload got its own one.\n *\n * @param dataDir the instance data directory\n */\n private async pruneCustomBuilds(dataDir: string): Promise<void> {\n if (removeLegacyCustomBuild(dataDir)) {\n this.log.info('removed the CometVisu build directory of the previous adapter version');\n }\n let uploads: string[];\n try {\n const entries = await this.readDirAsync(`${this.namespace}.files`, '/');\n uploads = entries.filter(entry => !entry.isDir).map(entry => entry.file);\n } catch {\n // without a file storage there is nothing to compare against, so keep what is there\n return;\n }\n for (const build of listCustomBuilds(dataDir)) {\n if (build.file === null || !uploads.includes(build.file)) {\n fs.rmSync(build.dir, { recursive: true, force: true });\n this.log.info(`removed unpacked build of deleted archive \"${build.file ?? build.dir}\"`);\n }\n }\n }\n\n /**\n * The admin component asks to remove the build of an archive it just deleted, so the unpacked\n * files go away right there instead of only at the next start.\n *\n * @param obj the admin message\n */\n private onMessage(obj: ioBroker.Message): void {\n if (!obj || typeof obj !== 'object' || obj.command !== 'deleteCustomBuild') {\n return;\n }\n const file = (obj.message as { file?: string } | undefined)?.file;\n let removed = false;\n if (file) {\n removed = removeCustomBuild(utils.getAbsoluteInstanceDataDir(this), file);\n if (removed) {\n this.log.info(`removed unpacked build of deleted archive \"${file}\"`);\n }\n }\n if (obj.callback) {\n this.sendTo(obj.from, obj.command, { removed }, obj.callback);\n }\n }\n\n private onUnload(callback: () => void): void {\n try {\n void this.setState('info.connection', { val: false, ack: true });\n callback();\n } catch (e) {\n this.log.error(`error during unload: ${e instanceof Error ? e.message : String(e)}`);\n callback();\n }\n }\n}\n\nif (require.main !== module) {\n // Export the constructor in compact mode\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new Cometvisu(options);\n} else {\n // otherwise start the instance directly\n (() => new Cometvisu())();\n}\n"],
|
|
4
|
+
"sourcesContent": ["/*\n * Created with @iobroker/create-adapter v3.1.5\n */\n\nimport * as utils from '@iobroker/adapter-core';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport {\n CUSTOM_FILE_PREFIX,\n type CustomBuildSource,\n OFFICIAL_FILE_PREFIX,\n type VersionSelection,\n ensureRelease,\n listCustomBuilds,\n pruneReleaseBuilds,\n readCustomBuild,\n removeCustomBuild,\n removeLegacyCustomBuild,\n resolveVersionSelection,\n unpackUploadedTarball,\n} from './lib/releases';\nimport { type WebNative, classifyWebSocket } from './lib/webSocket';\n\n// The visualisation is served by iobroker.web through the extension in lib/web.ts, which runs in\n// that adapter's process. This adapter only keeps the selected build ready on disk.\n\n// Settings of the web server this adapter used to run itself. They were dropped from io-package.json,\n// but an instance created before that still carries them: the upgrade only adds missing defaults\n// (extendNative() in @iobroker/js-controller-cli), it never removes what is gone.\nconst FORMER_OWN_SERVER_SETTINGS = [\n 'port',\n 'ip',\n 'findNextPort',\n 'secure',\n 'certPublic',\n 'certPrivate',\n 'certChained',\n 'auth',\n 'defaultUser',\n 'defaultUserPassword',\n 'ttl',\n 'ownServer',\n];\n\nclass Cometvisu extends utils.Adapter {\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({ ...options, name: 'cometvisu' });\n this.on('ready', this.onReady.bind(this));\n this.on('message', this.onMessage.bind(this));\n this.on('unload', this.onUnload.bind(this));\n }\n\n private async onReady(): Promise<void> {\n await this.setState('info.connection', { val: false, ack: true });\n await this.dropFormerServerSettings();\n\n // 1) make sure the selected CometVisu build is available locally\n const dataDir = utils.getAbsoluteInstanceDataDir(this);\n await this.pruneCustomBuilds(dataDir);\n let htmlRoot: string;\n // tag of the release being prepared, null while an uploaded build is used\n let servedTag: string | null = null;\n try {\n const selection = await this.resolveVersion();\n if (selection.kind === 'custom') {\n // the build the user uploaded through the admin\n htmlRoot = await this.prepareCustomBuild(dataDir, selection.file);\n this.log.info(`uploaded CometVisu build prepared in ${htmlRoot}`);\n } else {\n const result = await ensureRelease(dataDir, selection.tag, this.log);\n htmlRoot = result.htmlRoot;\n servedTag = result.tag;\n this.log.info(`CometVisu ${result.tag} prepared in ${htmlRoot}`);\n }\n } catch (e) {\n this.log.error(`could not prepare the CometVisu build: ${e instanceof Error ? e.message : String(e)}`);\n return;\n }\n\n // A release is cached as its unpacked directory, so only the one in use is kept - this\n // runs after the build was prepared, both because \"latest\" only resolves to a tag in there\n // and so a failed preparation leaves the existing cache alone.\n const dropped = pruneReleaseBuilds(dataDir, servedTag);\n if (dropped.length) {\n this.log.info(`removed unpacked release(s) no longer in use: ${dropped.join(', ')}`);\n }\n\n // A user provided \"resource\" folder next to the version folders acts as an overlay for\n // /resource/*, which the extension puts in front of the build's own resource folder. It is\n // created here so there is a place to drop files into even when it was never used.\n fs.mkdirSync(path.join(dataDir, 'resource'), { recursive: true });\n\n if (!this.config.webInstance) {\n this.log.error(\n 'the visualisation is served by the web adapter - please install it and select its ' +\n 'instance in the settings',\n );\n return;\n }\n if (!(await this.checkWebSocket())) {\n return;\n }\n await this.setState('info.connection', { val: true, ack: true });\n this.log.info(`the CometVisu build is ready, ${this.config.webInstance} serves it under /cometvisu`);\n }\n\n /**\n * Remove the settings of the web server this adapter used to run itself. They stay in an\n * instance that was created while they still existed, where they are misleading - the admin\n * keeps showing a port that nothing listens on, and an encrypted password no code reads.\n *\n * The object is written back as a whole instead of extending it: whether extendObject deletes a\n * single attribute is not something this could rely on. Changing our own configuration makes the\n * controller restart the instance once; there is nothing left to do on the next start, so this\n * just carries on afterwards.\n */\n private async dropFormerServerSettings(): Promise<void> {\n const id = `system.adapter.${this.namespace}`;\n let obj: ioBroker.Object | null | undefined;\n try {\n obj = await this.getForeignObjectAsync(id);\n } catch (e) {\n this.log.warn(`cannot read ${id}: ${e instanceof Error ? e.message : String(e)}`);\n return;\n }\n const native = obj?.native as Record<string, unknown> | undefined;\n if (!native) {\n return;\n }\n\n const found = FORMER_OWN_SERVER_SETTINGS.filter(key => key in native);\n if (!found.length) {\n return;\n }\n for (const key of found) {\n delete native[key];\n }\n\n try {\n await this.setForeignObjectAsync(id, obj!);\n this.log.info(`removed settings of the former own web server: ${found.join(', ')}`);\n } catch (e) {\n this.log.warn(`cannot clean up ${id}: ${e instanceof Error ? e.message : String(e)}`);\n }\n }\n\n /**\n * Report which socket CometVisu will use, and say so clearly when the web instance offers none.\n * The extension reads the same configuration in the process of the web adapter, but a CometVisu\n * problem is looked for in this log, so the message belongs here as well.\n */\n private async checkWebSocket(): Promise<boolean> {\n const instance = `system.adapter.${this.config.webInstance}`;\n let obj: ioBroker.Object | null | undefined;\n try {\n obj = await this.getForeignObjectAsync(instance);\n } catch (e) {\n this.log.warn(`cannot read ${instance}: ${e instanceof Error ? e.message : String(e)}`);\n return true;\n }\n if (!obj) {\n this.log.error(\n `${instance} does not exist - the visualisation is served by the web adapter, please ` +\n 'install it and select its instance in the settings',\n );\n return false;\n }\n\n const setup = classifyWebSocket(obj.native as WebNative);\n if (setup.kind === 'none') {\n this.log.error(\n `${this.config.webInstance} serves no socket (\"none\"), so CometVisu cannot reach ioBroker - ` +\n 'configure a socket in that web instance',\n );\n return false;\n }\n const where = setup.kind === 'external' ? setup.instance : this.config.webInstance;\n this.log.info(`CometVisu will talk ${setup.transport} to ${where}`);\n return true;\n }\n\n /**\n * What the configured version points at. The admin stores a prefixed entry of the version list,\n * but a bare value can still show up from an older configuration. Such a value is an uploaded\n * archive if a file by that name exists, and a release tag otherwise.\n */\n private async resolveVersion(): Promise<VersionSelection> {\n const value = (this.config.version || '').replace(/^\\/+/, '');\n const selection = resolveVersionSelection(value, this.config.buildUpload);\n if (selection.kind === 'custom' || !selection.tag) {\n return selection;\n }\n if (value.startsWith(OFFICIAL_FILE_PREFIX)) {\n return selection;\n }\n // a bare value: an upload if such a file exists, otherwise a release tag\n for (const candidate of [value, `${CUSTOM_FILE_PREFIX}${value}`]) {\n try {\n if (await this.fileExistsAsync(`${this.namespace}.files`, candidate)) {\n return { kind: 'custom', file: candidate };\n }\n } catch {\n // no file storage yet - treat the value as a release tag\n }\n }\n return selection;\n }\n\n /**\n * Return the directory to serve for an uploaded (\"custom\") build. Every upload has its own\n * directory, so switching back to a build that was unpacked before costs no unpacking at all.\n * Unpacking only happens when that directory is missing, or when the archive was uploaded again\n * under the same name - which the size and time of the stored file reveal.\n *\n * @param dataDir the instance data directory\n * @param file name of the uploaded archive to serve\n */\n private async prepareCustomBuild(dataDir: string, file: string): Promise<string> {\n if (!file) {\n throw new Error('no CometVisu build selected - please upload one in the adapter settings');\n }\n const source = await this.uploadSource(file);\n const current = readCustomBuild(dataDir, file);\n if (current && this.matchesUpload(current.source, source)) {\n this.log.debug(`uploaded CometVisu build \"${file}\" is already unpacked`);\n return current.htmlRoot;\n }\n\n const tmp = path.join(dataDir, 'upload.tar.gz');\n try {\n const stored = await this.readFileAsync(`${this.namespace}.files`, file);\n const buffer = Buffer.isBuffer(stored.file) ? stored.file : Buffer.from(stored.file, 'binary');\n\n fs.mkdirSync(dataDir, { recursive: true });\n fs.writeFileSync(tmp, buffer);\n const result = await unpackUploadedTarball(tmp, dataDir, this.log, source);\n return result.htmlRoot;\n } finally {\n fs.rmSync(tmp, { force: true });\n }\n }\n\n /**\n * Size and modification time of an uploaded archive, used to notice that it was replaced by a\n * new upload of the same name without reading the whole archive.\n *\n * @param file name of the uploaded archive\n */\n private async uploadSource(file: string): Promise<CustomBuildSource> {\n try {\n const entries = await this.readDirAsync(`${this.namespace}.files`, '/');\n const entry = entries.find(e => e.file === file);\n if (entry) {\n return { file, size: entry.stats?.size, modifiedAt: entry.modifiedAt };\n }\n } catch {\n // no file storage yet - the read of the archive itself will report the real problem\n }\n return { file };\n }\n\n /**\n * Whether an unpacked build was made from exactly the archive that is stored now. A marker\n * without size and time comes from an older adapter version, so that build is unpacked again.\n *\n * @param unpacked the source the build was unpacked from\n * @param stored the archive currently in the file storage\n */\n private matchesUpload(unpacked: CustomBuildSource | null, stored: CustomBuildSource): boolean {\n if (!unpacked || unpacked.file !== stored.file) {\n return false;\n }\n if (unpacked.size === undefined || unpacked.modifiedAt === undefined) {\n return false;\n }\n return unpacked.size === stored.size && unpacked.modifiedAt === stored.modifiedAt;\n }\n\n /**\n * Drop the builds whose archive is no longer in the file storage, plus the single directory all\n * uploads shared before every upload got its own one.\n *\n * @param dataDir the instance data directory\n */\n private async pruneCustomBuilds(dataDir: string): Promise<void> {\n if (removeLegacyCustomBuild(dataDir)) {\n this.log.info('removed the CometVisu build directory of the previous adapter version');\n }\n let uploads: string[];\n try {\n const entries = await this.readDirAsync(`${this.namespace}.files`, '/');\n uploads = entries.filter(entry => !entry.isDir).map(entry => entry.file);\n } catch {\n // without a file storage there is nothing to compare against, so keep what is there\n return;\n }\n for (const build of listCustomBuilds(dataDir)) {\n if (build.file === null || !uploads.includes(build.file)) {\n fs.rmSync(build.dir, { recursive: true, force: true });\n this.log.info(`removed unpacked build of deleted archive \"${build.file ?? build.dir}\"`);\n }\n }\n }\n\n /**\n * The admin component asks to remove the build of an archive it just deleted, so the unpacked\n * files go away right there instead of only at the next start.\n *\n * @param obj the admin message\n */\n private onMessage(obj: ioBroker.Message): void {\n if (!obj || typeof obj !== 'object' || obj.command !== 'deleteCustomBuild') {\n return;\n }\n const file = (obj.message as { file?: string } | undefined)?.file;\n let removed = false;\n if (file) {\n removed = removeCustomBuild(utils.getAbsoluteInstanceDataDir(this), file);\n if (removed) {\n this.log.info(`removed unpacked build of deleted archive \"${file}\"`);\n }\n }\n if (obj.callback) {\n this.sendTo(obj.from, obj.command, { removed }, obj.callback);\n }\n }\n\n private onUnload(callback: () => void): void {\n try {\n void this.setState('info.connection', { val: false, ack: true });\n callback();\n } catch (e) {\n this.log.error(`error during unload: ${e instanceof Error ? e.message : String(e)}`);\n callback();\n }\n }\n}\n\nif (require.main !== module) {\n // Export the constructor in compact mode\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new Cometvisu(options);\n} else {\n // otherwise start the instance directly\n (() => new Cometvisu())();\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "cometvisu",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.4",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.0.4": {
|
|
7
|
+
"en": "the adapter now requires node.js 22 and is tested on 22 and 24\nthe admin page is available in all eleven languages ioBroker ships\nupdated express to 5, TypeScript to 6 in both packages, axios and tar to their current releases\ndependabot updates are scheduled by cron, wait seven days and use the ioBroker automerge action",
|
|
8
|
+
"de": "Der Adapter benötigt jetzt node.js 22 und wurde am 22 und 24 getestet\nDie Admin-Seite ist in allen elf Sprachen verfügbar, ioBroker\nExpress auf 5 aktualisiert, TypeScript auf 6 in beiden Paketen, Axios und Tar\nDependabot-Updates werden von Cron geplant. Warten Sie 7 Tage und nutzen Sie IOBroker Automerge",
|
|
9
|
+
"ru": "адаптер теперь требует node.js 22 и протестирован на 22 и 24\nстраница администратора доступна на всех одиннадцати языках, поставляемых IOBroker\nобновлено express до 5, TypeScript до 6 в обоих пакетах, axios и tar до их текущих версий\nобновления dependabot запланированы cron, подождите семь дней и используйте действие IOBroker automerge",
|
|
10
|
+
"pt": "o adaptador agora exige node.js 22 e é testado em 22 e 24\na página de admin está disponível em onze idiomas.\natualizou express para 5, TypeScript para 6 em ambos os pacotes, axios e tar\nO dependabot é agendado por cron, espere sete dias e use a ação IOBroker automerge",
|
|
11
|
+
"nl": "de adapter heeft nu node.js 22 nodig en is getest op 22 en 24\nde beheerpagina is beschikbaar in alle elf talen die IOBroker levert\nexpress bijgewerkt naar 5, TypeScript naar 6 in beide pakketten, axios en tar naar hun huidige versies\nupdates van dependabot worden gepland met cron, wacht zeven dagen en gebruik de ioBroker automerge-actie",
|
|
12
|
+
"fr": "l'adaptateur nécessite node.js 22 et a été testé les 22 et 24\nla page d'administration est disponible dans les onze langues d'IObroker\na mis à jour Express à 5, TypeScript à 6 dans les deux packages, axios et tar aux versions actuelles\nles mises à jour de dependabot sont planifiées par cron, attendez sept jours et utilisez la fusion automatique IObroker",
|
|
13
|
+
"it": "l'adattatore ora richiede node.js 22 ed è testato su 22 e 24\nla pagina admin è disponibile in undici lingue fornite da IObroker\nexpress aggiornato a 5, TypeScript a 6 in entrambi i pacchetti, axios e tar alle versioni attuali\ngli aggiornamenti sono programmati da cron, attenda 7 giorni e usi IOBroker",
|
|
14
|
+
"es": "el adaptador ahora necesita node.js 22 y se ha probado los días 22 y 24\nla página de administración está disponible en los once idiomas que IOBroker\nactualizó express a 5, TypeScript a 6 en ambos paquetes, axios y tar a sus versiones actuales\nLas actualizaciones del dependabot se programan por cron, espere siete días y utilice la acción de autounión de ioBroker",
|
|
15
|
+
"pl": "adapter wymaga teraz pliku node.js 22 i jest testowany na 22 i 24\nstrona administracyjna jest dostępna we wszystkich jedenastu językach iOBroker\nzaktualizowano express do 5, TypeScript do 6 w obu pakietach, axios i tar do ich aktualnych wersji\naktualizacje dependabot są zaplanowane przez cron, poczekaj siedem dni i użyj akcji automerge ioBroker",
|
|
16
|
+
"uk": "адаптер тепер вимагає node.js 22 і тестується на 22 і 24\nсторінка адміністратора доступна на всіх одинадцяти мовах, що постачається IObroker\nоновлено express до 5, TypeScript до 6 в обох пакетах, axios і tar до їх поточних випусків\nоновлення dependabot заплановані за допомогою cron, зачекайте сім днів і використовуйте дію ioBroker automerge",
|
|
17
|
+
"zh-cn": "适配器现在需要 node.js 22 并在 22 和 24 上进行了测试\n管理页面支持 IOBroker 发布的所有十一种语言\n将两个包中的 express 更新为 5,TypeScript 更新为 6,axios 和 tar 更新到当前版本\ndependabot 更新由 cron 安排,等待七天后使用 IOBroker 自动合并操作"
|
|
18
|
+
},
|
|
19
|
+
"0.0.3": {
|
|
20
|
+
"en": "\"ioBroker\" is no longer listed in \"common.keywords\", where the adapter checker rejects it",
|
|
21
|
+
"de": "„ioBroker“ ist nicht mehr in common.keywords, der Adapter-Checker lehnt es ab",
|
|
22
|
+
"ru": "«IOBroker» больше не указан в «common.keywords», где средство проверки адаптеров отклоняет его",
|
|
23
|
+
"pt": "“IOBroker” não aparece mais em “common.keywords”, onde o Adapter Checker o rejeita",
|
|
24
|
+
"nl": "„IOBroker” staat niet langer vermeld in „common.keywords”, waar de adapterchecker dit weigert",
|
|
25
|
+
"fr": "« IOBroker » ne figure plus dans « common.keywords », où le vérificateur d'adaptateurs le rejette",
|
|
26
|
+
"it": "«ioBroker» non è più elencato in «common.keywords», dove Adapter Checker lo rifiuta",
|
|
27
|
+
"es": "«IOBroker» ya no aparece en «common.keywords», ya que el comprobador de adaptadores lo rechaza",
|
|
28
|
+
"pl": "„ioBroker” nie jest już wymieniony w „common.keywords”, gdzie moduł sprawdzania adaptera go odrzuca",
|
|
29
|
+
"uk": "«IObroker» більше не вказано в списку «common.keywords», де перевірка адаптера відхиляє його",
|
|
30
|
+
"zh-cn": "“ioBroker” 不再在 “common.keywords” 中列出,适配器检查器拒绝了它"
|
|
31
|
+
},
|
|
6
32
|
"0.0.2": {
|
|
7
33
|
"en": "keywords now contain \"ioBroker\", as the adapter checker asks for\nreleases are published by the workflow through trusted publishing",
|
|
8
34
|
"de": "Keywords enthalten nun „ioBroker“, wie es der Adapter-Checker verlangt\nReleases werden vom Workflow über Trusted Publishing veröffentlicht",
|
|
@@ -60,7 +86,6 @@
|
|
|
60
86
|
"Florian Schirmer <jolt@tuxbox.org>"
|
|
61
87
|
],
|
|
62
88
|
"keywords": [
|
|
63
|
-
"ioBroker",
|
|
64
89
|
"cometvisu",
|
|
65
90
|
"visualization",
|
|
66
91
|
"websocket",
|
|
@@ -95,7 +120,7 @@
|
|
|
95
120
|
],
|
|
96
121
|
"globalDependencies": [
|
|
97
122
|
{
|
|
98
|
-
"admin": ">=7.
|
|
123
|
+
"admin": ">=7.6.20"
|
|
99
124
|
}
|
|
100
125
|
],
|
|
101
126
|
"messagebox": true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.cometvisu",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Serves the CometVisu visualization through the ioBroker web adapter",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Florian Schirmer",
|
|
@@ -21,13 +21,13 @@
|
|
|
21
21
|
"url": "git+https://github.com/joltcoke/ioBroker.cometvisu.git"
|
|
22
22
|
},
|
|
23
23
|
"engines": {
|
|
24
|
-
"node": ">=
|
|
24
|
+
"node": ">= 22"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@iobroker/adapter-core": "^3.4.3",
|
|
28
|
-
"axios": "^1.
|
|
29
|
-
"express": "^
|
|
30
|
-
"tar": "^7.
|
|
28
|
+
"axios": "^1.20.0",
|
|
29
|
+
"express": "^5.2.1",
|
|
30
|
+
"tar": "^7.5.22"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@alcalzone/release-script": "^5.2.1",
|
|
@@ -38,13 +38,13 @@
|
|
|
38
38
|
"@iobroker/eslint-config": "^2.3.4",
|
|
39
39
|
"@iobroker/testing": "^5.3.0",
|
|
40
40
|
"@iobroker/types": "^7.2.2",
|
|
41
|
-
"@tsconfig/
|
|
42
|
-
"@types/express": "^
|
|
43
|
-
"@types/node": "^20.
|
|
41
|
+
"@tsconfig/node22": "^22.0.6",
|
|
42
|
+
"@types/express": "^5.0.6",
|
|
43
|
+
"@types/node": "^22.20.1",
|
|
44
44
|
"rimraf": "^6.1.3",
|
|
45
45
|
"source-map-support": "^0.5.21",
|
|
46
46
|
"ts-node": "^10.9.2",
|
|
47
|
-
"typescript": "~
|
|
47
|
+
"typescript": "~6.0.3"
|
|
48
48
|
},
|
|
49
49
|
"main": "build/main.js",
|
|
50
50
|
"files": [
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"CometVisu": "CometVisu",
|
|
3
3
|
"CometVisu version": "CometVisu-Version",
|
|
4
|
-
"The selected version is unpacked and activated when you save.": "Die gewählte Version wird beim Speichern entpackt und aktiviert.",
|
|
5
4
|
"Delivery": "Auslieferung",
|
|
6
5
|
"Serve through this web instance": "Über diese web-Instanz ausliefern",
|
|
6
|
+
"The selected version is unpacked and activated when you save.": "Die gewählte Version wird beim Speichern entpackt und aktiviert.",
|
|
7
7
|
"This web instance serves the visualisation under /cometvisu and protects it with its own login.": "Diese web-Instanz liefert die Visualisierung unter /cometvisu aus und schützt sie mit ihrer eigenen Anmeldung."
|
|
8
8
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"CometVisu": "CometVisu",
|
|
3
3
|
"CometVisu version": "CometVisu version",
|
|
4
|
-
"The selected version is unpacked and activated when you save.": "The selected version is unpacked and activated when you save.",
|
|
5
4
|
"Delivery": "Delivery",
|
|
6
5
|
"Serve through this web instance": "Serve through this web instance",
|
|
6
|
+
"The selected version is unpacked and activated when you save.": "The selected version is unpacked and activated when you save.",
|
|
7
7
|
"This web instance serves the visualisation under /cometvisu and protects it with its own login.": "This web instance serves the visualisation under /cometvisu and protects it with its own login."
|
|
8
8
|
}
|