@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.
- package/README.md +107 -0
- package/dist/src/adapters.d.ts +25 -0
- package/dist/src/adapters.js +244 -0
- package/dist/src/issueCompiler.d.ts +17 -0
- package/dist/src/issueCompiler.js +81 -0
- package/dist/src/orchestrator.d.ts +59 -0
- package/dist/src/orchestrator.js +594 -0
- package/dist/src/redaction.d.ts +6 -0
- package/dist/src/redaction.js +53 -0
- package/dist/src/runner.d.ts +26 -0
- package/dist/src/runner.js +423 -0
- package/dist/src/sdk.d.ts +64 -0
- package/dist/src/sdk.js +172 -0
- package/dist/src/sentry.d.ts +7 -0
- package/dist/src/sentry.js +125 -0
- package/dist/src/server.d.ts +10 -0
- package/dist/src/server.js +498 -0
- package/dist/src/store.d.ts +18 -0
- package/dist/src/store.js +58 -0
- package/dist/src/types.d.ts +147 -0
- package/dist/src/types.js +1 -0
- package/package.json +46 -0
- package/public/app.js +185 -0
- package/public/index.html +80 -0
- package/public/styles.css +253 -0
|
@@ -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 { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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>
|