Skip to content

Develop - #963

Merged
ajslater merged 333 commits into
mainfrom
develop
Sep 30, 2026
Merged

ajslater merged 333 commits into
mainfrom
develop

Conversation

@ajslater

Copy link
Copy Markdown
Owner
  • Features
    • Saved views can be deleted.
  • Fixes
    • Browsing a series or volume emptied by tagging returns to the top instead
      of a list with no way back.
    • Clearing a search or loading a saved view no longer leaves a list with no
      way back, and saved views keep their top collection.
    • The saved views menu no longer reopens after loading a view.

ajslater and others added 30 commits August 5, 2026 23:53
Three items `make complexity` flagged, all pure refactors.

AdminOnlineTagStartView.post (CC 11 -> C): three near-identical
"request value or stored default" blocks collapse into a
_FLAG_DEFAULT_FIELDS mapping plus a _resolve_flags helper.

test_telemeter_privacy._vocabularies (CC 12 -> C): split by vocabulary
source into _choice_vocabularies, _identifier_bucket_vocabulary and
_tagging_vocabularies, with the literal and OIDC sets hoisted to module
constants. The same string set is checked; the walk is not widened.

test_onlinetag_session_manager (MI 17.37 -> B): 916 lines in one module,
every function already rank A. Split along its seams into the scan pass,
prompt resolution and credentials, with the doubles and the comic factory
moved to tests/onlinetag_session_fakes.py. All 31 tests come across
unchanged; the move dedupes the BulkTagWriteTask filter into
write_tasks() and the prompt dict into a _prompt() factory.
test_onlinetag_tag_pass now imports the fakes from their new home.

make complexity, lint, ty clean; 815 pytest + 371 vitest pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A move whose destination path already belonged to another comic row
violated the (library, path) unique constraint inside bulk_update. The
IntegrityError aborted the whole import before the delete phase could
clear the stale row, so the poller rebuilt the same task and crashed on
every subsequent scan.

Mirror the folder guard for comics and custom covers: drop moves onto
paths an existing row holds, and moves that two sources in one batch
claim, which files_moved can express because it is a plain dict rather
than a bidict. Skipping converges. The destination row's stat refreshes,
the next scan sees two rows sharing an inode and suppresses the bogus
move, and the stale source falls through to the delete phase.

Also contain any move phase failure in all three move steps so a bad
batch degrades to a skipped phase the next scan reconciles instead of
aborting the import. This covers the same class of crash in the folder
step, where converting dirs_moved to a bidict raises on the duplicate
destinations the watcher can emit.

Fixes #807

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The self-update was not flaky, it was inert.

Nothing ever asked for an automatic update. update_latest_version()
had an `update` hook that queues JanitorCodexUpdateTask and no caller
passed it, and the update task was not in the nightly fan-out, so the
Auto Update flag promised a daily upgrade that could never happen. The
nightly check now forces a fetch and chains into the update when the
flag is on. The flag gate moves to that scheduling point, so the admin
Jobs tab button updates whether or not the flag is set -- it used to
silently no-op, since the flag defaults off and the button sends
force=False.

The installer ran `sys.executable -m pip install --upgrade codex` with
no timeout, no captured output, and a blanket except that swallowed
every failure while still logging "updated to the same version". uv and
pipx environments have no pip and the docker image uninstalls it, so
those installs always failed invisibly. Now: pip if importable, else
`uv pip install --python <sys.executable>`, else an ERROR naming both
and pointing docker at a new image. Failures log the installer's own
stderr, a hung installer times out instead of wedging the scribe queue
behind it, and the restart only fires when the install really happened.

Restarting exec'd __file__, which relied on run.py's executable bit and
its `#!/usr/bin/env python3` shebang resolving to a python that has
codex installed. Under macOS, pipx, uv, systemd and launchd that is
frequently a different interpreter and the server never came back. Exec
`sys.executable -m codex.run` instead.

_is_outdated() called Version() on unvalidated strings, raising on a
fresh install's empty cache and on a source checkout's "test" version.
Version comparison is now codex.version.is_outdated(), total by
construction, shared by the janitor and the version view.

The browser could not announce anything: semverGreaterThan(a > b) passed
one boolean into a two-argument function so `outdated` was always false,
and the comparison read 1.0.9 as newer than 1.1.0. The server ships
`outdated` and `docker` in the version payload and the javascript
comparison is gone. The footer renders a codex orange "upgrade to codex
vX.Y.Z" link to the Update Codex job, or to the image repo in docker,
where codex cannot install over itself. The Jobs tab grows section
anchors, names the version the job would install, and explains the
docker case.

While the cache is empty every /api/v4/version request queued a fetch
task, each on its own thread with a 5 second PyPI call. Add a process
wide in-flight lock and a cooldown after a failed fetch.

Tests cover the version comparison, the payload, installer selection and
failure paths, the flag gate and fetch guards, the nightly wiring, and
both frontend surfaces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nginx hides the upstream Server header by default (along with Date,
X-Pad and X-Accel-*) and substitutes its own, so codex/<version> never
reaches clients behind a reverse proxy. Granian passes the header set by
CodexMiddleware through untouched; nginx is the only clobberer.

Add proxy_pass_header Server to the README's example location, plus a
subsection covering the curl verification, why server_tokens off is not
a substitute, and the array-directive inheritance trap (a location that
declares any proxy_pass_header drops the one inherited from server{}).

Set the same directive in the test-proxy harness so the documented
config is the one actually exercised.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
OPDS responses were cached without varying on User-Agent while the body
depends on it, so a client could be served a variant rendered for a
different one until the entry expired. UserAgentNames switches facet
emission (FACET_SUPPORT), download mime types (SIMPLE_DOWNLOAD_MIME_TYPES),
order facet suppression (CLIENT_REORDERS) and absolute hrefs
(REQUIRE_ABSOLUTE_URL), and cache_page keys only on the URL plus the
headers named in Vary.

Add User-Agent to the existing vary_on_headers in opds_cached, which
covers every wrapped v1 and v2 feed, start, manifest and opensearch
route. Vary already includes Cookie, so per-session keys existed anyway
and this barely fragments the cache further; it also corrects what
intermediary caches are told. Cover routes keep the narrower vary since
covers don't depend on the client, and the static authentication
document is left alone.

Adds tests/test_opds_cache.py asserting the Vary header names User-Agent
and that a feed primed by one client isn't replayed to another. Both
fail without the fix.

Fixes #811


Claude-Session: https://claude.ai/code/session_01MAYYLr3w4xZJA1StYH2WwN

Co-authored-by: Claude <noreply@anthropic.com>
)

OPDS1TemplateEntrySerializer declared the field as `credits`, but the
entry object exposes `contributors` and the template iterates
`entry.contributors`. The template renders serialized data, so the
mismatch meant DRF looked for a `credits` attribute on the entry, found
none, and skipped the field: `credits` is read_only and not required, so
get_attribute raises SkipField and the key is omitted with no error.
`contributors` was never declared and so never serialized, leaving the
template's contributor loop iterating nothing.

The result is that comic credits which are not writing credits -- artists,
colorists, letterers, everyone outside AUTHOR_ROLES -- have never appeared
in a v1 feed. `<author>` was unaffected because all three layers agree on
that name. Atom allows zero or more `atom:contributor` in an entry, so the
schema tests could not catch it either.

Rename the field to match the entry property and the template. The payload
shape was already correct: get_credit_people and its batched variant return
objects with .name and .url, exactly what OPDS1CreditSerializer expects.

Adds tests/test_opds_contributors.py, which seeds a Writer and a Colorist
and asserts each lands in its own element. Without the rename the author
assertion still passes and the contributor one fails, which is the shape
of the bug.


Claude-Session: https://claude.ai/code/session_01MAYYLr3w4xZJA1StYH2WwN

Co-authored-by: Claude <noreply@anthropic.com>
Panels on iOS added OPDS facet and sort support in 3.13.0, so add it to
UserAgentNames.FACET_SUPPORT. Its CFNetwork style UA parses to "Panels"
through get_user_agent_name, so the existing exact-name match works.

Two more fixes came out of testing that allowlist.

Facet capable clients received the facets twice. The gate in the v1 feed
`entries` property had been commented out, so facets() ran unconditionally
and its OPDS1Link objects were appended to the entries list alongside the
real facet links from _links_facets. OPDS1TemplateEntrySerializer drops
every field those links don't have, so each one rendered as a dead entry:
empty id, no links, unclickable. Restore the gate so the fake navigation
folders are emitted only for clients that can't read facets.

The opds:facetGroup attribute carried internal query parameter names, and
clients like Panels show them verbatim as filter menu headings. Add a
display_name to the FacetGroup dataclass and emit that instead, so the
headings read "Order By", "Order Direction" and "Views". The query param
still drives hrefs and active-facet detection. facet_group also becomes a
CharField; it was typed as a collection-name ChoiceField whose choices
never included any value actually assigned to it.

Adds tests/test_opds_user_agent.py covering both facet variants, the
display names, and User-Agent parsing. Each test fails without its
corresponding fix.

Fixes #810


Claude-Session: https://claude.ai/code/session_01MAYYLr3w4xZJA1StYH2WwN

Co-authored-by: Claude <noreply@anthropic.com>
Panels' facet support is per platform and the platforms share one UA
name: the macOS build (951) does not render OPDS facets while iOS builds
(952 and later) do. Matching FACET_SUPPORT on the name alone sent macOS
Panels facet links it can't render, and with no fake nav folders either
it had no sort UI at all.

get_user_agent_name now also returns a build number, parsed from the
token right after the first slash for clients listed in _BUILD_UA_NAMES
(Panels only today). The auth mixin memoizes the pair and exposes it as
user_agent_name and user_agent_build, so existing name consumers are
unchanged. use_facets requires the client's build to meet
UserAgentNames.FACET_SUPPORT_MIN_BUILD (Panels: 952) in addition to name
membership; a missing or unparseable build fails the floor, falling back
to the fake nav folder sort that works on every client. Clients without
a floor, like kybooks, are unaffected.

No cache change needed: the response cache already varies on the full
User-Agent header, so builds 951 and 952 key separately.

Updates tests/test_opds_user_agent.py: the iOS constant moves to build
952, a new test pins that build 951 keeps the nav folder sort and gets
no facet links (it fails against the name-only gate), and the parse unit
tests cover the (name, build) pair including unparseable builds.


Claude-Session: https://claude.ai/code/session_01MAYYLr3w4xZJA1StYH2WwN

Co-authored-by: Claude <noreply@anthropic.com>
Django 6.1 deprecates the discrete EMAIL_* connection settings
(RemovedInDjango70Warning at django.setup()), and defining MAILERS makes
reading the old names an AttributeError.

- Replace the eight deprecated settings with an EMAIL_CONNECTION_OPTIONS
  dict (TOML/env layer, keyed by EmailBackend constructor kwarg) plus a
  MAILERS declaration pointing at the DB-aware DBEmailBackend.
- get_email_connection_kwargs / get_email_from_address coalesce the
  EmailSettings DB row over EMAIL_CONNECTION_OPTIONS instead of the
  removed settings.
- DBEmailBackend defaults its mailer alias so direct construction never
  hits the SMTP parent's pre-MAILERS settings fallback.
- The admin test-send view builds the backend directly and calls
  send_messages(), dropping deprecated get_connection() and
  EmailMessage(connection=...).
- Tests override MAILERS + EMAIL_CONNECTION_OPTIONS instead of
  EMAIL_BACKEND/EMAIL_HOST.


Claude-Session: https://claude.ai/code/session_01Bqa2ULBSaSVDwDSMEni1hf

Co-authored-by: Claude <noreply@anthropic.com>
#817)

The build >= 952 floor was built on a wrong premise: Panels build
numbers interleave across platforms. Real iOS builds run lower than the
macOS build - 942 (reported in the field) and 950 (issue #810's
reporter) both render facets natively, while macOS 951 does not - so no
floor can separate them, and 952 shut real iOS users out of facets,
handing them the fake nav folder sort instead.

Replace FACET_SUPPORT_MIN_BUILD with FACET_BLIND_BUILDS, a per-client
frozenset of known facet-blind builds (Panels: {951}). use_facets now
refuses only those builds; every other build - unknown and unparseable
ones included - gets facets, which is the pre-gate behavior that worked
on iOS. A future facet-blind macOS build must be added to the set as
discovered; until then it receives facets it ignores, the pre-gate
status quo.

The iOS test constant moves to the field-reported build 942, an
unparseable-build UA joins the facet-capable cases to pin the
facets-by-default behavior, and the macOS 951 test still asserts the
nav folder fallback. The 942 and unparseable cases fail under the old
floor gate and pass under the denylist.


Claude-Session: https://claude.ai/code/session_01MAYYLr3w4xZJA1StYH2WwN

Co-authored-by: Claude <noreply@anthropic.com>
ajslater and others added 29 commits September 28, 2026 13:53
FIT_TO_CLASSES mapped "O" to "Original", so the Original Size setting
at the default zoom produced fitToOriginal / fitToOriginalTwo /
fitToOriginalVertical while page-img.vue and pdf-doc.vue only style
fitToOrig*. The zoomed-in path already used "Orig". Map "O" to "Orig"
and have the zoom path read the same entry so the two can't drift.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
None of pdf-doc.vue's four canvas rules had matched anything since the
Vuetify 3 port (Dec 2022). That port moved them from an unscoped block
into top-level :deep() selectors, which compile to
`[data-v-x] .vue-pdf-embed.X > div > canvas` and need a scoped ancestor.
The embed is the component's root, so it has none. vue-pdf-embed 2.x
also nests the canvas one div deeper than `> div > canvas`.

With the selectors fixed (`.pdfDoc.X :deep(canvas)`), each rule was
checked in a browser with Force vector at DPR 2:

- Fit to Height/Screen two-page `width: inherit` and Fit to Width
  two-page `height: inherit` did nothing. vue-pdf-embed already gives
  the canvas an aspect-correct inline size. Deleted.
- Original two-page `width/height: inherit` resolved to auto, so each
  canvas showed at its bitmap size, 2x on HiDPI (1023x1581 instead of
  512x790). Zoom uses these classes too. Deleted.
- Fit to Screen `object-fit: contain` did nothing, because the box
  already matched the bitmap. Kept, and paired with max-width
  100vw/50vw as page-img.vue does for images. Wide pages now fit the
  screen instead of overflowing it: a spread plus a page in two-page
  mode at 1024px went from a 1491px scroll width to 1024px, and a wide
  page on a 390px phone from 1092px to 390px.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…errors (#952)

loadTagWriteErrors had the race loadTable had before #949. The Tagging
tab and the settings button read the list unforced when they mount, and
the WebSocket forces a reload whenever it changes. If a slow mount read
landed after the forced reload, it put the older list back and stamped
it when it arrived, so every unforced read for the next 5 s was served
the stale list. clearTagWriteErrors wrote [] directly, so a read that
went out before the clear could put the cleared errors back the same way.

Both now use loadTable's request counter. #949's landing check moves into
a claimLanding(key, request) helper that all three share, keyed like
timestamps. The tag-write reads take a number when they go out, land only
if no later request has landed, and stamp the time they went out. The
clear takes a number when its DELETE goes out, so an older read can't
undo it, and a read that went out after it and already landed isn't
wiped by it.

The singleton settings loads (tagging defaults, email, OIDC, site
defaults, throttling) are unchanged. Nothing forces them, and their only
writer is the same tab's own Save. That can't realistically fire before
the tab's mount read comes back.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* chore: update devenv

- Retire the dead .circleci ignore lines (devenv's new retirement lists).
- eslint base config: presets apply again and Markdown code blocks are
  linted, which reformats the compose YAML example in docs/DOCKER.md.
- pyproject template: T201 exemption for bin/release_info.py.
- eslint-plugin-package-json bump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci: compose CI from devenv's building blocks

ci.yml is now a caller of devenv's CI building blocks (copied in by
`make update-devenv`) plus codex's own image jobs:

- ci: devenv-check.yml with ci-target codex-ci and the same four checks.
  Its gate runs the Release Preflight and, on a main push, reuses the
  python-dist of an earlier run that passed on the same git tree (keyed
  by tree hash, replacing the "last merged PR" lookup). One image job
  builds codex-ci and pushes it by digest; each check pulls it instead of
  building and loading its own. The required check becomes
  "CI / Lint, Test & Build Dist".
- build, deploy, deploy-hub: unchanged in substance, but they read
  deploy/version/final from ci's outputs instead of testing the event or
  grepping pyproject.toml. deploy publishes to PyPI through devenv-pypi
  (uv publish --check-url) after the manifest. deploy-hub now skips at the
  job level for alphas.
- release: devenv-release.yml (tag, GitHub Release, merge-back), after
  deploy and deploy-hub.

Removes .github/actions/ci-container (now devenv-ci-container) and the
release-engine tests, which devenv owns now; codex keeps its golden
NEWS.md tests. tests/test_ci_workflow.py pins how codex wires the blocks.
CLAUDE.md describes the new graph and the current Dockerfile stages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* update deps

* fix(make): build the choices JSON before lint and fix

Since the ESLint preset fix (#931, #932), import-x/no-unresolved resolves
the frontend's `@/choices/*.json` imports, which bin/build-choices.sh
generates and git ignores. A checkout that never built them, such as CI's
fresh codex-ci container, fails `make lint` and `make fix` with 19
unresolved-import errors. Give lint and fix the same build-choices
prerequisite test-frontend and build already have.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…955)

* feat(browser): mark finished items with a check cap, not a thick bar

Issue 856 feedback: the 6px grey finished bar was hard to tell from the
3px reading bar when scanning a grid, and only really read when two cards
sat side by side. The dimmed caption was a second cue that made read
titles harder to read.

Every bar is now 3px. A finished comic or all-read collection ends its
bar in a 10px grey check circle, the same grey as the fill. The bar runs
under the cap to its centre so the two join with no gap. The check is
stroked in the page background instead of cut out, because a cut-out
would show the track through it. The fill is still the real bookmark
position, so a comic marked read but never opened is an empty track and
its cap.

Read titles and subtitles keep their normal colour.

The bar is now one component, ReadStateBar, used by both the browser
card and the metadata dialog. They had duplicated its CSS, and the cap
would have been a third copy to keep in sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(metadata): draw the read state under the cover, not behind it

The read state bar in the metadata dialog has been invisible since the
read state landed in v2.4.0. It sat 15px up over the cover's bottom edge,
where the old v-progress-linear used to be. That component is
positioned, so it painted over the cover. The plain div that replaced it
is not, and the absolutely positioned cover image painted over the bar.

It now sits 1px under the cover at every breakpoint, as on the browser
card, which moves the dialog's buttons down about 16px.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s bare root (#957)

A route whose collection pk resolves to nothing redirected to that
collection's own bare root. Under a Publishers top collection that is
`/series` (or `/volumes`) with no parent ids: the backend sends only
the root crumb, the client hides it as the current view, and `/`
resumes the saved last route, so the user is stranded until they
switch top collection.

A tag write that moves a series' or volume's comics elsewhere triggers
it on the next refresh: the ACL resolves through `comic__`, so the
emptied group no longer resolves.

Reuse `_get_up_page_redirect`, the out-of-bounds page redirect's
target: the top collection's root, or the folder / story arc root.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ands the browser (#958)

A bare nav-collection root below the top collection (e.g. /series under
Publishers) has no breadcrumbs, and "/" resumes it. Only a search belongs
there. Three frontend paths left the browser there after the search ended:

- "Clear Filters and Search" (clearFilters(true)) assigned the reset
  search straight into state, skipping _validateSearch, so the redirect
  made on entering the search was never undone.
- loadSavedSettings discarded the redirect _validateAndSaveSettings
  returned. A saved view without a search stayed at the search root.
  One with another top collection stayed on the old route, where the
  server's _validate_top_collection rewrote the view's top collection:
  a Folders view loaded at the Publishers root came back as Publishers.
- _validateSearch only undid the redirect at the current
  lowestShownCollection. Showing a deeper level mid-search, or loading a
  saved search that shows one, left the old search root behind.

clearFilters(true) now takes its redirect from _validateSearch,
loadSavedSettings goes through setSettings, and clearing a search leaves
any bare imprints/series/volumes root.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The saved-settings DELETE endpoint and API client already existed, but
nothing in the UI called them, so a saved view could never be removed.

Each saved view in the settings drawer's "Load Saved View" menu now has
a trash icon. It opens a "Delete View" confirm dialog naming the view;
the click is stopped so it doesn't also load the view. The dialog lives
outside the combobox so the menu closing can't unmount it. The item
slot keeps Vuetify's role="option" on each entry.

The new deleteSavedSettings store action reloads the list even when the
delete fails, so a view already deleted in another tab drops out.

Adds the first tests for the DELETE endpoint: it removes the view with
its filters and last-route rows, and 404s for another user's view, the
unnamed current-settings row, and an unknown pk.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
After a saved view was picked from the settings drawer's "Load Saved
View" combobox, the view loaded but the menu popped back open with
focus still in the input.

Vuetify closes the menu on select and keeps focus in the combobox.
When the view's settings land, the component's deep settings watcher
clears the combobox model. VCombobox's model watcher then sets its
search text to '', and its search watcher opens the menu whenever the
field is focused and the menu is closed.

The watcher now blurs the combobox before clearing a picked view, so
the name still clears and the menu stays shut. It only blurs when the
model holds a picked view (an object with a pk). Every browse page
load re-sets settings.breadcrumbs, which fires the same watcher. An
unconditional blur closed a menu the user had opened while a page was
still loading.

Checked in a real browser: mouse and keyboard picks, a view whose
settings match the current ones, and opening the menu mid page load.
Also checked merged with the unpushed per-view trash icon branch: a
pick from its item slot, and its Delete View dialog.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- Add alt text to the logo images in README and NEWS.
- Point in-page links at the ids readthedocs generates and add matching
  <a name> anchors so the same links work on GitHub, whose emoji-heading
  slugs differ.
- Link the Windows doc as docs/WINDOWS.md so it resolves on GitHub too.
- Drop the Troubleshooting heading's self-link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* update devenv

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci: let the deploy job publish to PyPI with trusted publishing

Grant id-token: write to the deploy job, which runs devenv-pypi after the
image manifest. devenv-pypi now publishes with PyPI trusted publishing
once the PYPI_TOKEN secret is deleted, and that needs the job to be able
to get a GitHub identity token. While the secret exists it still
publishes with the token. A new test pins the permission.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* update comicbox

* fix(reader): 404 a page past the end instead of caching an empty 200

comicbox 5.2.2 made get_page_by_index() return None for an index past
the last page; before, it raised StopIteration, which surfaced as a 500.
The page view turned that None into b"" and served it as a 200
image/jpeg, and both page routes (v4 reader and OPDS) mark 2xx
responses public for a week, so clients kept the empty image.

Raise NotFound on None instead. cache_control_2xx leaves 404s
uncached.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@ajslater
ajslater merged commit c4df79f into main Sep 30, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant