dexterm-cli 0.2.0 → 0.2.2
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/dist/packages/agents/src/spectator.js +28 -2
- package/dist/packages/agents/src/tiers.js +3 -0
- package/dist/packages/agents/src/tools.js +3 -0
- package/dist/packages/autotrade/src/backtest.js +6 -2
- package/dist/packages/autotrade/src/chat/connector.js +5 -4
- package/dist/packages/autotrade/src/chat/control.js +3 -0
- package/dist/packages/autotrade/src/chat/hub.js +37 -2
- package/dist/packages/autotrade/src/chat/index.js +3 -2
- package/dist/packages/autotrade/src/chat/readonly.js +530 -0
- package/dist/packages/autotrade/src/chat/service.js +12 -1
- package/dist/packages/autotrade/src/engine.js +75 -29
- package/dist/packages/autotrade/src/fleet.js +2 -2
- package/dist/packages/autotrade/src/index.js +4 -0
- package/dist/packages/autotrade/src/rugproof-probe.js +415 -0
- package/dist/packages/autotrade/src/rugproof.js +231 -0
- package/dist/packages/autotrade/src/runner.js +203 -30
- package/dist/packages/autotrade/src/service-install.js +6 -4
- package/dist/packages/autotrade/src/sessions.js +16 -7
- package/dist/packages/autotrade/src/store.js +1 -1
- package/dist/packages/autotrade/src/strategy-stats.js +344 -0
- package/dist/packages/autotrade/src/strategy.js +29 -1
- package/dist/packages/autotrade/src/tiers.js +194 -0
- package/dist/packages/classifier/src/calibrate.js +138 -0
- package/dist/packages/classifier/src/classify.js +107 -0
- package/dist/packages/classifier/src/config.js +298 -0
- package/dist/packages/classifier/src/dossier.js +95 -0
- package/dist/packages/classifier/src/hardware.js +63 -0
- package/dist/packages/classifier/src/index.js +19 -0
- package/dist/packages/classifier/src/models.js +111 -0
- package/dist/packages/classifier/src/providers/heuristic.js +77 -0
- package/dist/packages/classifier/src/providers/http.js +63 -0
- package/dist/packages/classifier/src/providers/jev-open.js +86 -0
- package/dist/packages/classifier/src/providers/llm.js +89 -0
- package/dist/packages/classifier/src/providers/systemone-http.js +95 -0
- package/dist/packages/classifier/src/questions.js +97 -0
- package/dist/packages/classifier/src/registry.js +42 -0
- package/dist/packages/classifier/src/snapshot.js +31 -0
- package/dist/packages/classifier/src/systemone.js +88 -0
- package/dist/packages/classifier/src/types.js +18 -0
- package/dist/packages/classifier/src/win-acl.js +71 -0
- package/dist/packages/cli/assets/dash/dash.css +139 -0
- package/dist/packages/cli/assets/dash/dash.js +200 -0
- package/dist/packages/cli/assets/dash/icon.svg +1 -0
- package/dist/packages/cli/assets/dash/index.html +36 -0
- package/dist/packages/cli/src/app-help-gen.js +13 -1
- package/dist/packages/cli/src/autotrade-cmd/fleet.js +12 -3
- package/dist/packages/cli/src/autotrade-cmd/run.js +14 -2
- package/dist/packages/cli/src/autotrade-cmd/service.js +3 -1
- package/dist/packages/cli/src/autotrade-cmd/signal.js +2 -2
- package/dist/packages/cli/src/autotrade-cmd/strategies.js +1 -1
- package/dist/packages/cli/src/autotrade-cmd/telegram-host.js +2 -0
- package/dist/packages/cli/src/classifier-cmd.js +361 -0
- package/dist/packages/cli/src/classifier-live.js +81 -0
- package/dist/packages/cli/src/dash/data.js +217 -0
- package/dist/packages/cli/src/dash/server.js +92 -0
- package/dist/packages/cli/src/dash-cmd.js +113 -0
- package/dist/packages/cli/src/doctor.js +34 -0
- package/dist/packages/cli/src/live/screens/token-live.js +4 -0
- package/dist/packages/cli/src/live/signals.js +8 -0
- package/dist/packages/cli/src/main.js +98 -6
- package/dist/packages/cli/src/mcp-follow.js +21 -1
- package/dist/packages/cli/src/narratives-cmd.js +147 -0
- package/dist/packages/cli/src/narratives.js +114 -0
- package/dist/packages/cli/src/overlay/model.js +147 -0
- package/dist/packages/cli/src/overlay/page.js +146 -0
- package/dist/packages/cli/src/overlay/pnl.js +75 -0
- package/dist/packages/cli/src/overlay/server.js +170 -0
- package/dist/packages/cli/src/overlay-cmd.js +367 -0
- package/dist/packages/cli/src/record-cmd.js +82 -11
- package/dist/packages/cli/src/rules-cmd.js +2 -1
- package/dist/packages/cli/src/rules.js +24 -4
- package/dist/packages/cli/src/screens/narrative.js +154 -2
- package/dist/packages/cli/src/skill-eval-build.js +7 -2
- package/dist/packages/cli/src/state.js +1 -0
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/custom/src/rules.js +5 -1
- package/dist/packages/mcp/assets/kb/app-help/autotrade-telegram.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/classification-models.md +41 -0
- package/dist/packages/mcp/assets/kb/app-help/narratives-tracker.md +45 -0
- package/dist/packages/mcp/assets/kb/app-help/strategy-dashboard.md +38 -0
- package/dist/packages/mcp/assets/kb/app-help/streamer-overlay.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-confidence-tiers-experimental-unproven-paper-only.md +56 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-rug-proof-entries-experimental.md +52 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-strategy-dashboard-dexterm-dash.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-copy-and-paste-without-surprises.md +0 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-dexterm-says-command-not-found.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-reopen-dexterm-later.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-terminal-shell-prompt-command-output-and-path.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-hosted-jev-with-your-own-key.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-how-they-measured-on-dexterm-s-own-history.md +52 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-set-up-kev-on-your-own-machine.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-settings.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-the-choices.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-where-it-plugs-in.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-which-model-for-which-machine.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-cli-invocations.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -0
- package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +2 -11
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-larp-paste-a-coin-on-the-web.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-how-coins-are-grouped.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-limits.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-is-measured.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-testing-showed.md +43 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-where-to-find-it.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-before-you-start.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-limits.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-options.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-security-model.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-set-it-up-in-obs-step-by-step.md +49 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-pnl-scene-paper-and-live.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-three-scenes.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-what-is-redacted.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay.md +25 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-bubble-map.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-streaming-it.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-read-only-data-check-top-and-signals-experimental.md +48 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-will-not-do.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +2 -2
- package/dist/packages/mcp/assets/kb/entries/narratives-tracker.md +45 -0
- package/dist/packages/mcp/src/server.js +106 -1
- package/dist/packages/mcp/src/spectator-sink.js +27 -0
- package/dist/packages/narratives/src/cluster.js +193 -0
- package/dist/packages/narratives/src/embed.js +184 -0
- package/dist/packages/narratives/src/export.js +60 -0
- package/dist/packages/narratives/src/index.js +10 -0
- package/dist/packages/narratives/src/label.js +48 -0
- package/dist/packages/narratives/src/normalize.js +179 -0
- package/dist/packages/narratives/src/recorder.js +93 -0
- package/dist/packages/narratives/src/tracker.js +328 -0
- package/dist/packages/packs/src/skill-care.js +10 -2
- package/dist/packages/recorder/src/exit-probes.js +215 -0
- package/dist/packages/recorder/src/index.js +1 -0
- package/dist/packages/recorder/src/recorder.js +11 -1
- package/dist/packages/recorder/src/retention.js +4 -0
- package/dist/packages/recorder/src/schema.js +20 -1
- package/dist/packages/tui/src/keymap.js +5 -0
- package/package.json +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +0 -39
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// The built-in question sets (task-1822). Question text is first-party and frozen per set id: changing a wording
|
|
2
|
+
// means a new id, so recorded results and calibration maps stay tied to the exact question they were fitted on.
|
|
3
|
+
// Validation mirrors the System One API limits (choice: 1-255 options, score: 2-10 levels).
|
|
4
|
+
import { ClassifierError } from "./types.js";
|
|
5
|
+
/** Rug / drain risk and the 6 h outcome class. Labels match the task-1734 winner-recall classes. */
|
|
6
|
+
export const COIN_RISK = {
|
|
7
|
+
id: 'coin-risk/1',
|
|
8
|
+
questions: [
|
|
9
|
+
{
|
|
10
|
+
id: 'drain_6h',
|
|
11
|
+
type: 'noul',
|
|
12
|
+
instructions: 'Within the next 6 hours, will this token be drained: its pool liquidity pulled, or its price collapsing below a fifth of the current price?',
|
|
13
|
+
criteria: { true: 'drained within 6 hours (rug pull or collapse)', false: 'liquidity and price hold above those floors for 6 hours' },
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: 'outcome_6h',
|
|
17
|
+
type: 'choice',
|
|
18
|
+
instructions: 'Which outcome is most likely for this token over the next 6 hours?',
|
|
19
|
+
criteria: {
|
|
20
|
+
RUG: 'drained (liquidity pulled or price below a fifth) without first doubling',
|
|
21
|
+
WIN: 'price reaches at least 3x the current price while the pool still holds at least $10k of liquidity',
|
|
22
|
+
DUD: 'neither: it drifts, fades or stalls',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
/** The LARP eval question: does the coin go bad (rug or dead) within 72 h. Matches the task-1401 eval label. */
|
|
28
|
+
export const LARP_BAD = {
|
|
29
|
+
id: 'larp-bad/1',
|
|
30
|
+
questions: [
|
|
31
|
+
{
|
|
32
|
+
id: 'bad_72h',
|
|
33
|
+
type: 'noul',
|
|
34
|
+
instructions: 'Within 72 hours, will this token be rugged or dead (liquidity pulled, or trading stopped)?',
|
|
35
|
+
criteria: { true: 'rugged or dead within 72 hours', false: 'still trading after 72 hours (flat, fading or running)' },
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
/** Narrative category of a coin (task-1818's tracker). Options are the tracker's coarse buckets. */
|
|
40
|
+
export const NARRATIVE = {
|
|
41
|
+
id: 'narrative/1',
|
|
42
|
+
questions: [
|
|
43
|
+
{
|
|
44
|
+
id: 'category',
|
|
45
|
+
type: 'choice',
|
|
46
|
+
instructions: 'Which narrative category best describes this token, judging only from its name, symbol and description?',
|
|
47
|
+
criteria: {
|
|
48
|
+
animal: 'an animal or pet meme (dogs, cats, frogs, ...)',
|
|
49
|
+
ai: 'AI, agents or robots',
|
|
50
|
+
politics: 'politicians, elections or news figures',
|
|
51
|
+
celebrity: 'a celebrity, influencer or brand',
|
|
52
|
+
culture: 'an internet meme, joke or cultural moment',
|
|
53
|
+
tech: 'a product, utility, protocol or tool',
|
|
54
|
+
other: 'none of these',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
export const QUESTION_SETS = {
|
|
60
|
+
[COIN_RISK.id]: COIN_RISK,
|
|
61
|
+
[LARP_BAD.id]: LARP_BAD,
|
|
62
|
+
[NARRATIVE.id]: NARRATIVE,
|
|
63
|
+
};
|
|
64
|
+
const ID_RE = /^[a-z][a-z0-9_]{0,39}$/;
|
|
65
|
+
/** Throws ClassifierError('invalid') on a malformed set. Returns the set for chaining. */
|
|
66
|
+
export function validateQuestionSet(set) {
|
|
67
|
+
if (!set || typeof set.id !== 'string' || !set.id)
|
|
68
|
+
throw new ClassifierError('invalid', 'question set needs an id');
|
|
69
|
+
if (!Array.isArray(set.questions) || set.questions.length === 0)
|
|
70
|
+
throw new ClassifierError('invalid', `${set.id}: no questions`);
|
|
71
|
+
const seen = new Set();
|
|
72
|
+
for (const q of set.questions) {
|
|
73
|
+
if (!ID_RE.test(q.id))
|
|
74
|
+
throw new ClassifierError('invalid', `${set.id}: bad question id ${JSON.stringify(q.id)}`);
|
|
75
|
+
if (seen.has(q.id))
|
|
76
|
+
throw new ClassifierError('invalid', `${set.id}: duplicate question id ${q.id}`);
|
|
77
|
+
seen.add(q.id);
|
|
78
|
+
if (typeof q.instructions !== 'string' || !q.instructions.trim())
|
|
79
|
+
throw new ClassifierError('invalid', `${set.id}/${q.id}: empty instructions`);
|
|
80
|
+
const n = labelsOf(q).length;
|
|
81
|
+
if (q.type === 'choice' && (n < 1 || n > 255))
|
|
82
|
+
throw new ClassifierError('invalid', `${set.id}/${q.id}: choice needs 1-255 options`);
|
|
83
|
+
if (q.type === 'score' && (n < 2 || n > 10))
|
|
84
|
+
throw new ClassifierError('invalid', `${set.id}/${q.id}: score needs 2-10 levels`);
|
|
85
|
+
if (q.type !== 'choice' && q.type !== 'noul' && q.type !== 'score')
|
|
86
|
+
throw new ClassifierError('invalid', `${set.id}/${q.id}: unknown type`);
|
|
87
|
+
}
|
|
88
|
+
return set;
|
|
89
|
+
}
|
|
90
|
+
/** The labels an answer to `q` carries probabilities for. noul: ['yes','no']; score: '0'..'n-1'. */
|
|
91
|
+
export function labelsOf(q) {
|
|
92
|
+
if (q.type === 'noul')
|
|
93
|
+
return ['yes', 'no'];
|
|
94
|
+
if (q.type === 'score')
|
|
95
|
+
return Array.isArray(q.criteria) ? q.criteria.map((_, i) => String(i)) : [];
|
|
96
|
+
return q.criteria && !Array.isArray(q.criteria) ? Object.keys(q.criteria) : [];
|
|
97
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Config -> Provider. The CLI and MCP server call `providerFromConfig`; the LLM runners (Claude CLI, local Ollama)
|
|
2
|
+
// are injected by the caller so this package never spawns a model process or opens a socket on its own.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { fileUsage, readTypesafeKey } from "./config.js";
|
|
5
|
+
import { heuristicProvider } from "./providers/heuristic.js";
|
|
6
|
+
import { jevOpenProvider } from "./providers/jev-open.js";
|
|
7
|
+
import { llmProvider } from "./providers/llm.js";
|
|
8
|
+
import { jevHostedProvider, kevLocalProvider } from "./providers/systemone-http.js";
|
|
9
|
+
import { ClassifierError } from "./types.js";
|
|
10
|
+
export const PROVIDERS = [
|
|
11
|
+
{ id: 'heuristic', title: 'Built-in scores (H1 p(rug) + LARP link score)', network: 'none', needs: 'nothing' },
|
|
12
|
+
{ id: 'kev-local', title: 'Kev decision model via llama.cpp (open, Apache-2.0)', network: 'local', needs: 'a downloaded Kev GGUF + llama-server' },
|
|
13
|
+
{ id: 'jev-hosted', title: "TypeSafe's hosted Jev (flagship, your own API key)", network: 'remote', needs: 'your TypeSafe API key' },
|
|
14
|
+
{ id: 'jev-open', title: 'JEV open students (autotrust/JEV-*) via vLLM', network: 'local', needs: 'a vLLM server with the JEV decision adapter (>= 24 GB GPU)' },
|
|
15
|
+
{ id: 'claude', title: 'Claude zero-shot (comparison baseline)', network: 'remote', needs: 'the Claude CLI, signed in' },
|
|
16
|
+
{ id: 'ollama-llm', title: 'Ollama chat model zero-shot (comparison baseline)', network: 'local', needs: 'Ollama with a chat model' },
|
|
17
|
+
];
|
|
18
|
+
export function providerFromConfig(c, id = c.provider, d = {}) {
|
|
19
|
+
switch (id) {
|
|
20
|
+
case 'heuristic': return heuristicProvider();
|
|
21
|
+
case 'kev-local': return kevLocalProvider({ endpoint: c.kev.endpoint, modelId: c.kev.model, fetch: d.fetch });
|
|
22
|
+
case 'jev-hosted': return jevHostedProvider({
|
|
23
|
+
endpoint: c.jev.endpoint, model: c.jev.model, apiKey: () => readTypesafeKey(d.env), pricePerMTokUsd: c.jev.pricePerMTokUsd,
|
|
24
|
+
maxCallsPerDay: c.jev.maxCallsPerDay, usage: fileUsage(d.env), fetch: d.fetch,
|
|
25
|
+
});
|
|
26
|
+
case 'jev-open': {
|
|
27
|
+
if (!c.jevOpen.headPath || !c.jevOpen.calibrationPath)
|
|
28
|
+
throw new ClassifierError('not_ready', 'jev-open needs jevOpen.headPath (adapter_vllm/decision_head.json) and jevOpen.calibrationPath (calibration.json) from the model repo');
|
|
29
|
+
const head = JSON.parse(fs.readFileSync(c.jevOpen.headPath, 'utf8'));
|
|
30
|
+
const calibration = JSON.parse(fs.readFileSync(c.jevOpen.calibrationPath, 'utf8'));
|
|
31
|
+
return jevOpenProvider({ endpoint: c.jevOpen.endpoint, modelId: c.jevOpen.model, head, calibration, fetch: d.fetch });
|
|
32
|
+
}
|
|
33
|
+
case 'claude':
|
|
34
|
+
if (!d.claudeRunner)
|
|
35
|
+
throw new ClassifierError('not_ready', 'claude baseline: no Claude runner wired');
|
|
36
|
+
return llmProvider({ id: 'claude', model: c.llm.claudeModel, run: d.claudeRunner });
|
|
37
|
+
case 'ollama-llm':
|
|
38
|
+
if (!d.ollamaRunner || !c.llm.ollamaModel)
|
|
39
|
+
throw new ClassifierError('not_ready', 'ollama-llm baseline: set llm.ollamaModel and have Ollama running');
|
|
40
|
+
return llmProvider({ id: 'ollama-llm', model: c.llm.ollamaModel, run: d.ollamaRunner });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// The app's live token row (@dexterm/data TokenSnapshot: token + pair + stats + audit) -> CoinInput, so the screener,
|
|
2
|
+
// the MCP tools and watch rules all build the same dossier the benchmark scored.
|
|
3
|
+
import { coinDossier } from "./dossier.js";
|
|
4
|
+
const n = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
|
5
|
+
export function coinInputFromSnapshot(s, now = Date.now()) {
|
|
6
|
+
const p = s.pair, st = s.stats, a = s.audit;
|
|
7
|
+
const created = n(s.token.createdAt) ?? n(p?.createdAt);
|
|
8
|
+
const h1 = st?.perWindow?.h1 ?? null;
|
|
9
|
+
return {
|
|
10
|
+
mint: s.mint ?? s.token.mint,
|
|
11
|
+
symbol: s.token.symbol ?? null,
|
|
12
|
+
name: s.token.name ?? null,
|
|
13
|
+
age_min: created === null ? null : Math.max(0, (now - created) / 60_000),
|
|
14
|
+
liquidity_usd: n(p?.liquidityUsd),
|
|
15
|
+
mcap_usd: n(p?.mcap) ?? n(p?.fdv),
|
|
16
|
+
vol5m_usd: n(p?.volume?.m5),
|
|
17
|
+
vol1h_usd: n(p?.volume?.h1),
|
|
18
|
+
buys5m: n(p?.txns?.m5?.buys), sells5m: n(p?.txns?.m5?.sells),
|
|
19
|
+
buys1h: n(p?.txns?.h1?.buys), sells1h: n(p?.txns?.h1?.sells),
|
|
20
|
+
change5m_pct: n(p?.priceChange?.m5), change1h_pct: n(p?.priceChange?.h1),
|
|
21
|
+
holders: n(st?.holderCount), organic_score: n(st?.organicScore),
|
|
22
|
+
traders1h: n(h1?.numTraders), net_buyers1h: n(h1?.numNetBuyers),
|
|
23
|
+
top10_pct: n(a?.top10Pct), dev_pct: n(a?.devPct), dev_other_mints: n(a?.devMints),
|
|
24
|
+
mint_authority: a?.mintAuthority ?? null, freeze_authority: a?.freezeAuthority ?? null,
|
|
25
|
+
launchpad: typeof s.token.launchpad === 'string' ? s.token.launchpad : null,
|
|
26
|
+
dex: p?.dex ?? null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function dossierFromSnapshot(s, now = Date.now()) {
|
|
30
|
+
return coinDossier(coinInputFromSnapshot(s, now));
|
|
31
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// The System One wire protocol (`POST /v1/systemone`): TypeSafe's public API, which llama.cpp's llama-server
|
|
2
|
+
// (PR #29818, 2026-10-02) and the Kev reference server also implement. One request = one state + named questions.
|
|
3
|
+
//
|
|
4
|
+
// Request : { model?, state, questions: { id: { type, instructions, criteria? } } }
|
|
5
|
+
// Response: { model, answers: { id: { type, choice?|noul?|score?, probabilities?, confidence?, legend? } }, usage }
|
|
6
|
+
//
|
|
7
|
+
// Normalised here into `Answer` (probs per label + ln-p logits + top + confidence). Anything malformed raises
|
|
8
|
+
// ClassifierError('bad_response') rather than guessing.
|
|
9
|
+
import { labelsOf } from "./questions.js";
|
|
10
|
+
import { ClassifierError } from "./types.js";
|
|
11
|
+
export function buildRequest(set, state, model) {
|
|
12
|
+
const questions = {};
|
|
13
|
+
for (const q of set.questions) {
|
|
14
|
+
const entry = { type: q.type, instructions: q.instructions };
|
|
15
|
+
if (q.criteria !== undefined)
|
|
16
|
+
entry.criteria = q.criteria;
|
|
17
|
+
questions[q.id] = entry;
|
|
18
|
+
}
|
|
19
|
+
return model ? { model, state, questions } : { state, questions };
|
|
20
|
+
}
|
|
21
|
+
const EPS = 1e-12;
|
|
22
|
+
/** Entropy-based confidence (1 - H/ln n): 0 for a uniform answer, 1 for a certain one. */
|
|
23
|
+
export function entropyConfidence(probs) {
|
|
24
|
+
const ps = Object.values(probs);
|
|
25
|
+
if (ps.length < 2)
|
|
26
|
+
return 1;
|
|
27
|
+
const h = -ps.reduce((s, p) => s + (p > 0 ? p * Math.log(p) : 0), 0);
|
|
28
|
+
return Math.max(0, Math.min(1, 1 - h / Math.log(ps.length)));
|
|
29
|
+
}
|
|
30
|
+
export function finishAnswer(type, probs, confidence) {
|
|
31
|
+
const sum = Object.values(probs).reduce((a, b) => a + b, 0);
|
|
32
|
+
if (!(sum > 0))
|
|
33
|
+
throw new ClassifierError('bad_response', 'answer has no probability mass');
|
|
34
|
+
const norm = {};
|
|
35
|
+
const logits = {};
|
|
36
|
+
let top = '';
|
|
37
|
+
let best = -1;
|
|
38
|
+
for (const [k, v] of Object.entries(probs)) {
|
|
39
|
+
const p = v / sum;
|
|
40
|
+
norm[k] = p;
|
|
41
|
+
logits[k] = Math.log(Math.max(p, EPS));
|
|
42
|
+
if (p > best) {
|
|
43
|
+
best = p;
|
|
44
|
+
top = k;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const conf = typeof confidence === 'number' && Number.isFinite(confidence) ? Math.max(0, Math.min(1, confidence)) : entropyConfidence(norm);
|
|
48
|
+
return { type, probs: norm, logits, top, confidence: conf };
|
|
49
|
+
}
|
|
50
|
+
function num(v) {
|
|
51
|
+
return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : null;
|
|
52
|
+
}
|
|
53
|
+
/** Parse a /v1/systemone response body against the question set that was asked. */
|
|
54
|
+
export function parseResponse(set, body) {
|
|
55
|
+
if (!body || typeof body !== 'object')
|
|
56
|
+
throw new ClassifierError('bad_response', 'response is not an object');
|
|
57
|
+
const b = body;
|
|
58
|
+
if (!b.answers || typeof b.answers !== 'object')
|
|
59
|
+
throw new ClassifierError('bad_response', 'response has no answers');
|
|
60
|
+
const raw = b.answers;
|
|
61
|
+
const answers = {};
|
|
62
|
+
for (const q of set.questions) {
|
|
63
|
+
const a = raw[q.id];
|
|
64
|
+
if (!a || typeof a !== 'object')
|
|
65
|
+
throw new ClassifierError('bad_response', `no answer for ${q.id}`);
|
|
66
|
+
if (q.type === 'noul') {
|
|
67
|
+
const p = num(a.noul);
|
|
68
|
+
if (p === null || p > 1)
|
|
69
|
+
throw new ClassifierError('bad_response', `${q.id}: noul is not a probability`);
|
|
70
|
+
answers[q.id] = finishAnswer('noul', { yes: p, no: 1 - p }, null);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const pr = a.probabilities;
|
|
74
|
+
if (!pr || typeof pr !== 'object')
|
|
75
|
+
throw new ClassifierError('bad_response', `${q.id}: no probabilities`);
|
|
76
|
+
const labels = labelsOf(q);
|
|
77
|
+
const probs = {};
|
|
78
|
+
for (const l of labels) {
|
|
79
|
+
const p = num(pr[l]);
|
|
80
|
+
if (p === null)
|
|
81
|
+
throw new ClassifierError('bad_response', `${q.id}: missing probability for ${l}`);
|
|
82
|
+
probs[l] = p;
|
|
83
|
+
}
|
|
84
|
+
answers[q.id] = finishAnswer(q.type, probs, num(a.confidence));
|
|
85
|
+
}
|
|
86
|
+
const tokens = num(b.usage?.input_tokens);
|
|
87
|
+
return { model: typeof b.model === 'string' ? b.model : '', answers, inputTokens: tokens };
|
|
88
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Shared types for @dexterm/classifier (task-1822).
|
|
2
|
+
//
|
|
3
|
+
// A classifier answers TYPED QUESTIONS about ONE DOCUMENT (the coin dossier) with a probability per label. The
|
|
4
|
+
// question shapes follow the System One API (`POST /v1/systemone`, TypeSafe's public API, also served by llama.cpp
|
|
5
|
+
// for Kev GGUFs): `choice` (one of named options), `noul` (yes/no, probability of yes), `score` (ordered levels).
|
|
6
|
+
// Every result names the provider, model id/version and how its probabilities were calibrated, and is labelled a
|
|
7
|
+
// model estimate. Nothing here signs, trades, arms anything or raises a cap.
|
|
8
|
+
export const PROVIDER_IDS = ['heuristic', 'kev-local', 'jev-hosted', 'jev-open', 'claude', 'ollama-llm'];
|
|
9
|
+
export class ClassifierError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
retryAfterMs;
|
|
12
|
+
constructor(code, message, retryAfterMs = null) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'ClassifierError';
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.retryAfterMs = retryAfterMs;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// task-2147: owner-only secret files on Windows. NTFS has no POSIX mode bits (chmod only toggles read-only and
|
|
2
|
+
// stat reports 0666), so the mode cannot say who may read a file; its DACL can. Writes lock the file to the current
|
|
3
|
+
// user with icacls, by SID (account names are localized); reads check the DACL as SDDL (locale-free). Any probe
|
|
4
|
+
// failure refuses the file: the caller names the TYPESAFE_API_KEY env var as the way out.
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
/**
|
|
8
|
+
* Well-known principals that may hold a key file besides its user (the set Windows OpenSSH accepts). `LA` is the
|
|
9
|
+
* built-in Administrator (RID 500), always a member of BA: SDDL writes that account's own SID in this alias form.
|
|
10
|
+
*/
|
|
11
|
+
const ALLOWED_WELL_KNOWN = new Set(['SY', 'S-1-5-18', 'BA', 'S-1-5-32-544', 'LA']);
|
|
12
|
+
/** The SDDL aliases that can stand for `sid` itself (task-2155: SDDL never spells RID 500 / 501 as S-1-…). */
|
|
13
|
+
const selfAliases = (sid) => (/-500$/.test(sid) ? ['LA'] : /-501$/.test(sid) ? ['LG'] : []);
|
|
14
|
+
const SID_RE = /^S-1-\d+(-\d+)+$/i;
|
|
15
|
+
const system32 = (...p) => path.win32.join(process.env.SystemRoot || process.env.windir || 'C:\\Windows', 'System32', ...p);
|
|
16
|
+
const defaultRun = (exe, args, env) => execFileSync(exe, args, { env, encoding: 'utf8', windowsHide: true, timeout: 15_000, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
17
|
+
/** The current user's SID (`whoami /user`, CSV: "host\user","S-1-5-21-..."). */
|
|
18
|
+
export function currentUserSid(deps = {}) {
|
|
19
|
+
const out = (deps.run ?? defaultRun)(system32('whoami.exe'), ['/user', '/fo', 'csv', '/nh'], process.env);
|
|
20
|
+
const sid = out.trim().split(/\r?\n/).pop()?.split(',').pop()?.replace(/"/g, '').trim() ?? '';
|
|
21
|
+
if (!SID_RE.test(sid))
|
|
22
|
+
throw new Error('could not read the current Windows account SID');
|
|
23
|
+
return sid;
|
|
24
|
+
}
|
|
25
|
+
/** Remove inherited access and grant only `sid` full control (icacls, no shell). */
|
|
26
|
+
export function lockOwnerOnly(p, sid, deps = {}) {
|
|
27
|
+
if (!SID_RE.test(sid))
|
|
28
|
+
throw new Error('bad SID');
|
|
29
|
+
(deps.run ?? defaultRun)(system32('icacls.exe'), [p, '/inheritance:r', '/grant:r', `*${sid}:(F)`], process.env);
|
|
30
|
+
}
|
|
31
|
+
/** The file's security descriptor as SDDL. The path goes through the environment, never into the command text. */
|
|
32
|
+
export function readSddl(p, deps = {}) {
|
|
33
|
+
const ps = system32('WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
34
|
+
return (deps.run ?? defaultRun)(ps, ['-NoProfile', '-NonInteractive', '-Command', '(Get-Acl -LiteralPath $env:DEXTERM_ACL_PATH).Sddl'], { ...process.env, DEXTERM_ACL_PATH: p }).replace(/\s+/g, ''); // SDDL has no whitespace: undo any console wrap
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Is this DACL owner-only? Every ACE that grants anything must name `sid` (as S-1-… or its alias), SYSTEM or
|
|
38
|
+
* Administrators. A missing or
|
|
39
|
+
* null DACL (everyone has access) and any ACE kind this parser does not know (e.g. conditional) are refused.
|
|
40
|
+
*/
|
|
41
|
+
export function sddlOwnerOnly(sddl, sid) {
|
|
42
|
+
const m = /D:([A-Z_]*)((?:\([^()]*\))*)(?:S:|$)/.exec(sddl.trim());
|
|
43
|
+
if (!m)
|
|
44
|
+
return { ok: false, reason: 'no readable access list' };
|
|
45
|
+
if (/NO_ACCESS_CONTROL/.test(m[1]))
|
|
46
|
+
return { ok: false, reason: 'no access list (everyone has access)' };
|
|
47
|
+
const others = new Set();
|
|
48
|
+
const self = new Set([sid.toUpperCase(), ...selfAliases(sid)]);
|
|
49
|
+
for (const ace of m[2].match(/\(([^()]*)\)/g) ?? []) {
|
|
50
|
+
const [type, flags = '', , , , who = ''] = ace.slice(1, -1).split(';');
|
|
51
|
+
if (type === 'D' || type === 'OD')
|
|
52
|
+
continue; // deny entries only take access away
|
|
53
|
+
if (type !== 'A' && type !== 'OA')
|
|
54
|
+
return { ok: false, reason: `unsupported access entry ${type}` };
|
|
55
|
+
if ((flags.match(/../g) ?? []).some((f) => f === 'IO'))
|
|
56
|
+
continue; // inherit-only (2-letter flags): not the file itself
|
|
57
|
+
const w = who.toUpperCase();
|
|
58
|
+
if (!self.has(w) && !ALLOWED_WELL_KNOWN.has(w))
|
|
59
|
+
others.add(w);
|
|
60
|
+
}
|
|
61
|
+
return others.size ? { ok: false, reason: `other accounts can access it (${[...others].join(', ')})` } : { ok: true };
|
|
62
|
+
}
|
|
63
|
+
/** Check a file's ACL: owner-only or a reason. Never throws. */
|
|
64
|
+
export function checkOwnerOnly(p, deps = {}) {
|
|
65
|
+
try {
|
|
66
|
+
return sddlOwnerOnly(readSddl(p, deps), currentUserSid(deps));
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
return { ok: false, reason: `its Windows access list could not be checked (${e instanceof Error ? e.message.split(/\r?\n/)[0] : String(e)})` };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/* dexterm dash (task-1749): the strategy performance dashboard. dexterm's terminal look (site/src/css/site.css tokens) in
|
|
2
|
+
dark, a paper-white counterpart in light. Mobile first; no web fonts, no external URL. */
|
|
3
|
+
:root {
|
|
4
|
+
color-scheme: light;
|
|
5
|
+
--bg: #f6f7f4; --panel: #ffffff; --panel-2: #eef1ec; --text: #0b120e; --muted: #47524b; --line: #d5dbd4; --line-2: #b9c3ba;
|
|
6
|
+
--accent: #006b78; --series: #0f7a34; --series-soft: rgba(15, 122, 52, .14); --dd: #b4233f; --dd-soft: rgba(180, 35, 63, .14);
|
|
7
|
+
--up: #0b6e2e; --down: #b0173d; --warn: #7a5a00; --ok-bg: #e2f3e6; --warn-bg: #fbf0cf; --down-bg: #f9e0e5; --idle-bg: #ebedea;
|
|
8
|
+
--focus: #006b78; --badge: #5a4300; --badge-bg: #fdf3c6;
|
|
9
|
+
--mono: ui-monospace, 'SF Mono', Menlo, Monaco, 'Cascadia Code', 'DejaVu Sans Mono', monospace;
|
|
10
|
+
--sans: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
11
|
+
--r: 10px; --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 24px;
|
|
12
|
+
}
|
|
13
|
+
@media (prefers-color-scheme: dark) {
|
|
14
|
+
:root:where(:not([data-theme="light"])) {
|
|
15
|
+
color-scheme: dark;
|
|
16
|
+
--bg: #020507; --panel: #04090c; --panel-2: #071114; --text: #eafff0; --muted: #9fd8b2; --line: #0c3a24; --line-2: #1b5a3a;
|
|
17
|
+
--accent: #00f0ff; --series: #39ff14; --series-soft: rgba(57, 255, 20, .14); --dd: #ff5c82; --dd-soft: rgba(255, 92, 130, .16);
|
|
18
|
+
--up: #5dff6e; --down: #ff6b8b; --warn: #f5ff2d; --ok-bg: #062512; --warn-bg: #2a2a05; --down-bg: #2d0812; --idle-bg: #0b1512;
|
|
19
|
+
--focus: #00f0ff; --badge: #f5ff2d; --badge-bg: #22230a;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
:root[data-theme="dark"] {
|
|
23
|
+
color-scheme: dark;
|
|
24
|
+
--bg: #020507; --panel: #04090c; --panel-2: #071114; --text: #eafff0; --muted: #9fd8b2; --line: #0c3a24; --line-2: #1b5a3a;
|
|
25
|
+
--accent: #00f0ff; --series: #39ff14; --series-soft: rgba(57, 255, 20, .14); --dd: #ff5c82; --dd-soft: rgba(255, 92, 130, .16);
|
|
26
|
+
--up: #5dff6e; --down: #ff6b8b; --warn: #f5ff2d; --ok-bg: #062512; --warn-bg: #2a2a05; --down-bg: #2d0812; --idle-bg: #0b1512;
|
|
27
|
+
--focus: #00f0ff; --badge: #f5ff2d; --badge-bg: #22230a;
|
|
28
|
+
}
|
|
29
|
+
* { box-sizing: border-box; }
|
|
30
|
+
html { -webkit-text-size-adjust: 100%; }
|
|
31
|
+
body { margin: 0; background: var(--bg); color: var(--text); font: 15px/1.45 var(--sans); }
|
|
32
|
+
a { color: var(--accent); }
|
|
33
|
+
:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 4px; }
|
|
34
|
+
main:focus, main:focus-visible { outline: none; } /* the route target (tabindex -1), focused for screen readers only */
|
|
35
|
+
.skip { position: absolute; left: -999px; top: 0; }
|
|
36
|
+
.skip:focus { left: 8px; top: 8px; background: var(--panel); padding: 8px; z-index: 10; }
|
|
37
|
+
.mono, .num, table, .tile .v, .brand { font-family: var(--mono); }
|
|
38
|
+
.num { font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
39
|
+
.up { color: var(--up); } .down { color: var(--down); } .muted { color: var(--muted); }
|
|
40
|
+
|
|
41
|
+
header.top { position: sticky; top: 0; z-index: 5; background: var(--bg); border-bottom: 1px solid var(--line); }
|
|
42
|
+
.bar { display: flex; align-items: center; gap: var(--s3); padding: var(--s2) var(--s4); max-width: 1440px; margin: 0 auto; }
|
|
43
|
+
.brand { font-weight: 700; letter-spacing: .02em; font-size: 16px; white-space: nowrap; }
|
|
44
|
+
.brand b { color: var(--series); }
|
|
45
|
+
.paper { display: inline-block; border: 1px solid var(--line-2); border-radius: 999px; padding: 2px 10px; font: 600 12px/1.6 var(--mono); color: var(--text); background: var(--panel-2); }
|
|
46
|
+
.updated { margin-left: auto; font-size: 12px; color: var(--muted); text-align: right; }
|
|
47
|
+
.updated .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--series); margin-right: 6px; vertical-align: 1px; }
|
|
48
|
+
.updated.stale .dot { background: var(--down); }
|
|
49
|
+
button.theme { background: var(--panel-2); color: var(--text); border: 1px solid var(--line-2); border-radius: 8px; padding: 6px 10px; font: 13px var(--mono); min-height: 36px; cursor: pointer; }
|
|
50
|
+
nav.tabs { display: flex; gap: 2px; overflow-x: auto; padding: 0 var(--s3); max-width: 1440px; margin: 0 auto; scrollbar-width: none; }
|
|
51
|
+
nav.tabs a { flex: none; padding: 10px 12px; min-height: 44px; display: inline-flex; align-items: center; color: var(--muted); text-decoration: none; border-bottom: 2px solid transparent; font: 600 14px var(--sans); }
|
|
52
|
+
nav.tabs a[aria-current="page"] { color: var(--text); border-bottom-color: var(--series); }
|
|
53
|
+
|
|
54
|
+
main { max-width: 1440px; margin: 0 auto; padding: var(--s4); }
|
|
55
|
+
h1 { font-size: 20px; margin: 4px 0 4px; } h2 { font-size: 16px; margin: var(--s5) 0 var(--s2); } h3 { font-size: 14px; margin: 0 0 6px; }
|
|
56
|
+
p.lead { margin: 0 0 var(--s3); color: var(--muted); font-size: 14px; }
|
|
57
|
+
.disclaimer { border: 1px dashed var(--line-2); border-radius: var(--r); padding: var(--s2) var(--s3); font-size: 13px; color: var(--muted); margin: 0 0 var(--s4); }
|
|
58
|
+
.disclaimer strong { color: var(--text); }
|
|
59
|
+
|
|
60
|
+
.grid { display: grid; gap: var(--s3); grid-template-columns: 1fr; }
|
|
61
|
+
@media (min-width: 640px) { .grid { grid-template-columns: repeat(2, 1fr); } }
|
|
62
|
+
@media (min-width: 1080px) { .grid { grid-template-columns: repeat(3, 1fr); } }
|
|
63
|
+
@media (min-width: 1360px) { .grid.g4 { grid-template-columns: repeat(4, 1fr); } }
|
|
64
|
+
.card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r); padding: var(--s3); min-width: 0; }
|
|
65
|
+
.card.base { border-color: var(--series); box-shadow: inset 0 0 0 1px var(--series); }
|
|
66
|
+
a.card { color: inherit; text-decoration: none; display: block; }
|
|
67
|
+
a.card:hover { border-color: var(--line-2); }
|
|
68
|
+
.card .head { display: flex; align-items: baseline; gap: var(--s2); flex-wrap: wrap; }
|
|
69
|
+
.card .id { font: 700 15px var(--mono); overflow-wrap: anywhere; }
|
|
70
|
+
.card .grp { font-size: 12px; color: var(--muted); }
|
|
71
|
+
.card .label { font-size: 13px; color: var(--muted); margin: 2px 0 6px; }
|
|
72
|
+
.kv { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px 10px; margin: 8px 0; }
|
|
73
|
+
.kv div { min-width: 0; } .kv .v.sm { font-size: 12px; } .kv .v.md { font-size: 13px; }
|
|
74
|
+
p.note { font-size: 12px; margin: 8px 0 0; }
|
|
75
|
+
.kv .k { font-size: 11.5px; color: var(--muted); } .kv .v { font: 600 14px var(--mono); font-variant-numeric: tabular-nums; }
|
|
76
|
+
.badge { display: inline-block; font: 600 11.5px/1.5 var(--mono); padding: 0 7px; border-radius: 999px; border: 1px solid currentColor; }
|
|
77
|
+
.badge.expl { color: var(--badge); background: var(--badge-bg); }
|
|
78
|
+
.badge.ok { color: var(--up); }
|
|
79
|
+
.badge.base { color: var(--series); }
|
|
80
|
+
.badge.none { color: var(--muted); }
|
|
81
|
+
.spark { display: block; width: 100%; height: 40px; }
|
|
82
|
+
.spark .ln { fill: none; stroke: var(--series); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
|
|
83
|
+
.spark .ref { stroke: var(--line-2); stroke-width: 1; stroke-dasharray: 3 3; }
|
|
84
|
+
|
|
85
|
+
.tiles { display: grid; gap: var(--s2); grid-template-columns: repeat(2, minmax(0, 1fr)); margin: var(--s3) 0; }
|
|
86
|
+
@media (min-width: 760px) { .tiles { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
|
|
87
|
+
@media (min-width: 1200px) { .tiles { grid-template-columns: repeat(6, minmax(0, 1fr)); } }
|
|
88
|
+
.tile { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r); padding: 10px 12px; min-width: 0; }
|
|
89
|
+
.tile .k { font-size: 12px; color: var(--muted); } .tile .v { font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; } .tile .s { font-size: 12px; color: var(--muted); font-family: var(--mono); }
|
|
90
|
+
|
|
91
|
+
.chart { width: 100%; height: auto; display: block; touch-action: pan-y; }
|
|
92
|
+
.chart .grid line { stroke: var(--line); stroke-width: 1; }
|
|
93
|
+
.chart .axis text, .chart .tick { fill: var(--muted); font: 11px var(--mono); }
|
|
94
|
+
.chart .ln { fill: none; stroke: var(--series); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
|
|
95
|
+
.chart .area { fill: var(--series-soft); }
|
|
96
|
+
.chart .dd-area { fill: var(--dd-soft); }
|
|
97
|
+
.chart .dd { fill: none; stroke: var(--dd); stroke-width: 2; stroke-linejoin: round; }
|
|
98
|
+
.chart .ref { stroke: var(--muted); stroke-width: 1; stroke-dasharray: 4 4; }
|
|
99
|
+
.chart .zero { stroke: var(--muted); stroke-width: 1.5; }
|
|
100
|
+
.chart .dot { fill: var(--series); stroke: var(--panel); stroke-width: 2; }
|
|
101
|
+
.chart .ci { stroke: var(--text); stroke-width: 2; stroke-linecap: round; }
|
|
102
|
+
.chart .mean { fill: var(--accent); stroke: var(--panel); stroke-width: 2; }
|
|
103
|
+
.chart .bar { fill: var(--series); }
|
|
104
|
+
.chart .bar.neg { fill: var(--dd); }
|
|
105
|
+
.chart .lbl { fill: var(--text); font: 12px var(--mono); }
|
|
106
|
+
.chart .xh { stroke: var(--muted); stroke-width: 1; }
|
|
107
|
+
.chart .hit { fill: transparent; }
|
|
108
|
+
.chartbox { position: relative; background: var(--panel); border: 1px solid var(--line); border-radius: var(--r); padding: var(--s2); }
|
|
109
|
+
.tip { position: absolute; pointer-events: none; background: var(--panel-2); color: var(--text); border: 1px solid var(--line-2); border-radius: 6px; padding: 4px 8px; font: 12px var(--mono); white-space: nowrap; transform: translate(-50%, -110%); display: none; z-index: 3; }
|
|
110
|
+
|
|
111
|
+
.tw { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--r); background: var(--panel); -webkit-overflow-scrolling: touch; }
|
|
112
|
+
table { border-collapse: collapse; width: 100%; font-size: 13px; }
|
|
113
|
+
th, td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--line); white-space: nowrap; vertical-align: top; }
|
|
114
|
+
td.wrap { white-space: normal; min-width: 220px; }
|
|
115
|
+
th { color: var(--muted); font-weight: 600; position: sticky; top: 0; background: var(--panel); }
|
|
116
|
+
th.r, td.r { text-align: right; }
|
|
117
|
+
tr.base td { background: var(--panel-2); }
|
|
118
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
119
|
+
caption { text-align: left; padding: 8px 10px; color: var(--muted); font-size: 12px; }
|
|
120
|
+
|
|
121
|
+
select { font: 14px var(--mono); padding: 8px; min-height: 44px; background: var(--panel); color: var(--text); border: 1px solid var(--line-2); border-radius: 8px; max-width: 100%; }
|
|
122
|
+
label.pick { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; font-size: 13px; color: var(--muted); }
|
|
123
|
+
.rule { border-left: 3px solid var(--accent); padding: 6px 10px; background: var(--panel); font-size: 13.5px; margin: var(--s2) 0 var(--s3); }
|
|
124
|
+
.status { border-left: 4px solid var(--line-2); }
|
|
125
|
+
.status.ok { border-left-color: var(--up); } .status.warn { border-left-color: var(--warn); } .status.down { border-left-color: var(--down); }
|
|
126
|
+
.st { font: 700 12px var(--mono); padding: 1px 8px; border-radius: 999px; }
|
|
127
|
+
.st.ok { background: var(--ok-bg); color: var(--up); } .st.warn { background: var(--warn-bg); color: var(--warn); } .st.down { background: var(--down-bg); color: var(--down); } .st.idle { background: var(--idle-bg); color: var(--muted); }
|
|
128
|
+
dl.items { display: grid; grid-template-columns: minmax(110px, 40%) 1fr; gap: 4px 10px; margin: 8px 0 0; font-size: 13px; }
|
|
129
|
+
dl.items dt { color: var(--muted); } dl.items dd { margin: 0; overflow-wrap: anywhere; }
|
|
130
|
+
footer { max-width: 1440px; margin: 0 auto; padding: var(--s4); color: var(--muted); font-size: 12.5px; border-top: 1px solid var(--line); }
|
|
131
|
+
footer ul { padding-left: 18px; margin: 6px 0; }
|
|
132
|
+
.empty { padding: var(--s4); color: var(--muted); }
|
|
133
|
+
@media (max-width: 480px) { .updated .long { display: none; } .bar { gap: 8px; } nav.tabs { padding: 0 4px; } nav.tabs a { padding: 10px 7px; font-size: 13px; } nav.tabs .wide { display: none; } }
|
|
134
|
+
@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }
|
|
135
|
+
@media (forced-colors: active) { .chart .ln, .chart .dd, .spark .ln { stroke: CanvasText; } }
|
|
136
|
+
|
|
137
|
+
.only-wide { display: none; } .only-narrow { display: grid; gap: var(--s2); }
|
|
138
|
+
@media (min-width: 640px) { .only-wide { display: block; } .only-narrow { display: none; } }
|
|
139
|
+
.card.rug .head .r { margin-left: auto; font: 700 15px var(--mono); }
|