Track fixed and ignored review findings
This commit is contained in:
+38
-1
@@ -570,6 +570,27 @@ paths:
|
||||
items: { $ref: "#/components/schemas/Review" }
|
||||
"404": { $ref: "#/components/responses/Error" }
|
||||
|
||||
/api/reviews/{reviewId}/findings/{index}:
|
||||
parameters:
|
||||
- { name: reviewId, in: path, required: true, schema: { type: string } }
|
||||
- { name: index, in: path, required: true, schema: { type: integer }, description: "The finding's position in the review's findings, from 0." }
|
||||
put:
|
||||
operationId: setFindingState
|
||||
description: Records that the human addressed a finding of a done review, by sending it to the agent to fix or by ignoring it.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/FindingStateUpdate" }
|
||||
responses:
|
||||
"200":
|
||||
description: The review.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Review" }
|
||||
"400": { $ref: "#/components/responses/Error" }
|
||||
"404": { $ref: "#/components/responses/Error" }
|
||||
|
||||
/api/tasks/{taskId}/snapshots/{n}/fork:
|
||||
parameters:
|
||||
- { name: taskId, in: path, required: true, schema: { type: string } }
|
||||
@@ -1147,7 +1168,7 @@ components:
|
||||
Review:
|
||||
type: object
|
||||
description: An agent session on the vendor the task's agent doesn't use, reviewing one snapshot's change.
|
||||
required: [id, task_id, snapshot_n, agent_session_id, vendor, model, state, error, created_at]
|
||||
required: [id, task_id, snapshot_n, agent_session_id, vendor, model, state, error, created_at, finding_states]
|
||||
properties:
|
||||
id: { type: string }
|
||||
task_id: { type: string }
|
||||
@@ -1164,6 +1185,22 @@ components:
|
||||
error: { type: string, description: "failed: why." }
|
||||
created_at: { type: string, format: date-time }
|
||||
finished_at: { type: string, format: date-time }
|
||||
finding_states:
|
||||
type: object
|
||||
description: "How the human addressed each finding, keyed by its index in result.findings; a finding not listed is open."
|
||||
additionalProperties: { $ref: "#/components/schemas/FindingState" }
|
||||
|
||||
FindingState:
|
||||
type: string
|
||||
enum: [fixed, ignored]
|
||||
x-enum-varnames: [FindingFixed, FindingIgnored]
|
||||
description: "fixed: sent to the task's agent to fix. ignored: dismissed, and left out of the Merge warning."
|
||||
|
||||
FindingStateUpdate:
|
||||
type: object
|
||||
required: [state]
|
||||
properties:
|
||||
state: { $ref: "#/components/schemas/FindingState" }
|
||||
|
||||
AgentSession:
|
||||
type: object
|
||||
|
||||
+3
-3
@@ -29,7 +29,7 @@ Every task worktree is made by `createTask` (`internal/agents/tasks.go`): new ta
|
||||
|
||||
## Closing a task
|
||||
|
||||
A task ends `merged` or `discarded`; either way its sessions are stopped, its snapshot refs, worktree and branch are removed, its `open` inbox items become `dismissed` (resolved by whoever closed it), and its record, summary and events remain. `GET /api/tasks/{id}/unmerged` reports what would be lost or merged: commits on the branch that aren't on the default branch, and files, lines and uncommitted paths that differ from the merge base. The UI's Merge and Discard buttons each ask a confirmation, opened just below the button like the task picker's switcher (centred when ⌘S opens Merge's from a tab without the button), and are disabled while the task's session is `running`. Merge is also disabled until `unmerged` reports a commit or a changed file; the transcript fetches it when the task is opened and again shortly after each tool result or turn end. Merge's confirmation (`confirmMerge` in `web/src/task.ts`) is kept brief: the commit title as text with a ✎ button that swaps in an input, a collapsed "N co-authors" line holding the trailers, and one warning line for each thing worth stopping for. Those are the latest snapshot's review still running, failed, missing or with findings (counted by severity); the task's open inbox questions, which merging dismisses; the worktree differing from the latest snapshot (`changes?since=` its commit), or no snapshot at all; and "Review status unavailable" when those fetches fail. Earlier snapshots' reviews aren't mentioned. Warnings never block Merge. The review and question warnings link to the Reviews tab at that snapshot or the Inbox tab; following one closes the dialog and keeps the title in the page, so the next Merge opens with it. While open, the dialog refetches on the task's `snapshot.created` and `review.*` events and redraws on inbox changes.
|
||||
A task ends `merged` or `discarded`; either way its sessions are stopped, its snapshot refs, worktree and branch are removed, its `open` inbox items become `dismissed` (resolved by whoever closed it), and its record, summary and events remain. `GET /api/tasks/{id}/unmerged` reports what would be lost or merged: commits on the branch that aren't on the default branch, and files, lines and uncommitted paths that differ from the merge base. The UI's Merge and Discard buttons each ask a confirmation, opened just below the button like the task picker's switcher (centred when ⌘S opens Merge's from a tab without the button), and are disabled while the task's session is `running`. Merge is also disabled until `unmerged` reports a commit or a changed file; the transcript fetches it when the task is opened and again shortly after each tool result or turn end. Merge's confirmation (`confirmMerge` in `web/src/task.ts`) is kept brief: the commit title as text with a ✎ button that swaps in an input, a collapsed "N co-authors" line holding the trailers, and one warning line for each thing worth stopping for. Those are the latest snapshot's review still running, failed, missing or with findings that weren't ignored (counted by severity); the task's open inbox questions, which merging dismisses; the worktree differing from the latest snapshot (`changes?since=` its commit), or no snapshot at all; and "Review status unavailable" when those fetches fail. Earlier snapshots' reviews aren't mentioned. Warnings never block Merge. The review and question warnings link to the Reviews tab at that snapshot or the Inbox tab; following one closes the dialog and keeps the title in the page, so the next Merge opens with it. While open, the dialog refetches on the task's `snapshot.created` and `review.*` events and redraws on inbox changes.
|
||||
|
||||
A task's summary is what its agent writes, before the task closes, about the whole task: what it set out to do, what was built or tried, the key decisions and why, and what was left out. The agent sets it with the `set_task_summary` MCP tool (`internal/agents/summary.go`), any time while the task is active; each call replaces `tasks.summary` and records a `task.summary` event carrying it, which the transcript shows as a note. The Merge turn's prompt and the discard turn's prompt (`summaryRequest`) both ask for it. Turn summaries don't stand in for it: each covers only its own turn, and a merged task's last one is always the Merge turn's. Tasks closed before the column existed have none. `sqlite3 byre.db "SELECT id, status, summary FROM tasks WHERE status != 'active' ORDER BY closed_at DESC LIMIT 5"` shows recent ones.
|
||||
|
||||
@@ -285,8 +285,8 @@ Every new snapshot of a task gets a review: a separate agent session on the vend
|
||||
- **Running.** Reviewer sessions get `reviewGuidance` and only the `submit_review` tool, whose arguments (`reviewSchema`: `change`, a few sentences of Markdown on what the change does, required and non-empty; a Markdown `summary`, which the guidance and schema require to be exactly "No significant findings." when there are no findings and otherwise a bulleted list with one line per finding; `findings`, each with `severity`, `category`, `title`, `detail`, `path`, `line`, `end_line` and `images`; and top-level `images`) become the review's `result` and mark it `done`. The guidance encourages, without requiring, Mermaid blocks for a data flow, call graph, state machine or schema the change alters, and screenshots when the change alters something visible.
|
||||
- **Screenshots.** Reviewer sessions also get the Playwright MCP server (`playwrightMCP` in `internal/agents/review.go`: `npx @playwright/mcp` at a pinned version, headless Chrome, isolated profile, absolute file paths), passed to Claude as a `stdio` entry in `--mcp-config` and to Codex as `mcp_servers` in the thread's `config`. Its automatically named files go to `worktrees/<project-id>/review-<review-id>-media`, removed with the checkout. Each `images` entry is `{path, caption}`, the path absolute or relative to the checkout; at `submit_review` byre reads every one, refuses the whole submission (nothing recorded) for a missing file or one that isn't one of `store.ImageTypes` within `store.MaxBlobBytes`, then stores each as a blob and replaces its `path` with `blob`, since the files are deleted with the checkout. They run without the Stop-hook bounce, mid-turn delivery, end-of-turn commit, snapshots or turn summaries, and their `user.message` events carry no `head`. When the reviewer's turn ends byre completes its session, kills its process and removes the worktree; a review still `running` then fails with why (no submission, with the final message; an error; an interrupt). A process that exits mid-turn fails the review the same way. Merging or discarding the task fails its running reviews ("the task was closed") and ends their reviewers. On startup, a running reviewer resumes like any session, and a running review whose reviewer isn't `running` fails.
|
||||
- **Questions.** A question's `reviews` is the review of the first snapshot the task took after the question was filed, the one at the end of the turn that asked it, computed when the item is read; the Inbox tab shows it under the question once it starts.
|
||||
- **Events.** `review.started` and `review.finished` (state `done`, `failed` or `skipped`; a skipped review emits only `review.finished`) carry the whole review, with the task's ids and no `agent_session_id`. A reviewer's own events carry its session id and the task's ids: the transcript skips every event of a session whose `session.created` has `role` `reviewer`, and draws each `review.finished` as a note with its outcome; the lead's `read_transcript` leaves the reviewer's events out and shows each `review.finished` with its summary and findings (`reviewLine`). `GET /api/tasks/{taskId}/sessions` and everything that looks for the task's agent (`ListSessions`) see only implementer sessions.
|
||||
- **UI.** The **Reviews** tab (`web/src/reviews.ts`) shows one review at a time, picked by snapshot (see "Snapshots" for the picker): its snapshot, state or severity counts, error, summary, screenshots and findings. The `change` field is not shown; it goes to the lead and into notes. A finding with `path` and `line` shows, under its title, the lines `line` to `end_line` plus 3 either side from that file's diff for the snapshot's step (`changes?snapshot=<n>&step=true`, the diff the reviewer was given), with new-file line numbers and the finding's own lines marked; it shows nothing when those lines aren't in the diff, and "diff not available" with the error once the task is closed. Screenshots link to the full image. Each finding has a **fix** button (also on review cards in the inbox) that sends the finding's severity, title, location and detail to the task's agent as a message from the user: through `POST /api/sessions/{id}/messages` to its latest live session (queued like any message while the agent is mid-turn, see "Agent sessions"), or starting a session when it has none. Once sent, the right pane switches to that task's Transcript tab. It loads when it opens, when the task changes, and on each `review.*` event in the task's stream. The tab follows the latest `review.*` event of the highest-numbered snapshot: while that review runs, the tab label (and its entry in the More menu) carries a spinner in place of any earlier review's count; once it finishes with findings, the tab (or the More menu holding it) is highlighted with a badge of the finding count. Each snapshot row's **review** button carries a mark over its top-right corner from that snapshot's latest `review.*` event: a spinner while the review runs, a grey ● when it was skipped, a green ✓ when it finished with no findings, and, when it has findings, the button highlighted with the count in a badge. A failed review leaves the button unmarked. A Task's `reviewing` is true while any of its reviews is `running`, and the global stream's `review.*` events reload that project's tasks; the task switcher and the Project tab's task list show **reviewing**, with the running dot, in place of an `idle` session state (`taskState` in `web/src/state.ts`). `GET /api/tasks/{taskId}/reviews` lists a task's reviews oldest first. `sqlite3 byre.db "SELECT id, task_id, snapshot_n, vendor, state, error FROM reviews ORDER BY created_at DESC LIMIT 20"` shows recent ones.
|
||||
- **Events.** `review.started`, `review.finished` (state `done`, `failed` or `skipped`; a skipped review emits only `review.finished`) and `review.finding` (a finding's state set, with the user as actor) carry the whole review, with the task's ids and no `agent_session_id`. A reviewer's own events carry its session id and the task's ids: the transcript skips every event of a session whose `session.created` has `role` `reviewer`, and draws each `review.finished` as a note with its outcome; the lead's `read_transcript` leaves the reviewer's events out and shows each `review.finished` with its summary and findings (`reviewLine`). `GET /api/tasks/{taskId}/sessions` and everything that looks for the task's agent (`ListSessions`) see only implementer sessions.
|
||||
- **UI.** The **Reviews** tab (`web/src/reviews.ts`) shows one review at a time, picked by snapshot (see "Snapshots" for the picker): its snapshot, state or severity counts, error, summary, screenshots and findings. The `change` field is not shown; it goes to the lead and into notes. A finding with `path` and `line` shows, under its title, the lines `line` to `end_line` plus 3 either side from that file's diff for the snapshot's step (`changes?snapshot=<n>&step=true`, the diff the reviewer was given), with new-file line numbers and the finding's own lines marked; it shows nothing when those lines aren't in the diff, and "diff not available" with the error once the task is closed. Screenshots link to the full image. Each open finding has **fix** and **ignore** buttons (also on review cards in the inbox). **fix** sends the finding's severity, title, location and detail to the task's agent as a message from the user: through `POST /api/sessions/{id}/messages` to its latest live session (queued like any message while the agent is mid-turn, see "Agent sessions"), or starting a session when it has none; the view stays where it is, so several findings can be sent in a row. Then it, or **ignore** on its own, records the finding's state (`fixed` or `ignored`) with `PUT /api/reviews/{reviewId}/findings/{index}`, the index being the finding's position in `result.findings`; the review's `finding_states` (the `reviews.finding_states` column, a JSON object from index to state) holds them, and the buttons give way to "fix sent" or "ignored". Only a `done` review's findings take a state, and there is no way to clear one. The tab loads when it opens, when the task changes, and on each `review.*` event in the task's stream. The tab follows the latest `review.*` event of the highest-numbered snapshot: while that review runs, the tab label (and its entry in the More menu) carries a spinner in place of any earlier review's count; once it finishes with open findings (those with no state), the tab (or the More menu holding it) is highlighted with a badge of their count. Each snapshot row's **review** button carries a mark over its top-right corner from that snapshot's latest `review.*` event: a spinner while the review runs, a grey ● when it was skipped, a green ✓ when it finished with no open findings, and, when it has open findings, the button highlighted with their count in a badge. A failed review leaves the button unmarked. A Task's `reviewing` is true while any of its reviews is `running`, and the global stream's `review.*` events reload that project's tasks; the task switcher and the Project tab's task list show **reviewing**, with the running dot, in place of an `idle` session state (`taskState` in `web/src/state.ts`). `GET /api/tasks/{taskId}/reviews` lists a task's reviews oldest first. `sqlite3 byre.db "SELECT id, task_id, snapshot_n, vendor, state, error FROM reviews ORDER BY created_at DESC LIMIT 20"` shows recent ones.
|
||||
|
||||
## Workflows
|
||||
|
||||
|
||||
+262
-185
@@ -183,6 +183,24 @@ func (e FileChangeStatus) Valid() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for FindingState.
|
||||
const (
|
||||
FindingFixed FindingState = "fixed"
|
||||
FindingIgnored FindingState = "ignored"
|
||||
)
|
||||
|
||||
// Valid indicates whether the value is a known member of the FindingState enum.
|
||||
func (e FindingState) Valid() bool {
|
||||
switch e {
|
||||
case FindingFixed:
|
||||
return true
|
||||
case FindingIgnored:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for InboxItemKind.
|
||||
const (
|
||||
Question InboxItemKind = "question"
|
||||
@@ -583,6 +601,15 @@ type FileDiff struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// FindingState fixed: sent to the task's agent to fix. ignored: dismissed, and left out of the Merge warning.
|
||||
type FindingState string
|
||||
|
||||
// FindingStateUpdate defines model for FindingStateUpdate.
|
||||
type FindingStateUpdate struct {
|
||||
// State fixed: sent to the task's agent to fix. ignored: dismissed, and left out of the Merge warning.
|
||||
State FindingState `json:"state"`
|
||||
}
|
||||
|
||||
// Focus defines model for Focus.
|
||||
type Focus struct {
|
||||
// ProjectId The project the right pane shows; the task's project when task_id is set
|
||||
@@ -760,9 +787,12 @@ type Review struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
// Error failed: why.
|
||||
Error string `json:"error"`
|
||||
FinishedAt *time.Time `json:"finished_at,omitempty"`
|
||||
Id string `json:"id"`
|
||||
Error string `json:"error"`
|
||||
|
||||
// FindingStates How the human addressed each finding, keyed by its index in result.findings; a finding not listed is open.
|
||||
FindingStates map[string]FindingState `json:"finding_states"`
|
||||
FinishedAt *time.Time `json:"finished_at,omitempty"`
|
||||
Id string `json:"id"`
|
||||
|
||||
// Model Empty when skipped.
|
||||
Model string `json:"model"`
|
||||
@@ -1156,6 +1186,9 @@ type CreateTaskJSONRequestBody = TaskCreate
|
||||
// StartTodoItemJSONRequestBody defines body for StartTodoItem for application/json ContentType.
|
||||
type StartTodoItemJSONRequestBody = TodoStart
|
||||
|
||||
// SetFindingStateJSONRequestBody defines body for SetFindingState for application/json ContentType.
|
||||
type SetFindingStateJSONRequestBody = FindingStateUpdate
|
||||
|
||||
// SendMessageJSONRequestBody defines body for SendMessage for application/json ContentType.
|
||||
type SendMessageJSONRequestBody = MessageCreate
|
||||
|
||||
@@ -1234,6 +1267,9 @@ type ServerInterface interface {
|
||||
// (POST /api/projects/{projectId}/todo/tasks)
|
||||
StartTodoItem(w http.ResponseWriter, r *http.Request, projectId string)
|
||||
|
||||
// (PUT /api/reviews/{reviewId}/findings/{index})
|
||||
SetFindingState(w http.ResponseWriter, r *http.Request, reviewId string, index int)
|
||||
|
||||
// (GET /api/sessions/{sessionId})
|
||||
GetSession(w http.ResponseWriter, r *http.Request, sessionId string)
|
||||
|
||||
@@ -1831,6 +1867,41 @@ func (siw *ServerInterfaceWrapper) StartTodoItem(w http.ResponseWriter, r *http.
|
||||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// SetFindingState operation middleware
|
||||
func (siw *ServerInterfaceWrapper) SetFindingState(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var err error
|
||||
_ = err
|
||||
|
||||
// ------------- Path parameter "reviewId" -------------
|
||||
var reviewId string
|
||||
|
||||
err = runtime.BindStyledParameterWithOptions("simple", "reviewId", r.PathValue("reviewId"), &reviewId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true})
|
||||
if err != nil {
|
||||
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "reviewId", Err: err})
|
||||
return
|
||||
}
|
||||
|
||||
// ------------- Path parameter "index" -------------
|
||||
var index int
|
||||
|
||||
err = runtime.BindStyledParameterWithOptions("simple", "index", r.PathValue("index"), &index, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "integer", Format: "", ValueIsUnescaped: true})
|
||||
if err != nil {
|
||||
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "index", Err: err})
|
||||
return
|
||||
}
|
||||
|
||||
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
siw.Handler.SetFindingState(w, r, reviewId, index)
|
||||
}))
|
||||
|
||||
for _, middleware := range siw.HandlerMiddlewares {
|
||||
handler = middleware(handler)
|
||||
}
|
||||
|
||||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// GetSession operation middleware
|
||||
func (siw *ServerInterfaceWrapper) GetSession(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -2911,6 +2982,7 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
|
||||
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/previews", wrapper.ListPreviews)
|
||||
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/snapshots", wrapper.ListSnapshots)
|
||||
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/reviews", wrapper.ListReviews)
|
||||
m.HandleFunc(http.MethodPut+" "+options.BaseURL+"/api/reviews/{reviewId}/findings/{index}", wrapper.SetFindingState)
|
||||
m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/tasks/{taskId}/snapshots/{n}/fork", wrapper.ForkSnapshot)
|
||||
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/sessions", wrapper.ListSessions)
|
||||
m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/tasks/{taskId}/sessions", wrapper.CreateSession)
|
||||
@@ -2938,188 +3010,193 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
|
||||
// const string: with thousands of chunks the chained `+` fold is several
|
||||
// times slower for the Go compiler than parsing a slice literal.
|
||||
var swaggerSpec = []string{
|
||||
"7L1tcxs5kiD8VxB8nghZuyXKnum9u5XiPrhtd4/n2mOfpb6Ou9kODVgFkhgVgTKAEsV2+L9fZCaAQhVR",
|
||||
"JCVbsnv7vnTLrCq8JDIT+Z4fJ6VeNVoJ5ezk7OPECNtoZQX+45Ux2sAfpVZOKAd/8qapZcmd1Or0n1Yr",
|
||||
"+M2WS7Hi8Nf/b8R8cjb5/067UU/pqT2l0T59+lRMKmFLIxsYZHJG00wn8MC/C0M9Xwjl3hldCov/boxu",
|
||||
"hHGSlrYUvHZL+Ks/lnVtec1WgivLOHOtUUxaZlqlpFowriqmNLPOCL5i4kYox5bcMm6MvBEVm2vDcIQr",
|
||||
"PnfCXFlRalXZcyZupROVH9ctBWtoYTD4QisxnRQTodrV5OzvE309KWgd8CN+OPm1mLhNI3CBRqrFpJjc",
|
||||
"nsAHJzfcKL6CTf198hfc1Fv4jv688KPQv175sT4Vk5pbd4Xrv+JuGwq/LIXqLRPeZ2ujnWCc1VIJ5jST",
|
||||
"znpQTIrJXJsVDDWpuBMnTq7EpJhow9YwlHTMOm6cqKaTM9XW9XA7n4pJIytYiP9dKicWwsADK6yVWl31",
|
||||
"nnffWcedQMyyTCuGx35Bn8TJ+ru7XArmBz2CHcD3mQU5bq/HJr0RqiLUHjz6VEyM+NBKIyo4kWTt3YDx",
|
||||
"c9p02MLwVIqApN3h69k/RelgAek2t7G7NII7UfmzzR7N1pbCN7NNdsdzba7jiANskW7J/PO50aszRB3a",
|
||||
"45FlsmJ6jj8hFrklUBL8Ez5h10I0tmCcrYS1fIG/shc1byvBXuhKFIEM8XddidtpbvXJ9NsLfI5THVk2",
|
||||
"l8Z6VGRG2HYlgBql9YsNWFGwsnWMOxY3XbBWVcIwzpRY+7evusM9Z3xmgRnAIrVbijiUza52BKtWuhL1",
|
||||
"9vLfiw+tsMg/4IVzJlaN2yTMJIK6EnPe1m7Knof5mVb1htm1dOVSWDYTbi2QIC3Ta9V9iSPnF6vXSpjt",
|
||||
"VQEV1XohFVsvtRWsFrxiHNCSQCptWCgAhTPAfiC3QJqZmYyuxfZERtxIsRbmjHFGf3fDsCdWCPYefz0+",
|
||||
"35rFslpaB0u7wZMWVjDdupTbylVTi5VQTgA9wiYmRZxym++mDCcOUdVAUf6SmBR4JdbicL6dEvMFjP6a",
|
||||
"Rtz6/X2cYuvRi27OPu8a3JXJieCJ7TqPjsv1B/lB17VeWwbUhgiI2IN/hXNZc8s8R2Fr6ZZnbNG4k3/x",
|
||||
"eAYXKjBrpOcCLlKz8XRDQ8Gzjgek51XizwjkStxmD2iLPLd3kAyeIJOsClYCMis227DZxohzpg3uC1fK",
|
||||
"3NIAzOA9Qu1WOdnbeGQyyLOQ01ikNvo0A+bBjTG4KYj4khuDeERuk909gnTU4+hFeiVk7xPneLlceSFt",
|
||||
"cJtwD7YMyxKqugJ5IH91yxVfZCgafz5jFXec/fz+pyzuXUtVpUQ2l7inSs7nkzAw/JMvDMofADIAgeKN",
|
||||
"XWq4PVtTZ9FjJcyK55DCD3bG3tAbzOrWlCK7vIbnpEdY5BkzotEnRtTcyRvB4M0p81vmiv5i8CaTJGR5",
|
||||
"nrXW5toZATfezOq6dQKQL46DAleebYLMvL2Z+fyMLaRDnonobbha5HcToLZDxjJuxznvEpXgGPbKSXja",
|
||||
"Obz8vtazDEaSPnFFr38cF2a+oACU3V6OeHuLuxsVvpTmlXJms71juVAaJxmeM5wwPSRZQMD3Z2y6kI5+",
|
||||
"RqUFnhB/FbdlDawPENCyFXflEsRzvBAqaUTptNkwPtM3oo9vM61rwRUsEy6uPJ7I38QYVaC2IH9DrJ9t",
|
||||
"nEBhI56MVO6/fNdNlor/m1Ut1XUyX7KSgABD9YW7DhhM2oLN8cpCHY75EZmD16zjGxsoMZDglIB1Nny5",
|
||||
"EyTiqwC5RkvlLFN6vRRGFB6Y4kaWomBWl9cCAdzIpneVVRIYumdsOOGB8kJAk8tNI17iIOkvP9CA6U9v",
|
||||
"afAhuuI5+hk7QPtjLCLOjWDqT9I6WOIWrgLUA9o6sbL7FPuI9fFAJ9wYvhnnsyB7RmQtejyydzZGa5dK",
|
||||
"ofAQfssyQWdaVQJ17pkPNf4VUJbfKOCGYmthBDMC7n1R5ehmAHzcWRGBlc6fA3i0pfRh7bWm/awpvJgd",
|
||||
"+yZ773PYbpbOUcy/2qOY34cHh1v/UCXkAihLBVkWl3VkyTBjkfFx1loBCs5cl62dtg1MX5GGStqLW4oN",
|
||||
"m4lag1aqp+wtKExI7fAlkyAeok4jViOSwKbWPF11B9jGaPhz9FoVH3qgGWeC4xfslsHhw8SDsfAH2C1x",
|
||||
"7/0DrOPFEsSEDDZUlQSo27wIMJOKm02eS1eiFjs+1XV1ladzI4BF+eOCV9hMzIHykJDx4U7pLKu7tTaV",
|
||||
"K3lViYrkajmX+CcuV5AeiPP7Ka5KBE1GqxuhbT9bkcAuBUYE2uhRdGbT/ll0wN7mUyRZWnXk2M+XP5z8",
|
||||
"N+bErTvH/wIyIzPMX+vjUPPX+iGIKm5ddowd3BWXttR1ZclageoU2mi83Qi2dDhH9beXh1E6s1/eGLhf",
|
||||
"gm6xBesGRKTslkbglV8VDZOdGjhTZt4e70jsSwrtxaOWTf8dkYhcLB1ruBLMLvXanqf6RngRLbSewRC7",
|
||||
"c3lz6GzfRX7J7fUln/m3bR4/4QmxX2DZTetIYS5ABtOmEmbKLqO0CsuiL0iAi2z4IMEiXc9Arhg1kFx6",
|
||||
"AI3Ab+sc9rECBARBL3f4r9VM3752YpVhuA91yY4MNdS40egIQOlM8AKFkGJiuBNXtVzlfRP9S7EP3jDo",
|
||||
"GUM16daxJ2+4ua70WqGdCMFeLrUsBUjl6Grglv3jhD1hx8jE/sFO8Z//Ev7ZyPL6RCuUxP9xwv7Ofk3f",
|
||||
"+/vtr+l7XG3IJogIdFywsKCCrZebq1kN0npVMI0LtuyJ4OXSL+jI4kgdph4XbNXWTja1YE+cacl4HeY5",
|
||||
"BsG01KuVUJWo2BM0lHFzLaqwwSMc77hgPFpf7JT1gH3G8H9TloL8zNtti+RXGj/ac+HBCT2gW+i48CP9",
|
||||
"h5oUd5ZUjLC6vrkjnsWPRjTqnq/wIIJGannvP8uRNRlu7S68I9EBXuuuF/QKeBtIZJHMaX3N0JGHv4Uh",
|
||||
"0LbJ7XU40/hhVDqjwwONgChL0vsSDlurUnjdM3wpbqV1PXs5sT8EysHsjszgObBEk/Ve4KIteZ9Jh2yZ",
|
||||
"VwdbRBLkSm2bWwwuHTkKsZ3wGsybCc9Lkag7/VFGS6hTbfPaz0TFF7giNC5K9Zo+fzY8hgFkuin3LNfm",
|
||||
"hPGOYRy84sTEm0GR+1wklajljTBf5vqBPYwhnOfGva1uY+Xwih8VRb3R6ODjvYQPPhUTUAgPRPmwG/9R",
|
||||
"NLKkB7dXHcvh2COgwoEn0Wew9OAs3N9WqHCHnjNxy0tXbxhc0uR6ind2d/36GxwNK8lLcGGHl3qc8ODz",
|
||||
"H6yTDHvInUH8IOWe7EbIkKZ+3WRX2FrDmX/M6+R7jtaDRH4RIEUEAOStTfdDwiGuRTTD8fYi0aWfNcaa",
|
||||
"NF7CI/NjMamkXUlrs0JdckH0hhAwAFd2DaygGwP/tm0jjBXViJz4hqxSY7iNvpIMsn1f6xmTlfdAtg1c",
|
||||
"EPBbsX2cID3SMHRKVlYinv7dEGrEPf8GPZZkXpSW7nyuKlZzkBy0EnbKfvFRMJWcz4VJPKed2xBHL3pO",
|
||||
"1MRtH60eIUzC26XyuJXF/Dd8w2bCyxgUluPhYgRbyBuhDtBmxvTnv2kn5z6u650Rc5uzystyifETEhCJ",
|
||||
"hBuE1FwqaZeiQthZpmCsDUGTbHBopZOGAa6hX5Y1sPIpyRap6RlE3MzcPZm6E6u2bSCBzDNj/M/waMre",
|
||||
"ooM4RFtkB+ok88xQqTC/azVGNNrkBrhE+RShhZIkyJzIfjiz7WrFzYY9gcdT0j3wkX9wxkBTOT7AntLB",
|
||||
"oltJEUDc32EOJd5xJUZsG5Xh8+A7GGwMtKj1UtaCcWKwiCpqqI6TuahVRO+elLcBiFbfrEW/mwdGnhm9",
|
||||
"BkxbS1XpNVr48dPoPPMDMUHBIkB+UkVe0i3sALCGNRUdGPLgM06WsuE5A2AlbVPzzdWoJ24h3ZVYcVmP",
|
||||
"Pg2f5uJD1iH+D8lvydGaaIVjHD3KshLKSbc5J+O5g7cXwjGlmTNc1sJk2RLa2/fLT/Ra0d9jsuZ0c1nA",
|
||||
"kRrwZaLiRgEM5JCLNIPfB459NCrA1WRuRAVa4tK5xp6dnv5H+/Tpn0vgZ0eWLbV1+IM4o99hJPrhdPp5",
|
||||
"ToFO5fIAxMXvFT/fkdr2KOGFnpdezQxXI+bWEYl09ISMWGk39qjRV4eZb1OwdZ9tLThOdzeHvwfxmPyz",
|
||||
"Y28Hb2Br7SPrcLrU9YiX8U5WH46kV7YuxAiTfeU79j/k9zvs1sHUZ0Qp5E10ugy2ze1IINK+KOFsZEKY",
|
||||
"98iyeVvXrBZq4Za9CIVtsjs09jeGbPUsHN5ogfsgYPnF7T0Wm4uDab3aFzxMvH7Xe2N7+QMHKoWyAxDI",
|
||||
"Ks8r3oDcWuq2roDtN9xY1Mwqif82otQLJa1gVqpSkDjmo7sLdi02ogLhiHY/zdka4YSVy5/GSlucAu5Y",
|
||||
"EJ0LEBiF9U6Cg01hfXz+tMcU48EYV5Y7ivfxUhlwfOX1jC7gNonJTa8Beq3SAm/T1orC2yGlWqD2G8yB",
|
||||
"R5aRp3Fbvs35BLaBGMJXu9DG81Tst9eyaUQ13cWm70LxIjCNQdgPSALVGVsvN/mQbS/1fwnj0Yhq9uqw",
|
||||
"TRth2zqDkJVWIjUYE0Tb2Uq6K29C5mbRou3lrBO8g0PjOCg3lVQLWzB0JZAoLm6EkW5TsJI7scAgFidd",
|
||||
"LQpWCcdlXZC3G9VI2ZMqO4yMQXtqJG8iaOoDVYgAQftKUM460Xi8wyQXDBArmNXwj7B/ZrhKQ5i6mGeA",
|
||||
"1aTwhw48jaY5MKKJqGsQ45z8+JJGT375IUyU/HYR5jw4e+Me6LI7Xjc5lKyBeyuONxi1g2ttj8DgQ76/",
|
||||
"hsGkMXrV3Ndksi0/42gHuM/pvSwsQqzvPV2negWEbJc8J8fTU3TkGCHgLZ/n0HDjFcA01OzIsr+8ev4y",
|
||||
"pjutuWWOXwv15bjsDe3pw7jP+sii0ck6n5oW1bjoaIJl+bva20CNCMo0xTdaF/fF2qaLOj4g9mNMQN9e",
|
||||
"8DttUVoZKEphmbYgPH127pnvHJPwxNyegrRxCq/b04+e6j6dwnenH9Wnu6pJxSSc7chDuVjkos5ca9SV",
|
||||
"UNWZ9w56DU6o6kTPT9AASOgzjea6Jx19URod2jbo89Ty7bSuWcnr2h4X+I8r+EfBYA/LdsXJtAg4Yhhv",
|
||||
"GsENxcxo1UGPEA+EMMFNLWGBrawrmzLuEA5YxM3An2E+4F1hQvgdpjuQkweivCTgvYnzDB5ctka9wlmH",
|
||||
"D7SuX9AaBk+e2+u/+BUNv6EF7uHNGXasJj0+kCBESnAdKuznz45ng+fgoPOEax3yWmIuunVN63xSGj5h",
|
||||
"CwH0MNswAPF5TKW05Efmde2dzSu25Dci2lKn7DmFqqQ5p+JG6jYM7GkPoyGehOjYmCG3EAVGuBz30826",
|
||||
"6aMJe2akmFNYweEXw4zbbOC28GZuXMGRJUc86diC5pwGDowk5dXwY+94IqaN2W2gTSJ50W6jjZk7DNAm",
|
||||
"FxM9Q5kMNluwdVwBcMB5rdcpz/QLWHHpPV3es92l3vm1Ttl7MUeDIXqRkmmIypdcDVcIc8tyyXi95hvb",
|
||||
"3zNLthpmQIAHciaQgFjBpTqUUmHW77kV78LH8Zc3OMq4XB3xNobceu9F2ILV6Dmj1CeSeT47A2zU8tNf",
|
||||
"21vlbQl6zp7B/fXs6VOQaw0vnTC2YK2SH1o6jSTsH446H0saBZV9MJDKOtOWYz6/vF0myIG75JwYtLoV",
|
||||
"ny1vxBXehnkVgDieT+LOyYWvEO50L4Vcb1ToQbwxvLyWalEwWdWiS6mUCjNIqoOV8V4mfoYXVLMrNLcc",
|
||||
"GGKaZEkfHkBdA6RwnxkoPE/3LywrWwMUUW9C4n9esPDEPwL7xpsgrjqf1MEWC5vxIO2x8HTokjFxkVC4",
|
||||
"FRNGGqm3VJCH52AbDgHmCn1QGeJA11Qom6D0umC6rmARwUxyN3sOjPdezLOxTVZclbUMJ5vVg81dhe1M",
|
||||
"EYftTf5Frxk6/nncKcp+K75hC40MRrcO2X9SL8L7cqVjZHZieL205XUeyX7Tq5nMke5FoEcfUxiW4C8z",
|
||||
"VmPyIeF0QbkQAzRHQl9yiyUtwrHQeR90KP8Hl7bXupbAP6H1LTpO6Knos7b+GfdpeYiJfbLZ5oAdQPOH",
|
||||
"vE23OY58ye11JiqfW3HlkzG3MGqHY6OstR0rrRA0OJSDQBJZCbMAscKwStqSG8+ID3TC6Dpn93itKnHL",
|
||||
"pPJmgJ9fB8Gr4bVwToD6YeVCkfcKJWCMhOEMBmwNIBCdGH2G+iSFMoCQg7EFINoitQAT4Audx/aH8CwR",
|
||||
"mLLe5l+WAkUOzl7SWz1hL/h+cU+UUUFHxTbC+YRGb/MLhWLWXDogwphslvCFIvwL5c8Kxb/+DGwm4GUj",
|
||||
"Vvomn0N2jzocKgSjRiOAN0/Qm4y7/FHsrKiRRMlLkrWTEeGTTEUMJOY7lcMAVL8imX57CUQI3oz5oeV2",
|
||||
"GW02KiMv9yzgDqX8NbI+X49EE2Vl10cL+dCKdheZvoG3cFATC3b4SAaVZDNIlWoOkaalWkSY+QhhQUuK",
|
||||
"+p4vpgOKCiDLUtR3IH3aAxqZR44THkVc6QGUdrbi18KeJeHR+EFXASG4QTDzjjbsjd3uKpl9FMJ7KLQD",
|
||||
"b0qfWFoBiM8HKVC8y0aMxIP4ea6A1115pWnHnAR/hIlEy78/Qe/Y9Ipo/8gDTfuHfl2ebUs3vjb64Gp/",
|
||||
"ZgqZCgjcocZGpENuPWPCtd6JDEH+u8or5pfRNBAi25PQBvgNo4PYkzgEEFT8x7/++3HBGm6BdXrQeVMz",
|
||||
"wEqYlVS8hpW/e/v+cqesPZ6f4B1pu06zi/rXSgw20hnPunJf+WPKpBDi1TfxNOYjIOlqPlAVB3mCtLzn",
|
||||
"Yazupzdh1O6nl934IH0Fot62uuhKX0mfXbSH5C/fvnw7XVE4Wl/kSI0fd0KooFfnZ4fdh8mjsSVOPDf6",
|
||||
"N5GKG+zJQjgAwS/h3aXH9VI3m+PewrhfvJfA84la3fquTKtGrtPwCvE9WXlLBQ7vEQktGYNt2CU3YjdQ",
|
||||
"rqzLFkfJTJtCikwOFGkKizCtGp8HrZl3iHPpJ2ogWvVvjijHDscvOuk3ZSRB5ExyYcfCY1Ii7i6EnvyW",
|
||||
"599jAjolMtu8nJ5HySBAGPKLgM7DTaxgFG/kE+RwCQPxMkbk/X3Ro6BIiSLUEurssJ33FatO5L3k9R2y",
|
||||
"UpIE7lxEvBG5jWMVi3m/cgKQ1pyQnQQkLG+AyFfFjexf/wDJPE7gOsLWRo9vxMU5xu2GMW/42tjgl5TM",
|
||||
"Gji4M1wRSAAnPd6EBRaTcEWBYqxm+jZJNYo0czCnn12mk13y2Ys43yWf/eCnhPe6WS/57LWf+JLP3se5",
|
||||
"L/ksBOn96nf2y06eGxkUT7ls6rT0FFmQIEF+zCW5MQchqGIulQyFobanCgt5Gd+DIf/38zc/Te8QaJk3",
|
||||
"miZzZ0/YQ+7CB30Ng6ZqKp0hlVzB+T/LCRxGr/e+tRVGVKMBQo+koF3qSmeqpWDu37jcss1PqORovLA5",
|
||||
"OTwGdU0SiSVyjsNMbbrSmJC8z67jFx7GH9txPrl5pquNL9q5nf8LA6I9vRLK0eUfdeLR+EH/0bw1CDaM",
|
||||
"aMua9kQ5jrTolfW8UAm4wx1bCl5h4SCsiRTWl9EnpR3LCU9ktPGFo3gNyy6i4ALniv45tPH5tKUAHR+E",
|
||||
"fUDSRri6AeYdAMYO7ALEvWzIednanK1X1KkCCH+Rp3rNFWhnmvlP4+pBBy6XPNpCufKXjLToqh5T3kaD",
|
||||
"SQ5Va0m21STRHgq4LJy8PXoLSntiTh6+pmxutT8rr5lkeOEqn6Hygh4EW0oq5FA+sxGg1mZNLXkNbk8R",
|
||||
"mCjnDEpNYpkynJJyp5Kik6icB5GiP2AxaRXtzWG6Pd6wiecKuQiwRmUBGKOrSkbJRLFwt7TEtfKz9WuK",
|
||||
"TQ64QFbebB0kj2R9/cox6cKyR+5TVR8sbeTg7I5iopLcsKsmJIftuoO2s8k+J0kku4IxoP2MFaK2QXdv",
|
||||
"yAwWfnAay7b8lAtTCwIdXiB1DCKgiK5EfJ/52wSm9qZrjDvxUamgw3ptGm5iUm+DFdJHqKDHaiWsD1oY",
|
||||
"RJtcBHtOKIEWpT0MVBjKjskuPmbk5oU+gR9P7LVsTkLC7QnW2QO8dqYVZIpZ3EE5oticOyXp+xl2nc8F",
|
||||
"uQAyxLZrk3eXeLvBcqvxvrjMItwYzg7rvQRHmhc+gtOMDg/+ik/8DZSv/HLPMu4HJzP5shB+Y9uw+IRs",
|
||||
"fa4TXXEy2xiBV6SheumTp9On06eUzy4Ub+TkbPLn6dPpn6lQ0xJBd8obecqrlVSnnc1vIVA0AhAjN3ld",
|
||||
"Tc4mPwp3EcwbvUoSf3r69Iu1P/AzZPofXAhzAwSJ1eKRBlvLF2IK734qaB+zWlNBpkbbjMn7wmEB0a48",
|
||||
"rTastRgjFiuzU10mECJD8Kvha3r/nOzwmwaF4Eo4UQaDIYWIbZygXMpVa0HsY+/+9mPB/vru1Y8F+/H1",
|
||||
"D0wb9ouYvZuy545SQP6NvZHfE+vow/rnGLs8IbwQ1n3vlYkEzris04Wc9wEcfSVddbAtZKNP/9mIxX2/",
|
||||
"bdS9P12LWXPXb3sE4lnjAA2ffTE0RMhnkJDCobQRFQNcmwJxfUfonxsuru80puh89+zPB789QOzTj/C/",
|
||||
"19WnXSQacWYngf7BEGf7EAGSR5ZIljRRb2RHECGR+8P97g7Hhe4tvhJOGDs5+/vHCciKoTYe3YQTOsPJ",
|
||||
"EJuLZE/DHfwa0IBKfibHn2ORJ+ggoFen7BUIPvgPalIhvedMfDin0uSey2F1VJBm/nrx9m9T+mcQgn7i",
|
||||
"1p3gLyevX7In3i5Kzz60wmxY3PYx1lkwAsRVIBJSfTFKJ6wITf/+IixCLUTugo8qlDU1ArM2zulwasGr",
|
||||
"/45w6qon+je91lvyusbUpa5IatwGFQ1QDOursoX2Y+iuAmqOCVPWIA0xyR/tUvAKFTF/uD1ITYocno5F",
|
||||
"730q/JgI0W5ICqr+IkMlCTyjuDbyqW8ZsfVdl/7/616m48StIxQ+8Q11epS8l24JRenTexBnICIybo/R",
|
||||
"0OuuakeM5qMoPvaDrOH0meLGeCeeL+5jEJl4EiidNgO5kWKdw6+fpHXB4J1DrsER9NxWdz7Azzj7kLx1",
|
||||
"2NWZFlg7ACl239GHFykbMeZuIdEb7kqywmFhlmCNlYbFhU7ZS7L0oFkPq7H46iVDLDrtbS4vdD7HkkG2",
|
||||
"l/9Cc0uKjsdUS2pPwsLLBvXVXrYavBrybFBplVSfo6qF79hF7n7g7rGmCbLRVoWyRTks9NXiXkeXz5iw",
|
||||
"eX9pqleV7iBJ7mtjCYgIvop2PPs7S3p34E7w9r/fl5edfoQl7pELu+0/oPaWwDgPUzRNR2mro7iHEbUI",
|
||||
"LPcTtchqMQbNN+IhwYhmzREIgpgD7AuFFg+INrNIsvD5dX55kk6MiA9A0PcFTiBZD5y7UWxHVStxOg8l",
|
||||
"ncYwgGo+PeA+aYLMRmPfjxQVjuxWGec+dgybrZXaVDZxY1EVttC0rhurK6FdeIOpYY7PptvScgqTL49w",
|
||||
"CTgeD9dGzyCxA1DLhYe9HnqoCaL4qZcEdhm7hKps0nHQh70PkKZTlgryF1LzmLRpWs75y36JRcXwVWnZ",
|
||||
"SlYnvhJwr37fUtQULmpD2r2zTIlbah6Wk0h8Nu1PoUvdl0emfkHGg5DqT19s8l5TyxHc6sE/BejaFzLQ",
|
||||
"JsIWk1k+h9chQoXEt1HF6HIX8sS8uaHO9JzCy9AXz0LHQB/DCzhBQd8UiEDl7bZ1JECEkAc0eQxRsX9C",
|
||||
"h0mLXSfMPnBTpxxRa5slVme9U2tYOjLWieyDf2e5yJwosO1tfBjaynk1/xMKCHA3nkQpYeclu05inOiy",
|
||||
"ldQHD9XQYdFHymJLvtmQEzKpAknMOGHCG9TzkiGlKzrm7GPouWIr6ndG5i8/c8mt8DbBpV5TaQbaxyyH",
|
||||
"TBfCdSUvHwaDuvEPwpzvxiAvqs844jTtNSsDAmd61+XyPTxXCjGQBzCk53UdpLYoBnohob8JugHDyA90",
|
||||
"nL3qf4/syIlAy/MDYLYeUF8AV04/+r/26OIpvB+ICe7Zd3/PX1rvjlC4n+qdA+dpzP8fJcZLn0b78JSI",
|
||||
"ibEHygWdSRh3MKz09y3BvxjRJIhybS8jqssEiMH00Q8+Wi5k6zKhkS+pqe1D8J4kxP6RGQ/hyDjXAUA+",
|
||||
"mtaYpygfqD0q7ZP8qedpDHboLRm8bmNnje2DsEkbdbvaOvofhcNI8QfkgTj+DkMk9myQlrLh5ry2oqsl",
|
||||
"lolFVzoA4vfENnWlO9751VhHSMRAt1iag0fdozDxqYtd9kaCmNdXpLpil1fZi6RnlMLhA8xjME8oUNRP",
|
||||
"Ue6l8fsPLV/RSHlXMDcuxvk/EK+KUel/PFZ1X/9H0LpPP/q/9kheQal/QK5ziHnHL/aB+EgExf34SA6m",
|
||||
"p96KcxgTuecCxpjIc3vtsyqSCk2x1DDxlCVXlZ7P2ZOF5nXBKlFKb49C7zX8wismVGULMphEF2lBNWCP",
|
||||
"ffsSCkVpjfIvp51KpI0mrMr33QX6GNRERkaCrAVDAbHOl+ddkWOF5XpO12daGf7zgsA/ir6Pa50Mq0cw",
|
||||
"JbnT069J9qcYNW3a5usg6Svl3Sm+rBZZt7cO8nVY5DdylBFoeI6x1dnXPcmec+OxD/KCSJVImzCcqFba",
|
||||
"zgofS852nAGYUzDJeXGnY1Whck6sjTNwklDVFZ9ZwLEiYhL8gYF2ndskFE+lFvyd9XwE5S6EqroCpX9I",
|
||||
"d0qonTJgVN+aNJNU/N0jx0SV+aFUpx0iYSIOfmnJhXZ+P7GlD7zTsquLkFVwX3VZQ/3yzNGe8STmnhWx",
|
||||
"JECr6C/KP1BYPBJ+h/2KEp2n04V0cqG0EccMM5hhsKJfZeEORRbGFWwdKg9hgwpkSdTfjF32S3brtcIs",
|
||||
"41ty3wIHaUtMZ8JQ3FDfoOhKZUvbb93qy34KXu3fCIVRY4WJsUIRYeW7ykT4AvVONFSls83KRZ4UuuIC",
|
||||
"AxwbqXfudNiGD0aizYVz6DZUMNvCYcA5os8o8OwQUb0UvJqyv2nHeF3rdexC5jeDVQZzIZVwZvtiMYeL",
|
||||
"TyCkzry3JmBNV8dAqkHbXcO9J4mHUsg3/XzN7AJD1fnMGntxxpmyLuHbsz6Qs6c9aC5M53MUi071CzZr",
|
||||
"RR3d8kGqovncAOXPtDN6JMywzXfCnKAxDJSRby2W8LEZsz7hrVv6YrGjxkd0lNpQfCngLgX5hyaAUYDq",
|
||||
"MfCuzAl7gi7TpBJX+AR50JJXmAuGznYKke0kYNaqivyv0mA+6XHXEd3zCWmG1qbnIV17k64WuLKvQos1",
|
||||
"BGnxaYs5xtmLABZRncDnvsNcnue90M89CB/F8Zj06DvQ69Ed8rcvKVRyPt+Jiq2ScykqTM0P5bCQmmfC",
|
||||
"rYVQjGN+Pl130ROy6F1N07Gr6yXMflD8v68BcPiGxzIBqC7RZ4/jD+Szx9F1Fapr7VTXHi7KT9YCj2HM",
|
||||
"hcddufxjM+1KmlESeasEq6QRpdNmMyg4GMSMIr4hhTd3FdRR07d8mLJXyuFD+FeFUjQGtdRi7phu3TmL",
|
||||
"cnXFhH8XXgjlLsjahlWigRFjZykUhGrBb0S/jw3MbXS7WLLplFJ17GZVS3Vd+BYsrc0nS0SiNftkzYua",
|
||||
"2+WJFfCSIxWh5uR10P0iZEZrF+rswFq6An/B2TYm83xVqnkpzU/Suh05lhEpsGsMntgfnYqwyN9hZq37",
|
||||
"zDtm0/pR3vT99yQOwUVWiQrO37elBuRcG+lEp1n44s9FV5DWl+719dTs0AiGCaKYUp8ax0P1NfKkKkFt",
|
||||
"41C8W0uLpfispuppw5LS3hIfSr57kRAbkFe+Gs1KcIUuvnNWGd1YxtWGomNRIDsPMXVD+z5b6rW4Qeua",
|
||||
"n4VKcNmsRQAG8ep5qF3lSzVG5tN9mFQuZ0nrKnJ3FrGmNnzoE1mNWHGphj7LtHK2n61gvDaCVxtfSjvO",
|
||||
"5MVaGxycvj6uPacGLUld3RKUorZh1MkOtk0cFcVWepbjf74ka9789KdHMz8BMsq69tss/J6q7erm069j",
|
||||
"utuXqJ3gg0gTrTmz4sMwhPodJ42i4dYGN3nNscnEBzbjJSr/+H0+fBqAuTOJ+eESjrFBQW+oFb/1hQaf",
|
||||
"Pn1a7K47+CjJowiZQ9San7FNHDUW8Yf2JBi2/u3p0+NvX9EBnWUUJZ8zIxZtzQ2pNnmb6JRdiltHMpKv",
|
||||
"f0Td8ziz8jdB0DlnVIqB/Kr+TWDc/uAeXDoDef7Limd7BLA73dAPqsa88AON8E44kT+2CIZlRMaJIFYG",
|
||||
"AhqIAY1bygwI57GgZ2gjqir/MXeOl8sVVbtATJcWxHpdtw5zwMdw7dxfA0ZYXd8Ar/eIj8bdUFRIqtis",
|
||||
"tKPLfqWhAusM4ZIu/tePBSvhfqReibdOKIw9Gaef1ytyFR5Sk+DLE8DvtTSNvVn86+2q/uqVbQB1Q2Wb",
|
||||
"e9H6n/+YnAEVhcdXzTDzK2EySR36mLKp1VyaFaho1D3bkWssps5hxJGvS4tqkR/mRKp+lV2pbqQvNeyo",
|
||||
"3bX1Cgb2LMlFQDEFaFjXm8L3wMeeKSMKEvYMAa0y+Nlg2MRTmNmjVnvcnMEqPjCqeyt63myODJoM7j1z",
|
||||
"VGeWLljT4jakT6eUCwDWnFt3MtdmzUN2dLewcinKa4AkNvfwp5Cqizk9MaMXUpXaGCqyo2tOqvF1tWBD",
|
||||
"3mdwWj4ZNO6BF6xwx2nQrG+p040gFcyyMMLa4axpu6ajLe1+qJ101YelZXN5GwriYbpw1zYHt4d9pexW",
|
||||
"UO52dIy3fcXVBnUXmwGFAlJmIZz3Exah0anvXaMV4CuSBv3SpSzI2Fxs2OBm8B5B9yzpchPtJrAie01t",
|
||||
"XvwaHXviY3jk3FdmCQnRx0UEaVgfAYIljc6ooEu6ynAqYWpsvO0j8IWqBg1e4mvYII5wYtBCwuNFdyjq",
|
||||
"OGdr6CA2OBbMOXbpcQTjxwCSU/ZKepzGM+qE9qFJI2OqCCEEej2SG24Wn5swku8xiQkAwxKue/tJ9VpI",
|
||||
"Aczg1TM8GSpyzn2px//6p6Q9K8Cdbg6y8xKc0PqTLauOzzPNwbhUwwbBfkE6tLkb791Lh3UWMDh+kKGG",
|
||||
"c8oY7RFZr882xzLFW61KdvWI8CcJO3tDnye/+Ga92Os6U/n08aLLdlmiVlu9urTx/cp+B+Flp0FEeHyZ",
|
||||
"JwZ096zRkZnuQtsOT5NgqSLRmLjaoMBUy9JZH1C5EAp5iFow/ycQnqzhLrrwUyfLScPM8S6yISR0zJi9",
|
||||
"90obZWWvv3oEsId5PpT7G0fhJjSo2WVopaZtW+gmbhvscomCJP3jCl4tEpckp6vYCnPjBVAua1tyxA9z",
|
||||
"I3oh//AxU+1qRgEiIC0fWbbU1hVM3wjD/nJ5+e6iGApFMt/tdKRe4LuuJc9jZJzjZIenutLavn1L6CFo",
|
||||
"E/01+GqBV3ojTBIu2ROEs6f1/hEP6/2dzup3c1SZujTbcP7mq8P0OM9IrZxv5yj2pUao/kbGAqh90aeK",
|
||||
"lGy61FeN6yc95XMWKJUgzZH58lkLfvSvkx5+yM2cuM9/H9dxbC96CGONLw8tCuHW5bFR4Z269l0LQDEH",
|
||||
"Kqc9hUs4t8rTj+rTYRfxuY/CWQf/vLfRuyV3Y5f0RYTDY/CjMNvBlarC6n4HzD8sFc7rdK7N9YMpK0V2",
|
||||
"IHXIGD2P9b48+JBQ7WMWcoU0UATvvE9J2D8RRBHrmVPx3x4vRustIJhUrbdLJgP4UlBa3QhjeWhXKB2z",
|
||||
"TutY4KEfnE8tt3weSZgGzuKYrH8JmzpKuXqoLN+LkaYqgGgu2hXoQh3oyXDmrQ5hTSEMyPgaT35PYXyO",
|
||||
"jU193XmZJdEftLm+6PIb/l9e/V7GHluDjkcP/CJmF7q8Fs67P8k6mobl+zEKxhn2tWJ2KeqaqpWlztUk",
|
||||
"zqzUtQW1C9mvnMem8EGrZpRMj41hCM1I3liHEqmx7fYSu5eXQrl6wxoDJFt5Ez5aG1vXtF4yof5PBeM1",
|
||||
"duv2wQwhJ5XmLGtJRIfSjfLfDl711gcrfyNbL5Zuiw+XuqaGiyxt5Bl9MMKDR9zK4OWhfXrbMZJBqSvB",
|
||||
"vnv69Gm0MMPrjBr5xFA6we1Of2/SgvUAl2/o/znKFXf3Es0Pit1E7z3o0Kn8jIh4ILxigdsOLTuEbYx2",
|
||||
"utT1Hzsyok3aJ2ZpPNYdHsr51IYNLx50AoyZ7iotrDpy2VSI2LzxIQtDhjnGikP656G34fQ/2QmnjepH",
|
||||
"pfOxHvWHdE9mP3EQjUUlvekVezkKu9U4vtLqyHkwj1zSP/bb4D90snOcZyyqAoCg4g6+QdE5LG23peSX",
|
||||
"+NZjqCbD7oEHaCiYl502fFQY3IDIFN/rbLSp0BIhcPoRnn0iBAcM3IbFS/x9HLsyRUXpk2rKsNAg8Tui",
|
||||
"DenYtRCNzwksdbO5F36MJfvfjwY2fBiMdFD40KDVJnXRHO9yjjyAqyTmzSfwhKIVD0QpvmHk3Yxa7Q7N",
|
||||
"DMPympqXSTAH8irUw8heVQsQXseBgZkPf714+7egGWFJMRu9wcAHn4H88ezp0543OMGo4FsnzOqtZIhj",
|
||||
"Ga75ru1jyyFWs4MQ5fHKN2/xjJ3V9wcM+U7VWz99+r8BAAD//w==",
|
||||
"7L1tcxu3liD8V1B8nirbMy3KuTezuyPVfnBsJ/FsfOO1lE3t3klpQDZI4qoJ0AAoinH5v2+dF6DRTTRJ",
|
||||
"yZaSTPZLYrG78XJwzsF5Px9HU7tcWaNM8KOzjyOn/Moar/CP185ZB/+YWhOUCfBPuVo1eiqDtub0H94a",
|
||||
"+M1PF2op4V//v1Oz0dno/zttRz2lp/6URvv06VM1qpWfOr2CQUZnNM14BA/4XRjqxVyZ8M7ZqfL498rZ",
|
||||
"lXJB09IWSjZhAf/qjuXDenotlkoaL6QIa2eE9sKtjdFmLqSphbHCB6fkUqgbZYJYSC+kc/pG1WJmncAR",
|
||||
"ruQsKHfl1dSa2p8LdauDqnncsFBiRQuDwefWqPGoGimzXo7O/j6y16OK1gE/4oejX6pR2K4ULtBpMx9V",
|
||||
"o9sT+ODkRjojl7Cpv4++x039CN/RPy94FPrrNY/1qRo10ocrXP+VDLtQ+HmhTGeZ8L7YOBuUkKLRRolg",
|
||||
"hQ6eQTGqRjPrljDUqJZBnQS9VKNqZJ3YwFA6CB+kC6oej87Mumn62/lUjVa6hoXw79oENVcOHnjlvbbm",
|
||||
"qvO8/c4HGRRilhfWCDz2C/okTdbd3eVCCR70CewAvi8sKEh/PTTpjTI1oXbv0adq5NSHtXaqhhPJ1t4O",
|
||||
"mD6nTcct9E+likjaHr6d/ENNAywg3+Yudk+dkkHVfLbFo9nZUvxmsi3ueGbddRqxhy06LAQ/nzm7PEPU",
|
||||
"oT0+8ULXws7wJ8SisABKgj/hE3Gt1MpXQoql8l7O8VfxspHrWomXtlZVJEP83dbqdlxafTb97gJf4FRP",
|
||||
"vJhp5xkVhVN+vVRAjdrzYiNWVGK6DkIGkTZdibWplRNSGLXht6/awz0XcuKBGcAibVioNJQvrnYAq5a2",
|
||||
"Vs3u8t+rD2vlkX/AC+dCLVdhmzGTBOpazeS6CWPxIs4vrGm2wm90mC6UFxMVNgoJ0gu7Me2XOHJ5sXZj",
|
||||
"lNtdFVBRY+faiM3CeiUaJWshAS0JpNrHhQJQpADsB3KLpFmYydlG7U7k1I1WG+XOhBT073YY8dQrJd7j",
|
||||
"r8/Od2bxotE+wNJu8KSVV8KuQ85t9XLVqKUyQQE9wiZGVZpyl+/mDCcNUTdAUXxJjCq8Eht1PN/OifkC",
|
||||
"Rn9DI+78/j5NsfPoZTtnl3f17srsRPDE9p1Hy+W6g3xrm8ZuvABqQwRE7MF/xXPZSC+Yo4iNDoszMV+F",
|
||||
"k39iPIMLFZg10nMFF6nbMt3QUPCs5QH5eU3xZwRyrW6LB7RDnrs7yAbPkEnXlZgCMhsx2YrJ1qlzYR3u",
|
||||
"C1cqwsIBzOA9Qu21Cbqz8cRkkGchp/FIbfRpAcy9G6N3UxDxZTcG8YjSJtt7BOmow9Gr/Eoo3ichyOli",
|
||||
"yUJa7zaRDLYCy1KmvgJ5oHx166WcFygafz4TtQxS/PT+hyLuXWtT50Q207inWs9mozgw/CnnDuUPABmA",
|
||||
"wMiVX1i4PdeuKaLHUrmlLCEFD3Ym3tIbwtu1m6ri8layJD3CIs+EUyt74lQjg75RAt4cC96yNPQvAW8K",
|
||||
"TUIW86yNddfBKbjxJt4266AA+dI4KHCV2SbIzLubmc3OxFwH5JmI3k6aeXk3EWp7ZCwX9pzzPlEJjuGg",
|
||||
"nISnXcLLbxo7KWAk6RNX9PrHYWHmCwpAxe2ViLezuLtR4SvtXpvgtrs71nNjcZL+OcMJ00OSBRR8fybG",
|
||||
"cx3oZ1Ra4AnxV3U7bYD1AQJ6sZRhugDxHC+EWjs1DdZthZzYG9XFt4m1jZIGlgkXVxlP9K9qiCpQW9C/",
|
||||
"ItZPtkGhsJFORpvwX75uJ8vF/+2y0eY6my9bSUSAvvoiQwsMoX0lZnhloQ4neEQR4DUf5NZHSowkOCZg",
|
||||
"nfVfbgWJ9CpAbmW1CV4Yu1kopyoGprrRU1UJb6fXCgG80qvOVVZrYOjM2HDCI+WFiCaX25V6hYPkv3xL",
|
||||
"A+Y//UiD99EVz5FnbAHNx1glnBvA1B+0D7DEHVwFqEe0DWrpDyn2CevTgY6kc3I7zGdB9kzIWnV4ZOds",
|
||||
"nLUhl0LhIfxWZILBrc0UqPPAfKjxL4GyeKOAG0ZslFPCKbj3VV2imx7wcWdVAlY+fwngyZbShTVrTYdZ",
|
||||
"U3yxOPZN8d6XsN0inaOYf3VAMb8PD463/rFKyAVQlomyLC7riSfDjEfGJ8XaK1BwZna69uP1CqavSUMl",
|
||||
"7SUs1FZMVGNBK7Vj8SMoTEjt8KXQIB6iTqOWA5LAtrEyX3UL2JWz8M/Ba1V96IBmmAkOX7A7BocPIwZj",
|
||||
"xQfYLvHg/QOs4+UCxIQCNtS1Bqj7sggw0Ua6bZlL16pRez61TX1VpnOngEXxccErYqJmQHlIyPhwr3RW",
|
||||
"1N3WPpcrZV2rmuRqPdP4T1yuIj0Q5+cprqYImoJWN0DbPFuVwS4HRgLa4FG0ZtPuWbTA3uVTJFl68ySI",
|
||||
"ny6/PflvIqjbcI7/BWRGZli+1oehxtf6MYiqbkNxjD3cFZe2sE3tyVqB6hTaaNhuBFs6nqPy7cUwymfm",
|
||||
"5Q2B+xXoFjuwXoGIVNzSALzKq6JhylObWpv5RTQr9IWnWyAA4kE21xfY1mLFTN+OWQasz0St/VJ7r+oK",
|
||||
"GWCjZkHYdYLlW+XmSmykM9rMc2kEJyre+vtEEV78t/wx//kmjtHb3k/IfndhnEwq+8SEDpx2+B7+WgQv",
|
||||
"MP7CsXZYc2a+M2iOHzQc83fEgfR8EcRKGiX8wm78eX488UU0gDP/ptsklK3Nk0MAuJT++lJO+G1fJn94",
|
||||
"Qrcb3IirdSB7RAUirnW1cmNxmZQBWBZ9QfJxuuWOktvy9fTEtkH70yUDaAB+O+dwiNMiIAh6pcN/Yyb2",
|
||||
"9k1Qy8J99lAyzMBQfYMG2nQBKK2HQ6GMV42cDOqq0cuy66crc3TBGwc9E6iF3gbx9K1017XdGDTDIdin",
|
||||
"C6unCpQe9ORIL/7jRDwVz/CO+A9xin/+U/xzpafXJ9agovMfJ+Lv4pf8vb/f/pK/J82WTK6IQM8qERdU",
|
||||
"ic1iezVpQBmqK2FxwV48VXK64AU98ThSi6nPKrFcN0GvGiWeBrcm30Cc5xnI/VO7XCpTq1o8RTukdNeq",
|
||||
"jht8guM9q4RMxi0/Fh1gnwn831jkID9js3iV/UrjJ3M5PDihB3TJP6t4pH83o+rOgqBT3jY3d8Sz9NGA",
|
||||
"waLjij2KoJFa3vNnJbImu7jfh3ckmcFr7e2NThc2MSUWKYK11wL9pPhbHAJNx9JfxzNNHyadPvmT0MaK",
|
||||
"ojq9r+GwrZkqVu3jl+pW+9BxRxD7Q6Acze7Iy1ACy1HXFwKXL6/9FjMyFV8dbXDKkCs3He8wuHzkpCO0",
|
||||
"ukG0Hmc8L0ei9vQHGS2hTr3Laz8TFV/iitB2q80b+vyr/jH0INNOeWC5vqTrtAzj6BVnFvQCitznIqlV",
|
||||
"o2+U+zLXD+xhCOGYG3e2uouV/St+UNJnm9zRx3sJH3yqRqBvH4nycTf8UbJh5Qd3UNst4dgjoMKRJ9Fl",
|
||||
"sPTgLN7fXpl4h54LdSunodkKuKTJs5fu7Pb65Rsc7VbZS3Bhx5c6nPDo8++tk+ymyJ1B/Mj1FmRIY143",
|
||||
"mW121nDGj2WTfS/ROJPJLwqkiAiAsjHvfkjYx7WEZjjeQSS65FlTKM+KJTyy7lYjVs6KQl12QXSGUDCA",
|
||||
"NH4DrKAdA//t1yvlvKoH5MS3ZPQbwm10RRWQ7ZvGToSu2cG7XsEFAb9Vu8cJ0iMNQ6fkda3S6d8NoQai",
|
||||
"H96iQ5ist9rTnY+KrQTJwRrlx+JnDjKq9WymXOaYbr2yOHrV8VFnURHJqBSjUNjsV8atIua/lVsxUSxj",
|
||||
"UNQTw8UpMdc3yhyhzQyZJ/5mg55x2Nw7p2a+5PTQ0wWGp2hAJBJuEFIzbbRfqBph54WBsbYETTJxohFU",
|
||||
"OwG4hm5vsYKVj0m2yC37IOIW5u7I1K1YtWtiimReGON/xkdj8SP632MwS3GgVjIvDJUL8/tW49TKutIA",
|
||||
"lyifIrRQkgSZE9mPFH69XEq3FU/h8Zh0D3zED84EaCrPjjBXtbBoV1JFEHd3WEKJd9KoAdtG7eQsumZ6",
|
||||
"GwMtarPQjRKSGCyiiumr42SNWxuidyblXQCiUb3oMGnngZEnzm4A0zba1HaDDhT8NPkmeSChKBYHyE+b",
|
||||
"xEvahR0B1rimqgVDGXwu6KleyZJ9tdZ+1cjt1aCjc67DlVpK3Qw+jZ+Wwm82MbwSyW8h0VjrVRASHfa6",
|
||||
"VibosD0n30SAt+cqCGNFcFI3yhXZErozDstP9FrV3WO25nxzRcCRGvBlgg4HAQzkUArkg997cRNoVICr",
|
||||
"yd2oGrTERQgrf3Z6+u/r58//OgV+9sSLhfUBf1Bn9DuMRD+cjj/P59KqXAxAXPxB8fMdqW2PEr3JvPRq",
|
||||
"4qQZsGYPSKSDJ+TU0oahRyt7dZx1PAdb+9nOgtN0d4unYBAPyT979nb0BnbWPrCOYKe2GXDi3snqI5H0",
|
||||
"pusQQ7DJvvK1+B/6mz1262jqc2qq9E3yafW2Lf1AnNehIOxi4Eec94kXs3XTiEaZeVh0AkB2ye7Y0OoU",
|
||||
"EdexcLDRAvdBwOLFHTwWXwozWrPaFx14snnXeWN3+T3/NGUKABDIKi9ruQK5dWrXTQ1sfyWdR82s1vi3",
|
||||
"U1M7N9or4bWZKhLHOHi+Etdqq2oQjmj345KtEU7YhPJpLK3HKeCOBdG5AoFReXYSHG0K6+LzpwOmGAZj",
|
||||
"WlnpKN6nS6XH8Q3rGW08cxbyvOsOq63C23TtVcV2SG3mqP1Gc+ATL8iRuyvflnwCu0CM0cFt5Oh5Lvb7",
|
||||
"a71aqXq8j03fheJVZBo9xyBIAvWZ2Cy25Yh4cpddoVlvLxIf73Tro/f35DISi/VSGiHr2imPYpycLgQv",
|
||||
"IENa4Fba1AolTqc8SPf8lj8XMn4BGgp6ERQ6zEArKSJ61Gu+hHlsQPl8fdyx0l4KQZjWqNwkTjiznix1",
|
||||
"uGIjuXTzNVqXzlrVIrpsnkX1DSFUEVhJ2VA3yumwrcRUBjXHKKigQ6MqUasgdVNRuAQqyrojN7fwS1Gf",
|
||||
"ZiDxpuyRZkDQvjKi8kGtmLIwSwojDCvhLfwR9y+cNLnXuQ2aB1iNKkZr4No0zZF+aOIfvSD57MdXNHr2",
|
||||
"y7dxouy3izjn0ek/90CX/QHf2aEUTfg7geDRbB+dhx3zfY8JlDgvJxH8FjailbPL1X2tRLsqA452REAG",
|
||||
"vVeERYwev6e32C6Bsv1CllQXeoq+K6cUvMWZMyvpWOfNgxefePH96xevUgLdRnoR5LUyX+5iuaE9fRh2",
|
||||
"0z/xaGfzgZMdk+aafGuwLBZP2OzrVLQfUMSsD2lfYr1q49iPiCYa0kl2F/zOerzberphXKavCE+/Omdu",
|
||||
"PMO0TjXzpyBgncLr/vQjk+GnU/ju9KP5dFfNsBrFsx14qOfzUhxjWDtzpUx9xg5RVlqVqU/s7ARtnoQ+",
|
||||
"42ShfNrSFyVmojmHPs+N/cHaRkxl0/hnFf5xBX9UAvbA97apBeCIE3K1UtJRFJY1LfQI8eAKV9I1Gha4",
|
||||
"1k3tc04eA0yrtBn4Z5wPmFmcEH6H6Y5k7ZEoLwl4b9M8vQeXa2de46z9B9Y2L2kNvScv/PX3vKL+N7TA",
|
||||
"A8y6wJ/NqMMHMoTICa5FhYM67EWQxXBMOOgy4fqAvJaYi12H1TpwmiM+EXMF9DDZCgDxeUrO9eQ6l03D",
|
||||
"/vWlWMgblczHY/GConPyLGZ1o+06Dsy0hwEgT2O8dcq5nKsKg3qedRMY2+mT1X7itJpRJMXxF8NE+mIq",
|
||||
"gGLLPq7giafYAzIrKJpzHDkwkhRbHp6xr42YNuZLggKN5EW7TWZ1GTDkn7xq9AyFNNhsJTZpBcABZ43d",
|
||||
"5DyTF7CUmp177Mxvkzl5rWPxXs3QRoqOs2waovKFNP0Vwtx6uhCy2cit7+5ZZFuNMyDAIzkTSEDOkNoc",
|
||||
"S6kw6zfSq3fx4/TLWxxlWNBOeJuCuNlhE7fgLToLKZmOhKDPzikcNHZ11/ajYfOJnYmv4P766vlzEHSd",
|
||||
"nAblfCXWRn9Y02lkiSRw1OXo5CSoHIKBNj649XTIzVk2RUXBcJ+ck8KgdyL+9Y26wtuwrBMQx+OyACW5",
|
||||
"8DXCne6lWD0AbRgg3jg5vUaVUNeNapN0tcGcpPpo+0OntkOBF9STK7QwHRm0nOXdHx+S3wCkcJ8FKLzI",
|
||||
"96+8mK4dUESzjaUkyoIFE/8A7Fdsdblq3XBHG2l8wWl2wKjVokvBqkdC4U4YHKmobJwhp9bRZisCzBW6",
|
||||
"3QrEgd64WIjD2E0lbFPDIqJl6G4mLBjvvZoVw7m8upo2Op5sUTF2dxW2C2VBdjf5vd0IjHWQaaco+y3l",
|
||||
"VswtMhi7Dsj+swok7L7WQZClTeD1sp5el5HsV7uc6BLpXkR65DDKuAS+zESD6ayE0xRcLntojoS+kB6L",
|
||||
"pMRjofM+6lD+Dy7toEExg39G6zt0nNFT1WVt3TPu0nIfE7tks8sBW4CWD3mXbksc+VL660Keh/TqitN7",
|
||||
"dzBqjy9n2lg/VKwjanAoB4EkslRuDmKFE7X2U+mYER/pd7JNyRDyhq18bAb46U0UvFayUSEoUD+8nhty",
|
||||
"2KEEjME/UsCAawcIRCdGn6E+SdEbIORgOAVmNQC1ABOQc1vG9odwphGYig72nxcKRQ4pXtFbHWEvurtx",
|
||||
"T5SjQ0cltipwiiwbAWPpoY3UAYgwpS9mfKGKf6H8WaP4151BTBS87NTS3pSzEu9R2cXE+NtkBGDzBL0p",
|
||||
"ZCgfxd4aLVligCZZOxsRPinUWEFivlOBFUD1K5Lpd5dAhMB2zQ9r6RfJZmMK8nLH6B9Qyt8g6+MKN5Yo",
|
||||
"q7g+WsiHtVrvI9OYq+OFSyVgOHjDZAkc2uSaQ6JpbeYJZhwUrWhJSd/j8kygqACyLFRzB9KnPaDVeeA4",
|
||||
"4VHClQ5AaWdLea38WRYRjh+0NTWi5wdzOWnDbP0OV9nsgxA+QKEteHP6xGIdQHwcl0EhPls1EALD81wB",
|
||||
"r7tipWnPnAR/hIlGVwCfIPtyWRHtHnmkaX7I62K2rcPw2uiDq8PJOGQqIHDHqi2JDqVnxoRrvRMZgvx3",
|
||||
"VVbML5NpIAbzZ9Ec8BsGRImnaQggqPTHP//rs0qsJDqbGHRsagZYKbfURjaw8nc/vr/cK2sPp2Sw73Df",
|
||||
"abaJDtao3kZa41lbQK58TIWkVLz6RkxjHPRJV/ORqjjIE6TlvYhjtT+9jaO2P71qxwfpKxL1rtXF1vZK",
|
||||
"c0LVAZK//PHVj+MlReB1RY7c+HEnhIp6dXl22H2cPBlb0sQzZ39Vubghns5VABD8HN9dMK5P7Wr7rLMw",
|
||||
"yYtnCbycm9au78qtzcB1Gl8hvqdrtlTg8IxIaMnobcMvpFP7gXLlQ7HcTmHaHFJkcqDgWliEW5vhedCa",
|
||||
"eYfQnm5uCqJV9+ZIcmx//KqVfnNGEkXOLLt6KCIoJ+L2QujIb2X+PSSgU2q8L8vpZZSMAoQjvwjoPNKl",
|
||||
"mljpRj5BDpcxEJYxEu/vih4VBYdUsTpVa4dt3bFYx6QcGNDcIREnKwlQSgJwqrRxrIsy69biANKaEbKT",
|
||||
"gIQFMxD56rSRw+vvIRnjBK4jbm3w+AZcnEPcrh/mh68NDX5J+buRgwcnDYEEcJLxJi6wGsUrChRjM7G3",
|
||||
"WXZVopmjOf3kMp/sUk5epvku5eRbnhLea2e9lJM3PPGlnLxPc1/KSYxL/IV39vNenpsYlMy5bO60ZIqs",
|
||||
"SJAgP+aC3Ji9qFs100bHUmO7U8WFvErvwZD/+8XbH8Z3iC0tG02zuYsnzJC74Di3fpxYQ8VYtNFLOP+v",
|
||||
"SgKHs5uDb+1ETjVogLADWXeXtraF+juY7jgst+zyEypimy5sSQ6PXqWcTGJJnOM4U5utLeZgH7Lr8MLj",
|
||||
"+EM7LudzT2y95TKwuynPMOATCj4ygS7/pBMPhkzyR7O1Q7BhEF/RtKemw0iLXlnmhUbBHR7EQkmMc6Iq",
|
||||
"W3F9BX1S+6E0+ExGG144itew7CoJLnCu6J9DGx9nakXocNz5EXkq8eoGmLcAGDqwCxD3ilH207Uv2XpV",
|
||||
"kyuAbYTZRhrQzqzgT9PqQQeeLmSyhUrDl4z26KoeUt4Gg0mOVWtJtrUk0R4LuCKc2B69Wxtjf8zJw1cp",
|
||||
"Lq32J8OaSYEXLstJOS/pQbSl5EIOpXA7BWpt0dRS1uAOlBVKck6veCkWvsMpKV0sK2OKynkUKboDVqO1",
|
||||
"ob0FrDCAN2zmuUIuAqzReADG4KqyUQpRLDIsPHGt8mzdKnWjIy6QJZuto+SRra9biyhfWPHIOTv3wTJl",
|
||||
"jk5oqUYmS4e7WsV8uH130G4C3efkxRRXMAS0oaI394ZMb+FHZ+7syk+lMLUo0OEF0qQgAoroysT3Cd8m",
|
||||
"MDWbrjHuhMNUQYdlbRpuYlJvoxWSI1TQY7VUnoMWetEmF9GeE4vqJWkPAxX6smO2i48FuXluT+DHE3+t",
|
||||
"Vycxx/gEKzcCXge3VmSKmd9BOaLYnDvVJeAZ9p3PBbkACsS2b5N3l3jbwUqrYV9cYRFhCGf7JW6iI42F",
|
||||
"j+g0o8ODf6UnfAOVi93cszHA0flbXAmDN7YLi0/I1mc20xVHk61TeEU6qsA/ej5+Pn5OKfzKyJUenY3+",
|
||||
"On4+/iuV/log6E7lSp/KeqnNaWvzmysUjQDEyE3e1KOz0XcqXETzRqd4xl+eP/9iDTV4hkJHjQvlboAg",
|
||||
"sf8A0uDay7kaw7ufKtrHpLFUg2plfcHkfRGwJG1b8Ng6sfYYI5Zq/VMpKhAiY/Crkxt6/5zs8NsVCsG1",
|
||||
"CmoaDYYUIrYNitJHl2sPYp9497fvKvFv715/V4nv3nwrrBM/q8m7sXgRKOvlX8Rb/Q2xji6sf0qxyyPC",
|
||||
"C+XDN6xMZHDGZZ3O9awL4OQraevN7SAbffqPlZrf99uVufenGzVZ3fXbDoEwa+yh4VdfDA0R8gUkpHAo",
|
||||
"61QtANfGQFxfE/qXhkvrO01ZSV9/9dej3+4h9ulH+N+b+tM+Ek04s5dA/2SIs3uIAMknnkiWNFE2siOI",
|
||||
"kMj5cL++w3Ghe0suVVDOj87+/nEEsmKstkg34YjOcNTH5irbU38Hv0Q0oCKy2fGXWOQJOgjo1bF4DYIP",
|
||||
"/kFtTzR7ztSHcyp2z1wO6+2CNPNvFz/+bUx/RiHoB+nDCf5y8uaVeMp2UXr2Ya3cVqRtP8PSEk6BuApE",
|
||||
"QqovRunEFaHpny/CKlbXlCH6qGKhXKcwa+OcDqdRsv7vCKe2Hie/yVrvVDYN5jK1ZXfTNqhOghFYsVfM",
|
||||
"LY9h25q6JSZMiZI0xKh8tAsla1TE+HA7kBpVJTwdit77VPGYCNF2SAqq/iJDZRk9g7g28Ck3Idn5rq14",
|
||||
"8MtBphPUbSAUPuEWTR1KPki3hKL06T2IMxIRGbeHaOhNW6gkRfNRFJ/4Vjdw+sJI59iJx/WMHCKTzAKl",
|
||||
"8/YyN1ptSvj1g/YhGrxLyNU7go7b6s4H+BlnH7O5jrs685pyRyDF/jv6+LpsA8bcHSR6K8OUrHBYiyZa",
|
||||
"Y7UTaaFj8YosPWjWwwI0XLClj0Wnnc2Vhc4XWCXJd/JfaG5N0fGYe0kNb0R82aG+2slWg1djng0qrZpK",
|
||||
"ktSN4h5w5O4H7p7KuCAbXZtYqamEhVwg701y+QwJm/eXpjqF+I6S5H5rLAERgeuyp7O/s6R3B+4Eb//r",
|
||||
"fXnZ6UdY4gG5sN3+A2pvGYzLMEXTdJK2Wop7GFGLwHI/UYusFkPQfKseEoxo1hyAIIg5wL5QaGFArAuL",
|
||||
"JAsfr/PLk3RmRHwAgr4vcCLJMnDuRrEtVS3V6SxWsRrCACpz9YD7pAkKG02dZHJUeOJ3Kld3saPfvm9q",
|
||||
"Xe0zNxYVnottENux2qrhFRtMnQhyMt6VlnOYfHmEy8DxeLg2eAaZHYCaeDzs9dBBTRDFT1kS2GfsUqb2",
|
||||
"WQ9LDnvvIU2rLFXkL6R2RHkbvpLzV/yc6qjhq9qLpa5PuPhxp2ThQjUULupj2n3wwqhbakdXkkg4m/aH",
|
||||
"2PfwyyNTtwblUUj1ly82eadN6gBudeCfA3TDhQysS7DFZJbP4XWIUDHxbVAxutyHPClvrq8zvaDwMvTF",
|
||||
"i9iDkmN4ASco6JsCEaii366OBIgQ84BGjyEqdk/oOGmx7a3aBW7ulCNqXReJNXh2avWrZabSmF3w762Q",
|
||||
"WRIFdr2ND0NbJa/mf0IBAe7GkyQl7L1kN1mME122mjorohrar3NJWWzZN1tyQmaFL4kZZ0x4i3peNqQO",
|
||||
"VcucOYZeGrGkDnpk/uKZp9Irtgku7IZKM9A+JiVkulChrfL5MBjUjn8U5nw9BHlVf8YR52mvRRkQONO7",
|
||||
"Npfv4blSjIE8giG9aJootSUxkIWE7iboBowjP9BxdgoePrIjJwGtzA+A2TKgvgCunH7kfx3QxXN4PxAT",
|
||||
"PLDv7p6/tN6doHA/1bsEztOU/z9IjJecRvvwlIiJsUfKBa1JGHfQL274e4J/NaBJEOX6TkZUmwmQgumT",
|
||||
"H3ywXMjOZUIjX1Kb5IfgPVmI/SMzHsKRYa4DgHw0rbFMURyoPSjtk/xpZ3kMduxWGr1uQ2eNHZOw7R81",
|
||||
"+No5+u9UwEjxB+SBOP4eQyS2qdCesuFmsvGqrSVWiEU3NgLij8Q2bW1b3vmbsY6YiIFusTwHjxpmYeJT",
|
||||
"G7vMRoKU11flumKbV9mJpBeUwsEB5imYJxYo6qYod9L4+UMvlzRS2RUsXUhx/g/Eq1JU+p+PVd3X/8EJ",
|
||||
"Sqcf6R+A8bE06elHLOv66Ti8j9/fCe2rcgIxzv/Ei1Wv/h/NgTGssXoq3pnPsRZJwX0B6z9mQa3j/5eD",
|
||||
"9l4ZitVx20K3diYk1W2g9WJVNq/ooe5V0aROn8I6LKU7N9bRW2XzcF6394GsxLuNPR/Z+hA7spXJiUD6",
|
||||
"aPd+NEqdfuR/HVBMos3rAeFzjPWTF/tA12wCxf2u2RJMT9nIeRyvuecChu7YF/6ak46yAmap+DhduQtp",
|
||||
"ajubiadzK5tK1Gqq2VyLwR3wi6yFMrWvyJ6YIggqqpn8jBsaEQNZO8Mv572LtE8W3pobncP10auSjvcs",
|
||||
"3rwYKYtl8PhqTxd6XC4LAt07vXA9vyTwD6Lv4xrv4+oRTFlpgfFj3YpFFMWkArde/TZI+tqwt5GrzpHz",
|
||||
"Z+cg38RF/k6OMgENzzE1P/xtT7Lj+3vsg7wgUiXSJgwnqtW+dVKliswtZwDmFC3WrA20rCoWlkqlo3o+",
|
||||
"RCpKxIk3EguGZrFRGIfaehVjbWFM4Jetc2kA5S6Uqdv6vX9Kb2MsLdRjVL83YT8riH1AjkkWpYeyLOzR",
|
||||
"mDJt6UtLLrTz+4ktXeCdTtuyIUX7z+s2qa5bvTyZ+56m1MwqVcxYG/oXpecYrK0Kv8N+1RRjC8ZzHaid",
|
||||
"/zOBCf4wWNUtQnKHGiTD9icbC3NhyxpkSdTxUFx2K9rbjYkdQNDbKoJdTzHbDyPVY/mPqq0kr323mTNX",
|
||||
"xVWyPrwRyjLAAixDdVTiyvdVUeGGDkGtqIjtuigXMSm0tTd6ODbQDiDYuA2O1aPNxXNoN1QJv4bDgHNE",
|
||||
"l2rk2THhYKFkPRZ/s0HIprGb1JeQN5MU337EMZzZ6E6q94scQuaMnZkRa9oyH9r0GnE7yY5WGSuF33TT",
|
||||
"mYsLjE0Z9mrjVbHqUfz2rAvk4mn32o3T+TxJNdm69cytoR6P5Rhutfrc+P3PNMMzEhbY5jvlTtBWDMrI",
|
||||
"7y3U9rEZsz2R67DgWsqDtnm03/hYmyziLuXAxLagSYDqMPC2CpB4ihEFWaG6+AnyoIWsMVUSY1EogryV",
|
||||
"gMXa1BSeoB2mWz9Diz8a+5lPaNc3xr6I1Qy2+WqBK3ORZiyxSYvPm04KKV5GsKj6BD7nnpNlnvfSvmAQ",
|
||||
"PopfPuvaeaRTsD3k37+kUOvZbC8qro2eaVVj5YpYLQ6peaLCRikjJJavoOsuOQrnnatpPHR1vYLZj0qP",
|
||||
"4RIZd7LeFhNlqGzXZ4/DB/LZ49imjsXn9qprDxcEqxuFxzDk4ZZhuvhzM+1au0ES+dEoUWunpsG6ba8e",
|
||||
"ZxQzqvSGVmzuqqjHLndEGYvXJuBD+KtGKRpjvho1C8Kuw7lIcnUtFL8LL8RqMGRtwyLqwIixExsKQo2S",
|
||||
"N6rb5gnmdnY9X4jxmDLZ/HbZaHNdcYeitS/nEiWidYdkzYtG+sWJV/BSIBWhkeSUs90afc7aEMtQwVra",
|
||||
"+pfRFz0k8/ymVPNKux+0D3tSkBNSYFMlPLE/OxVhDczjzFr3mXfIpvWdvumGt5A4BBdZrWo4f25UD8i5",
|
||||
"cTqoVrPg2uhVW6+ZK1tzuUHfN4Jh/jRWnMiN47E4IQUaGEVtFlG822iPlSq9peKC/YrrbImPHRFYJIQH",
|
||||
"suZiTUslDXrAz0Xt7MoLabYUPI4C2XkMOe3b98XCbtQNWtd4FqpQ54sWARiE1fNY2o0rmSbm036YFfYX",
|
||||
"WWc3igaoUsl5+JDzvJ1aSm36Lv28sDzPVgnZOCXrLVeaTzOxWOuj/5/LR/tz6l+UlZ2eglK0Xgnq/Ajb",
|
||||
"Jo6KYis9K/E/rlhcNj/95dHMT4CMuml4mxXvqd4t/j/+bUx3h+oYZPig8joEUnj1oZ9h8E6SRrGS3sco",
|
||||
"kkZiD5YPYiKnqPzj9+XsAgDm3hz/h8vHx/4dnaGW8pbrcD5//rzaX5bzUXKrETLHqDU/YRdF6rvDh/Y0",
|
||||
"Grb+5fnzZ79/RQd0lkGUfCGcmq8b6Ui1KdtEx+JS3QaSkbg8GDWXlMLrXxVB51xQpRLyq/KbwLj54B5c",
|
||||
"OgN5/suKZwcEsDvd0A+qxrzkgQZ4J5zIn1sEwyo7w0SQCmcBDaR43x1lBoTzVO82dtk1NX8sQ5DTxZKK",
|
||||
"wSCmaw9ivW3WAUskDOHaOV8DTnnb3ACvZ8RH426suaVN6uXb0mW3EFeFZbhwSRf/67tKTOF+pFait0EZ",
|
||||
"jD0Zpp83S3IVHlOy48sTwB+1cpO/mf/z7bL5zQs/AerGwk/3ovW//jk5AyoKj6+aYWJkxmSyNg0po9ma",
|
||||
"mXZLUNGo23wg11jKLMWII45XRLWIhznRpluEWpsbzZW4A7WH96xgYEufUgSUMICGTbOtKAWTWgoNKEjY",
|
||||
"Uge0yuhng2EzT2Fhj9YccHNGq3jPqM5W9LLZHBk0Gdw75qjWLF2J1Rq3oTnbWM8BWDPpw8nMuo2MxQPa",
|
||||
"hU0XanoNkMTeN3wKubpY0hMLeiEVcU6hInuaSuUaX1sqOaZFR6fl015fK3jBq/AsjynnjlPtCNrALHOn",
|
||||
"vO/Pmncze7Kj3fe1k7Y4t/Zipm9jvUjMpm+7SuH2sO2a34lZ342OYdtXWm1Ud7FXVqyv5uYqsJ+win2A",
|
||||
"ubWTNYCvSBr0S5vRo1PvvX7/p957BN2zrAlUspvAivw1dUHiNQbxlGN49IwLF8V6Ac+qBNK4PgKEyPoA",
|
||||
"Ur2jfJXxVOLU2Jeeg5uVqXv9j9Jr2D+RcKLXYYXxoj0U86xka2gh1jsWTMkP+XFE40cPkmPxWjNO4xm1",
|
||||
"QnvfpFEwVcQQArsZKJ3g5p+bT1VuwYr5Mf0KxwfbrXU6rAHM4NUzPBnqASC5Eup//UvWvRjgTjcH2XkJ",
|
||||
"Tmj9KXYdwOeF3nlSm37/bF6QjV0gh1tb02GdRQxOHxSo4ZwSqjtE1mlDL7GK904nn30tVPgkYWdv6fPs",
|
||||
"F+5lja3gC4WBHy+6bJ8larnTys46buf3BwgvO40iwuPLPCmgu2ONTsx0H9q2eJoFS1WZxiTNFgWmRk+D",
|
||||
"54DKuTLIQ8xc8D+B8HQDd9EFT50tJw8zx7vIx5DQIWP2wSttkJW9+c0jgBnm5VDu3zkKr2L/pn2GVupp",
|
||||
"uINu6naFTWBRkKQ/ruDVKnNJSrqKvXI3LIBK3fipRPxwN6oT8g8fC7NeTihABKTlJ14srA+VsDfKie8v",
|
||||
"L99dVH2hSJebAQ+U03zXdqx6jIIMnOtzbCY4re33bwk9Bm2SvwZfrfBKXymXhUt2BOHiab1/xMN6f6ez",
|
||||
"+sMcVaFs0y6cf/fFkzqcZ6CU1O/nKA6lRpjuRoYCqLkmWk1KNl3qy1XoJj2VcxYolSDPkfnyWQs8+m9T",
|
||||
"PeGYmzlzn/8xruPUffcYxppe7lsU4q0rUx/POzW1vFaAYgFUTn8Kl3BplacfzafjLuJzjsLZRP882+jD",
|
||||
"QoahS/oiweEx+FGc7ehCbnF1fwDmH5cK53U6s+76wZSVqjiQuXt2+oEyEbHeAMcslOrMoAjeep+ysH8i",
|
||||
"iCqV+6fa2B1ejNZbQDBt1myXzAbgSmnW3CjnZezmqYPwwdpU/6QbnE8d6TiPJE4DZ/GMrH8Zm3qSc/XY",
|
||||
"eKETI01FMtFctC/QBQ1kNRnO2OoQ1xTDgByXQOM9xfEl9v3ltgy6SKLfWnd90eY3/L+yEwcZe+qcOxw9",
|
||||
"8LOaXNjptQrs/iTraB6Wz2NUQgps+yb8QjUNFfPLnatZnNnUNh7ULmS/ekYl4DOtWlAyPfZNIjQjeWMT",
|
||||
"q0CkrvQLbO4/VSY0W7FyQLI1m/DR2rgOqzVLJtQerRKywWb2HMwQc1JpzmmjiehQujH8be9Vtj54/SvZ",
|
||||
"erGyYXq4sA31IxV5n9vkg1EMHnWro5eH9sm2YySDqa2V+Pr58+fJwgyvC+pzlULplPR7/b1Zh+IjXL6x",
|
||||
"Pe4gV9zfarc8KDbbvfegfafyV0TEPeEV6z+3aNki7MrZYKe2+XNHRqyz7qJFGk9luftyPnUpxIsHnQBD",
|
||||
"prvaKm+ehGIqROpt+pB1U+McQ7VT+Xls/Tn+T3bCm6yn+KB0nlqLp+v4+Obi4gcJorGqNZtesdWp8rkE",
|
||||
"j2PX1jwJDOaBS5p5U+qD/sDJe2meoagKAIJJO/gdis5xafstJT+ntx5DNek31zxCQ8G87LwfqsHgBkSm",
|
||||
"9F5ro82FlgSB04/w7BMhOGDgLixe4e/D2FWouUuf1GOBdTiJ3xFt6CCulVpxTuDUrrb3wo+hZP/70cBW",
|
||||
"9oORjgof6nWipSazu01sY1tY5AHSZDFvnMATi1Y8EKVwP9W7GbXWezQzDMtbNXKaBXMgr0I9jOxVjQLh",
|
||||
"dRgYmPnwbxc//i1qRlhxzydvMPDBr0D++Or58443OMOo6FsnzOqspI9jBa75bt3FlmOsZkchyuPVF9vh",
|
||||
"GXubU/QY8p2KG3/69H8DAAD//w==",
|
||||
}
|
||||
|
||||
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"g.fc.run/misc/byre/internal/api"
|
||||
)
|
||||
|
||||
func (s *Server) ListReviews(w http.ResponseWriter, r *http.Request, taskID string) {
|
||||
@@ -18,3 +21,30 @@ func (s *Server) ListReviews(w http.ResponseWriter, r *http.Request, taskID stri
|
||||
}
|
||||
writeJSON(w, http.StatusOK, reviews)
|
||||
}
|
||||
|
||||
func (s *Server) SetFindingState(w http.ResponseWriter, r *http.Request, reviewID string, index int) {
|
||||
req := api.FindingStateUpdate{}
|
||||
if !readJSON(w, r, &req) {
|
||||
return
|
||||
}
|
||||
if !req.State.Valid() {
|
||||
writeError(w, http.StatusBadRequest, errors.New("state must be fixed or ignored"))
|
||||
return
|
||||
}
|
||||
if index < 0 {
|
||||
writeError(w, http.StatusBadRequest, errors.New("index must not be negative"))
|
||||
return
|
||||
}
|
||||
|
||||
err := s.store.SetFindingState(r.Context(), reviewID, index, req.State, actor(r))
|
||||
if err != nil {
|
||||
writeStoreError(w, err)
|
||||
return
|
||||
}
|
||||
review, err := s.store.GetReview(r.Context(), reviewID)
|
||||
if err != nil {
|
||||
writeStoreError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, review)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"g.fc.run/misc/byre/internal/api"
|
||||
)
|
||||
|
||||
const reviewColumns = `id, task_id, snapshot_n, coalesce(agent_session_id, ''), vendor, model, state, result, error, created_at, finished_at`
|
||||
const reviewColumns = `id, task_id, snapshot_n, coalesce(agent_session_id, ''), vendor, model, state, result, error, created_at, finished_at, finding_states`
|
||||
|
||||
func (s *Store) CreateReview(ctx context.Context, r api.Review) error {
|
||||
finishedAt := ""
|
||||
@@ -26,9 +26,9 @@ func (s *Store) CreateReview(ctx context.Context, r api.Review) error {
|
||||
return err
|
||||
}
|
||||
if r.State == api.ReviewStateRunning {
|
||||
return emitReview(ctx, tx, emit, "review.started", r.Id)
|
||||
return emitReview(ctx, tx, emit, "review.started", r.Id, "byre")
|
||||
}
|
||||
return emitReview(ctx, tx, emit, "review.finished", r.Id)
|
||||
return emitReview(ctx, tx, emit, "review.finished", r.Id, "byre")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -86,7 +86,27 @@ func (s *Store) FinishReview(ctx context.Context, id string, state api.ReviewSta
|
||||
if n == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
return emitReview(ctx, tx, emit, "review.finished", id)
|
||||
return emitReview(ctx, tx, emit, "review.finished", id, "byre")
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) SetFindingState(ctx context.Context, id string, index int, state api.FindingState, actor string) error {
|
||||
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
res, err := tx.ExecContext(ctx, `
|
||||
UPDATE reviews SET finding_states = json_set(finding_states, '$."' || ? || '"', ?)
|
||||
WHERE id = ? AND state = ? AND ? < json_array_length(nullif(result, ''), '$.findings')
|
||||
`, index, string(state), id, string(api.ReviewStateDone), index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
return emitReview(ctx, tx, emit, "review.finding", id, actor)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -123,7 +143,7 @@ func (s *Store) FirstTaskMessage(ctx context.Context, taskID string) (string, er
|
||||
return text, err
|
||||
}
|
||||
|
||||
func emitReview(ctx context.Context, tx *sql.Tx, emit emitFunc, kind, id string) error {
|
||||
func emitReview(ctx context.Context, tx *sql.Tx, emit emitFunc, kind, id, actor string) error {
|
||||
r, err := scanReview(tx.QueryRowContext(ctx, `SELECT `+reviewColumns+` FROM reviews WHERE id = ?`, id))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -133,7 +153,7 @@ func emitReview(ctx context.Context, tx *sql.Tx, emit emitFunc, kind, id string)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return emit(api.Event{Kind: kind, ProjectId: &projectID, TaskId: &r.TaskId, Actor: "byre", Payload: ToPayload(r)})
|
||||
return emit(api.Event{Kind: kind, ProjectId: &projectID, TaskId: &r.TaskId, Actor: actor, Payload: ToPayload(r)})
|
||||
}
|
||||
|
||||
func questionReviews(ctx context.Context, q querier, item api.InboxItem) ([]api.Review, error) {
|
||||
@@ -171,11 +191,17 @@ func scanReview(row scanner) (api.Review, error) {
|
||||
result := ""
|
||||
createdAt := ""
|
||||
finishedAt := ""
|
||||
err := row.Scan(&r.Id, &r.TaskId, &r.SnapshotN, &r.AgentSessionId, &r.Vendor, &r.Model, &state, &result, &r.Error, &createdAt, &finishedAt)
|
||||
findingStates := ""
|
||||
err := row.Scan(&r.Id, &r.TaskId, &r.SnapshotN, &r.AgentSessionId, &r.Vendor, &r.Model, &state, &result, &r.Error, &createdAt, &finishedAt, &findingStates)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
r.State = api.ReviewState(state)
|
||||
r.FindingStates = map[string]api.FindingState{}
|
||||
err = json.Unmarshal([]byte(findingStates), &r.FindingStates)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
if result != "" {
|
||||
res := map[string]any{}
|
||||
err = json.Unmarshal([]byte(result), &res)
|
||||
|
||||
@@ -425,6 +425,10 @@ UPDATE tasks SET discard_state = 'held' WHERE discarding_by != '' AND status = '
|
||||
ALTER TABLE tasks ADD COLUMN parent_task_id TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE tasks ADD COLUMN parent_merge_by TEXT NOT NULL DEFAULT '';
|
||||
CREATE INDEX tasks_parent ON tasks(parent_task_id) WHERE parent_task_id != '';
|
||||
`}, {"add-review-finding-states", `
|
||||
ALTER TABLE reviews ADD COLUMN finding_states TEXT NOT NULL DEFAULT '{}';
|
||||
UPDATE events SET payload = json_set(payload, '$.finding_states', json('{}'))
|
||||
WHERE kind IN ('review.started', 'review.finished') AND json_type(payload, '$.finding_states') IS NULL;
|
||||
`}}
|
||||
|
||||
const (
|
||||
|
||||
Vendored
+1
-1
@@ -115,7 +115,7 @@ export function taskPane() {
|
||||
if (t === "reviews" && reviewing) {
|
||||
return h("span", { class: "spinner", title: "review in progress" });
|
||||
}
|
||||
return count(t) > 0 && h("span", { class: "badge", title: t === "inbox" ? "inbox items" : "findings in the latest review" }, count(t));
|
||||
return count(t) > 0 && h("span", { class: "badge", title: t === "inbox" ? "inbox items" : "open findings in the latest review" }, count(t));
|
||||
};
|
||||
const layout = () => {
|
||||
for (const { button } of buttons) {
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+45
-14
@@ -1,7 +1,7 @@
|
||||
import { get, post } from "./api.js";
|
||||
import { get, post, put } from "./api.js";
|
||||
import { ago, errorText, h, replace, showAlert } from "./dom.js";
|
||||
import { markdown } from "./markdown.js";
|
||||
import { focusTask, onEach, pickReview, state } from "./state.js";
|
||||
import { onEach, pickReview, state } from "./state.js";
|
||||
const severities = ["critical", "high", "medium", "low", "info"];
|
||||
export function isReviewEvent(ev) {
|
||||
return ev.kind.startsWith("review.");
|
||||
@@ -11,14 +11,19 @@ function findingsOf(r) {
|
||||
return Array.isArray(res["findings"]) ? res["findings"] : [];
|
||||
}
|
||||
export function findingCount(r) {
|
||||
return r.state === "done" ? findingsOf(r).length : 0;
|
||||
return r.state === "done" ? findingsOf(r).filter((_, i) => !r.finding_states[String(i)]).length : 0;
|
||||
}
|
||||
export function unignoredFindings(r) {
|
||||
return r.state === "done" ? findingsOf(r).filter((_, i) => r.finding_states[String(i)] !== "ignored") : [];
|
||||
}
|
||||
export function latestReview(events) {
|
||||
const all = events.filter(isReviewEvent).map((ev) => ev.payload);
|
||||
return all.reduce((best, r) => (!best || r.snapshot_n >= best.snapshot_n ? r : best), undefined);
|
||||
}
|
||||
export function reviewCounts(r) {
|
||||
const findings = findingsOf(r);
|
||||
return severityCounts(findingsOf(r));
|
||||
}
|
||||
export function severityCounts(findings) {
|
||||
const parts = severities.map((s) => [s, findings.filter((f) => f["severity"] === s).length]).filter(([, n]) => n > 0);
|
||||
return parts.length > 0 ? parts.map(([s, n]) => `${n} ${s}`).join(", ") : "no findings";
|
||||
}
|
||||
@@ -42,13 +47,13 @@ function reviewResult(r, taskId) {
|
||||
const findings = findingsOf(r);
|
||||
const md = { taskId };
|
||||
return h("div", { class: "review-result" }, markdown(String(res["summary"] ?? ""), "md review-summary", md), reviewImages(res["images"]), findings.length > 0
|
||||
? h("ol", { class: "findings" }, ...findings.map((f) => {
|
||||
? h("ol", { class: "findings" }, ...findings.map((f, i) => {
|
||||
const severity = String(f["severity"] ?? "");
|
||||
const path = typeof f["path"] === "string" ? f["path"] : "";
|
||||
const line = typeof f["line"] === "number" && f["line"] > 0 ? f["line"] : 0;
|
||||
const end = typeof f["end_line"] === "number" && f["end_line"] > line ? f["end_line"] : line;
|
||||
const where = path ? `${path}${line ? `:${line}${end > line ? `–${end}` : ""}` : ""}` : "";
|
||||
return h("li", { class: `finding ${severity}` }, h("div", { class: "finding-head" }, h("span", { class: `severity ${severity}` }, severity), typeof f["category"] === "string" && f["category"] ? h("span", { class: "finding-category" }, f["category"]) : null, h("span", { class: "finding-title" }, String(f["title"] ?? "")), where ? h("code", { class: "finding-where" }, where) : null, taskId ? fixButton(taskId, r, f, where) : null), taskId && path && line ? findingDiff(taskId, r.snapshot_n, path, line, end) : null, markdown(String(f["detail"] ?? ""), "md finding-detail", md), reviewImages(f["images"]));
|
||||
return h("li", { class: `finding ${severity}` }, h("div", { class: "finding-head" }, h("span", { class: `severity ${severity}` }, severity), typeof f["category"] === "string" && f["category"] ? h("span", { class: "finding-category" }, f["category"]) : null, h("span", { class: "finding-title" }, String(f["title"] ?? "")), where ? h("code", { class: "finding-where" }, where) : null, taskId ? findingActions(taskId, r, i, f, where) : null), taskId && path && line ? findingDiff(taskId, r.snapshot_n, path, line, end) : null, markdown(String(f["detail"] ?? ""), "md finding-detail", md), reviewImages(f["images"]));
|
||||
}))
|
||||
: null);
|
||||
}
|
||||
@@ -139,11 +144,21 @@ function findingDiff(taskId, n, path, line, end) {
|
||||
}, (err) => replace(box, h("div", { class: "hint" }, `diff not available: ${errorText(err)}`)));
|
||||
return box;
|
||||
}
|
||||
function fixButton(taskId, r, f, where) {
|
||||
const button = h("button", { type: "button", class: "finding-fix", title: "Send this finding to the task's agent to fix" }, "fix");
|
||||
button.onclick = async () => {
|
||||
function findingActions(taskId, r, index, f, where) {
|
||||
const box = h("span", { class: "finding-actions" });
|
||||
const done = (s) => replace(box, h("span", { class: "finding-done" }, s === "fixed" ? "fix sent" : "ignored"));
|
||||
const current = r.finding_states[String(index)];
|
||||
if (current) {
|
||||
done(current);
|
||||
return box;
|
||||
}
|
||||
const mark = (s) => put("/api/reviews/{reviewId}/findings/{index}", { path: { reviewId: r.id, index }, body: { state: s } });
|
||||
const fix = h("button", { type: "button", class: "finding-fix", title: "Send this finding to the task's agent to fix" }, "fix");
|
||||
const ignore = h("button", { type: "button", class: "finding-ignore", title: "Mark this finding done without fixing it; Merge won't warn about it" }, "ignore");
|
||||
fix.onclick = async () => {
|
||||
const text = `Fix this finding from the review of snapshot #${r.snapshot_n}:\n\n**[${String(f["severity"] ?? "")}] ${String(f["title"] ?? "")}**${where ? ` (\`${where}\`)` : ""}\n\n${String(f["detail"] ?? "")}`;
|
||||
button.disabled = true;
|
||||
fix.disabled = true;
|
||||
ignore.disabled = true;
|
||||
try {
|
||||
const sessions = await get("/api/tasks/{taskId}/sessions", { path: { taskId } });
|
||||
const live = sessions.findLast((s) => s.state !== "completed");
|
||||
@@ -153,15 +168,31 @@ function fixButton(taskId, r, f, where) {
|
||||
else {
|
||||
await post("/api/tasks/{taskId}/sessions", { path: { taskId }, body: { prompt: text } });
|
||||
}
|
||||
replace(button, "sent");
|
||||
focusTask(taskId);
|
||||
}
|
||||
catch (err) {
|
||||
button.disabled = false;
|
||||
fix.disabled = false;
|
||||
ignore.disabled = false;
|
||||
void showAlert(errorText(err));
|
||||
return;
|
||||
}
|
||||
done("fixed");
|
||||
await mark("fixed").catch((err) => showAlert(`fix sent, but not recorded: ${errorText(err)}`));
|
||||
};
|
||||
ignore.onclick = async () => {
|
||||
fix.disabled = true;
|
||||
ignore.disabled = true;
|
||||
try {
|
||||
await mark("ignored");
|
||||
done("ignored");
|
||||
}
|
||||
catch (err) {
|
||||
fix.disabled = false;
|
||||
ignore.disabled = false;
|
||||
void showAlert(errorText(err));
|
||||
}
|
||||
};
|
||||
return button;
|
||||
replace(box, fix, ignore);
|
||||
return box;
|
||||
}
|
||||
export function mountReviews(root) {
|
||||
const head = h("header", { class: "reviews-header" });
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+5
@@ -292,6 +292,11 @@ export function handleEvent(ev) {
|
||||
void loadInbox();
|
||||
}
|
||||
break;
|
||||
case "review.finding":
|
||||
if (state.inbox.some((i) => i.task_id === ev.task_id && i.kind === "question")) {
|
||||
void loadInbox();
|
||||
}
|
||||
break;
|
||||
case "focus.updated":
|
||||
if (ev.actor !== state.me?.login) {
|
||||
applyFocus(ev.payload);
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+6
-6
@@ -2,7 +2,7 @@ import { get, post } from "./api.js";
|
||||
import { ago, copyable, errorText, h, replace, showAlert, showConfirm } from "./dom.js";
|
||||
import { markdown } from "./markdown.js";
|
||||
import { previewURL } from "./preview.js";
|
||||
import { findingCount, reviewCounts } from "./reviews.js";
|
||||
import { findingCount, reviewCounts, severityCounts, unignoredFindings } from "./reviews.js";
|
||||
import { composerText, endSend, flushDrafts, forgetLostImages, lostImageCount, setComposerImages, setComposerText, startSend } from "./drafts.js";
|
||||
import { currentSession, findTask, focusTab, focusTask, on, onEach, pickReview, pinSnapshot, setFocus, showReview, state } from "./state.js";
|
||||
import { taskStyle } from "./taskcolor.js";
|
||||
@@ -377,7 +377,7 @@ export function mountChat(root, src, makeHead) {
|
||||
put(snapRow(p, ctx));
|
||||
continue;
|
||||
}
|
||||
if (ev.kind === "review.started" || ev.kind === "review.finished") {
|
||||
if (ev.kind === "review.started" || ev.kind === "review.finished" || ev.kind === "review.finding") {
|
||||
const r = p;
|
||||
const button = ctx.reviewButtons.get(r.snapshot_n);
|
||||
if (button) {
|
||||
@@ -797,10 +797,10 @@ function markReview(button, r) {
|
||||
return;
|
||||
}
|
||||
if (n === 0) {
|
||||
replace(button, "review", h("span", { class: "review-mark clean", title: "no findings" }, "✓"));
|
||||
replace(button, "review", h("span", { class: "review-mark clean", title: Object.keys(r.finding_states).length > 0 ? "every finding addressed" : "no findings" }, "✓"));
|
||||
return;
|
||||
}
|
||||
replace(button, "review", h("span", { class: "badge", title: `${n} findings` }, n));
|
||||
replace(button, "review", h("span", { class: "badge", title: `${n} open findings` }, n));
|
||||
}
|
||||
function snapRow(snap, ctx) {
|
||||
const taskId = ctx.taskId;
|
||||
@@ -1142,8 +1142,8 @@ async function reviewWarnings(taskId, leave) {
|
||||
else if (r.state === "failed") {
|
||||
items.push(mergeWarning("Review of the latest snapshot failed", open));
|
||||
}
|
||||
else if (findingCount(r) > 0) {
|
||||
items.push(mergeWarning(`Review of the latest snapshot found ${reviewCounts(r)}`, open));
|
||||
else if (unignoredFindings(r).length > 0) {
|
||||
items.push(mergeWarning(`Review of the latest snapshot found ${severityCounts(unignoredFindings(r))}`, open));
|
||||
}
|
||||
const changes = await get("/api/tasks/{taskId}/changes", { path: { taskId }, query: { since: latest.commit_sha } });
|
||||
if (changes.files.length > 0) {
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -518,6 +518,27 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/reviews/{reviewId}/findings/{index}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
reviewId: string;
|
||||
/** @description The finding's position in the review's findings, from 0. */
|
||||
index: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
/** @description Records that the human addressed a finding of a done review, by sending it to the agent to fix or by ignoring it. */
|
||||
put: operations["setFindingState"];
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/tasks/{taskId}/snapshots/{n}/fork": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -1061,6 +1082,18 @@ export interface components {
|
||||
created_at: string;
|
||||
/** Format: date-time */
|
||||
finished_at?: string;
|
||||
/** @description How the human addressed each finding, keyed by its index in result.findings; a finding not listed is open. */
|
||||
finding_states: {
|
||||
[key: string]: components["schemas"]["FindingState"];
|
||||
};
|
||||
};
|
||||
/**
|
||||
* @description fixed: sent to the task's agent to fix. ignored: dismissed, and left out of the Merge warning.
|
||||
* @enum {string}
|
||||
*/
|
||||
FindingState: "fixed" | "ignored";
|
||||
FindingStateUpdate: {
|
||||
state: components["schemas"]["FindingState"];
|
||||
};
|
||||
AgentSession: {
|
||||
id: string;
|
||||
@@ -2115,6 +2148,36 @@ export interface operations {
|
||||
404: components["responses"]["Error"];
|
||||
};
|
||||
};
|
||||
setFindingState: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
reviewId: string;
|
||||
/** @description The finding's position in the review's findings, from 0. */
|
||||
index: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["FindingStateUpdate"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description The review. */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["Review"];
|
||||
};
|
||||
};
|
||||
400: components["responses"]["Error"];
|
||||
404: components["responses"]["Error"];
|
||||
};
|
||||
};
|
||||
forkSnapshot: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
+1
-1
@@ -133,7 +133,7 @@ export function taskPane(): HTMLElement {
|
||||
if (t === "reviews" && reviewing) {
|
||||
return h("span", { class: "spinner", title: "review in progress" });
|
||||
}
|
||||
return count(t) > 0 && h("span", { class: "badge", title: t === "inbox" ? "inbox items" : "findings in the latest review" }, count(t));
|
||||
return count(t) > 0 && h("span", { class: "badge", title: t === "inbox" ? "inbox items" : "open findings in the latest review" }, count(t));
|
||||
};
|
||||
|
||||
const layout = () => {
|
||||
|
||||
+47
-14
@@ -1,9 +1,10 @@
|
||||
import { get, post, type ByreEvent, type Schemas } from "./api.js";
|
||||
import { get, post, put, type ByreEvent, type Schemas } from "./api.js";
|
||||
import { ago, errorText, h, replace, showAlert } from "./dom.js";
|
||||
import { markdown } from "./markdown.js";
|
||||
import { focusTask, onEach, pickReview, state } from "./state.js";
|
||||
import { onEach, pickReview, state } from "./state.js";
|
||||
|
||||
type Review = Schemas["Review"];
|
||||
type FindingState = Schemas["FindingState"];
|
||||
|
||||
const severities = ["critical", "high", "medium", "low", "info"];
|
||||
|
||||
@@ -17,7 +18,11 @@ function findingsOf(r: Review): Record<string, unknown>[] {
|
||||
}
|
||||
|
||||
export function findingCount(r: Review): number {
|
||||
return r.state === "done" ? findingsOf(r).length : 0;
|
||||
return r.state === "done" ? findingsOf(r).filter((_, i) => !r.finding_states[String(i)]).length : 0;
|
||||
}
|
||||
|
||||
export function unignoredFindings(r: Review): Record<string, unknown>[] {
|
||||
return r.state === "done" ? findingsOf(r).filter((_, i) => r.finding_states[String(i)] !== "ignored") : [];
|
||||
}
|
||||
|
||||
export function latestReview(events: ByreEvent[]): Review | undefined {
|
||||
@@ -26,7 +31,10 @@ export function latestReview(events: ByreEvent[]): Review | undefined {
|
||||
}
|
||||
|
||||
export function reviewCounts(r: Review): string {
|
||||
const findings = findingsOf(r);
|
||||
return severityCounts(findingsOf(r));
|
||||
}
|
||||
|
||||
export function severityCounts(findings: Record<string, unknown>[]): string {
|
||||
const parts = severities.map((s) => [s, findings.filter((f) => f["severity"] === s).length] as const).filter(([, n]) => n > 0);
|
||||
return parts.length > 0 ? parts.map(([s, n]) => `${n} ${s}`).join(", ") : "no findings";
|
||||
}
|
||||
@@ -73,7 +81,7 @@ function reviewResult(r: Review, taskId: string | undefined): HTMLElement {
|
||||
? h(
|
||||
"ol",
|
||||
{ class: "findings" },
|
||||
...findings.map((f) => {
|
||||
...findings.map((f, i) => {
|
||||
const severity = String(f["severity"] ?? "");
|
||||
const path = typeof f["path"] === "string" ? f["path"] : "";
|
||||
const line = typeof f["line"] === "number" && f["line"] > 0 ? f["line"] : 0;
|
||||
@@ -89,7 +97,7 @@ function reviewResult(r: Review, taskId: string | undefined): HTMLElement {
|
||||
typeof f["category"] === "string" && f["category"] ? h("span", { class: "finding-category" }, f["category"]) : null,
|
||||
h("span", { class: "finding-title" }, String(f["title"] ?? "")),
|
||||
where ? h("code", { class: "finding-where" }, where) : null,
|
||||
taskId ? fixButton(taskId, r, f, where) : null,
|
||||
taskId ? findingActions(taskId, r, i, f, where) : null,
|
||||
),
|
||||
taskId && path && line ? findingDiff(taskId, r.snapshot_n, path, line, end) : null,
|
||||
markdown(String(f["detail"] ?? ""), "md finding-detail", md),
|
||||
@@ -205,11 +213,21 @@ function findingDiff(taskId: string, n: number, path: string, line: number, end:
|
||||
return box;
|
||||
}
|
||||
|
||||
function fixButton(taskId: string, r: Review, f: Record<string, unknown>, where: string): HTMLElement {
|
||||
const button = h("button", { type: "button", class: "finding-fix", title: "Send this finding to the task's agent to fix" }, "fix");
|
||||
button.onclick = async () => {
|
||||
function findingActions(taskId: string, r: Review, index: number, f: Record<string, unknown>, where: string): HTMLElement {
|
||||
const box = h("span", { class: "finding-actions" });
|
||||
const done = (s: FindingState) => replace(box, h("span", { class: "finding-done" }, s === "fixed" ? "fix sent" : "ignored"));
|
||||
const current = r.finding_states[String(index)];
|
||||
if (current) {
|
||||
done(current);
|
||||
return box;
|
||||
}
|
||||
const mark = (s: FindingState) => put("/api/reviews/{reviewId}/findings/{index}", { path: { reviewId: r.id, index }, body: { state: s } });
|
||||
const fix = h("button", { type: "button", class: "finding-fix", title: "Send this finding to the task's agent to fix" }, "fix");
|
||||
const ignore = h("button", { type: "button", class: "finding-ignore", title: "Mark this finding done without fixing it; Merge won't warn about it" }, "ignore");
|
||||
fix.onclick = async () => {
|
||||
const text = `Fix this finding from the review of snapshot #${r.snapshot_n}:\n\n**[${String(f["severity"] ?? "")}] ${String(f["title"] ?? "")}**${where ? ` (\`${where}\`)` : ""}\n\n${String(f["detail"] ?? "")}`;
|
||||
button.disabled = true;
|
||||
fix.disabled = true;
|
||||
ignore.disabled = true;
|
||||
try {
|
||||
const sessions = await get("/api/tasks/{taskId}/sessions", { path: { taskId } });
|
||||
const live = sessions.findLast((s) => s.state !== "completed");
|
||||
@@ -218,14 +236,29 @@ function fixButton(taskId: string, r: Review, f: Record<string, unknown>, where:
|
||||
} else {
|
||||
await post("/api/tasks/{taskId}/sessions", { path: { taskId }, body: { prompt: text } });
|
||||
}
|
||||
replace(button, "sent");
|
||||
focusTask(taskId);
|
||||
} catch (err) {
|
||||
button.disabled = false;
|
||||
fix.disabled = false;
|
||||
ignore.disabled = false;
|
||||
void showAlert(errorText(err));
|
||||
return;
|
||||
}
|
||||
done("fixed");
|
||||
await mark("fixed").catch((err: unknown) => showAlert(`fix sent, but not recorded: ${errorText(err)}`));
|
||||
};
|
||||
ignore.onclick = async () => {
|
||||
fix.disabled = true;
|
||||
ignore.disabled = true;
|
||||
try {
|
||||
await mark("ignored");
|
||||
done("ignored");
|
||||
} catch (err) {
|
||||
fix.disabled = false;
|
||||
ignore.disabled = false;
|
||||
void showAlert(errorText(err));
|
||||
}
|
||||
};
|
||||
return button;
|
||||
replace(box, fix, ignore);
|
||||
return box;
|
||||
}
|
||||
|
||||
export function mountReviews(root: HTMLElement): () => void {
|
||||
|
||||
@@ -351,6 +351,11 @@ export function handleEvent(ev: ByreEvent): void {
|
||||
void loadInbox();
|
||||
}
|
||||
break;
|
||||
case "review.finding":
|
||||
if (state.inbox.some((i) => i.task_id === ev.task_id && i.kind === "question")) {
|
||||
void loadInbox();
|
||||
}
|
||||
break;
|
||||
case "focus.updated":
|
||||
if (ev.actor !== state.me?.login) {
|
||||
applyFocus(ev.payload as unknown as Focus);
|
||||
|
||||
+6
-6
@@ -2,7 +2,7 @@ import { get, post, type AgentSession, type ByreEvent, type Participant, type Sc
|
||||
import { ago, copyable, errorText, h, replace, showAlert, showConfirm } from "./dom.js";
|
||||
import { markdown } from "./markdown.js";
|
||||
import { previewURL } from "./preview.js";
|
||||
import { findingCount, reviewCounts } from "./reviews.js";
|
||||
import { findingCount, reviewCounts, severityCounts, unignoredFindings } from "./reviews.js";
|
||||
import { composerText, endSend, flushDrafts, forgetLostImages, lostImageCount, setComposerImages, setComposerText, startSend } from "./drafts.js";
|
||||
import { currentSession, findTask, focusTab, focusTask, on, onEach, pickReview, pinSnapshot, setFocus, showReview, state, type Topic } from "./state.js";
|
||||
import { taskStyle } from "./taskcolor.js";
|
||||
@@ -458,7 +458,7 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
|
||||
put(snapRow(p as unknown as Snapshot, ctx));
|
||||
continue;
|
||||
}
|
||||
if (ev.kind === "review.started" || ev.kind === "review.finished") {
|
||||
if (ev.kind === "review.started" || ev.kind === "review.finished" || ev.kind === "review.finding") {
|
||||
const r = p as unknown as Review;
|
||||
const button = ctx.reviewButtons.get(r.snapshot_n);
|
||||
if (button) {
|
||||
@@ -932,10 +932,10 @@ function markReview(button: HTMLElement, r: Review): void {
|
||||
return;
|
||||
}
|
||||
if (n === 0) {
|
||||
replace(button, "review", h("span", { class: "review-mark clean", title: "no findings" }, "✓"));
|
||||
replace(button, "review", h("span", { class: "review-mark clean", title: Object.keys(r.finding_states).length > 0 ? "every finding addressed" : "no findings" }, "✓"));
|
||||
return;
|
||||
}
|
||||
replace(button, "review", h("span", { class: "badge", title: `${n} findings` }, n));
|
||||
replace(button, "review", h("span", { class: "badge", title: `${n} open findings` }, n));
|
||||
}
|
||||
|
||||
function snapRow(snap: Snapshot, ctx: Ctx): HTMLElement {
|
||||
@@ -1341,8 +1341,8 @@ async function reviewWarnings(taskId: string, leave: (go: () => void) => void):
|
||||
items.push(mergeWarning("Review of the latest snapshot is still running", open));
|
||||
} else if (r.state === "failed") {
|
||||
items.push(mergeWarning("Review of the latest snapshot failed", open));
|
||||
} else if (findingCount(r) > 0) {
|
||||
items.push(mergeWarning(`Review of the latest snapshot found ${reviewCounts(r)}`, open));
|
||||
} else if (unignoredFindings(r).length > 0) {
|
||||
items.push(mergeWarning(`Review of the latest snapshot found ${severityCounts(unignoredFindings(r))}`, open));
|
||||
}
|
||||
const changes = await get("/api/tasks/{taskId}/changes", { path: { taskId }, query: { since: latest.commit_sha } });
|
||||
if (changes.files.length > 0) {
|
||||
|
||||
+16
-1
@@ -1924,8 +1924,14 @@ pre.diff .mark {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.finding-fix {
|
||||
.finding-actions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.finding-actions button {
|
||||
padding: 0 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
@@ -1933,6 +1939,15 @@ pre.diff .mark {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.finding-actions button.finding-ignore {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.finding-done {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.severity {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
|
||||
Reference in New Issue
Block a user