Skip to content

Release v0.17.0: Mobile UX, Writer VC, Privacy, Playwright CI#138

Merged
ywatanabe1989 merged 150 commits intomainfrom
develop
Apr 24, 2026
Merged

Release v0.17.0: Mobile UX, Writer VC, Privacy, Playwright CI#138
ywatanabe1989 merged 150 commits intomainfrom
develop

Conversation

@ywatanabe1989
Copy link
Copy Markdown
Owner

Summary

  • P4 Mobile UX refinements: hamburger menu, safe-area support, responsive sidebar
  • P5 Writer version control dashboard: git history, diff viewer, commit creation
  • P6 Privacy settings: analytics opt-out, privacy settings page
  • P7 Playwright CI: e2e test infrastructure with 24 tests, mobile test workflows
  • Bug fixes: writer_app import fix, weasyprint lazy-import, Vite modulepreload, numerous chore: Bump version to 0.9.0-alpha #44 browser fixes
  • New features: comms_app, MCP REST API, text-snippet comment anchoring, collaboration models

Commits

340 commits from develop since last merge (PR #132)

Test plan

  • Deployed and serving on prod (scitex.ai returning 200)
  • Writer app accessible
  • Workspace apps accessible
  • CI tests pass on GitHub Actions
  • Playwright e2e tests pass

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com

ywatanabe1989 and others added 30 commits March 25, 2026 13:24
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Claude Code skill system ignores name in sub-skill files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ngo conventions, vite, NAS safety

Add 3 new skill files (development-environment, django-conventions, vite-frontend)
and enrich 3 existing ones (deployment-production with NAS cgroup/safety lessons,
refactoring-rules with correct thresholds, deployment-staging with make rebuild).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server-side provider data is now passed as a data attribute on the
settings page element, eliminating the client-side API fetch on every
page load. All users (including first-time onboarding) get instant
form rendering. Background sync removed — server Django cache handles
the litellm import cost once per deployment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Updated comment header to reflect planned migration from mousedown to
pointerdown for mobile touch support. Code unchanged — implementation
will be done in scitex-ui package.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Hide password requirements on login page until password field focused
- Change post-login redirect from profile page to /workspace/
- Hide site footer on mobile (<=768px) in workspace pages
- Show @username at top of mobile menu when authenticated
- Add Workspace navigation link
- Style user identity item with bold text
Authenticated non-visitor users at / now get workspace-page without
landing-page class, so the mobile footer-hide CSS applies correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add VITE_HOST_IP env var with auto-detection support
- Document full setup in deployment/docs/11_MOBILE_DEV_TESTING.md
- Update mobile-testing skill with real device instructions
- Add known mobile issues (touch resizer, swipe stuck)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
VisitorPool decorators used timezone.now() without importing timezone,
causing "name 'timezone' is not defined" error on visitor allocation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Hide password requirements on blur (prevents overlay on Sign In button on mobile)
- Add missing timezone import in visitor pool decorators

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously visitors at / were redirected back to landing page,
creating a frustrating loop with "Go to Workspace" button.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Log full traceback when visitor auto-login fails (was only logging str(e))
- Upgrade hub index unauthenticated redirect log from info to error
- Comment clarifies failure could be bug, not just pool exhaustion

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove border-bottom from session tabs and border-top from input area
on mobile viewports. Also reduces padding for better mobile space usage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Terminal now starts in the user's project directory (or user home)
instead of /tmp. Falls back to /tmp only if neither path exists.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Project names like "default-project" were truncated to "default-proje..."
due to max-width:140px. Increased to 200px for better readability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
These action buttons clutter the small session tab bar on mobile.
Only delete remains visible; others accessible via hamburger menu.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reduce padding and gaps in session/console tab bars for mobile viewports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Safari doesn't properly handle background shorthand in @Keyframes.
Removed background from animation — keep SVG static, only animate
transform and box-shadow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User confirmed: remove the horizontal line above input area on both
desktop and mobile, not just mobile.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Module tab switcher injects HTML but never called initNewResizers(),
so resizer elements in dynamically loaded content (Files, Scholar,
Tools) were not initialized. FigRecipe worked because it uses React
hooks instead of data-attribute resizers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the mobile drawer pattern (hidden sidebar + hamburger menu) with
an always-visible icon-only rail (52px). Users no longer need to discover
a hidden hamburger to access workspace navigation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The root URL / triggers redirects and visitor allocation, which can
timeout under load. Use the lightweight /healthz/ endpoint and increase
timeout from 3s to 10s to prevent false unhealthy marks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace mousedown/mousemove/mouseup with pointerdown/pointermove/pointerup
which works on mouse, touch, and pen. Add setPointerCapture for reliable
tracking and touch-action:none CSS to prevent browser gesture interception.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tic 1.0.0

BUILD_ID was never set in prod, so all CSS URLs had ?v=1.0.0 forever.
Now reads .build-timestamp from Vite output, falling back to current
timestamp. This ensures browsers fetch fresh CSS after each deploy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
No-fallbacks: /tmp fallback now logs error with full paths so admins
can see exactly which directories are missing. Previously silently
fell back without any indication.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
os.chdir() to host paths fails silently inside Docker container,
causing /tmp fallback. Use srun --chdir flag to set working directory
on the host side where SLURM jobs actually run.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…isible)

Revert icon-only rail approach. Mobile now uses same sidebar as desktop:
collapsed by default with icons+labels, expandable via toggle. Header
is visible on mobile workspace pages for project context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…spy stacks on 504 spikes

- Add timeout (5s for exec, 10s for logs) so a hung docker call never
  freezes the sampler (one such hang caused the timer to stall for
  30min on first deploy).
- When nginx 504_1m >= 3 and cooldown elapsed, dump daphne stacks via
  py-spy into logs/obs/stacks/<ts>_504-N.txt so we capture a stack
  signature aligned with each outage window.

Related: #147 #152
$threads/$est_conns were local to the per-container loop iteration and
got reset to empty before the stack-capture block ran, so captured
stacks had blank headers. Save them as django_threads/django_est_conns
inside the django branch so they survive past the loop boundary.

Related: #147 #152
…#147)

Under daphne/ASGI, a sync middleware chain wraps in one sync_to_async
and the innermost sync call to the async view bridges back via
async_to_sync. py-spy (see #152 day-0 analysis) showed this bridge pair
serialized ~6 concurrent requests through the single event loop:
one thread blocked in run_until_future waiting for the loop to pick up
the async_to_sync callback, one thread in thread_handler holding the
sync chain. nginx upstream_connect_timeout expired at 120s → 504.

Fix: mark Visitor* + OnSiteAuth + GuestSession middlewares as
sync_capable=async_capable=True with markcoroutinefunction(). Under ASGI
Django keeps the chain async end-to-end, so there is no outer
sync_to_async wrap and no async_to_sync(view) hop.

Each middleware's sync body is kept intact as a private method and the
async path runs it once via sync_to_async(thread_sensitive=True) before
awaiting the next layer. Behaviour is byte-for-byte identical; only the
bridging changes.

Expected effect on 504 rate (see #152): drops from sustained 8-10/min
to near zero at current load. Observer will confirm post-deploy.

Related: #147 #152
…header (fix #149)

bot crawl (Bytespider, Googlebot, bingbot, ...) was the primary driver
of concurrent request load hitting the sync middleware chain; see
#147/#152 for the chain that converts that load into 504s.

Approach is a positive signal, not UA-based whitelisting:
- bots.conf defines $is_bot (UA regex denylist of known crawlers) and
  $is_scitex_agent (any non-empty X-SciTeX-Agent header).
- $bot_block = 1 iff is_bot AND NOT is_scitex_agent.
- Location / in nginx_prod.conf returns 429 when $bot_block is set.

Dev agents (apptainer-hosted, MCP tools, local CLIs) just need to send
X-SciTeX-Agent: <anything> on their HTTP calls → always allowed, never
UA-guessed. Mirrors the existing X-SciTeX-OnSite pattern used by
OnSiteAuthMiddleware for MCP auth.

docker-compose.prod.yml: bind-mount bots.conf into the nginx container
alongside nginx.conf.

Related: #147 #149 #152
@ywatanabe1989
Copy link
Copy Markdown
Owner Author

Additional commits: 504 outage fix (#147, #149) + observer scaffold

Pushed 5 more commits on top of this PR (tip now d7ba541a):

commit what
a634f849 ops(observability): per-minute read-only sampler (NAS systemd timer)
d1178f95 ops(observe): timeouts + py-spy stack auto-capture on 504 spikes
cc54608b ops(observe): persist django metrics into stack-capture header
75292fb5 perf(middleware): convert 5 sync middlewares to hybrid sync+async (fix #147)
d7ba541a feat(nginx): block bot crawlers, allow dev agents via X-SciTeX-Agent (fix #149)

Context: scitex.ai was returning widespread 504s. Day-0 observation (see #152) ruled out memory / CPU / network — the signature was flat 8–10 × 504 per minute regardless of request volume, with py-spy showing the event loop parked in asgiref ThreadPoolExecutor.shutdown().join(). The 5 sync middlewares chained through sync_to_async / async_to_sync were serializing concurrent requests onto the single asyncio event loop; ~6 parked pairs → 120 s upstream_connect_timeout → 504.

The fix converts VisitorAutoLogin, VisitorExpiration, VisitorAppRedirect, OnSiteAuth, GuestSession to hybrid sync+async (sync_capable = async_capable = True, markcoroutinefunction). Sync bodies are kept byte-for-byte identical. Under ASGI the chain stays async end-to-end, eliminating the bridge-pair queueing.

Nginx bot block (fix #149) cuts the load driver: UA denylist for Bytespider/Googlebot/bingbot/GPTBot/ClaudeBot/etc.; dev + MCP agents bypass by sending X-SciTeX-Agent: <anything> (same pattern as existing X-SciTeX-OnSite).

CodeQL

35 alerts surfaced but all are in pre-existing files (paths.py, files.py, ws-client.ts, comments.py, etc.) that this 5-commit push does not touch. The PR banner confirms: "Alerts not introduced by this pull request might have been detected because the code changes were too large."

Post-merge deploy + validation

git -C ~/proj/scitex-cloud checkout main && git -C ~/proj/scitex-cloud pull
docker compose -f deployment/docker/docker-compose.yml \
               -f deployment/docker/docker-compose.prod.yml \
               build django
docker compose -f deployment/docker/docker-compose.yml \
               -f deployment/docker/docker-compose.prod.yml \
               up -d --force-recreate django nginx

The observer timer is already live on the NAS; it'll write the post-fix _nginx rows into logs/obs/YYYY-MM-DD.tsv. Expected: 504_1m drops from 8–10 to near zero within a few minutes. If not, #153 (gunicorn+uvicorn multi-worker) is the next lever.

Closes #147, #149. Related: #148, #150, #151, #152, #153.

ywatanabe1989 and others added 10 commits April 19, 2026 16:53
Adds a non-destructive path so scitex-cloud can start consuming the
shared `scitex_writer._django` implementation without touching the
existing legacy writer_app routes.

- `writer_app/urls/writer_django.py` — new wrapper module mirroring the
  `figrecipe_app/urls/figrecipe.py` pattern: `@login_required` views that
  inject `working_dir` from the user's current project and delegate to
  `scitex_writer._django.views.{editor_page, viewer_page, api_dispatch}`.
  Routes:
    /apps/writer/editor-v2/           — canonical editor
    /apps/writer/viewer-v2/           — live-paper viewer (unblocks #133)
    /apps/writer/v2/<path:endpoint>   — API dispatcher
- `writer_app/urls/__init__.py` — include the new wrapper before the
  legacy include lines
- `pyproject.toml` — declare `scitex-writer>=2.14.0` as a dep

Stacked on ywatanabe1989/scitex-writer#84-#89 (PR1-PR6). Flipping the
default route from the legacy TemplateView to the v2 wrapper is a
follow-up commit once the v2 UX is reviewed.

This PR is the consumer side of scitex-writer#82 — closes most of
scitex-cloud#146.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI for PR #157 failed because scitex-writer isn't on PyPI yet — the
sibling-installer only knew about scitex-ui and figrecipe. Adding
scitex-writer to .scitex-apps.json so install_apps.sh pip-installs it
via the sibling checkout in the CI runner.

Also harden writer_app/urls/__init__.py: guard the v2 include() behind a
try-import so the URL conf still loads in environments without
scitex-writer (e.g., a rollback or a minimal production slice that
hasn't pulled the dep yet).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause of the pre-rebuild warning "Permission fix needs sudo":
sandbox contains ~2.4k files owned by sub-UIDs left behind by prior
`apptainer --fakeroot` sessions that ywatanabe cannot chmod from the
host. The old `chmod -R a+rX || echo warning` masked the real failure
and still printed "Sandbox permissions fixed".

Changes:
- Preflight: fail fast if `apptainer` is missing or /etc/subuid lacks
  an entry for $USER (fakeroot requires the subuid map); surface setup
  problems before wasting time on docker build.
- Replace host-side chmod with `apptainer exec --fakeroot --writable
  --contain --no-home --no-mount home,tmp,cwd` running
  `find / -xdev -not -path /proc* -not -path /sys* -not -path /dev*
  -exec chmod a+rX {} +` inside the sandbox. Fakeroot owns the sub-UID
  files so chmod succeeds without sudo. --contain/--no-home prevents
  walking into host $HOME (~/.scitex/...); -xdev + -not -path skips
  kernel pseudo-filesystems that no one can chmod.
- Fail loud on genuine errors instead of printing a misleading success.

Result: step 6 drops from effectively-hung/failing to ~35s and exits 0.
- Bump version 0.17.0-alpha → 0.17.1-alpha
- CHANGELOG entry for deploy/rebuild.sh fix, CI fix, and writer v2 wrapper
- releases/v0.17.1-alpha/RELEASE_NOTES.md
Migration 0008 was amended to also create the Comment model with its three
indexes, but 0009 still carried the original `CreateModel(name="Comment")`
plus three `AddIndex` ops. Applying 0009 on a fresh DB raised::

    django.db.utils.ProgrammingError:
        relation "writer_app_comment" already exists
    Applying writer_app.0009_add_comment_model...

This was the sole cause of the 16+ days of red "E2E Mobile Tests" CI on
develop (and on every PR that re-triggered it).

Keep 0009 as an empty graph node so 0010_add_comment_anchor_fields's
`dependencies = [("writer_app", "0009_add_comment_model")]` still resolves.
`python manage.py makemigrations --check --dry-run writer_app` reports
"No changes detected" with the model state fully satisfied by 0008.
Replaces click.prompt/click.confirm in 6 CLI sites with required
flags backed by SCITEX_CLOUD_* env vars and an optional config
file at ~/.scitex/scitex-cloud/config.yaml. Precedence per spec
§6b: --flag > env var > config file.

Missing credentials or destructive-action confirmation now exit
with code 2 and a guidance message to stderr, instead of blocking
on stdin. This lets scitex-cloud's CLI run under CI, agent
fleets, and cron without hanging.

Sites:
- _workspace_auth.py::get_jwt_token — env SCITEX_CLOUD_WORKSPACE_{USER,PASSWORD,URL}
- _gitea_auth.py::login — env SCITEX_CLOUD_GITEA_{TOKEN,USER,PASSWORD,URL}
- _gitea_auth.py::logout --delete-token — env SCITEX_CLOUD_GITEA_PASSWORD
- setup.py — env SCITEX_CLOUD_ENV
- project.py::delete — requires --yes (no fallback prompt)
- _gitea_repo.py::delete — requires --yes (no fallback prompt)

Adds scitex_cloud/_config/_loader.py — minimal non-failing YAML
loader with spec §6b precedence; returns {} on missing config.

Adds tests/scitex_cloud/test_no_interactive_prompts.py — 8 tests
covering each refactored site, including positive env-var paths
and exit-2 fail-fast paths.

Fixes item C1 in cli-skill-audit-2026-04-22-verified.md.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…info (#163)

- Mount common/nginx/bots.conf into nginx prod container so feat(nginx)
  bot-block (bdc38da / fix #149) activates in production.
- Add user_type field to /oauth/userinfo/ response for external apps
  (orochi et al.): visitor / readonly / member, derived from
  username + email patterns.
- SKILL.md: document actual CLI groups, MCP tool counts (55 across 6
  categories), Python API, and list all 21 sub-skills (previously only
  15 were indexed)
- README: fix stale MCP tool table (was: 2 categories/23 tools; now: 6
  categories/55 tools), stale hardcoded version string, and wrong
  docker subcommands (docker status/logs -> docker up/down/ps/build/
  restart)
- MANIFEST.in: belt-and-braces _skills inclusion for sdist on top of
  existing pyproject.toml [tool.setuptools.package-data] glob

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment on lines 308 to +311
}
}

// Update mobile hamburger menu countdown

Check warning

Code scanning / CodeQL

Useless assignment to local variable

The value assigned to timeStr here is unused.

Copilot Autofix

AI 4 days ago

To fix this cleanly without changing functionality, remove the unused timeStr variable and its associated timestamp formatting block in updateServerHealth().

Best single fix in this snippet:

  • In static/shared/ts/components/header.ts, inside updateServerHealth:
    • Keep timestamp only if it is used elsewhere. In this snippet it is not, so remove it as well.
    • Remove the entire “Format timestamp for display” block (let timeStr = "now"; ...).
  • Leave tooltip behavior unchanged (Server: ${statusMsg}), since that reflects current functionality.

No new imports, methods, or dependencies are needed.

Suggested changeset 1
static/shared/ts/components/header.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/static/shared/ts/components/header.ts b/static/shared/ts/components/header.ts
--- a/static/shared/ts/components/header.ts
+++ b/static/shared/ts/components/header.ts
@@ -352,18 +352,7 @@
 
         const status = data.status; // "healthy" | "warning" | "error" | "starting"
         const statusColor = data.color; // Hex color from API
-        const timestamp = data.timestamp; // ISO timestamp from API
 
-        // Format timestamp for display
-        let timeStr = "now";
-        if (timestamp) {
-          const date = new Date(timestamp);
-          timeStr = date.toLocaleTimeString([], {
-            hour: "2-digit",
-            minute: "2-digit",
-          });
-        }
-
         // Build simple tooltip: just overall status
         let statusMsg = "healthy";
         if (status === "starting") {
EOF
@@ -352,18 +352,7 @@

const status = data.status; // "healthy" | "warning" | "error" | "starting"
const statusColor = data.color; // Hex color from API
const timestamp = data.timestamp; // ISO timestamp from API

// Format timestamp for display
let timeStr = "now";
if (timestamp) {
const date = new Date(timestamp);
timeStr = date.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
});
}

// Build simple tooltip: just overall status
let statusMsg = "healthy";
if (status === "starting") {
Copilot is powered by AI and may make mistakes. Always verify output.
ywatanabe1989 and others added 11 commits April 23, 2026 18:51
pip install <pkg> alone enables `import <mod> as sio` only; the
scitex.<subname> namespaced form requires `pip install scitex` as
well. Every package SKILL.md now documents both forms (or standalone-
only when the umbrella has no re-export, confirmed empirically
2026-04-23 in a python:3.11-slim container).

Cross-references ../general/02_interface-python-api.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…surface

Enumerates all 6 MCP categories (project_*, repo_*, cloud_sdk_*,
api_*, app_*, onsite_*) with representative trigger phrases and
drop-in replacements (curl+git+Playwright against the Django
instance). Fixes stale allowed-tools to mcp__scitex__cloud_*.
Verified against actual async-def tool inventory; stays under the
4096B index size cap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ella scitex.browser

Tests should depend on the leaf package (scitex_browser) rather than the
umbrella namespace re-export. Same runtime behavior, narrower dep scope.
- public_status.py: take main (richer docstring + issue #82 ref + cache-backend-unavailable fallback)
- context_processors.py: hybrid — develop's .build-timestamp chain + main's SCITEX_CLOUD_BUILD_ID env name
- settings_dev.py: hybrid — develop's VITE_USE_BUILD + 'auto' gateway detection + main's env-prefix (SCITEX_CLOUD_VITE_HOST_IP / SCITEX_CLOUD_VITE_USE_BUILD)
- nginx_prod.conf: take develop (variable-based proxy_pass for Docker DNS re-resolution; NAS-reboot-resilient pattern already in use for gitea/crossref/ws_ssh_proxy/umami)
- pyproject.toml: bump to 0.17.2-alpha
Encodes which interface (python/cli/mcp/hook/mixed) a user should reach
for first, without dropping or reordering the existing 5-interface template.
Machine-readable via frontmatter; human-visible via callout at top of body.
Replaces single primary_interface label with a full ratings dict:
  interfaces:
    python: 0..3
    cli: 0..3
    mcp: 0..3
    skills: 0..3
    hook: 0..3
    http: 0..3

0=absent, 1=thin wrapper, 2=useful, 3=primary.
Callout in body uses stars (e.g. `Python ⭐⭐⭐ (primary) · CLI ⭐`).
Derived primary_interface field preserved for single-value consumers.
Drops the kanji (守破離) from the section heading in favour of the
already-used Shu-Ha-Ri transliteration. Line 93 of the same file
already writes "This is the Shu-Ha-Ri principle"; the header now
matches, and the new audit_english_only.py auditor no longer flags
this file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ywatanabe1989 ywatanabe1989 merged commit 3f82740 into main Apr 24, 2026
14 of 17 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 24, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants