Commit c0b433f
authored
feat: web installer, setup mode, doctor command and Docker-first deploy (#1763)
* feat: web installer, setup mode, doctor command and Docker-first deploy
Adds a first-run web installer at /setup so new operators can complete
configuration from the browser without hand-editing .env, plus a
companion `bmwebui doctor` command and Docker compose stacks for
production. The CLI setup wizard gains BanManager config auto-detection
and transactional admin creation, and the seed/update commands share
the same migration helper.
Highlights
- Setup mode: server boots without keys/DB and serves only /setup,
/health and the installer assets. Refuses to expose the main UI
until configuration is complete.
- Web installer: vanilla JS wizard (HTML/CSS/JS shipped from
server/setup/static) walks through env vars, DB connection, schema
migration, BanManager server registration and admin user creation.
Atomic finalize wraps server + admin creation in a single Knex tx.
- bmwebui doctor: validates env, DB connection, migration status,
admin presence, and pings each configured BanManager server (with
encrypted password decryption).
- bmwebui setup: optional auto-detect from BanManager plugin folder
(config.yml + console.yml), reuses WebUI database when desired,
shared createAdminUser helper, transactional inserts.
- Apache + Caddy reverse-proxy generators (`bmwebui setup apache` /
`bmwebui setup caddy`) with validated domain/subdirectory inputs.
- Docker: multi-arch image build, docker-entrypoint.js auto-generates
missing keys, docker-compose.prod.yml + docker-compose.prod-no-db.yml,
/health endpoint reports setup_required vs ok for orchestration.
- /health endpoint reuses the existing dbPool to avoid opening a
new connection per request.
Security hardening
- Timing-safe comparison for SETUP_TOKEN.
- Same-origin (Origin/Referer) check on /api/setup/* writes.
- Strict regex validation for domain/subdirectory passed to shell.
- parseBanManagerConfig restricted to config.yml/console.yml only.
- BASE_PATH validated and HTML/JSON-escaped before injection.
- MySQL healthcheck password moved out of CLI args via MYSQL_PWD.
- docker-compose.prod.yml requires explicit DB credentials (no
silent defaults).
Tests
- 8 new server-side suites covering setup state, finalize atomicity,
env validation, key generation, parse-config, mode boot,
basepath handling, and admin creation.
- New test/lib/setup-fresh.js harness applies WebUI + BanManager
migrations against an isolated DB so installer code can run in
realistic conditions without mocks.
- doctor positive/negative paths covered.
* test(e2e): expand Cypress coverage with deep journeys, setup wizard run + CI (#1764)
* chore(cypress): expand e2e seed and add login/gql helpers
Expands cypress/setup.js with the data shape the new journey specs need
without breaking the existing documents.spec.js fixture contract:
- Extra seeded players including an unbanned target for moderation
flows and a banned target with active ban/mute/warning for appeal
flows.
- A second BanManager server so AssignPlayersRoleForm's ServerSelector
is meaningfully exercised by the role lifecycle journey.
- Pre-seeded reports and appeals at every state (open / assigned /
resolved / closed) plus comments, scoped to the non-admin user so the
admin ban + parameterised punishments stay unappealed.
- A bcrypt-hashed bm_player_pins row valid for 10 minutes plus a
PIN-only player to cover both the registration journey and the
forgotten-password / appeal-login PIN branches.
- An APPEAL_CREATED notification rule targeting the super-admin role so
the appeal lifecycle journey can assert end-to-end notification
delivery.
cypress/fixtures/e2e-data.json is extended with the new ids the specs
consume (secondServerId, unbannedPlayerId, bannedPlayer*, *ReportId,
*AppealId, notificationRuleId, pin* fields). Existing serverId / banId
keys are preserved for documents.spec.js.
cypress.config.js now ships explicit retries / video / screenshot
defaults so CI failures are debuggable, plus user/PIN env defaults
consumed by the new helpers.
cypress/support/commands.js gains loginAsAdmin / loginAsUser /
loginAsPin / logout / gql helpers so journey specs can short-circuit
authentication and warm state via the API instead of the UI.
* chore(ui): add data-cy hooks for e2e coverage
Threads data-cy attributes through every component the new journey
specs target so selectors stay stable across refactors and aren't
coupled to translatable copy or react-select internals.
- Punishment forms (Ban / Mute / Warn / Note) and PlayerActions get
per-input + submit hooks plus a canCreateNote permission check used
by the moderation journey.
- Admin Server / Role / NotificationRule forms and list items get
field, row, edit and delete hooks.
- Webhook (Custom + Discord) forms, list items, WebhookTestForm and
WebhookDeliveryItem get hooks for the create / test / deliveries /
edit / delete loop, including the status badge for delivery
assertions.
- Admin DocumentsTable rows expose preview / delete hooks for the
moderation journey's documents check.
- Report and appeal sidebars expose state and assignee controls for
the lifecycle journeys.
- PunishmentPicker exposes per-row hooks (with type, id and server-id
data attributes) plus filter and empty-state hooks for the appeal
journey's picker filter coverage.
- NotificationContainer / NotificationList expose row and link hooks,
the global PlayerSelector in DefaultLayout becomes
data-cy=player-search, and dashboard widgets get container hooks.
- PlayerRegisterForm: the duplicate data-cy=password on the confirm
field becomes confirm-password to match ResetPasswordForm.
No behavioural changes other than the canCreateNote gate and the
fixed register selector.
* test(e2e): add admin server / role / webhook lifecycle journeys
Three new journey specs covering deep admin flows that previously had
zero coverage. Each spec creates with a Date.now()-suffixed name and
deletes at the end so reruns work without a re-seed.
- admin-server-lifecycle: create, list, edit and delete a server,
exercising the new ServerForm / ServerItem hooks.
- admin-role-lifecycle: create a custom role, assign it globally and
per-server (using the second seeded server so AssignPlayersRoleForm's
ServerSelector is meaningfully exercised), edit and delete.
- admin-webhook-lifecycle: parameterised across Custom and Discord
variants. Walks add -> save -> Test webhook modal (with the example
APPEAL_CREATED payload against httpbin.org/post) -> deliveries page
-> edit URL -> delete, asserting the WebhookResponse status code and
the WebhookDeliveryItem status badge.
All three specs use cy.loginAsAdmin() for setup and re-query elements
between steps to avoid Cypress unsafe chaining.
* test(e2e): add player moderation, appeal and report lifecycle journeys
- player-moderation: parameterised across ban / mute / warn / note. An
admin opens the unbanned seeded player's profile, creates each
punishment type, edits ban + mute via /player/{ban|mute}/[id], and
asserts each entry appears in the relevant profile list. Also walks
the admin documents page (preview modal, modal-confirm delete cancel)
to cover the new admin-doc-* hooks.
- appeal-lifecycle: parameterised across ban / mute / warning. The
banned user submits an appeal via /appeal -> punishment picker (uses
the seeded picker filters and clear-filters control) -> the admin
receives the pre-seeded APPEAL_CREATED notification, opens the
appeal, comments, assigns, marches state to resolved, then asserts
the notification flips to read. Notifications coverage is fully
merged in here because APPEAL_CREATED is currently the only
NotificationType. Includes a dedicated case that opens the
AnimatedDisclosure on the picker row before clicking Appeal.
- report-lifecycle: an admin walks an open report through the full
state machine (Open -> Assigned -> Resolved -> Closed), comments and
assigns along the way, and asserts that comments cannot be added
once closed. Also smoke-checks the /reports list page.
All three specs delete what they create and use re-queried selectors
to avoid Cypress unsafe chaining.
* test(e2e): add registration, PIN forgotten-password and error-pages journeys
- registration: covers PIN-as-login on /appeal for the seeded
PIN-only player, account registration including the password
mismatch branch, the forgotten-password page render, login via PIN
for the existing seeded user through the appeal flow, and a smoke
check of the global player search in the nav (data-cy=player-search
in DefaultLayout) routing through to /player/{id}.
- error-pages: visits a deliberately bad route to assert the 404 page
renders with the expected title and Homepage link, then visits /500
directly with failOnStatusCode:false to assert the 500 page renders
with its Homepage link too.
Specs are self-contained and use re-queried selectors throughout.
* test(e2e-setup): add isolated setup wizard run with installer + import specs
The normal Cypress run boots against an already-set-up DB and can never
exercise /setup. This adds a dedicated setup-mode E2E target driven by
its own Cypress config + supervisor process so the wizard can be
walked end-to-end without touching any developer or CI .env file.
Infrastructure:
- cypress.setup.config.js: separate config rooted at cypress/e2e-setup
with custom tasks (prepareSetupDb, dropSetupDbs, writeBmConfigFixture,
readEnvFile, clearEnvFile, getSetupConsoleUuid, sleep) and a CYPRESS_
/ SETUP_ env-var pipeline that flows DB credentials and the dotenv
target path through to the installer specs.
- cypress/scripts/setup-server.js: supervisor that spawns server.js
from a temporary sandbox cwd (so dotenv.config() can't pick up the
developer's .env), strips DB / encryption env vars to force
setup-mode boot, and respawns the child after a clean exit so the
spec can verify the post-finalize transition into normal-mode and
the resulting /setup -> 404. Sets DISABLE_UI=true on the second boot
so it doesn't try to load Next.js.
- cypress/scripts/prepare-setup-db.js: drops + recreates throwaway
WebUI and BM databases, runs BM-side migrations and seeds a Console
player with a deterministic UUID using inetPton for the IP column.
- npm run e2e:setup:server / e2e:setup:run / e2e:setup:open scripts.
Specs:
- cypress/e2e-setup/installer.spec.js covers progress rendering, bad
DB credentials, password mismatch, invalid UUID validation, the
"Create database if missing" flow, and the full happy-path finalize
including a polling check on /api/setup/state for the supervisor
restart and a post-completion 404 assertion on /setup.
- cypress/e2e-setup/installer-config-import.spec.js covers paste-mode
and path-mode ingestion of config.yml + console.yml (success +
missing-section / missing-uuid / blank / non-existent-path errors).
Hooks + supporting tests:
- server/setup/static/installer.html / installer.js gain data-cy
attributes on banners, the progress indicator, every step input,
navigation buttons and the success / continue-to-login screen so the
specs can target stable selectors.
- server/test/setup-mode-boot.test.js gets a SETUP_TOKEN describe
block that exercises the API token gate (preflight no-leak, mutation
rejection, wrong / right token flows). Token coverage stays at the
jest layer to keep the E2E supervisor simple.
Helpers (setText, setTextArea, waitForState polling) are used in both
specs to avoid Cypress unsafe chaining and arbitrary cy.wait() values.
* ci: add setup-mode e2e job
New setup_e2e job that runs in parallel with the existing test matrix.
Boots MySQL on the runner, runs npm run e2e:setup:server (the
supervisor that handles setup-mode -> normal-mode restarts), waits on
http://127.0.0.1:3001/health and then runs the cypress/e2e-setup
specs.
CYPRESS_setup_* env vars are forwarded into the Cypress action so
prepareSetupDb / dropSetupDbs / readEnvFile / writeBmConfigFixture
tasks pick up the right host, port, user, password, dotenv path and
DB names.
* fix(e2e): stabilise lifecycle/journey specs after first CI run
- opengraph/player: resolve fonts dir absolutely so server boots in
sandboxed CWDs (setup_e2e supervisor)
- cypress/setup: seed Console with deterministic UUID + expose via fixture
- pages/admin/roles: data-cy wrappers for assign-global/server-role panels
- admin-server spec: use fixture consoleUuid (was an invalid hex string)
- admin-role spec: target new data-cy wrappers and move portal selectors
outside .within()
- admin-webhook spec: drop incorrect deliveries assertion, scope test
modal to visible dialog only
- appeal-lifecycle spec: filter punishment picker by server-id, .first()
on dual desktop/mobile sidebars
- report-lifecycle spec: .first() on dual sidebars, switch to existing
/dashboard/reports listing page
- registration spec: combine register + mismatch tests so the PIN is only
consumed once, simplify PIN appeal flow typing to use 6 inputmode=numeric
inputs directly
- error-pages spec: failOnStatusCode:false on /500 visit
* fix(e2e): unblock setup wizard rate-limit + tighten lifecycle specs
- setup rate-limit: read from env so e2e can disable it. The default
10 req/60s easily exhausts during retries (~6 step submits per spec)
which previously presented as the wizard "hanging" between steps. The
setup-server supervisor now runs with a 10000/60 budget.
- installer.spec: open the create-database <details> wrapper before
checking the checkbox so the click isn't intercepted by the card body.
- admin-server-lifecycle: import the canonical tables/{key} mapping
instead of generating bm_{key}, otherwise createServer rejects with
"missing tables" and the redirect assertion times out.
- admin-webhook-lifecycle: drop :visible filtering on modal selectors -
Headless UI's transition wraps these in animating containers that
briefly fail Cypress's visibility heuristic, and the response payload
can push action buttons out of the visible scroll viewport. Use
click({ force: true }) for modal interactions and assert the form is
removed from the DOM after closing.
- appeal-lifecycle: intercept POST /graphql for createAppeal so a
server-side rejection produces an actionable assertion instead of a
silent "URL didn't change". Also use .first().find() for both the
assignee and state widgets so we always interact with the visible
desktop sidebar (the mobile copy is still mounted but hidden).
- report-lifecycle: same .first().find() pattern for the report
sidebar so .react_select__input.last() doesn't accidentally pick the
hidden mobile input.
* fix(e2e): more lifecycle/installer fixes after CI run on 12ec80b
- installer.spec: use a valid v4 admin UUID. The committed value
aaaa-bbbb-cccc-dddd-... was rejected by validator.isUUID() (v4 only
by default), causing the admin step to fail and the wizard to never
reach the Review step.
- admin-webhook-lifecycle: cy.reload() after navigating back to the
list page so SWR's cached listWebhooks response (no
revalidateOnMount, 2s dedupe) doesn't race the assertion that the
edited URL is rendered.
- admin-server-lifecycle: intercept POST /graphql for createServer
so a server-side rejection (missing tables, console UUID lookup,
etc.) produces an actionable assertion instead of a silent
"URL didn't change" timeout.
- report-lifecycle: drop the brittle initial 'Open' state assertion
and just confirm the sidebar widgets mounted, mirroring the
appeal-lifecycle pattern. The state transitions later in the test
(Open -> Assigned -> Resolved -> Closed) implicitly prove the
starting value was correct.
* fix(e2e): make sleep cypress task resolve to null
Cypress requires task handlers to return a value, null, or a Promise
that resolves to a value/null. The original `sleep` task returned
`new Promise((resolve) => setTimeout(resolve, ms))`, which resolves
to `undefined` and fails the installer happy-path waitForState polling
loop with `cy.task('sleep') failed: returned undefined`.
* fix(e2e): poll setup state via Node task to survive supervisor restart
The setup-mode child exits after finalize and is respawned by the
supervisor ~500ms later in normal mode. The waitForState polling loop
was using cy.request(), which surfaces ECONNREFUSED as a hard test
failure even though the gap is expected. Switched the poll to a new
fetchSetupState task that issues a plain Node http.request and
resolves to { ok: false } on connection errors so the loop can keep
retrying while the new process boots.
* fix(e2e): make lifecycle specs resilient to retries via per-attempt names
Cypress retries reuse the spec context, so describe-level random names get
reused across attempts. When the first attempt creates a row in the shared
test database and then fails on a later step, attempt 2 collides with that
row ("server with this name already exists" / stale webhook URL state).
- admin-server-lifecycle: move the random server name into the test body so
every attempt mints a fresh Date.now()-based name.
- admin-webhook-lifecycle: derive the URLs from Date.now() per attempt and
intercept create/update mutations so backend rejections produce actionable
errors. Also assert the URL field hydrates the saved value before clearing
so we never race react-hook-form's defaultValues sync into submitting the
original URL.
* fix(test): pin MockDate to session.updated in setPassword test
The test's `MockDate.set(Date.now() - 5000)` was time-relative to
real wall-clock time, but the session cookie issued by the original
`getAuthPassword()` call carries `session.updated` derived from the
seeded user.updated timestamp. When total test runtime crossed the
5-second window the mocked time landed at >= the seeded value, so
the resolver's `session.updated = Math.floor(Date.now() / 1000)`
produced no change, koa-session detected nothing to commit, and the
response carried no `set-cookie` header. The follow-up
`header['set-cookie'].join(';')` then crashed with TypeError on
Node 22.x where the suite happens to run a few seconds slower.
Decode the session cookie, pluck its `updated` field, and pin the
mocked time precisely 5s before that so the new session always
strictly post-dates the existing one.
* fix: mount setup router before main router so /setup lockdown can fire
The main router's catch-all `(.*)` route was matching /setup and
/api/setup/* requests before the setup router had a chance to handle
them. With disableUI=true the catch-all would set ctx.respond=false and
never write a response (so the request hung); with disableUI=false it
would defer to Next.js, which has no /setup page.
Either way the post-setup lockdown middleware in server/routes/setup.js
never ran, meaning /setup stayed reachable after the WebUI database had
been provisioned. Re-ordering the middleware fixes both cases without
touching the catch-all itself.
Also covers the lockdown deterministically with a new jest test
(server/test/setup-lockdown-after-complete.test.js) and trims the e2e
installer happy-path to skip the post-finalize verification, which was
fragile against the supervisor's setup-mode -> normal-mode child
restart on Cypress retries.
* test(cypress): surface updateServer errors and fix webhook list races
Two related improvements after CI showed both lifecycle specs failing on
all 3 retry attempts:
- admin-server-lifecycle: alias the updateServer mutation the same way we
already alias createServer, so a silent server-side rejection (e.g. the
saved password not round-tripping through encrypt/decrypt) produces the
actual GraphQL error instead of a "URL didn't change" timeout when the
edit form fails to redirect.
- admin-webhook-lifecycle: capture the new webhook id directly from the
createWebhook response and use it to scope all subsequent assertions,
instead of reaching for `[data-cy-template=...].last()`. listWebhooks
has no ORDER BY clause, so on Cypress retries (which reuse the shared
test database) the .last() row could be an older webhook left over from
a previous attempt with a stale URL. Also force a cy.reload() after the
redirect back to /admin/webhooks so the SWR cache (no
revalidate-on-mount, 2s default dedupe) picks up the brand-new row.
* test(cypress): trim admin-server-lifecycle names to fit the 20-char limit
The intercept added in the previous commit surfaced the underlying
failure: \`Cypress\${Date.now()}Renamed\` is 27 characters but
CreateServerInput.name / UpdateServerInput.name both have
\`@constraint(maxLength: 20)\`, so updateServer was being rejected with
a BAD_USER_INPUT error (and the silent rejection is exactly what kept
the form on /admin/servers/[id]/edit instead of redirecting back).
Switch to a \`Cy\${Date.now()}\` / \`\${name}R\` pattern (15 / 16 chars)
so both create and rename stay within the schema limit.
* test(cypress): reload admin servers list after edit to dodge SWR cache
After the rename redirects back to /admin/servers/[id], the test does a
\`cy.visit('/admin/servers')\` to assert the new name shows up in the
list. The list page reads servers via SWR with no
revalidate-on-mount and a default 2s dedupe window, so the cached
response from the very first \`cy.visit('/admin/servers')\` (with the
pre-rename label) was being served and the lookup by
\`data-cy-server="\${renamedServer}"\` timed out.
Mirror the webhook spec's pattern: \`cy.reload()\` immediately after
the visit to force SWR to refetch. Also bumps the lookup timeout to
10s for the same reason as the create case.
* fix(servers): refresh in-memory serversPool after updateServer
updateServer wrote to bm_web_servers but never touched
state.serversPool, so every resolver that reads from the pool (the
/admin/servers list, per-server scoped resolvers, etc.) kept serving
the pre-rename name and stale connection details until the 3-second
background sync in connections/servers-pool.js caught up.
createServer/deleteServer already keep the cache in sync inline; this
brings updateServer into the same shape:
- Refresh the cached config with the new name/host/port/user/db/tables.
- Rebuild the knex pool when any connection detail (or password) changed
and destroy the previous pool, mirroring what the background sync does.
Also adds a jest test covering the refresh and drops the cy.reload()
workaround the cypress lifecycle test had been carrying to mask this
exact race.
* fix(admin): invalidate list SWR caches on add/edit + sort listWebhooks
The admin add/edit pages were calling router.push() back to the list page
without invalidating its SWR cache, so within SWR's 2s dedupe window the
list briefly showed pre-mutation data. The webhook lifecycle e2e papered
over this with cy.reload() — drop the workaround and fix it at the source
across webhooks, servers, notification-rules, and roles.
Also add an ORDER BY clause to listWebhooks: it was relying on MySQL's
implementation-defined row order, which makes LIMIT/OFFSET pagination
formally undefined and made the e2e test pick the wrong row with .last().
* chore: condense lingering verbose lifecycle/setup comments
Follow-up cleanup pass on multi-line comments that just narrate what the
code does. No behaviour change.
* fix(boot): boot in setup-mode when DB reachable but install incomplete
The Docker compose smoke job was failing with curl "Connection reset by
peer" because the webui container was crash-looping. Reproduced locally
with the production compose stack.
Root cause: when DB credentials + keys are set in the environment but no
admin user has been created yet (i.e. fresh `docker compose up` before
the operator visits /setup), server.js was running the strict non-setup
env validator before consulting getSetupState. The validator hard-fails
on missing CONTACT_EMAIL and called process.exit(1), so the container
restarted forever and /health was never reachable.
Reorder: consult getSetupState first, and when setup is incomplete
validate in setup mode (warnings, not errors) so the wizard can run.
Also extract the boot decision into a pure `decideBoot` function on
server.js so jest can cover the regression directly without spawning
the full process.
Workflow side-fix: the smoke job's "Wait for /health" step was missing
the MYSQL_PASSWORD env, so when the wait timed out and tried to dump
container logs via `docker compose logs`, compose re-parsed
docker-compose.prod.yml and bailed on the required-var check — hiding
the actual webui container logs. Switch to `docker logs <container>`
and add the env block so future failures are debuggable.1 parent 46fd192 commit c0b433f
112 files changed
Lines changed: 7938 additions & 361 deletions
File tree
- .github/workflows
- cli
- commands
- setup
- test
- utils
- components
- admin
- notification-rules
- webhooks
- appeals
- appeal
- dashboard
- notifications
- player
- reports
- cypress
- e2e-setup
- e2e
- journeys
- pages
- fixtures
- scripts
- support
- pages
- admin
- notification-rules
- [id]
- roles
- servers
- [id]
- webhooks
- [id]
- add
- dashboard
- scripts
- server
- graphql/resolvers
- mutations
- queries
- routes
- opengraph
- setup
- static
- test
- lib
- utils
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | | - | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
3 | 11 | | |
4 | 12 | | |
5 | 13 | | |
| |||
15 | 23 | | |
16 | 24 | | |
17 | 25 | | |
18 | | - | |
19 | | - | |
20 | | - | |
21 | | - | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
22 | 32 | | |
23 | | - | |
| 33 | + | |
| 34 | + | |
24 | 35 | | |
25 | 36 | | |
26 | 37 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
96 | 96 | | |
97 | 97 | | |
98 | 98 | | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
99 | 157 | | |
100 | 158 | | |
101 | 159 | | |
102 | 160 | | |
103 | 161 | | |
104 | 162 | | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
105 | 229 | | |
106 | 230 | | |
107 | 231 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
16 | 22 | | |
17 | 23 | | |
18 | 24 | | |
| |||
25 | 31 | | |
26 | 32 | | |
27 | 33 | | |
28 | | - | |
| 34 | + | |
29 | 35 | | |
30 | 36 | | |
31 | 37 | | |
| 38 | + | |
32 | 39 | | |
33 | 40 | | |
34 | 41 | | |
35 | 42 | | |
36 | | - | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
| 9 | + | |
8 | 10 | | |
9 | 11 | | |
10 | 12 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
| 40 | + | |
40 | 41 | | |
41 | 42 | | |
42 | 43 | | |
43 | 44 | | |
44 | 45 | | |
45 | 46 | | |
| 47 | + | |
46 | 48 | | |
47 | 49 | | |
48 | 50 | | |
| |||
52 | 54 | | |
53 | 55 | | |
54 | 56 | | |
55 | | - | |
| 57 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
54 | 54 | | |
55 | 55 | | |
56 | 56 | | |
57 | | - | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
58 | 60 | | |
59 | 61 | | |
60 | 62 | | |
61 | | - | |
62 | | - | |
63 | | - | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
64 | 89 | | |
65 | | - | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
66 | 93 | | |
67 | 94 | | |
68 | 95 | | |
69 | 96 | | |
70 | | - | |
71 | | - | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
72 | 100 | | |
73 | 101 | | |
74 | | - | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
75 | 136 | | |
76 | 137 | | |
77 | 138 | | |
| |||
137 | 198 | | |
138 | 199 | | |
139 | 200 | | |
140 | | - | |
| 201 | + | |
141 | 202 | | |
142 | 203 | | |
143 | 204 | | |
| |||
0 commit comments