@yanolja-next/noya-sdk 0.1.0

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.
@@ -0,0 +1,58 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ export function createInitialState() {
4
+ return {
5
+ meta: { version: 1, nextSeq: 1 },
6
+ projects: {},
7
+ reports: {},
8
+ issues: {},
9
+ jobs: {},
10
+ audit: [],
11
+ };
12
+ }
13
+ export class JsonStore {
14
+ filePath;
15
+ state;
16
+ constructor(filePath) {
17
+ this.filePath = filePath;
18
+ this.state = createInitialState();
19
+ this.load();
20
+ }
21
+ load() {
22
+ if (!this.filePath || !fs.existsSync(this.filePath))
23
+ return;
24
+ this.state = JSON.parse(fs.readFileSync(this.filePath, "utf8"));
25
+ }
26
+ save() {
27
+ if (!this.filePath)
28
+ return;
29
+ fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
30
+ fs.writeFileSync(this.filePath, `${JSON.stringify(this.state, null, 2)}\n`);
31
+ }
32
+ id(prefix) {
33
+ const seq = this.state.meta.nextSeq++;
34
+ this.save();
35
+ return `${prefix}_${String(seq).padStart(5, "0")}`;
36
+ }
37
+ audit(action, details = {}) {
38
+ const entry = {
39
+ id: this.id("aud"),
40
+ action,
41
+ details,
42
+ created_at: new Date().toISOString(),
43
+ };
44
+ this.state.audit.unshift(entry);
45
+ this.state.audit = this.state.audit.slice(0, 200);
46
+ this.save();
47
+ return entry;
48
+ }
49
+ }
50
+ export function publicState(state) {
51
+ return {
52
+ projects: Object.values(state.projects),
53
+ reports: Object.values(state.reports).sort((a, b) => b.created_at.localeCompare(a.created_at)),
54
+ issues: Object.values(state.issues).sort((a, b) => b.created_at.localeCompare(a.created_at)),
55
+ jobs: Object.values(state.jobs).sort((a, b) => b.created_at.localeCompare(a.created_at)),
56
+ audit: state.audit,
57
+ };
58
+ }
@@ -0,0 +1,147 @@
1
+ export type JsonPrimitive = string | number | boolean | null;
2
+ export type JsonValue = JsonPrimitive | JsonValue[] | {
3
+ [key: string]: JsonValue;
4
+ };
5
+ export type JsonObject = Record<string, unknown>;
6
+ export type Capabilities = {
7
+ collect?: {
8
+ enabled?: boolean;
9
+ auto?: boolean;
10
+ };
11
+ fix?: {
12
+ enabled?: boolean;
13
+ auto?: boolean;
14
+ require_approval?: boolean;
15
+ };
16
+ };
17
+ export type CollectorSource = JsonObject & {
18
+ name: string;
19
+ type: "fixture_logs" | "fixture_domain" | "repo_snapshot" | "command_ref";
20
+ when?: {
21
+ has?: string;
22
+ };
23
+ command_ref?: "workspace_manifest_v1";
24
+ max_entries?: number;
25
+ max_bytes?: number;
26
+ };
27
+ export type Project = {
28
+ project_key: string;
29
+ name: string;
30
+ environment: string;
31
+ issue_provider: string;
32
+ artifact_dir: string;
33
+ retention_days?: number;
34
+ created_at: string;
35
+ integrations?: JsonObject;
36
+ telemetry?: {
37
+ provider?: string;
38
+ org_slug?: string;
39
+ project_slug?: string;
40
+ };
41
+ sentry?: {
42
+ org_slug?: string;
43
+ project_slug?: string;
44
+ };
45
+ runner?: {
46
+ enabled?: boolean;
47
+ repo_path?: string;
48
+ token_env?: string;
49
+ lease_timeout_seconds?: number;
50
+ collect?: {
51
+ enabled?: boolean;
52
+ auto?: boolean;
53
+ };
54
+ fix?: {
55
+ enabled?: boolean;
56
+ auto?: boolean;
57
+ require_approval?: boolean;
58
+ };
59
+ sources?: CollectorSource[];
60
+ };
61
+ };
62
+ export type Report = {
63
+ id: string;
64
+ project_key: string;
65
+ message?: string;
66
+ impact?: string;
67
+ environment?: string;
68
+ release?: string;
69
+ route?: string;
70
+ browser?: string;
71
+ request_id?: string;
72
+ screenshot_url?: string;
73
+ feedback?: JsonValue;
74
+ telemetry?: {
75
+ provider?: string;
76
+ event_id?: string;
77
+ replay_id?: string;
78
+ feedback_id?: string;
79
+ event_url?: string;
80
+ replay_url?: string;
81
+ };
82
+ sentry?: {
83
+ event_id?: string;
84
+ replay_id?: string;
85
+ feedback_id?: string;
86
+ event_url?: string;
87
+ replay_url?: string;
88
+ };
89
+ context?: JsonObject;
90
+ reproduction_steps?: string[];
91
+ expected_behavior?: string;
92
+ actual_behavior?: string;
93
+ suspected_area?: string;
94
+ suspected_files?: string[];
95
+ hints?: Record<string, string>;
96
+ status: string;
97
+ created_at: string;
98
+ };
99
+ export type Issue = {
100
+ id: string;
101
+ project_key: string;
102
+ report_id: string;
103
+ title: string;
104
+ body: string;
105
+ fingerprint: string;
106
+ status: string;
107
+ created_at: string;
108
+ integration?: JsonObject | null;
109
+ duplicate_report_ids?: string[];
110
+ fix_job_id?: string;
111
+ fix?: JsonObject;
112
+ };
113
+ export type Job = {
114
+ id: string;
115
+ type: "collect" | "fix";
116
+ project_key: string;
117
+ report_id?: string;
118
+ issue_id?: string;
119
+ status: "queued" | "leased" | "completed" | "failed";
120
+ payload: JsonObject;
121
+ created_at: string;
122
+ runner_id?: string;
123
+ leased_at?: string;
124
+ heartbeat_at?: string;
125
+ lease_count?: number;
126
+ completed_at?: string;
127
+ failed_at?: string;
128
+ result?: JsonObject;
129
+ error?: JsonObject;
130
+ };
131
+ export type AuditEntry = {
132
+ id: string;
133
+ action: string;
134
+ details: JsonObject;
135
+ created_at: string;
136
+ };
137
+ export type State = {
138
+ meta: {
139
+ version: number;
140
+ nextSeq: number;
141
+ };
142
+ projects: Record<string, Project>;
143
+ reports: Record<string, Report>;
144
+ issues: Record<string, Issue>;
145
+ jobs: Record<string, Job>;
146
+ audit: AuditEntry[];
147
+ };
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@yanolja-next/noya-sdk",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Production issue reports into pre-triaged tickets and local draft PR artifacts.",
6
+ "type": "module",
7
+ "main": "./dist/src/sdk.js",
8
+ "types": "./dist/src/sdk.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/src/sdk.d.ts",
12
+ "import": "./dist/src/sdk.js"
13
+ },
14
+ "./runner": {
15
+ "types": "./dist/src/runner.d.ts",
16
+ "import": "./dist/src/runner.js"
17
+ },
18
+ "./server": {
19
+ "types": "./dist/src/server.d.ts",
20
+ "import": "./dist/src/server.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist/src",
25
+ "public"
26
+ ],
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "scripts": {
31
+ "build": "tsc -p tsconfig.json",
32
+ "prepack": "npm run build",
33
+ "start": "node dist/src/server.js",
34
+ "dev": "npm run build && node dist/src/server.js",
35
+ "runner": "npm run build && node dist/src/runner.js",
36
+ "test": "npm run build && node --test dist/tests/*.test.js",
37
+ "test:e2e": "npm run build && node --test dist/tests/e2e-flow.test.js && node dist/tests/browser-smoke.js"
38
+ },
39
+ "engines": {
40
+ "node": ">=20.11.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^24.0.0",
44
+ "typescript": "^5.9.0"
45
+ }
46
+ }
package/public/app.js ADDED
@@ -0,0 +1,185 @@
1
+ async function fetchState() {
2
+ const response = await fetch("/api/dashboard/state", authOptions());
3
+ if (!response.ok) throw new Error(`State failed: ${response.status}`);
4
+ return response.json();
5
+ }
6
+
7
+ async function post(url) {
8
+ const response = await fetch(url, authOptions({ method: "POST" }));
9
+ if (!response.ok) throw new Error(`POST ${url} failed: ${response.status}`);
10
+ return response.json();
11
+ }
12
+
13
+ async function postJson(url, body) {
14
+ const response = await fetch(url, {
15
+ ...authOptions(),
16
+ method: "POST",
17
+ headers: { ...authHeaders(), "content-type": "application/json" },
18
+ body: JSON.stringify(body),
19
+ });
20
+ if (!response.ok) {
21
+ const payload = await response.json().catch(() => ({}));
22
+ throw new Error(payload.error ?? `POST ${url} failed: ${response.status}`);
23
+ }
24
+ return response.json();
25
+ }
26
+
27
+ function authHeaders() {
28
+ const token = localStorage.getItem("noya_admin_token");
29
+ return token ? { authorization: `Bearer ${token}` } : {};
30
+ }
31
+
32
+ function authOptions(options = {}) {
33
+ return { ...options, headers: { ...(options.headers ?? {}), ...authHeaders() } };
34
+ }
35
+
36
+ let configDirty = false;
37
+ let configProjectKey = null;
38
+
39
+ function renderMetric(label, value) {
40
+ return `<div class="metric"><strong>${value}</strong><span class="muted">${label}</span></div>`;
41
+ }
42
+
43
+ function esc(value) {
44
+ return String(value ?? "").replace(/[&<>"']/g, (ch) => {
45
+ return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;" }[ch];
46
+ });
47
+ }
48
+
49
+ async function refresh() {
50
+ const state = await fetchState();
51
+ document.querySelector("#last-refresh").textContent = new Date().toLocaleTimeString();
52
+ document.querySelector("#metrics").innerHTML = [
53
+ renderMetric("projects", state.projects.length),
54
+ renderMetric("reports", state.reports.length),
55
+ renderMetric("issues", state.issues.length),
56
+ renderMetric("jobs", state.jobs.length),
57
+ ].join("");
58
+
59
+ const project = state.projects.find((item) => item.project_key === "agency-platform") ?? state.projects[0];
60
+ document.querySelector("#connection").innerHTML = project
61
+ ? [
62
+ renderConnection("SDK endpoint", `${location.origin}/api/reports`),
63
+ renderConnection("Sentry webhook", `${location.origin}/api/webhooks/sentry`),
64
+ renderConnection("Runner", "npm run runner -- examples/agency-platform/noya.runner.json"),
65
+ ].join("")
66
+ : `<div class="item muted">No project configured.</div>`;
67
+
68
+ const readiness = state.readiness?.find((item) => item.project_key === project?.project_key);
69
+ document.querySelector("#readiness").innerHTML = readiness
70
+ ? readiness.checks.map(renderReadiness).join("")
71
+ : `<div class="item muted">No readiness checks available.</div>`;
72
+
73
+ renderProjectConfig(project);
74
+
75
+ document.querySelector("#issues").innerHTML =
76
+ state.issues
77
+ .map((issue) => {
78
+ const canApprove = issue.status === "issue_created";
79
+ return `<article class="item" data-test="issue">
80
+ <div class="row">
81
+ <h3>${esc(issue.title)}</h3>
82
+ <span class="tag ${issue.status === "fix_queued" ? "warn" : ""}">${esc(issue.status)}</span>
83
+ </div>
84
+ <p class="muted">${esc(issue.id)} · report ${esc(issue.report_id)}</p>
85
+ <p class="muted">${renderIssueLink(issue)}</p>
86
+ ${renderDraftPrLink(issue)}
87
+ ${
88
+ canApprove
89
+ ? `<button class="secondary" data-test="approve-fix" data-issue-id="${esc(issue.id)}">Approve local fix</button>`
90
+ : ""
91
+ }
92
+ </article>`;
93
+ })
94
+ .join("") || `<div class="item muted">No issues yet.</div>`;
95
+
96
+ document.querySelector("#jobs").innerHTML =
97
+ state.jobs
98
+ .map(
99
+ (job) => `<article class="item" data-test="job">
100
+ <div class="row">
101
+ <h3>${esc(job.type)}</h3>
102
+ <span class="tag">${esc(job.status)}</span>
103
+ </div>
104
+ <p class="muted">${esc(job.id)} · ${esc(job.project_key)}</p>
105
+ </article>`,
106
+ )
107
+ .join("") || `<div class="item muted">No jobs yet.</div>`;
108
+
109
+ document.querySelector("#audit").innerHTML =
110
+ state.audit
111
+ .slice(0, 12)
112
+ .map((entry) => `<div><span class="muted">${esc(entry.created_at)}</span><strong>${esc(entry.action)}</strong></div>`)
113
+ .join("") || `<div><span></span><strong>No audit entries.</strong></div>`;
114
+ }
115
+
116
+ function renderConnection(label, value) {
117
+ return `<div class="item"><h3>${esc(label)}</h3><code>${esc(value)}</code></div>`;
118
+ }
119
+
120
+ function renderReadiness(check) {
121
+ return `<div class="item" data-test="readiness-check">
122
+ <div class="row">
123
+ <h3>${esc(check.name)}</h3>
124
+ <span class="status ${esc(check.status)}">${esc(check.status)}</span>
125
+ </div>
126
+ <code>${esc(check.detail)}</code>
127
+ </div>`;
128
+ }
129
+
130
+ function renderProjectConfig(project) {
131
+ const textarea = document.querySelector("#project-config");
132
+ if (!project) {
133
+ if (!configDirty) textarea.value = "";
134
+ return;
135
+ }
136
+ if (configDirty && configProjectKey === project.project_key) return;
137
+ configProjectKey = project.project_key;
138
+ textarea.value = JSON.stringify(project, null, 2);
139
+ document.querySelector("#project-config-status").textContent = "";
140
+ }
141
+
142
+ function renderIssueLink(issue) {
143
+ const href = issue.integration?.web_url ?? issue.integration?.url;
144
+ if (!href) return "local issue pending";
145
+ return `<a data-test="issue-artifact" href="${esc(href)}" target="_blank" rel="noreferrer">${esc(href)}</a>`;
146
+ }
147
+
148
+ function renderDraftPrLink(issue) {
149
+ const href = issue.fix?.draft_pr?.web_url ?? issue.fix?.draft_pr?.url;
150
+ if (!href) return "";
151
+ return `<p class="muted"><a data-test="draft-pr-artifact" href="${esc(href)}" target="_blank" rel="noreferrer">${esc(href)}</a></p>`;
152
+ }
153
+
154
+ document.querySelector("#sample-report").addEventListener("click", async () => {
155
+ await post("/api/dev/sample/agency-platform-report");
156
+ await refresh();
157
+ });
158
+
159
+ document.querySelector("#project-config").addEventListener("input", () => {
160
+ configDirty = true;
161
+ });
162
+
163
+ document.querySelector("#save-project").addEventListener("click", async () => {
164
+ const status = document.querySelector("#project-config-status");
165
+ try {
166
+ const parsed = JSON.parse(document.querySelector("#project-config").value);
167
+ const saved = await postJson("/api/projects", parsed);
168
+ configDirty = false;
169
+ configProjectKey = saved.project_key;
170
+ status.textContent = `Saved ${saved.project_key}`;
171
+ await refresh();
172
+ } catch (error) {
173
+ status.textContent = error instanceof Error ? error.message : String(error);
174
+ }
175
+ });
176
+
177
+ document.body.addEventListener("click", async (event) => {
178
+ const button = event.target.closest("[data-test='approve-fix']");
179
+ if (!button) return;
180
+ await post(`/api/issues/${button.dataset.issueId}/approve-fix`);
181
+ await refresh();
182
+ });
183
+
184
+ refresh();
185
+ setInterval(refresh, 3000);
@@ -0,0 +1,80 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Noya</title>
7
+ <link rel="stylesheet" href="/styles.css" />
8
+ </head>
9
+ <body>
10
+ <header class="topbar">
11
+ <div>
12
+ <h1>Noya</h1>
13
+ <p>Production reports into pre-triaged issues and local draft PR artifacts.</p>
14
+ </div>
15
+ <button data-test="sample-report" id="sample-report">Send agency-platform sample</button>
16
+ </header>
17
+
18
+ <main>
19
+ <section class="band">
20
+ <div class="section-title">
21
+ <h2>Pipeline</h2>
22
+ <span id="last-refresh">Loading</span>
23
+ </div>
24
+ <div class="metrics" id="metrics"></div>
25
+ </section>
26
+
27
+ <section class="band">
28
+ <div class="section-title">
29
+ <h2>Project Connection</h2>
30
+ </div>
31
+ <div id="connection" class="connection"></div>
32
+ </section>
33
+
34
+ <section class="band">
35
+ <div class="section-title">
36
+ <h2>Readiness</h2>
37
+ </div>
38
+ <div id="readiness" class="readiness"></div>
39
+ </section>
40
+
41
+ <section class="band">
42
+ <div class="section-title">
43
+ <h2>Project Config</h2>
44
+ <button class="secondary" data-test="save-project" id="save-project">Save config</button>
45
+ </div>
46
+ <textarea
47
+ aria-label="Project config JSON"
48
+ data-test="project-config"
49
+ id="project-config"
50
+ spellcheck="false"
51
+ ></textarea>
52
+ <p class="muted" data-test="project-config-status" id="project-config-status"></p>
53
+ </section>
54
+
55
+ <section class="grid">
56
+ <div>
57
+ <div class="section-title">
58
+ <h2>Issues</h2>
59
+ </div>
60
+ <div id="issues" class="list"></div>
61
+ </div>
62
+ <div>
63
+ <div class="section-title">
64
+ <h2>Runner Jobs</h2>
65
+ </div>
66
+ <div id="jobs" class="list"></div>
67
+ </div>
68
+ </section>
69
+
70
+ <section class="band">
71
+ <div class="section-title">
72
+ <h2>Audit Log</h2>
73
+ </div>
74
+ <div id="audit" class="audit"></div>
75
+ </section>
76
+ </main>
77
+
78
+ <script type="module" src="/app.js"></script>
79
+ </body>
80
+ </html>