hanoman 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 +73 -0
- package/bin/hanoman.mjs +3 -0
- package/dist/build-info.json +5 -0
- package/dist/cli.js +6332 -0
- package/dist/server.js +23609 -0
- package/package.json +38 -0
- package/prisma/migrations/20260730000000_init_sqlite/migration.sql +431 -0
- package/prisma/migrations/migration_lock.toml +2 -0
- package/prisma/schema.prisma +425 -0
- package/web/assets/index-BXg9KRmB.css +41 -0
- package/web/assets/index-a6SFYnK5.js +7542 -0
- package/web/favicon.svg +8 -0
- package/web/index.html +14 -0
- package/web/sounds/notify-alert.wav +0 -0
- package/web/sounds/notify-bell.wav +0 -0
- package/web/sounds/notify-blip.wav +0 -0
- package/web/sounds/notify-chime.wav +0 -0
- package/web/sounds/notify-coin.wav +0 -0
- package/web/sounds/notify-fanfare.wav +0 -0
- package/web/sounds/notify-long.wav +0 -0
- package/web/sounds/notify-marimba.wav +0 -0
- package/web/sounds/notify-medium.wav +0 -0
- package/web/sounds/notify-ping.wav +0 -0
- package/web/sounds/notify-pop.wav +0 -0
- package/web/sounds/notify-short.wav +0 -0
- package/web/sounds/notify-success.wav +0 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
generator client {
|
|
2
|
+
provider = "prisma-client-js"
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
datasource db {
|
|
6
|
+
provider = "sqlite"
|
|
7
|
+
url = env("DATABASE_URL")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
model Project {
|
|
11
|
+
id String @id
|
|
12
|
+
name String
|
|
13
|
+
desc String
|
|
14
|
+
kind String
|
|
15
|
+
repoDir String?
|
|
16
|
+
gitRemote String? // SPEC-213 · git remote resmi untuk clone di client; repoDir tetap LOCAL-only, tak disync
|
|
17
|
+
stack String @default("")
|
|
18
|
+
createdAt DateTime @default(now())
|
|
19
|
+
version Int @default(0) // SPEC-213 · version-stamp sync (ADR-0045)
|
|
20
|
+
updatedAt DateTime @updatedAt
|
|
21
|
+
ingestKeyHash String? // SPEC-249 · sha256(ingest key) hex; null = monitoring off. TAK PERNAH ke client.
|
|
22
|
+
ingestKeyPrefix String? // SPEC-249 · ~12-16 char awal key untuk hint UI (bukan rahasia)
|
|
23
|
+
helpEnabled Boolean @default(false) // SPEC-253 · opt-in Help Center publik
|
|
24
|
+
schedulerOptIn Boolean @default(false) // SPEC-294 · ADR-0072 · opt-in scheduler otonom (LOCAL — tak masuk FIELDS sync)
|
|
25
|
+
specs Spec[]
|
|
26
|
+
errorGroups ErrorGroup[]
|
|
27
|
+
tickets Ticket[]
|
|
28
|
+
sourceMaps SourceMapArtifact[] // SPEC-276 · source-map ter-upload per release (symbolication)
|
|
29
|
+
linksOut ProjectLink[] @relation("ProjectLinkFrom") // SPEC-337 · project ini bergantung pada …
|
|
30
|
+
linksIn ProjectLink[] @relation("ProjectLinkTo") // SPEC-337 · … yang bergantung pada project ini
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// SPEC-337 · ADR-0075 · relasi integrasi/dependency BERARAH antar project: from BERGANTUNG PADA to.
|
|
34
|
+
// LOCAL-only (tak masuk SYNCED): id cuid + unique pasangan bertabrakan saat upsert-by-id lintas device.
|
|
35
|
+
// onUpdate: Cascade → rename project (ADR-0064) merambat gratis; tak ada ref longgar baru.
|
|
36
|
+
model ProjectLink {
|
|
37
|
+
id String @id @default(cuid())
|
|
38
|
+
fromProjectId String
|
|
39
|
+
toProjectId String
|
|
40
|
+
kind String // api | sdk | data | event | lainnya (zLinkKind)
|
|
41
|
+
note String @default("") // bentuk integrasinya — disalin apa adanya ke prompt sesi audit lintas
|
|
42
|
+
createdAt DateTime @default(now())
|
|
43
|
+
updatedAt DateTime @updatedAt
|
|
44
|
+
from Project @relation("ProjectLinkFrom", fields: [fromProjectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
|
45
|
+
to Project @relation("ProjectLinkTo", fields: [toProjectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
|
46
|
+
|
|
47
|
+
@@unique([fromProjectId, toProjectId])
|
|
48
|
+
@@index([toProjectId])
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
model Spec {
|
|
52
|
+
id String @id
|
|
53
|
+
projectId String
|
|
54
|
+
title String
|
|
55
|
+
source String
|
|
56
|
+
stage String
|
|
57
|
+
priority String
|
|
58
|
+
author String
|
|
59
|
+
objective String
|
|
60
|
+
payload Json?
|
|
61
|
+
branchFrom String? // SPEC-143 · branch sumber worktree; null = default project (main)
|
|
62
|
+
baseSha String? // SPEC-176 · ADR-0030 · commit detach worktree sesi (nilai balik addWorktree)
|
|
63
|
+
headSha String? // SPEC-176 · ADR-0030 · commit HEAD worktree di akhir sesi (sebelum removeWorktree)
|
|
64
|
+
version Int @default(0) // SPEC-213 · version-stamp sync (ADR-0045)
|
|
65
|
+
updatedAt DateTime @updatedAt
|
|
66
|
+
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
model Setting {
|
|
70
|
+
id Int @id @default(1)
|
|
71
|
+
data Json
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// SPEC-180/184 · notifikasi. type "done" (backlog selesai) | "decision" (menunggu keputusan
|
|
75
|
+
// manusia). Dedup selesai lewat `key` unik ("done:<specId>"); decision di-dedup di sisi scan
|
|
76
|
+
// (key null; NULL berulang diizinkan Postgres pada kolom unik). sessionId = target redirect.
|
|
77
|
+
model Notification {
|
|
78
|
+
id String @id @default(cuid())
|
|
79
|
+
type String @default("done")
|
|
80
|
+
key String? @unique
|
|
81
|
+
specId String?
|
|
82
|
+
sessionId String?
|
|
83
|
+
title String
|
|
84
|
+
projectId String?
|
|
85
|
+
createdAt DateTime @default(now())
|
|
86
|
+
readAt DateTime?
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// SPEC-169 · auth. passwordHash = scrypt "<saltHex>:<hashHex>". Tak ada RBAC — semua user setara.
|
|
90
|
+
model User {
|
|
91
|
+
id String @id @default(cuid())
|
|
92
|
+
email String @unique
|
|
93
|
+
passwordHash String
|
|
94
|
+
createdAt DateTime @default(now())
|
|
95
|
+
sessions Session[]
|
|
96
|
+
deviceTokens DeviceToken[]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// SPEC-169 · sesi opaque revocable. id = sha256(token); token asli hanya ada di cookie httpOnly.
|
|
100
|
+
model Session {
|
|
101
|
+
id String @id
|
|
102
|
+
userId String
|
|
103
|
+
createdAt DateTime @default(now())
|
|
104
|
+
expiresAt DateTime
|
|
105
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// SPEC-164 · VPS yang dikelola hanoman. keyPath menunjuk berkas di mesin server —
|
|
109
|
+
// isi private key TIDAK PERNAH ada di database.
|
|
110
|
+
model Vps {
|
|
111
|
+
id String @id @default(cuid())
|
|
112
|
+
name String
|
|
113
|
+
host String
|
|
114
|
+
port Int @default(22)
|
|
115
|
+
user String
|
|
116
|
+
keyPath String?
|
|
117
|
+
createdAt DateTime @default(now())
|
|
118
|
+
lastSeenAt DateTime? // healthcheck sukses terakhir
|
|
119
|
+
health Json? // { uptime, disk, mem, load }
|
|
120
|
+
lastAuditAt DateTime?
|
|
121
|
+
audit Json? // VpsCheck[] — [{ check, status, detail }]
|
|
122
|
+
hardened Boolean @default(false) // derived: semua check kritis pass
|
|
123
|
+
version Int @default(0) // SPEC-213 · version-stamp sync (ADR-0045)
|
|
124
|
+
updatedAt DateTime @updatedAt
|
|
125
|
+
snapshots VpsAuditSnapshot[] // SPEC-220 · riwayat audit kepatuhan
|
|
126
|
+
itemStates VpsItemState[] // SPEC-220 · keputusan human per item (N/A, attest)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// SPEC-220 · ADR-0050 · hasil satu audit kepatuhan (append-only, fondasi drift Fase 3).
|
|
130
|
+
// results = { [itemId]: { status: pass|fail|warn|na|unknown, detail } } untuk item ber-probe.
|
|
131
|
+
model VpsAuditSnapshot {
|
|
132
|
+
id String @id @default(cuid())
|
|
133
|
+
vpsId String
|
|
134
|
+
createdAt DateTime @default(now())
|
|
135
|
+
results Json
|
|
136
|
+
scoreTotal Float
|
|
137
|
+
scoreBySection Json
|
|
138
|
+
detected Json? // SPEC-221 · { [section]: { present, detail } } — deteksi stack app-layer (advisory)
|
|
139
|
+
vps Vps @relation(fields: [vpsId], references: [id], onDelete: Cascade)
|
|
140
|
+
|
|
141
|
+
@@index([vpsId, createdAt])
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// SPEC-220 · ADR-0050 · keputusan human durable per item: N/A (keluar denominator skor)
|
|
145
|
+
// dan attestasi INFO. actorEmail = jejak pelaku dari sesi auth.
|
|
146
|
+
model VpsItemState {
|
|
147
|
+
id String @id @default(cuid())
|
|
148
|
+
vpsId String
|
|
149
|
+
itemId String
|
|
150
|
+
na Boolean @default(false)
|
|
151
|
+
naReason String?
|
|
152
|
+
attested Boolean @default(false)
|
|
153
|
+
attestNote String?
|
|
154
|
+
actorEmail String?
|
|
155
|
+
updatedAt DateTime @default(now())
|
|
156
|
+
vps Vps @relation(fields: [vpsId], references: [id], onDelete: Cascade)
|
|
157
|
+
|
|
158
|
+
@@unique([vpsId, itemId])
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// SPEC-213 · ADR-0044 · identitas mesin per-device. tokenHash = sha256(token); plaintext
|
|
162
|
+
// hanya ditampilkan sekali saat diterbitkan. Terikat ke user → atribusi author. Revoke = set revokedAt.
|
|
163
|
+
model DeviceToken {
|
|
164
|
+
id String @id @default(cuid())
|
|
165
|
+
userId String
|
|
166
|
+
name String
|
|
167
|
+
tokenHash String @unique
|
|
168
|
+
createdAt DateTime @default(now())
|
|
169
|
+
lastSeenAt DateTime?
|
|
170
|
+
revokedAt DateTime?
|
|
171
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// SPEC-257 · ADR-0065 · kredensial AI agent eksternal. Server-local (cermin DeviceToken, TAK disync).
|
|
175
|
+
// capabilities = Json string[] divalidasi zod (@hanoman/shared). tokenHash = sha256(plaintext).
|
|
176
|
+
model AgentToken {
|
|
177
|
+
id String @id @default(cuid())
|
|
178
|
+
name String
|
|
179
|
+
tokenHash String @unique
|
|
180
|
+
tokenPrefix String
|
|
181
|
+
capabilities Json
|
|
182
|
+
enabled Boolean @default(true)
|
|
183
|
+
createdBy String?
|
|
184
|
+
createdAt DateTime @default(now())
|
|
185
|
+
lastUsedAt DateTime?
|
|
186
|
+
revokedAt DateTime?
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// SPEC-213 · ADR-0047 · ringkasan hasil sesi (activity log). Append-only; whitelist field.
|
|
190
|
+
model SessionResult {
|
|
191
|
+
id String @id
|
|
192
|
+
projectId String
|
|
193
|
+
specId String?
|
|
194
|
+
oldStage String?
|
|
195
|
+
newStage String?
|
|
196
|
+
commitSha String?
|
|
197
|
+
branch String?
|
|
198
|
+
prUrl String?
|
|
199
|
+
status String
|
|
200
|
+
deviceId String?
|
|
201
|
+
author String?
|
|
202
|
+
version Int @default(0)
|
|
203
|
+
createdAt DateTime @default(now())
|
|
204
|
+
updatedAt DateTime @updatedAt
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// SPEC-362 · ADR-0079 · riwayat sesi terminal. LOCAL-only: sesi hidup di tmux mesin ini dan
|
|
208
|
+
// transkripnya berkas di disk mesin ini — menyiarkannya ke hub akan mengirim baris yang menunjuk
|
|
209
|
+
// berkas yang tak ada di sana (cermin LocalBinding & SchedulerQueueItem). Tanpa `version`/notifySynced.
|
|
210
|
+
model SessionHistory {
|
|
211
|
+
id String @id
|
|
212
|
+
// id tmux sesi. BUKAN primary key: sessionIdForSpec() deterministik, jadi satu backlog yang
|
|
213
|
+
// dibuka-tutup lima kali menghasilkan lima baris dengan sessionId yang sama.
|
|
214
|
+
sessionId String
|
|
215
|
+
// Tanpa FK: sesi VPS memakai projectId sintetis "vps:<id>"/"vps-console:<id>" (routes/vps.ts).
|
|
216
|
+
// Konvensi yang sama dipakai SessionResult.
|
|
217
|
+
projectId String
|
|
218
|
+
specId String?
|
|
219
|
+
title String?
|
|
220
|
+
kind String
|
|
221
|
+
flow String?
|
|
222
|
+
agent String
|
|
223
|
+
model String?
|
|
224
|
+
effort String?
|
|
225
|
+
branch String?
|
|
226
|
+
cwd String
|
|
227
|
+
startedAt DateTime @default(now())
|
|
228
|
+
endedAt DateTime?
|
|
229
|
+
exitCode Int?
|
|
230
|
+
transcriptKey String?
|
|
231
|
+
transcriptBytes Int?
|
|
232
|
+
createdAt DateTime @default(now())
|
|
233
|
+
updatedAt DateTime @updatedAt
|
|
234
|
+
|
|
235
|
+
@@index([projectId, startedAt])
|
|
236
|
+
@@index([specId])
|
|
237
|
+
@@index([sessionId])
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// SPEC-213 · ADR-0045 · change-feed server-authoritative. seq = kursor global sync.
|
|
241
|
+
model SyncLog {
|
|
242
|
+
// SPEC-398 · ADR-0086 · `Int`, BUKAN `BigInt`. Di SQLite sebuah kolom hanya jadi alias rowid
|
|
243
|
+
// (yang auto-terisi) bila tipe deklarasinya **tepat** `INTEGER`; `BigInt` membuat Prisma
|
|
244
|
+
// memancarkan `BIGINT NOT NULL PRIMARY KEY` — valid SQL, **tanpa** AUTOINCREMENT, tanpa
|
|
245
|
+
// peringatan dari `migrate diff` — sehingga setiap insert changefeed gagal
|
|
246
|
+
// "Null constraint violation on the fields: (`seq`)". Kursor tetap string di wire
|
|
247
|
+
// (`String(log.seq)`), jadi kontrak API tak berubah; 2^31 baris changefeed jauh lebih dari cukup.
|
|
248
|
+
seq Int @id @default(autoincrement())
|
|
249
|
+
entity String
|
|
250
|
+
recordId String
|
|
251
|
+
version Int
|
|
252
|
+
data Json
|
|
253
|
+
deviceId String?
|
|
254
|
+
createdAt DateTime @default(now())
|
|
255
|
+
|
|
256
|
+
@@index([entity, recordId])
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// SPEC-213 · LOCAL-ONLY (tak pernah disync): map projectId → repoDir per-device (AC-6/7).
|
|
260
|
+
model LocalBinding {
|
|
261
|
+
projectId String @id
|
|
262
|
+
repoDir String
|
|
263
|
+
createdAt DateTime @default(now())
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// SPEC-213 · LOCAL-ONLY: antre write lokal untuk push saat online (AC-17/18).
|
|
267
|
+
model SyncOutbox {
|
|
268
|
+
id String @id @default(cuid())
|
|
269
|
+
entity String
|
|
270
|
+
recordId String
|
|
271
|
+
createdAt DateTime @default(now())
|
|
272
|
+
|
|
273
|
+
@@unique([entity, recordId])
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// SPEC-213 · LOCAL-ONLY: kursor pull terakhir (singleton).
|
|
277
|
+
model SyncState {
|
|
278
|
+
id Int @id @default(1)
|
|
279
|
+
cursor String @default("0")
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// SPEC-270 · ADR-0067 · LOCAL-ONLY: antrean konflik dua-sisi menunggu keputusan manusia (modal).
|
|
283
|
+
model SyncConflict {
|
|
284
|
+
id String @id @default(cuid())
|
|
285
|
+
entity String
|
|
286
|
+
recordId String
|
|
287
|
+
localData Json
|
|
288
|
+
localVersion Int
|
|
289
|
+
localUpdatedAt DateTime
|
|
290
|
+
serverData Json
|
|
291
|
+
serverVersion Int
|
|
292
|
+
serverUpdatedAt DateTime
|
|
293
|
+
detectedAt DateTime @default(now())
|
|
294
|
+
resolvedAt DateTime?
|
|
295
|
+
|
|
296
|
+
@@unique([entity, recordId])
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// SPEC-294 · ADR-0072 · LOCAL-ONLY (tak disync, cermin SyncOutbox): antrean durable kandidat peluncuran
|
|
300
|
+
// scheduler. specId @unique = idempoten satu-sesi-per-spec (ADR-0015). Unit peluncuran selalu Spec.
|
|
301
|
+
model SchedulerQueueItem {
|
|
302
|
+
id String @id @default(cuid())
|
|
303
|
+
specId String @unique
|
|
304
|
+
projectId String
|
|
305
|
+
source String // backlog | errors | triase (asal checker)
|
|
306
|
+
priority String // tinggi | sedang | rendah (urutan drain)
|
|
307
|
+
status String @default("queued") // queued | launched | done | failed
|
|
308
|
+
sessionId String?
|
|
309
|
+
note String? // alasan gagal (diisi daun #5)
|
|
310
|
+
enqueuedAt DateTime @default(now())
|
|
311
|
+
launchedAt DateTime?
|
|
312
|
+
|
|
313
|
+
@@index([status])
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// SPEC-215 · ADR-0049 · LOCAL-ONLY: override config runtime (env → DB). Tak pernah disync.
|
|
317
|
+
model RuntimeConfig {
|
|
318
|
+
key String @id
|
|
319
|
+
value String
|
|
320
|
+
updatedAt DateTime @updatedAt
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// SPEC-249 · ADR-0060 · grup error per project (fingerprint dedup). SPEC-268/ADR-0066: agregat
|
|
324
|
+
// grup kini tersync (version-stamp); ErrorEvent mentah tetap server-local (tak disync).
|
|
325
|
+
model ErrorGroup {
|
|
326
|
+
id String @id @default(cuid())
|
|
327
|
+
projectId String
|
|
328
|
+
fingerprint String
|
|
329
|
+
type String
|
|
330
|
+
message String
|
|
331
|
+
sampleStack String?
|
|
332
|
+
sampleFrames Json? // SPEC-276 · ADR-0070 · frame mentah sample (disymbolikasi saat display)
|
|
333
|
+
release String? // SPEC-276 · ADR-0070 · release terakhir (korelasi build + surface di UI)
|
|
334
|
+
environment String
|
|
335
|
+
status String @default("new") // new | escalated | resolved
|
|
336
|
+
count Int @default(0)
|
|
337
|
+
firstSeenAt DateTime @default(now())
|
|
338
|
+
lastSeenAt DateTime @default(now())
|
|
339
|
+
specId String?
|
|
340
|
+
createdAt DateTime @default(now())
|
|
341
|
+
updatedAt DateTime @updatedAt
|
|
342
|
+
version Int @default(0) // SPEC-268 · version-stamp sync (ADR-0066)
|
|
343
|
+
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
344
|
+
events ErrorEvent[]
|
|
345
|
+
|
|
346
|
+
@@unique([projectId, fingerprint])
|
|
347
|
+
@@index([projectId, lastSeenAt])
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// SPEC-249 · ADR-0060 · kejadian error mentah, dipangkas retensi (cap per grup + umur).
|
|
351
|
+
model ErrorEvent {
|
|
352
|
+
id String @id @default(cuid())
|
|
353
|
+
groupId String
|
|
354
|
+
projectId String
|
|
355
|
+
type String
|
|
356
|
+
message String
|
|
357
|
+
stack String?
|
|
358
|
+
frames Json? // SPEC-276 · ADR-0070 · frame terstruktur mentah dari SDK
|
|
359
|
+
environment String
|
|
360
|
+
release String?
|
|
361
|
+
context Json?
|
|
362
|
+
receivedAt DateTime @default(now())
|
|
363
|
+
group ErrorGroup @relation(fields: [groupId], references: [id], onDelete: Cascade)
|
|
364
|
+
|
|
365
|
+
@@index([groupId, receivedAt])
|
|
366
|
+
@@index([projectId, receivedAt])
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// SPEC-276 · ADR-0070 · source-map ter-upload per release untuk symbolication. Byte map di
|
|
370
|
+
// HANOMAN_UPLOAD_DIR (server-local, TAK disync — cermin TicketAttachment biner & ErrorEvent).
|
|
371
|
+
model SourceMapArtifact {
|
|
372
|
+
id String @id @default(cuid())
|
|
373
|
+
projectId String
|
|
374
|
+
release String
|
|
375
|
+
filename String // basename artifact hasil-build (mis. index-4f3a2b.js) yang dipetakan map ini
|
|
376
|
+
debugId String?
|
|
377
|
+
storageKey String // berkas opaque (uuid.map) di upload dir
|
|
378
|
+
size Int
|
|
379
|
+
createdAt DateTime @default(now())
|
|
380
|
+
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
381
|
+
|
|
382
|
+
@@unique([projectId, release, filename])
|
|
383
|
+
@@index([projectId, release])
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// SPEC-253 · ADR-0062 · tiket keluhan Help Center. SPEC-268/ADR-0066: metadata tiket kini tersync
|
|
387
|
+
// (version-stamp); lampiran (TicketAttachment, file biner) tetap server-local (tak disync).
|
|
388
|
+
model Ticket {
|
|
389
|
+
id String @id @default(cuid())
|
|
390
|
+
projectId String
|
|
391
|
+
number Int // nomor pendek human-readable per project (Tiket #<n>)
|
|
392
|
+
category String // bug | fitur | pertanyaan | lainnya (zTicketCategory)
|
|
393
|
+
title String
|
|
394
|
+
detail String
|
|
395
|
+
reporterEmail String
|
|
396
|
+
status String @default("new") // new | accepted | rejected (zTicketStatus)
|
|
397
|
+
accessKeyHash String @unique // sha256(kunci opaque) — lookup cek status. Plaintext hanya sekali.
|
|
398
|
+
shareToken String? @unique // SPEC-293 · token bagikan link status publik (hnm_shr_…); operator → pelapor
|
|
399
|
+
specId String? // soft-link Spec hasil promosi (cermin ErrorGroup.specId)
|
|
400
|
+
createdAt DateTime @default(now())
|
|
401
|
+
updatedAt DateTime @updatedAt
|
|
402
|
+
version Int @default(0) // SPEC-268 · version-stamp sync (ADR-0066)
|
|
403
|
+
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
404
|
+
attachments TicketAttachment[]
|
|
405
|
+
|
|
406
|
+
@@unique([projectId, number])
|
|
407
|
+
@@index([projectId, createdAt])
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// SPEC-253 · ADR-0062 · lampiran gambar tiket. Berkas di HANOMAN_UPLOAD_DIR (server-local, tak disync).
|
|
411
|
+
model TicketAttachment {
|
|
412
|
+
id String @id @default(cuid())
|
|
413
|
+
ticketId String
|
|
414
|
+
projectId String // denormal — isolasi & query murah (pola ErrorEvent.projectId)
|
|
415
|
+
filename String // nama asli tersanitasi (display saja)
|
|
416
|
+
mimeType String // image/png | image/jpeg | image/webp
|
|
417
|
+
size Int
|
|
418
|
+
storageKey String // nama berkas opaque di upload dir (uuid+ext)
|
|
419
|
+
createdAt DateTime @default(now())
|
|
420
|
+
version Int @default(0) // SPEC-272 · version-stamp sync (ADR-0068)
|
|
421
|
+
updatedAt DateTime @updatedAt // SPEC-272 · jam LWW (lampiran immutable → praktis konstan)
|
|
422
|
+
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
|
423
|
+
|
|
424
|
+
@@index([ticketId])
|
|
425
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
@import"https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&family=IBM+Plex+Serif:ital,wght@0,400;0,500;0,600;0,700;1,400&family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;1,400&display=swap";:root{--ink-900: #17130c;--ink-800: #241d13;--ink-700: #3a3125;--ink-600: #574b3a;--ink-500: #6f6250;--ink-400: #90826c;--ink-300: #b3a794;--ink-200: #d6ccb9;--bone-000: #fffdf8;--bone-100: #faf6ec;--bone-200: #f2ebdc;--bone-300: #e8dfcc;--bone-400: #dccfb6;--brass-700: #7a5417;--brass-600: #97671f;--brass-500: #b8863b;--brass-400: #cf9d55;--brass-300: #e3c988;--brass-100: #f3e6c4;--brass-050: #faf2df;--wind-700: #2f5560;--wind-600: #3f6e7a;--wind-500: #517f8c;--wind-100: #dbe7ea;--wind-050: #eef4f5;--leaf-600: #4d6b30;--leaf-500: #5f8340;--leaf-100: #e6eed7;--amber-600: #b3771a;--amber-500: #cf8f28;--amber-100: #f7e9c9;--clay-600: #a23b2e;--clay-500: #bd4a3a;--clay-100: #f5dcd6;--term-bg: #1c1810;--term-fg: #e9e0cd;--term-dim: #8f8467;--term-line: #2c2618;--surface-page: var(--bone-100);--surface-panel: var(--bone-200);--surface-card: var(--bone-000);--surface-sunken: var(--bone-200);--surface-inverse: var(--ink-900);--surface-code: var(--term-bg);--text-strong: var(--ink-900);--text-body: var(--ink-700);--text-muted: var(--ink-500);--text-subtle: var(--ink-400);--text-on-brass: var(--ink-900);--text-on-dark: var(--term-fg);--text-link: var(--wind-600);--border-hair: var(--bone-300);--border-strong: var(--bone-400);--border-focus: var(--brass-500);--accent: var(--brass-500);--accent-hover: var(--brass-600);--accent-press: var(--brass-700);--accent-tint: var(--brass-100);--accent-on: var(--ink-900);--info: var(--wind-600);--info-tint: var(--wind-100);--status-ok: var(--leaf-600);--status-ok-tint: var(--leaf-100);--status-warn: var(--amber-600);--status-warn-tint: var(--amber-100);--status-err: var(--clay-600);--status-err-tint: var(--clay-100);--ring: 0 0 0 3px color-mix(in srgb, var(--brass-500) 45%, transparent)}:root{--font-serif: "IBM Plex Serif", Georgia, "Times New Roman", serif;--font-sans: "IBM Plex Sans", ui-sans-serif, system-ui, -apple-system, sans-serif;--font-mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace;--font-display: var(--font-serif);--font-body: var(--font-sans);--font-ui: var(--font-sans);--font-code: var(--font-mono);--text-2xs: 11px;--text-xs: 12px;--text-sm: 13px;--text-md: 14px;--text-base:16px;--text-lg: 18px;--text-xl: 20px;--text-2xl: 24px;--text-3xl: 30px;--text-4xl: 38px;--text-5xl: 48px;--text-6xl: 62px;--weight-regular: 400;--weight-medium: 500;--weight-semibold: 600;--weight-bold: 700;--leading-tight: 1.12;--leading-snug: 1.3;--leading-normal: 1.5;--leading-relaxed:1.65;--tracking-tight: -.02em;--tracking-normal: 0;--tracking-wide: .02em;--tracking-caps: .14em}:root{--space-0: 0;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 20px;--space-6: 24px;--space-7: 32px;--space-8: 40px;--space-9: 48px;--space-10: 64px;--space-11: 80px;--space-12: 96px;--radius-xs: 3px;--radius-sm: 5px;--radius-md: 8px;--radius-lg: 12px;--radius-xl: 16px;--radius-pill: 999px;--border-thin: 1px;--border-med: 1.5px;--border-thick:2px;--content-max: 1200px;--sidebar-w: 248px;--topbar-h: 56px}:root{--shadow-xs: 0 1px 1px rgba(35, 27, 15, .05);--shadow-sm: 0 1px 2px rgba(35, 27, 15, .06), 0 1px 1px rgba(35, 27, 15, .04);--shadow-md: 0 2px 4px rgba(35, 27, 15, .07), 0 4px 12px rgba(35, 27, 15, .06);--shadow-lg: 0 8px 24px rgba(35, 27, 15, .1), 0 2px 6px rgba(35, 27, 15, .06);--shadow-xl: 0 20px 48px rgba(35, 27, 15, .16), 0 4px 12px rgba(35, 27, 15, .08);--shadow-inset: inset 0 1px 2px rgba(35, 27, 15, .08);--shadow-gild: 0 1px 0 rgba(255, 253, 248, .6) inset, 0 0 0 1px var(--brass-300);--elevation-flat: none;--elevation-raised: var(--shadow-sm);--elevation-float: var(--shadow-md);--elevation-overlay:var(--shadow-lg);--elevation-modal: var(--shadow-xl);--ease-out: cubic-bezier(.22, .61, .36, 1);--ease-inout: cubic-bezier(.65, 0, .35, 1);--dur-fast: .12s;--dur-base: .18s;--dur-slow: .28s;--transition-base: all var(--dur-base) var(--ease-out);--transition-fast: all var(--dur-fast) var(--ease-out)}*,*:before,*:after{box-sizing:border-box}body{margin:0;background:var(--surface-page);color:var(--text-body);font-family:var(--font-body);font-size:var(--text-base);line-height:var(--leading-normal);-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}h1,h2,h3,h4{font-family:var(--font-display);color:var(--text-strong);line-height:var(--leading-tight);letter-spacing:var(--tracking-tight);font-weight:var(--weight-semibold);margin:0 0 var(--space-3)}p{margin:0 0 var(--space-3)}a{color:var(--text-link);text-decoration:none;border-bottom:1px solid color-mix(in srgb,var(--text-link) 35%,transparent);transition:var(--transition-fast)}a:hover{color:var(--wind-700);border-bottom-color:var(--wind-700)}code,kbd,samp,pre{font-family:var(--font-code);font-size:.92em}.hn-eyebrow{font-family:var(--font-mono);font-size:var(--text-2xs);font-weight:var(--weight-medium);letter-spacing:var(--tracking-caps);text-transform:uppercase;color:var(--text-muted)}::selection{background:var(--brass-100);color:var(--ink-900)}html,body{margin:0;height:100%;background:var(--surface-page)}#root{height:100vh;overflow:hidden}button{--accent-on: #fff}a{color:var(--text-link);text-decoration:none}a:hover{color:var(--wind-700);text-decoration:underline}.hn-md{color:var(--text-body);font-family:var(--font-sans);font-size:15px;line-height:1.72;overflow-wrap:anywhere}.hn-md>:first-child{margin-top:8px}.hn-md h1,.hn-md h2,.hn-md h3,.hn-md h4{font-family:var(--font-display);font-weight:600;letter-spacing:-.02em;color:var(--ink-900);line-height:1.2;margin:1.6em 0 .5em}.hn-md h1{font-size:30px;margin-top:.4em;padding-bottom:.34em;border-bottom:1px solid var(--border-hair)}.hn-md h2{font-size:22px}.hn-md h3{font-size:18px}.hn-md h4{font-size:15px}.hn-md p{margin:.7em 0}.hn-md ul,.hn-md ol{margin:.6em 0;padding-left:1.5em}.hn-md li{margin:.28em 0}.hn-md li::marker{color:var(--brass-500)}.hn-md ul.contains-task-list,.hn-md li.task-list-item{list-style:none}.hn-md li.task-list-item{padding-left:0}.hn-md input[type=checkbox]{accent-color:var(--brass-500);margin-right:8px;transform:translateY(1px)}.hn-md strong{font-weight:600;color:var(--ink-900)}.hn-md em{color:var(--text-muted)}.hn-md a{color:var(--text-link)}.hn-md hr{border:none;border-top:1px solid var(--border-hair);margin:1.6em 0}.hn-md code{font-family:var(--font-mono);font-size:.86em;background:var(--bone-200);color:var(--ink-800);padding:1.5px 5px;border-radius:var(--radius-xs);border:1px solid var(--border-hair)}.hn-md pre{background:var(--surface-code);border:1px solid var(--term-line);border-radius:var(--radius-lg);padding:14px 16px;overflow:auto;margin:1em 0;white-space:pre-wrap;overflow-wrap:anywhere}.hn-md pre code{background:transparent;border:none;padding:0;color:var(--term-fg);font-size:12.5px;line-height:1.7}.hn-md blockquote{margin:1em 0;padding:4px 16px;border-left:3px solid var(--brass-500);background:var(--brass-050);color:var(--text-body);border-radius:0 var(--radius-sm) var(--radius-sm) 0}.hn-md blockquote p{margin:.4em 0}.hn-md table{border-collapse:collapse;width:100%;margin:1em 0;font-size:14px;table-layout:fixed}.hn-md th,.hn-md td{overflow-wrap:anywhere}.hn-md th{font-family:var(--font-mono);font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted);text-align:left;padding:8px 12px;background:var(--bone-100);border:1px solid var(--border-hair)}.hn-md td{padding:8px 12px;border:1px solid var(--border-hair);color:var(--text-body)}.hn-md td code{font-size:.82em}.hn-md img{max-width:100%;border-radius:var(--radius-sm)}@keyframes hn-fade-in{0%{opacity:0}to{opacity:1}}@keyframes hn-modal-in{0%{opacity:0;transform:translateY(8px) scale(.99)}to{opacity:1;transform:none}}@keyframes hn-toast-in{0%{opacity:0;transform:translate(-50%,10px)}to{opacity:1;transform:translate(-50%)}}/**
|
|
2
|
+
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
|
3
|
+
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
|
4
|
+
* https://github.com/chjj/term.js
|
|
5
|
+
* @license MIT
|
|
6
|
+
*
|
|
7
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
* in the Software without restriction, including without limitation the rights
|
|
10
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
* furnished to do so, subject to the following conditions:
|
|
13
|
+
*
|
|
14
|
+
* The above copyright notice and this permission notice shall be included in
|
|
15
|
+
* all copies or substantial portions of the Software.
|
|
16
|
+
*
|
|
17
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
23
|
+
* THE SOFTWARE.
|
|
24
|
+
*
|
|
25
|
+
* Originally forked from (with the author's permission):
|
|
26
|
+
* Fabrice Bellard's javascript vt100 for jslinux:
|
|
27
|
+
* http://bellard.org/jslinux/
|
|
28
|
+
* Copyright (c) 2011 Fabrice Bellard
|
|
29
|
+
* The original design remains. The terminal itself
|
|
30
|
+
* has been extended to include xterm CSI codes, among
|
|
31
|
+
* other features.
|
|
32
|
+
*/.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) *::selection{color:transparent}.xterm .xterm-accessibility-tree{font-family:monospace;-webkit-user-select:text;user-select:text;white-space:pre}.xterm .xterm-accessibility-tree>div{transform-origin:left;width:fit-content}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}.xterm .xterm-scrollable-element>.scrollbar{cursor:default}.xterm .xterm-scrollable-element>.scrollbar>.scra{cursor:pointer;font-size:11px!important}.xterm .xterm-scrollable-element>.visible{opacity:1;background:#0000;transition:opacity .1s linear;z-index:11}.xterm .xterm-scrollable-element>.invisible{opacity:0;pointer-events:none}.xterm .xterm-scrollable-element>.invisible.fade{transition:opacity .8s linear}.xterm .xterm-scrollable-element>.shadow{position:absolute;display:none}.xterm .xterm-scrollable-element>.shadow.top{display:block;top:0;left:3px;height:3px;width:100%;box-shadow:var(--vscode-scrollbar-shadow, #000) 0 6px 6px -6px inset}.xterm .xterm-scrollable-element>.shadow.left{display:block;top:3px;left:0;height:100%;width:3px;box-shadow:var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset}.xterm .xterm-scrollable-element>.shadow.top-left-corner{display:block;top:0;left:0;height:3px;width:3px}.xterm .xterm-scrollable-element>.shadow.top.left{box-shadow:var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset}pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
|
33
|
+
Theme: GitHub
|
|
34
|
+
Description: Light theme as seen on github.com
|
|
35
|
+
Author: github.com
|
|
36
|
+
Maintainer: @Hirse
|
|
37
|
+
Updated: 2021-05-15
|
|
38
|
+
|
|
39
|
+
Outdated base version: https://github.com/primer/github-syntax-light
|
|
40
|
+
Current colors taken from GitHub's CSS
|
|
41
|
+
*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-variable,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id{color:#005cc5}.hljs-regexp,.hljs-string,.hljs-meta .hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-comment,.hljs-code,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}
|